/**
 * Copyright (c) 2012 Todoroo Inc
 *
 * See the file "LICENSE" for the full license governing this code.
 */
package com.todoroo.astrid.backup;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import org.xmlpull.v1.XmlSerializer;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.util.Xml;
import android.widget.Toast;

import com.timsu.astrid.R;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.PropertyVisitor;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.utility.AstridPreferences;

public class TasksXmlExporter {

    // --- public interface

    /**
     * Import tasks from the given file
     *
     * @param context context
     * @param exportType from service, manual, or on upgrade
     * @param runAfterExport runnable to run after exporting
     * @param backupDirectoryOverride new backupdirectory, or null to use default
     */
    public static void exportTasks(Context context, ExportType exportType,
            Runnable runAfterExport, File backupDirectoryOverride, String versionName) {
        new TasksXmlExporter(context, exportType, runAfterExport,
                backupDirectoryOverride, versionName);
    }

    public static enum ExportType {
        EXPORT_TYPE_SERVICE,
        EXPORT_TYPE_MANUAL,
        EXPORT_TYPE_ON_UPGRADE
    }

    // --- implementation

    private static final int FORMAT = 2;

    private final Context context;
    private int exportCount = 0;
    private XmlSerializer xml;
    private final Database database = PluginServices.getDatabase();
    private final TaskService taskService = PluginServices.getTaskService();
    private final MetadataService metadataService = PluginServices.getMetadataService();
    private final ExceptionService exceptionService = PluginServices.getExceptionService();

    private final ProgressDialog progressDialog;
    private final Handler handler;
    private final File backupDirectory;
    private final String latestSetVersionName;

    private void setProgress(final int taskNumber, final int total) {
        handler.post(new Runnable() {
            public void run() {
                progressDialog.setMax(total);
                progressDialog.setProgress(taskNumber);
            }
        });
    }

    private TasksXmlExporter(final Context context, final ExportType exportType,
            final Runnable runAfterExport, File backupDirectoryOverride, String versionName) {
        this.context = context;
        this.exportCount = 0;
        this.backupDirectory = backupDirectoryOverride == null ?
                BackupConstants.defaultExportDirectory() : backupDirectoryOverride;
        this.latestSetVersionName = versionName;

        handler = new Handler();
        progressDialog = new ProgressDialog(context);
        if(exportType == ExportType.EXPORT_TYPE_MANUAL) {
            progressDialog.setIcon(android.R.drawable.ic_dialog_info);
            progressDialog.setTitle(R.string.export_progress_title);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setProgress(0);
            progressDialog.setCancelable(false);
            progressDialog.setIndeterminate(false);
            progressDialog.show();
            if(context instanceof Activity)
                progressDialog.setOwnerActivity((Activity)context);
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String output = setupFile(backupDirectory,
                            exportType);
                    int tasks = taskService.countTasks();

                    if(tasks > 0)
                        doTasksExport(output);

                    Preferences.setLong(BackupPreferences.PREF_BACKUP_LAST_DATE,
                            DateUtilities.now());
                    Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, null);

                    if (exportType == ExportType.EXPORT_TYPE_MANUAL)
                        onFinishExport(output);
                } catch (IOException e) {
                    switch(exportType) {
                    case EXPORT_TYPE_MANUAL:
                        exceptionService.displayAndReportError(context,
                            context.getString(R.string.backup_TXI_error), e);
                        break;
                    case EXPORT_TYPE_SERVICE:
                        exceptionService.reportError("background-backup", e); //$NON-NLS-1$
                        Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, e.toString());
                        break;
                    case EXPORT_TYPE_ON_UPGRADE:
                        exceptionService.reportError("background-backup", e); //$NON-NLS-1$
                        Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, e.toString());
                        break;
                    }
                } finally {
                    if(runAfterExport != null)
                        runAfterExport.run();
                }
            }
        }).start();
    }


    @SuppressWarnings("nls")
    private void doTasksExport(String output) throws IOException {
        File xmlFile = new File(output);
        xmlFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(xmlFile);
        xml = Xml.newSerializer();
        xml.setOutput(fos, BackupConstants.XML_ENCODING);

        xml.startDocument(null, null);
        xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);

        xml.startTag(null, BackupConstants.ASTRID_TAG);
        xml.attribute(null, BackupConstants.ASTRID_ATTR_VERSION,
                Integer.toString(AstridPreferences.getCurrentVersion()));
        xml.attribute(null, BackupConstants.ASTRID_ATTR_FORMAT,
                Integer.toString(FORMAT));

        serializeTasks();

        xml.endTag(null, BackupConstants.ASTRID_TAG);
        xml.endDocument();
        xml.flush();
        fos.close();
    }

    private void serializeTasks() throws IOException {
        TodorooCursor<Task> cursor;
        cursor = taskService.query(Query.select(
                Task.PROPERTIES).orderBy(Order.asc(Task.ID)));
        try {
            Task task = new Task();
            int length = cursor.getCount();
            for(int i = 0; i < length; i++) {
                cursor.moveToNext();
                task.readFromCursor(cursor);

                setProgress(i, length);

                xml.startTag(null, BackupConstants.TASK_TAG);
                serializeModel(task, Task.PROPERTIES, Task.ID);
                serializeMetadata(task);
                xml.endTag(null, BackupConstants.TASK_TAG);
                this.exportCount++;
            }
        } finally {
            cursor.close();
        }
    }

    private synchronized void serializeMetadata(Task task) throws IOException {
        TodorooCursor<Metadata> cursor = metadataService.query(Query.select(
                Metadata.PROPERTIES).where(MetadataCriteria.byTask(task.getId())));
        try {
            Metadata metadata = new Metadata();
            for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                metadata.readFromCursor(cursor);

                xml.startTag(null, BackupConstants.METADATA_TAG);
                serializeModel(metadata, Metadata.PROPERTIES, Metadata.ID, Metadata.TASK);
                xml.endTag(null, BackupConstants.METADATA_TAG);
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * Turn a model into xml attributes
     * @param model
     */
    private void serializeModel(AbstractModel model, Property<?>[] properties, Property<?>... excludes) {
        outer: for(Property<?> property : properties) {
            for(Property<?> exclude : excludes)
                if(property.name.equals(exclude.name))
                    continue outer;

            try {
                property.accept(xmlWritingVisitor, model);
            } catch (Exception e) {
                Log.e("astrid-exporter", //$NON-NLS-1$
                        "Caught exception while reading " + property.name + //$NON-NLS-1$
                        " from " + model.getDatabaseValues(), e); //$NON-NLS-1$
            }
        }
    }

    private final XmlWritingPropertyVisitor xmlWritingVisitor = new XmlWritingPropertyVisitor();
    public static final String XML_NULL = "null"; //$NON-NLS-1$

    public class XmlWritingPropertyVisitor implements PropertyVisitor<Void, AbstractModel> {

        @Override
        public Void visitInteger(Property<Integer> property, AbstractModel data) {
            try {
                Integer value = data.getValue(property);
                String valueString = (value == null) ? XML_NULL : value.toString();
                xml.attribute(null, property.name, valueString);
            } catch (UnsupportedOperationException e) {
                // didn't read this value, do nothing
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalStateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return null;
        }

        @Override
        public Void visitLong(Property<Long> property, AbstractModel data) {
            try {
                Long value = data.getValue(property);
                String valueString = (value == null) ? XML_NULL : value.toString();
                xml.attribute(null, property.name, valueString);
            } catch (UnsupportedOperationException e) {
                // didn't read this value, do nothing
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalStateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return null;
        }

        @Override
        public Void visitDouble(Property<Double> property, AbstractModel data) {
            try {
                Double value = data.getValue(property);
                String valueString = (value == null) ? XML_NULL : value.toString();
                xml.attribute(null, property.name, valueString);
            } catch (UnsupportedOperationException e) {
                // didn't read this value, do nothing
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalStateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return null;
        }

        @Override
        public Void visitString(Property<String> property, AbstractModel data) {
            try {
                String value = data.getValue(property);
                if(value == null)
                    return null;
                xml.attribute(null, property.name, value);
            } catch (UnsupportedOperationException e) {
                // didn't read this value, do nothing
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalStateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return null;
        }

    }

    private void onFinishExport(final String outputFile) {
        handler.post(new Runnable() {
            @Override
            public void run() {

                if(exportCount == 0)
                    Toast.makeText(context, context.getString(R.string.export_toast_no_tasks), Toast.LENGTH_LONG).show();
                else {
                    CharSequence text = String.format(context.getString(R.string.export_toast),
                            context.getResources().getQuantityString(R.plurals.Ntasks, exportCount,
                                    exportCount), outputFile);
                    Toast.makeText(context, text, Toast.LENGTH_LONG).show();
                    if(progressDialog.isShowing() && context instanceof Activity)
                        DialogUtilities.dismissDialog((Activity) context, progressDialog);
                }
            }
        });
    }

    /**
     * Creates directories if necessary and returns fully qualified file
     * @param directory
     * @return output file name
     * @throws IOException
     */
    private String setupFile(File directory, ExportType exportType) throws IOException {
        File astridDir = directory;
        if (astridDir != null) {
            // Check for /sdcard/astrid directory. If it doesn't exist, make it.
            if (astridDir.exists() || astridDir.mkdir()) {
                String fileName = ""; //$NON-NLS-1$
                switch(exportType) {
                case EXPORT_TYPE_SERVICE:
                    fileName = String.format(BackupConstants.BACKUP_FILE_NAME, BackupDateUtilities.getDateForExport());
                    break;
                case EXPORT_TYPE_MANUAL:
                    fileName = String.format(BackupConstants.EXPORT_FILE_NAME, BackupDateUtilities.getDateForExport());
                    break;
                case EXPORT_TYPE_ON_UPGRADE:
                    fileName = String.format(BackupConstants.UPGRADE_FILE_NAME, latestSetVersionName);
                    break;
                default:
                     throw new IllegalArgumentException("Invalid export type"); //$NON-NLS-1$
                }
                return astridDir.getAbsolutePath() + File.separator + fileName;
            } else {
                // Unable to make the /sdcard/astrid directory.
                throw new IOException(context.getString(R.string.DLG_error_sdcard,
                        astridDir.getAbsolutePath()));
            }
        } else {
            // Unable to access the sdcard because it's not in the mounted state.
            throw new IOException(context.getString(R.string.DLG_error_sdcard_general));
        }
    }

}
