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

import java.io.FileReader;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.StringTokenizer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.WindowManager.BadTokenException;

import com.google.ical.values.RRule;
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.ContextManager;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.legacy.LegacyImportance;
import com.todoroo.astrid.legacy.LegacyRepeatInfo;
import com.todoroo.astrid.legacy.LegacyRepeatInfo.LegacyRepeatInterval;
import com.todoroo.astrid.legacy.LegacyTaskModel;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.UpgradeService;
import com.todoroo.astrid.tags.TagService;

public class TasksXmlImporter {

    // --- public interface

    /**
     * Import tasks from the given file
     *
     * @param input
     * @param runAfterImport
     */
    public static void importTasks(Context context, String input, Runnable runAfterImport) {
        new TasksXmlImporter(context, input, runAfterImport);
    }

    // --- implementation

    private final Handler handler;
    private int taskCount;
    private int importCount = 0;
    private int skipCount = 0;
    private int errorCount = 0;
    private final String input;

    private final Context context;
    private final TaskService taskService = PluginServices.getTaskService();
    private final MetadataService metadataService = PluginServices.getMetadataService();
    private final ExceptionService exceptionService = PluginServices.getExceptionService();
    private final ProgressDialog progressDialog;
    private final Runnable runAfterImport;

    private void setProgressMessage(final String message) {
        handler.post(new Runnable() {
            public void run() {
                progressDialog.setMessage(message);
            }
        });
    }

    /**
     * Import tasks.
     * @param runAfterImport optional runnable after import
     */
    private TasksXmlImporter(final Context context, String input, Runnable runAfterImport) {
        this.input = input;
        this.context = context;
        this.runAfterImport = runAfterImport;

        handler = new Handler();
        progressDialog = new ProgressDialog(context);
        progressDialog.setIcon(android.R.drawable.ic_dialog_info);
        progressDialog.setTitle(R.string.import_progress_title);
        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        progressDialog.setCancelable(false);
        progressDialog.setIndeterminate(true);
        try {
            progressDialog.show();
            if(context instanceof Activity)
                progressDialog.setOwnerActivity((Activity)context);
        } catch (BadTokenException e) {
            // Running from a unit test or some such thing
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    performImport();
                } catch (IOException e) {
                    exceptionService.displayAndReportError(context,
                            context.getString(R.string.backup_TXI_error), e);
                } catch (XmlPullParserException e) {
                    exceptionService.displayAndReportError(context,
                            context.getString(R.string.backup_TXI_error), e);
                }
            }
        }).start();
    }

    @SuppressWarnings("nls")
    private void performImport() throws IOException, XmlPullParserException {
        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        XmlPullParser xpp = factory.newPullParser();
        xpp.setInput(new FileReader(input));

        try {
            while (xpp.next() != XmlPullParser.END_DOCUMENT) {
                String tag = xpp.getName();
                if (xpp.getEventType() == XmlPullParser.END_TAG) {
                    // Ignore end tags
                    continue;
                }
                if (tag != null) {
                    // Process <astrid ... >
                    if (tag.equals(BackupConstants.ASTRID_TAG)) {
                        String format = xpp.getAttributeValue(null, BackupConstants.ASTRID_ATTR_FORMAT);
                        if(TextUtils.equals(format, FORMAT1))
                            new Format1TaskImporter(xpp);
                        else if(TextUtils.equals(format, FORMAT2))
                            new Format2TaskImporter(xpp);
                        else
                            throw new UnsupportedOperationException(
                                    "Did not know how to import tasks with xml format '" +
                                    format + "'");
                    }
                }
            }
        } finally {
            Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
            ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if(progressDialog.isShowing() && context instanceof Activity)
                       DialogUtilities.dismissDialog((Activity) context, progressDialog);
                    showSummary();
                }
            });
        }
    }

    private void showSummary() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(R.string.import_summary_title);
        Resources r = context.getResources();
        String message = context.getString(R.string.import_summary_message,
                input,
                r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount),
                r.getQuantityString(R.plurals.Ntasks, importCount, importCount),
                r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount),
                r.getQuantityString(R.plurals.Ntasks, errorCount, errorCount));
        builder.setMessage(message);
        builder.setPositiveButton(context.getString(android.R.string.ok),
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                        if (runAfterImport != null) {
                            handler.post(runAfterImport);
                        }
                    }
        });

        builder.show();
    }

    // --- importers

    // =============================================================== FORMAT2

    private static final String FORMAT2 = "2"; //$NON-NLS-1$
    private class Format2TaskImporter {

        private int version;
        private final XmlPullParser xpp;
        private final Task currentTask = new Task();
        private final Metadata metadata = new Metadata();

        public Format2TaskImporter(XmlPullParser xpp) throws XmlPullParserException, IOException {
            this.xpp = xpp;

            try {
                this.version = Integer.parseInt(xpp.getAttributeValue(null, BackupConstants.ASTRID_ATTR_VERSION));
            } catch (NumberFormatException e) {
                // can't read version, assume max version
                this.version = Integer.MAX_VALUE;
            }

            while (xpp.next() != XmlPullParser.END_DOCUMENT) {
                String tag = xpp.getName();
                if (tag == null || xpp.getEventType() == XmlPullParser.END_TAG)
                    continue;

                try {
                    if (tag.equals(BackupConstants.TASK_TAG)) {
                        // Parse <task ... >
                        parseTask();
                    } else if (tag.equals(BackupConstants.METADATA_TAG)) {
                        // Process <metadata ... >
                        parseMetadata();
                    }
                } catch (Exception e) {
                    errorCount++;
                    Log.e("astrid-importer", //$NON-NLS-1$
                            "Caught exception while reading from " + //$NON-NLS-1$
                            xpp.getText(), e);
                }
            }
        }

        @SuppressWarnings("nls")
        private void parseTask() {
            taskCount++;
            setProgressMessage(context.getString(R.string.import_progress_read,
                    taskCount));
            currentTask.clear();

            String title = xpp.getAttributeValue(null, Task.TITLE.name);
            String created = xpp.getAttributeValue(null, Task.CREATION_DATE.name);
            String deletionDate = xpp.getAttributeValue(null, Task.DELETION_DATE.name);
            String completionDate = xpp.getAttributeValue(null, Task.COMPLETION_DATE.name);

            // if we don't have task name or creation date, skip
            if (created == null || title == null) {
                skipCount++;
                return;
            }

            // if the task's name and creation date match an existing task, skip
            long existingTask = 0;
            TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID,
                        Task.COMPLETION_DATE, Task.DELETION_DATE).
                    where(Criterion.and(Task.TITLE.eq(title), Task.CREATION_DATE.eq(created))));
            try {
                if(cursor.getCount() > 0) {
                    cursor.moveToNext();

                    // fix for failed migration in 4.0.6
                    if(version < UpgradeService.V4_0_6) {
                        if(!completionDate.equals("0") &&
                                !completionDate.equals(Long.toString(cursor.get(Task.COMPLETION_DATE))))
                            existingTask = cursor.get(Task.ID);

                        if(!deletionDate.equals("0") &&
                                !deletionDate.equals(Long.toString(cursor.get(Task.DELETION_DATE))))
                            existingTask = cursor.get(Task.ID);
                    }

                    if(existingTask == 0) {
                        skipCount++;
                        return;
                    }
                }
            } finally {
                cursor.close();
            }

            // else, make a new task model and add away.
            deserializeModel(currentTask, Task.PROPERTIES);
            if(version < UpgradeService.V4_0_6)
                adjustDueDateScheme(currentTask);

            if(existingTask > 0)
                currentTask.setId(existingTask);
            else
                currentTask.setId(Task.NO_ID);

            // Save the task to the database.
            taskService.save(currentTask);
            importCount++;
        }

        private void adjustDueDateScheme(Task model) {
            long dueDate = model.getValue(Task.DUE_DATE);

            if (dueDate > 0) {
                Date date = new Date(dueDate);
                if (date.getHours() == 23 && date.getMinutes() == 59 && date.getSeconds() == 59) {
                    date.setHours(12);
                    date.setMinutes(0);
                    date.setSeconds(0);
                } else {
                    date.setSeconds(1);
                }
                model.setValue(Task.DUE_DATE, date.getTime());
            }
        }

        private void parseMetadata() {
            if(!currentTask.isSaved())
                return;
            metadata.clear();
            deserializeModel(metadata, Metadata.PROPERTIES);
            metadata.setId(Metadata.NO_ID);
            metadata.setValue(Metadata.TASK, currentTask.getId());
            metadataService.save(metadata);
        }

        /**
         * Turn a model into xml attributes
         * @param model
         */
        private void deserializeModel(AbstractModel model, Property<?>[] properties) {
            for(Property<?> property : properties) {
                try {
                    property.accept(xmlReadingVisitor, model);
                } catch (Exception e) {
                    Log.e("astrid-importer", //$NON-NLS-1$
                            "Caught exception while writing " + property.name + //$NON-NLS-1$
                            " from " + xpp.getText(), e); //$NON-NLS-1$
                }
            }
        }

        private final XmlReadingPropertyVisitor xmlReadingVisitor = new XmlReadingPropertyVisitor();

        private class XmlReadingPropertyVisitor implements PropertyVisitor<Void, AbstractModel> {

            @Override
            public Void visitInteger(Property<Integer> property,
                    AbstractModel data) {
                String value = xpp.getAttributeValue(null, property.name);
                if(value != null)
                    data.setValue(property, TasksXmlExporter.XML_NULL.equals(value) ?
                            null : Integer.parseInt(value));
                return null;
            }

            @Override
            public Void visitLong(Property<Long> property, AbstractModel data) {
                String value = xpp.getAttributeValue(null, property.name);
                if(value != null)
                    data.setValue(property, TasksXmlExporter.XML_NULL.equals(value) ?
                            null : Long.parseLong(value));
                return null;
            }

            @Override
            public Void visitDouble(Property<Double> property,
                    AbstractModel data) {
                String value = xpp.getAttributeValue(null, property.name);
                if(value != null)
                    data.setValue(property, TasksXmlExporter.XML_NULL.equals(value) ?
                            null : Double.parseDouble(value));
                return null;
            }

            @Override
            public Void visitString(Property<String> property,
                    AbstractModel data) {
                String value = xpp.getAttributeValue(null, property.name);
                if(value != null)
                    data.setValue(property, value);
                return null;
            }
        }
    }

    // =============================================================== FORMAT1

    private static final String FORMAT1 = null;
    private class Format1TaskImporter {

        private final XmlPullParser xpp;
        private Task currentTask = null;
        private String upgradeNotes = null;
        private boolean syncOnComplete = false;

        private final LinkedHashSet<String> tags = new LinkedHashSet<String>();

        public Format1TaskImporter(XmlPullParser xpp) throws XmlPullParserException, IOException {
            this.xpp = xpp;

            while (xpp.next() != XmlPullParser.END_DOCUMENT) {
                String tag = xpp.getName();

                try {
                    if(BackupConstants.TASK_TAG.equals(tag) && xpp.getEventType() == XmlPullParser.END_TAG)
                        saveTags();
                    else if (tag == null || xpp.getEventType() == XmlPullParser.END_TAG)
                        continue;
                    else if (tag.equals(BackupConstants.TASK_TAG)) {
                        // Parse <task ... >
                        currentTask = parseTask();
                    } else if (currentTask != null) {
                        // These tags all require that we have a task to associate
                        // them with.
                        if (tag.equals(BackupConstants.TAG_TAG)) {
                            // Process <tag ... >
                            parseTag();
                        } else if (tag.equals(BackupConstants.ALERT_TAG)) {
                            // Process <alert ... >
                            parseAlert();
                        } else if (tag.equals(BackupConstants.SYNC_TAG)) {
                            // Process <sync ... >
                            parseSync();
                        }
                    }
                } catch (Exception e) {
                    errorCount++;
                    Log.e("astrid-importer", //$NON-NLS-1$
                            "Caught exception while reading from " + //$NON-NLS-1$
                            xpp.getText(), e);
                }
            }
        }

        private boolean parseSync() {
            String service = xpp.getAttributeValue(null, BackupConstants.SYNC_ATTR_SERVICE);
            String remoteId = xpp.getAttributeValue(null, BackupConstants.SYNC_ATTR_REMOTE_ID);
            if (service != null && remoteId != null) {
                StringTokenizer strtok = new StringTokenizer(remoteId, "|"); //$NON-NLS-1$
                String taskId = strtok.nextToken();
                String taskSeriesId = strtok.nextToken();
                String listId = strtok.nextToken();

                Metadata metadata = new Metadata();
                metadata.setValue(Metadata.TASK, currentTask.getId());
                metadata.setValue(Metadata.VALUE1, (listId));
                metadata.setValue(Metadata.VALUE2, (taskSeriesId));
                metadata.setValue(Metadata.VALUE3, (taskId));
                metadata.setValue(Metadata.VALUE4, syncOnComplete ? "1" : "0"); //$NON-NLS-1$ //$NON-NLS-2$
                metadataService.save(metadata);
                return true;
            }
            return false;
        }

        private boolean parseAlert() {
            // drop it
            return false;
        }

        private boolean parseTag() {
            String tagName = xpp.getAttributeValue(null, BackupConstants.TAG_ATTR_NAME);
            tags.add(tagName);
            return true;
        }

        private void saveTags() {
            if(currentTask != null && tags.size() > 0) {
                TagService.getInstance().synchronizeTags(currentTask.getId(), tags);
            }
            tags.clear();
        }

        @SuppressWarnings("nls")
        private Task parseTask() {
            taskCount++;
            setProgressMessage(context.getString(R.string.import_progress_read,
                    taskCount));

            String taskName = xpp.getAttributeValue(null, LegacyTaskModel.NAME);
            Date creationDate = null;
            String createdString = xpp.getAttributeValue(null,
                    LegacyTaskModel.CREATION_DATE);
            if (createdString != null) {
                creationDate = BackupDateUtilities.getDateFromIso8601String(createdString);
            }

            // if we don't have task name or creation date, skip
            if (creationDate == null || taskName == null) {
                skipCount++;
                return null;
            }

            // if the task's name and creation date match an existing task, skip
            TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID).
                    where(Criterion.and(Task.TITLE.eq(taskName),
                            Task.CREATION_DATE.like(creationDate.getTime()/1000L + "%"))));
            try {
                if(cursor.getCount() > 0) {
                    skipCount++;
                    return null;
                }
            } finally {
                cursor.close();
            }

            // else, make a new task model and add away.
            Task task = new Task();
            int numAttributes = xpp.getAttributeCount();
            for (int i = 0; i < numAttributes; i++) {
                String fieldName = xpp.getAttributeName(i);
                String fieldValue = xpp.getAttributeValue(i);
                if(!setTaskField(task, fieldName, fieldValue)) {
                    Log.i("astrid-xml-import", "Task: " + taskName + ": Unknown field '" +
                            fieldName + "' with value '" + fieldValue + "' disregarded.");
                }
            }

            if(upgradeNotes != null) {
                if(task.containsValue(Task.NOTES) && task.getValue(Task.NOTES).length() > 0)
                    task.setValue(Task.NOTES, task.getValue(Task.NOTES) + "\n" + upgradeNotes);
                else
                    task.setValue(Task.NOTES, upgradeNotes);
                upgradeNotes = null;
            }

            // Save the task to the database.
            taskService.save(task);
            importCount++;
            return task;
        }

        /** helper method to set field on a task */
        @SuppressWarnings("nls")
        private final boolean setTaskField(Task task, String field, String value) {
            if(field.equals(LegacyTaskModel.ID)) {
                // ignore
            }
            else if(field.equals(LegacyTaskModel.NAME)) {
                task.setValue(Task.TITLE, value);
            }
            else if(field.equals(LegacyTaskModel.NOTES)) {
                task.setValue(Task.NOTES, value);
            }
            else if(field.equals(LegacyTaskModel.PROGRESS_PERCENTAGE)) {
                // ignore
            }
            else if(field.equals(LegacyTaskModel.IMPORTANCE)) {
                task.setValue(Task.IMPORTANCE, LegacyImportance.valueOf(value).ordinal());
            }
            else if(field.equals(LegacyTaskModel.ESTIMATED_SECONDS)) {
                task.setValue(Task.ESTIMATED_SECONDS, Integer.parseInt(value));
            }
            else if(field.equals(LegacyTaskModel.ELAPSED_SECONDS)) {
                task.setValue(Task.ELAPSED_SECONDS, Integer.parseInt(value));
            }
            else if(field.equals(LegacyTaskModel.TIMER_START)) {
                task.setValue(Task.TIMER_START,
                        BackupDateUtilities.getDateFromIso8601String(value).getTime());
            }
            else if(field.equals(LegacyTaskModel.DEFINITE_DUE_DATE)) {
                String preferred = xpp.getAttributeValue(null, LegacyTaskModel.PREFERRED_DUE_DATE);
                if(preferred != null) {
                    Date preferredDate = BackupDateUtilities.getDateFromIso8601String(value);
                    upgradeNotes = "Project Deadline: " +
                            DateUtilities.getDateString(ContextManager.getContext(),
                                    preferredDate);
                }
                task.setValue(Task.DUE_DATE,
                        BackupDateUtilities.getTaskDueDateFromIso8601String(value).getTime());
            }
            else if(field.equals(LegacyTaskModel.PREFERRED_DUE_DATE)) {
                String definite = xpp.getAttributeValue(null, LegacyTaskModel.DEFINITE_DUE_DATE);
                if(definite != null)
                    ; // handled above
                else
                    task.setValue(Task.DUE_DATE,
                            BackupDateUtilities.getTaskDueDateFromIso8601String(value).getTime());
            }
            else if(field.equals(LegacyTaskModel.HIDDEN_UNTIL)) {
                task.setValue(Task.HIDE_UNTIL,
                        BackupDateUtilities.getDateFromIso8601String(value).getTime());
            }
            else if(field.equals(LegacyTaskModel.BLOCKING_ON)) {
                // ignore
            }
            else if(field.equals(LegacyTaskModel.POSTPONE_COUNT)) {
                task.setValue(Task.POSTPONE_COUNT, Integer.parseInt(value));
            }
            else if(field.equals(LegacyTaskModel.NOTIFICATIONS)) {
                task.setValue(Task.REMINDER_PERIOD, Integer.parseInt(value) * 1000L);
            }
            else if(field.equals(LegacyTaskModel.CREATION_DATE)) {
                task.setValue(Task.CREATION_DATE,
                        BackupDateUtilities.getDateFromIso8601String(value).getTime());
            }
            else if(field.equals(LegacyTaskModel.COMPLETION_DATE)) {
                String completion = xpp.getAttributeValue(null, LegacyTaskModel.PROGRESS_PERCENTAGE);
                if("100".equals(completion)) {
                    task.setValue(Task.COMPLETION_DATE,
                        BackupDateUtilities.getDateFromIso8601String(value).getTime());
                }
            }
            else if(field.equals(LegacyTaskModel.NOTIFICATION_FLAGS)) {
                task.setValue(Task.REMINDER_FLAGS, Integer.parseInt(value));
            }
            else if(field.equals(LegacyTaskModel.LAST_NOTIFIED)) {
                task.setValue(Task.REMINDER_LAST,
                        BackupDateUtilities.getDateFromIso8601String(value).getTime());
            }
            else if(field.equals("repeat_interval")) {
                // handled below
            }
            else if(field.equals("repeat_value")) {
                int repeatValue = Integer.parseInt(value);
                String repeatInterval = xpp.getAttributeValue(null, "repeat_interval");
                if(repeatValue > 0 && repeatInterval != null) {
                    LegacyRepeatInterval interval = LegacyRepeatInterval.valueOf(repeatInterval);
                    LegacyRepeatInfo repeatInfo = new LegacyRepeatInfo(interval, repeatValue);
                    RRule rrule = repeatInfo.toRRule();
                    task.setValue(Task.RECURRENCE, rrule.toIcal());
                }
            }
            else if(field.equals(LegacyTaskModel.FLAGS)) {
                if(Integer.parseInt(value) == LegacyTaskModel.FLAG_SYNC_ON_COMPLETE)
                    syncOnComplete = true;
            }
            else {
                return false;
            }

            return true;
        }
    }

}
