/FileExtraction/Java_unseen/rcgroot_open-gpstracker/studio/app/src/main/java/nl/sogeti/android/gpstracker/actions/tasks/XmlCreator.java
/*------------------------------------------------------------------------------
 **     Ident: Sogeti Smart Mobile Solutions
 **    Author: rene
 ** Copyright: (c) Apr 24, 2011 Sogeti Nederland B.V. All Rights Reserved.
 **------------------------------------------------------------------------------
 ** Sogeti Nederland B.V.            |  No part of this file may be reproduced  
 ** Distributed Software Engineering |  or transmitted in any form or by any        
 ** Lange Dreef 17                   |  means, electronic or mechanical, for the      
 ** 4131 NJ Vianen                   |  purpose, without the express written    
 ** The Netherlands                  |  permission of the copyright holder.
 *------------------------------------------------------------------------------
 *
 *   This file is part of OpenGPSTracker.
 *
 *   OpenGPSTracker is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   OpenGPSTracker is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with OpenGPSTracker.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package nl.sogeti.android.gpstracker.actions.tasks;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Window;

import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.channels.FileChannel;
import java.util.Date;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import nl.sogeti.android.gpstracker.actions.utils.ProgressListener;
import nl.sogeti.android.gpstracker.integration.ContentConstants.Media;
import nl.sogeti.android.gpstracker.integration.ContentConstants.Tracks;
import nl.sogeti.android.gpstracker.integration.ContentConstants.Waypoints;
import nl.sogeti.android.gpstracker.util.Constants;
import timber.log.Timber;

/**
 * Async XML creation task Execute without parameters (Void) Update posted with single Integer And result is a filename
 * in a String
 *
 * @author rene (c) May 29, 2011, Sogeti B.V.
 * @version $Id$
 */
public abstract class XmlCreator extends AsyncTask<Void, Integer, Uri> {
    public ProgressAdmin mProgressAdmin;
    protected Context mContext;
    protected Uri mTrackUri;
    String mChosenName;
    String mFileName;
    private File mExportDirectoryPath;
    private boolean mNeedsBundling;
    private ProgressListener mProgressListener;
    private String mErrorText;
    private Exception mException;
    private String mTask;

    XmlCreator(Context context, Uri trackUri, String chosenFileName, ProgressListener listener) {
        mChosenName = chosenFileName;
        mContext = context;
        mTrackUri = trackUri;
        mProgressListener = listener;
        mProgressAdmin = new ProgressAdmin();

        String trackName = extractCleanTrackName();
        mFileName = cleanFilename(mChosenName, trackName);
    }

    /**
     * Removes all non-word chars (\W) from the text
     *
     * @param fileName
     * @param defaultName
     * @return a string larger then 0 with either word chars remaining from the input or the default provided
     */
    public static String cleanFilename(String fileName, String defaultName) {
        if (fileName == null || "".equals(fileName)) {
            fileName = defaultName;
        } else {
            fileName = fileName.replaceAll("\\W", "");
            fileName = (fileName.length() > 0) ? fileName : defaultName;
        }
        return fileName;
    }

    public static boolean deleteRecursive(File file) {
        if (file.isDirectory()) {
            String[] children = file.list();
            for (int i = 0; i < children.length; i++) {
                boolean success = deleteRecursive(new File(file, children[i]));
                if (!success) {
                    return false;
                }
            }
        }
        return file.delete();
    }

    public static String convertStreamToString(InputStream is) throws IOException {
        String result = "";
      /*
       * To convert the InputStream to String we use the Reader.read(char[] buffer) method. We iterate until the Reader
       * return -1 which means there's no more data to read. We use the StringWriter class to produce the string.
       */
        if (is != null) {
            Writer writer = new StringWriter();

            char[] buffer = new char[8192];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                int n;
                while ((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                is.close();
            }
            result = writer.toString();
        }
        return result;
    }

    public static InputStream convertStreamToLoggedStream(String tag, InputStream is) throws IOException {
        String result = "";
      /*
       * To convert the InputStream to String we use the Reader.read(char[] buffer) method. We iterate until the Reader
       * return -1 which means there's no more data to read. We use the StringWriter class to produce the string.
       */
        if (is != null) {
            Writer writer = new StringWriter();

            char[] buffer = new char[8192];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                int n;
                while ((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                is.close();
            }
            result = writer.toString();
        }
        InputStream in = new ByteArrayInputStream(result.getBytes("UTF-8"));
        return in;
    }

    private String extractCleanTrackName() {
        Cursor trackCursor = null;
        ContentResolver resolver = mContext.getContentResolver();
        String trackName = "Untitled";
        try {
            trackCursor = resolver.query(mTrackUri, new String[]{Tracks.NAME}, null, null, null);
            if (trackCursor.moveToLast()) {
                trackName = cleanFilename(trackCursor.getString(0), trackName);
            }
        } finally {
            if (trackCursor != null) {
                trackCursor.close();
            }
        }
        return trackName;
    }

    public void executeOn(Executor executor) {
        if (Build.VERSION.SDK_INT >= 11) {
            executeOnExecutor(executor);
        } else {
            execute();
        }
    }

    /**
     * Calculated the total progress sum expected from a export to file This is the sum of the number of waypoints and
     * media entries times 100. The whole number is doubled when compression is needed.
     */
    public void determineProgressGoal() {
        if (mProgressListener != null) {
            Uri allWaypointsUri = Uri.withAppendedPath(mTrackUri, "waypoints");
            Uri allMediaUri = Uri.withAppendedPath(mTrackUri, "media");
            Cursor cursor = null;
            ContentResolver resolver = mContext.getContentResolver();
            try {
                cursor = resolver.query(allWaypointsUri, new String[]{"count(" + Waypoints.TABLE + "." + Waypoints._ID
                        + ")"}, null, null, null);
                if (cursor.moveToLast()) {
                    mProgressAdmin.setWaypointCount(cursor.getInt(0));
                }
                cursor.close();
                cursor = resolver.query(allMediaUri, new String[]{"count(" + Media.TABLE + "." + Media._ID + ")"}, null,
                        null, null);
                if (cursor.moveToLast()) {
                    mProgressAdmin.setMediaCount(cursor.getInt(0));
                }
                cursor.close();
                cursor = resolver.query(allMediaUri, new String[]{"count(" + Tracks._ID + ")"}, Media.URI
                        + " LIKE ? and " + Media.URI + " NOT LIKE ?", new String[]{"file://%", "%txt"}, null);
                if (cursor.moveToLast()) {
                    mProgressAdmin.setCompress(cursor.getInt(0) > 0);
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        } else {
            Timber.w("Exporting %s without progress!", mTrackUri);
        }
    }

    /**
     * Includes media into the export directory and returns the relative path of the media
     *
     * @param input either file:// or content:// uri
     * @return file path relative to the export dir
     * @throws IOException
     */
    protected File includeMediaFile(Uri input) throws IOException {
        mNeedsBundling = true;
        File target;
        if ("file".equals(input.getScheme()) || input.getScheme() == null) {
            target = includeFileMediaFile(input);
        }
        else {
            target = includeContentMediaFile(input);
        }

        return target;
    }

    @Nullable
    private File includeContentMediaFile(Uri input) throws IOException {
        File target = new File(mExportDirectoryPath + "/" + input.getLastPathSegment());
        InputStream is = null;
        BufferedInputStream bis = null;
        FileOutputStream os = null;
        BufferedOutputStream bos = null;
        try {
            is = mContext.getContentResolver().openInputStream(input);
            bis = new BufferedInputStream(is, 8192);
            target.createNewFile();
            os = new FileOutputStream(target);
            bos = new BufferedOutputStream(os, 8192);

            byte[] buffer = new byte[8192];
            while (bis.read(buffer) != -1) {
                bos.write(buffer);
            }
        }
        finally {
            close(bis, is, bos, os);
        }

        return target;
    }

    private void close(Closeable... closeables) {
        for (Closeable closeable: closeables) {
            try {
                if (closeable != null) {
                    closeable.close();
                }
            } catch (IOException e) {
                Timber.w(e, "Failed to close.");
            }
        }
    }

    @NonNull
    private File includeFileMediaFile(Uri input) throws IOException {
        File source = new File(input.getPath());
        File target = new File(mExportDirectoryPath + "/" + source.getName());

        //      Log.d( TAG, String.format( "Copy %s to %s", source, target ) );
        if (source.exists()) {
            FileInputStream fileInputStream = new FileInputStream(source);
            FileChannel inChannel = fileInputStream.getChannel();
            FileOutputStream fileOutputStream = new FileOutputStream(target);
            FileChannel outChannel = fileOutputStream.getChannel();
            try {
                inChannel.transferTo(0, inChannel.size(), outChannel);
            } finally {
                if (inChannel != null) {
                    inChannel.close();
                }
                if (outChannel != null) {
                    outChannel.close();
                }
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            }
        } else {
            Timber.w("Failed to add file to new XML export. Missing: %s", source.getAbsolutePath());
        }
        mProgressAdmin.addMediaProgress();
        return target;
    }

    /**
     * Create a zip of the export directory based on the given filename
     *
     * @param fileName  The directory to be replaced by a zipped file of the same name
     * @param extension
     * @return full path of the build zip file
     * @throws IOException
     */
    protected File bundlingMediaAndXml(String fileName, String extension) throws IOException {
        File zipFilePath;
        if (fileName.endsWith(".zip") || fileName.endsWith(extension)) {
            zipFilePath = new File(Constants.getStorageDirectory(mContext), fileName);
        } else {
            zipFilePath = new File(Constants.getStorageDirectory(mContext), fileName + extension);
        }
        String[] filenames = mExportDirectoryPath.list();
        byte[] buf = new byte[1024];
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new FileOutputStream(zipFilePath));
            for (int i = 0; i < filenames.length; i++) {
                String entryFilePath = mExportDirectoryPath + "/" + filenames[i];
                FileInputStream in = new FileInputStream(entryFilePath);
                zos.putNextEntry(new ZipEntry(filenames[i]));
                int len;
                while ((len = in.read(buf)) >= 0) {
                    zos.write(buf, 0, len);
                }
                zos.closeEntry();
                in.close();
                mProgressAdmin.addCompressProgress();
            }
        } finally {
            if (zos != null) {
                zos.close();
            }
        }

        deleteRecursive(mExportDirectoryPath);

        return zipFilePath;
    }

    public File getExportDirectoryPath() {
        return mExportDirectoryPath;
    }

    public void setExportDirectoryPath(File exportDirectoryPath) {
        this.mExportDirectoryPath = exportDirectoryPath;
    }

    public void quickTag(XmlSerializer serializer, String ns, String tag, String content)
            throws IllegalArgumentException, IllegalStateException, IOException {
        if (tag == null) {
            tag = "";
        }
        if (content == null) {
            content = "";
        }
        serializer.text("\n");
        serializer.startTag(ns, tag);
        serializer.text(content);
        serializer.endTag(ns, tag);
    }

    public boolean needsBundling() {
        return mNeedsBundling;
    }

    protected abstract String getContentType();

    protected void handleError(String task, Exception e, String text) {
        Timber.e(e, "Unable to save ");
        mTask = task;
        mException = e;
        mErrorText = text;
        cancel(false);
        throw new CancellationException(text);
    }

    @Override
    protected void onPreExecute() {
        if (mProgressListener != null) {
            mProgressListener.started();
        }
    }

    @Override
    protected void onPostExecute(Uri resultFilename) {
        if (mProgressListener != null) {
            mProgressListener.finished(resultFilename);
        }
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        if (mProgressListener != null) {
            mProgressListener.setProgress(mProgressAdmin.getProgress());
        }
    }

    @Override
    protected void onCancelled() {
        if (mProgressListener != null) {
            mProgressListener.finished(null);
            mProgressListener.showError(mTask, mErrorText, mException);
        }
    }

    public class ProgressAdmin {
        long lastUpdate;
        private boolean compressCount;
        private boolean compressProgress;
        private boolean uploadCount;
        private boolean uploadProgress;
        private int mediaCount;
        private int mediaProgress;
        private int waypointCount;
        private int waypointProgress;
        private long photoUploadCount;
        private long photoUploadProgress;

        public void addMediaProgress() {
            mediaProgress++;
        }

        public void addCompressProgress() {
            compressProgress = true;
        }

        public void addUploadProgress() {
            uploadProgress = true;
        }

        public void addPhotoUploadProgress(long length) {
            photoUploadProgress += length;
        }

        /**
         * Get the progress on scale 0 ... Window.PROGRESS_END
         *
         * @return Returns the progress as a int.
         */
        public int getProgress() {
            int blocks = 0;
            if (waypointCount > 0) {
                blocks++;
            }
            if (mediaCount > 0) {
                blocks++;
            }
            if (compressCount) {
                blocks++;
            }
            if (uploadCount) {
                blocks++;
            }
            if (photoUploadCount > 0) {
                blocks++;
            }
            int progress;
            if (blocks > 0) {
                int blockSize = Window.PROGRESS_END / blocks;
                progress = waypointCount > 0 ? blockSize * waypointProgress / waypointCount : 0;
                progress += mediaCount > 0 ? blockSize * mediaProgress / mediaCount : 0;
                progress += compressProgress ? blockSize : 0;
                progress += uploadProgress ? blockSize : 0;
                progress += photoUploadCount > 0 ? blockSize * photoUploadProgress / photoUploadCount : 0;
            } else {
                progress = 0;
            }
            //Log.d( TAG, "Progress updated to "+progress);
            return progress;
        }

        public void setWaypointCount(int waypoint) {
            waypointCount = waypoint;
            considerPublishProgress();
        }

        public void considerPublishProgress() {
            long now = new Date().getTime();
            if (now - lastUpdate > 1000) {
                lastUpdate = now;
                publishProgress();
            }
        }

        public void setMediaCount(int media) {
            mediaCount = media;
            considerPublishProgress();
        }

        public void setCompress(boolean compress) {
            compressCount = compress;
            considerPublishProgress();
        }

        public void setUpload(boolean upload) {
            uploadCount = upload;
            considerPublishProgress();
        }

        public void setPhotoUpload(long length) {
            photoUploadCount += length;
            considerPublishProgress();
        }

        public void addWaypointProgress(int i) {
            waypointProgress += i;
            considerPublishProgress();
        }
    }
}
