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

import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.StringTokenizer;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;

import com.google.ical.values.RRule;
import com.timsu.astrid.R;
import com.timsu.astrid.utilities.LegacyTasksXmlExporter;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.DatabaseDao;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.PropertyVisitor;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.alarms.AlarmFields;
import com.todoroo.astrid.backup.TasksXmlImporter;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.legacy.AlarmDatabase;
import com.todoroo.astrid.legacy.LegacyAlertModel;
import com.todoroo.astrid.legacy.LegacyRepeatInfo;
import com.todoroo.astrid.legacy.LegacyTaskModel;
import com.todoroo.astrid.legacy.TransitionalAlarm;
import com.todoroo.astrid.tags.TagService;

public class Astrid2To3UpgradeHelper {

    @Autowired
    private TaskDao taskDao;

    @Autowired
    private MetadataDao metadataDao;

    @Autowired
    private MetadataService metadataService;

    @Autowired
    private Database database;

    @Autowired
    private String tasksTable;

    @Autowired
    private String tagsTable;

    @Autowired
    private String tagTaskTable;

    @Autowired
    private String alertsTable;

    @Autowired
    private String syncTable;

    @Autowired
    private ExceptionService exceptionService;

    // --- implementation

    public Astrid2To3UpgradeHelper() {
        DependencyInjectionService.getInstance().inject(this);
    }


    /**
     * Upgrade helper class that reads a database
     */
    private static class Astrid2UpgradeHelper extends SQLiteOpenHelper {

        public Astrid2UpgradeHelper(Context context, String name,
                CursorFactory factory, int version) {
            super(context, name, factory, version);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            // do nothing
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            // do nothing
        }

    }

    // ---------------------------------------------------------------- 3 => 3.1

    /**
     * Perform the upgrade from Astrid 3 to 3.1
     * @param context
     * @param upgradeService
     * @param from
     */
    public void upgrade3To3_1(final Context context, final int from) {
        if(!checkIfDatabaseExists(context, alertsTable))
            return;

        database.openForWriting();
        migrateAlarmsToMetadata();
    }

    // ----------------------------------------------------------------- 2 => 3

    /**
     * Perform the upgrade from Astrid 2 to Astrid 3
     * @param context2
     */
    @SuppressWarnings("deprecation")
    public void upgrade2To3(final Context context, final int from) {

        // if from < 1 (we don't know what version, and database exists, leave it alone)
        if(from < 1 && checkIfDatabaseExists(context, database.getName()))
            return;

        // if you don't have a legacy task table, skip this step
        if(!checkIfDatabaseExists(context, tasksTable))
            return;

        // else, if there's already a database table, clear it out (!!!)
        if(checkIfDatabaseExists(context, database.getName()))
            context.deleteDatabase(database.getName());
        database.openForWriting();

        // initiate a backup
        String backupFile = legacyBackup();

        try {

            // --- upgrade tasks table
            HashMap<String, Property<?>> propertyMap =
                new HashMap<String, Property<?>>();
            propertyMap.put("_id", Task.ID); //$NON-NLS-1$
            propertyMap.put(LegacyTaskModel.NAME, Task.TITLE);
            propertyMap.put(LegacyTaskModel.NOTES, Task.NOTES);
            // (don't update progress percentage, we don't use this anymore)
            propertyMap.put(LegacyTaskModel.IMPORTANCE, Task.IMPORTANCE);
            propertyMap.put(LegacyTaskModel.ESTIMATED_SECONDS, Task.ESTIMATED_SECONDS);
            propertyMap.put(LegacyTaskModel.ELAPSED_SECONDS, Task.ELAPSED_SECONDS);
            propertyMap.put(LegacyTaskModel.TIMER_START, Task.TIMER_START);
            propertyMap.put(LegacyTaskModel.DEFINITE_DUE_DATE, Task.DUE_DATE);
            propertyMap.put(LegacyTaskModel.HIDDEN_UNTIL, Task.HIDE_UNTIL);
            propertyMap.put(LegacyTaskModel.POSTPONE_COUNT, Task.POSTPONE_COUNT);
            propertyMap.put(LegacyTaskModel.NOTIFICATIONS, Task.REMINDER_PERIOD);
            propertyMap.put(LegacyTaskModel.NOTIFICATION_FLAGS, Task.REMINDER_FLAGS);
            propertyMap.put(LegacyTaskModel.LAST_NOTIFIED, Task.REMINDER_LAST);
            propertyMap.put(LegacyTaskModel.REPEAT, Task.RECURRENCE);
            propertyMap.put(LegacyTaskModel.CREATION_DATE, Task.CREATION_DATE);
            propertyMap.put(LegacyTaskModel.COMPLETION_DATE, Task.COMPLETION_DATE);
            propertyMap.put(LegacyTaskModel.CALENDAR_URI, Task.CALENDAR_URI);
            propertyMap.put(LegacyTaskModel.FLAGS, Task.FLAGS);
            upgradeTable(context, tasksTable,
                    propertyMap, new Task(), taskDao);

            // --- upgrade tags tables
            migrateTagsToMetadata();

            // --- upgrade alerts
            AlarmDatabase alarmsDatabase = new AlarmDatabase();
            alarmsDatabase.openForWriting();
            propertyMap.clear();
            propertyMap.put("_id", TransitionalAlarm.ID); //$NON-NLS-1$
            propertyMap.put(LegacyAlertModel.TASK, TransitionalAlarm.TASK);
            propertyMap.put(LegacyAlertModel.DATE, TransitionalAlarm.TIME);
            upgradeTable(context, alertsTable, propertyMap, new TransitionalAlarm(),
                    alarmsDatabase.getDao());
            alarmsDatabase.close();

            // --- upgrade RTM sync mappings
            migrateSyncMappingToMetadata();

            // --- clean up database
            metadataService.cleanup();

            // --- upgrade properties
            SharedPreferences prefs = Preferences.getPrefs(context);
            Editor editor = prefs.edit();
            int random = Preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, -1);
            if(random != -1) {
                // convert days => hours
                editor.putString(context.getString(R.string.p_rmd_default_random_hours),
                        Integer.toString(random * 24));
            }
        } catch (Exception e) {
            exceptionService.reportError("backup-error", e); //$NON-NLS-1$
            if(backupFile != null) {
                // try to restore the latest XML
                TasksXmlImporter.importTasks(context, backupFile, null);
            }
        }
    }

    // --- database upgrade helpers

    /**
     * Create a legacy backup file
     */
    private String legacyBackup() {
        try {
            LegacyTasksXmlExporter exporter = new LegacyTasksXmlExporter(true);
            exporter.setContext(ContextManager.getContext());
            return exporter.exportTasks(LegacyTasksXmlExporter.getExportDirectory());
        } catch (Exception e) {
            // unable to create a backup before upgrading :(
            return null;
        }
    }

    protected static final class UpgradeVisitorContainer<TYPE extends AbstractModel> {
        public int columnIndex;
        public Cursor cursor;
        public TYPE model;
        public StringBuilder upgradeNotes;
    }

    /**
     * Visitor that reads from a visitor container and writes to the model
     * @author Tim Su <tim@todoroo.com>
     *
     */
    @SuppressWarnings("nls")
    protected static final class ColumnUpgradeVisitor implements PropertyVisitor<Void, UpgradeVisitorContainer<?>> {
        @Override
        public Void visitDouble(Property<Double> property, UpgradeVisitorContainer<?> data) {
            double value = data.cursor.getDouble(data.columnIndex);
            data.model.setValue(property, value);
            Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
            return null;
        }

        @Override
        public Void visitInteger(Property<Integer> property, UpgradeVisitorContainer<?> data) {
            int value = data.cursor.getInt(data.columnIndex);
            data.model.setValue(property, value);
            Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
            return null;
        }

        @Override
        public Void visitLong(Property<Long> property, UpgradeVisitorContainer<?> data) {
            long value = data.cursor.getLong(data.columnIndex);

            // special handling for due date
            if(property == Task.DUE_DATE) {
                long preferredDueDate = data.cursor.getLong(data.cursor.getColumnIndex(LegacyTaskModel.PREFERRED_DUE_DATE));
                if(value == 0)
                    value = preferredDueDate;
                else if(preferredDueDate != 0) {
                    // had both absolute and preferred due dates. write
                    // preferred due date into notes field
                    if(data.upgradeNotes == null)
                        data.upgradeNotes = new StringBuilder();
                    data.upgradeNotes.append("Goal Deadline: " +
                            DateUtilities.getDateString(ContextManager.getContext(),
                                    new Date(preferredDueDate)));
                }
            } else if(property == Task.REMINDER_PERIOD) {
                // old period was stored in seconds
                value *= 1000L;
            } else if(property == Task.COMPLETION_DATE) {
                // check if the task was actually completed
                int progress = data.cursor.getInt(data.cursor.getColumnIndex(LegacyTaskModel.PROGRESS_PERCENTAGE));
                if(progress < 100)
                    value = 0;
            }

            data.model.setValue(property, value);
            Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
            return null;
        }

        @Override
        public Void visitString(Property<String> property, UpgradeVisitorContainer<?> data) {
            String value = data.cursor.getString(data.columnIndex);

            if(property == Task.RECURRENCE) {
                LegacyRepeatInfo repeatInfo = LegacyRepeatInfo.fromSingleField(data.cursor.getInt(data.columnIndex));
                if(repeatInfo == null)
                    data.model.setValue(property, "");
                else {
                    RRule rrule = repeatInfo.toRRule();
                    data.model.setValue(property, rrule.toIcal());
                }
            } else {
                data.model.setValue(property, value);
            }

            Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
            return null;
        }
    }

    /**
     * Helper that reads entries from legacy database and row-by-row
     * creates new models and saves them.
     *
     * @param context
     * @param legacyTable
     * @param propertyMap
     * @param model
     * @param dao
     */
    @SuppressWarnings("nls")
    private static final <TYPE extends AbstractModel> void upgradeTable(Context context, String legacyTable,
            HashMap<String, Property<?>> propertyMap, TYPE model,
            DatabaseDao<TYPE> dao) {

        if(!checkIfDatabaseExists(context, legacyTable))
            return;

        SQLiteDatabase upgradeDb = new Astrid2UpgradeHelper(context, legacyTable,
                null, 1).getReadableDatabase();

        Cursor cursor = upgradeDb.rawQuery("SELECT * FROM " + legacyTable, null);
        UpgradeVisitorContainer<TYPE> container = new UpgradeVisitorContainer<TYPE>();
        container.cursor = cursor;
        container.model = model;
        ColumnUpgradeVisitor visitor = new ColumnUpgradeVisitor();
        for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
            model.clear();
            for(Entry<String, Property<?>> entry : propertyMap.entrySet()) {
                container.columnIndex = cursor.getColumnIndex(entry.getKey());
                entry.getValue().accept(visitor, container);
            }

            // special tweak for adding upgrade notes to tasks
            if(container.upgradeNotes != null) {
                if(container.model.getValue(Task.NOTES).length() == 0)
                    container.model.setValue(Task.NOTES, container.upgradeNotes.toString());
                else {
                    container.model.setValue(Task.NOTES,
                            container.model.getValue(Task.NOTES) + "\n\n" +
                            container.upgradeNotes);
                }
                container.upgradeNotes = null;
            }
            dao.createNew(container.model);
        }
        cursor.close();

        upgradeDb.close();
    }

    private static boolean checkIfDatabaseExists(Context context, String legacyTable) {
        return context.getDatabasePath(legacyTable).exists();
    }

    /**
     * Move data from tags tables into metadata table. We do this by looping
     * through both the tags and tagTaskMap databases, reading data from
     * both and adding to the Metadata table. This way, we are able to
     * do everything in one pass without loading too much into memory
     */
    @SuppressWarnings("nls")
    private void migrateTagsToMetadata() {
        Context context = ContextManager.getContext();

        if(!checkIfDatabaseExists(context, tagsTable) ||
                !checkIfDatabaseExists(context, tagTaskTable))
            return;

        SQLiteDatabase tagsDb = new Astrid2UpgradeHelper(context, tagsTable,
                null, 1).getReadableDatabase();
        SQLiteDatabase tagTaskDb = new Astrid2UpgradeHelper(context, tagTaskTable,
                null, 1).getReadableDatabase();

        Cursor tagCursor = tagsDb.rawQuery("SELECT _id, name FROM " + tagsTable +
                " ORDER BY _id ASC", null);
        Cursor mapCursor = null;
        try {
            mapCursor = tagTaskDb.rawQuery("SELECT tag, task FROM " + tagTaskTable +
                " ORDER BY tag ASC", null);
            if(tagCursor.getCount() == 0)
                return;

            Metadata metadata = new Metadata();
            metadata.setValue(Metadata.KEY, TagService.KEY);
            long tagId = -1;
            String tag = null;
            for(mapCursor.moveToFirst(); !mapCursor.isAfterLast(); mapCursor.moveToNext()) {
                long mapTagId = mapCursor.getLong(0);

                while(mapTagId > tagId && !tagCursor.isLast()) {
                    tagCursor.moveToNext();
                    tagId = tagCursor.getLong(0);
                    tag = null;
                }

                if(mapTagId == tagId) {
                    if(tag == null)
                        tag = tagCursor.getString(1);
                    long task = mapCursor.getLong(1);
                    metadata.setValue(Metadata.TASK, task);
                    metadata.setValue(Metadata.KEY, TagService.KEY);
                    metadata.setValue(TagService.TAG, tag);
                    metadataDao.createNew(metadata);
                    metadata.clearValue(Metadata.ID);
                }
            }
        } finally {
            tagCursor.close();
            if(mapCursor != null)
                mapCursor.close();
            tagsDb.close();
            tagTaskDb.close();
        }
    }

    /**
     * Move data from sync table into metadata table.
     */
    @SuppressWarnings("nls")
    private void migrateSyncMappingToMetadata() {
        Context context = ContextManager.getContext();

        if(!checkIfDatabaseExists(context, syncTable))
            return;

        SQLiteDatabase syncDb = new Astrid2UpgradeHelper(context, syncTable,
                null, 1).getReadableDatabase();

        Cursor cursor = syncDb.rawQuery("SELECT task, remoteId FROM " + syncTable, null);
        try {
            if(cursor.getCount() == 0)
                return;

            Metadata metadata = new Metadata();
            metadata.setValue(Metadata.KEY, "rmilk");
            for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                long task = cursor.getLong(0);
                String id = cursor.getString(1);
                if(TextUtils.isEmpty(id))
                    continue;

                StringTokenizer strtok = new StringTokenizer(id, "|");
                String taskId = strtok.nextToken();
                String taskSeriesId = strtok.nextToken();
                String listId = strtok.nextToken();

                metadata.setValue(Metadata.TASK, task);
                metadata.setValue(Metadata.VALUE1, (listId));
                metadata.setValue(Metadata.VALUE2, (taskSeriesId));
                metadata.setValue(Metadata.VALUE3, (taskId));
                metadata.setValue(Metadata.VALUE4,  "0"); // not accurate, but not important
                metadataDao.createNew(metadata);
                metadata.clearValue(Metadata.ID);
            }
        } finally {
            cursor.close();
            syncDb.close();
        }
    }

    /**
     * Move data from alert table into metadata table.
     */
    private void migrateAlarmsToMetadata() {
        Context context = ContextManager.getContext();

        if(!checkIfDatabaseExists(context, AlarmDatabase.NAME))
            return;

        AlarmDatabase alarmsDatabase = new AlarmDatabase();
        DatabaseDao<TransitionalAlarm> dao = new DatabaseDao<TransitionalAlarm>(
                TransitionalAlarm.class, alarmsDatabase);

        TodorooCursor<TransitionalAlarm> cursor = dao.query(Query.select(TransitionalAlarm.PROPERTIES));
        try {
            if(cursor.getCount() == 0)
                return;

            Metadata metadata = new Metadata();
            metadata.setValue(Metadata.KEY, AlarmFields.METADATA_KEY);
            for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                long task = cursor.get(TransitionalAlarm.TASK);
                long time = cursor.get(TransitionalAlarm.TIME);

                metadata.setValue(Metadata.TASK, task);
                metadata.setValue(AlarmFields.TIME, time);
                metadata.setValue(AlarmFields.TYPE, AlarmFields.TYPE_SINGLE);
                metadataDao.createNew(metadata);
                metadata.clearValue(Metadata.ID);
            }
        } finally {
            cursor.close();
            alarmsDatabase.close();
        }
    }


}
