package com.fsck.k9.mailstore.migrations;


import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.VisibleForTesting;

import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend;
import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand;
import com.fsck.k9.controller.MessagingControllerCommands.PendingEmptyTrash;
import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
import com.fsck.k9.controller.PendingCommandSerializer;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag;

import static java.util.Collections.singletonList;


class MigrationTo60 {
    private static final String PENDING_COMMAND_MOVE_OR_COPY = "com.fsck.k9.MessagingController.moveOrCopy";
    private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK = "com.fsck.k9.MessagingController.moveOrCopyBulk";
    private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW = "com.fsck.k9.MessagingController.moveOrCopyBulkNew";
    private static final String PENDING_COMMAND_EMPTY_TRASH = "com.fsck.k9.MessagingController.emptyTrash";
    private static final String PENDING_COMMAND_SET_FLAG_BULK = "com.fsck.k9.MessagingController.setFlagBulk";
    private static final String PENDING_COMMAND_SET_FLAG = "com.fsck.k9.MessagingController.setFlag";
    private static final String PENDING_COMMAND_APPEND = "com.fsck.k9.MessagingController.append";
    private static final String PENDING_COMMAND_MARK_ALL_AS_READ = "com.fsck.k9.MessagingController.markAllAsRead";
    private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge";


    public static void migratePendingCommands(SQLiteDatabase db) {
        List<PendingCommand> pendingCommands = new ArrayList<>();

        if (columnExists(db, "pending_commands", "arguments")) {
            for (OldPendingCommand oldPendingCommand : getPendingCommands(db)) {
                PendingCommand newPendingCommand = migratePendingCommand(oldPendingCommand);
                pendingCommands.add(newPendingCommand);
            }

            db.execSQL("DROP TABLE IF EXISTS pending_commands");
            db.execSQL("CREATE TABLE pending_commands (" +
                    "id INTEGER PRIMARY KEY, " +
                    "command TEXT, " +
                    "data TEXT" +
                    ")");

            PendingCommandSerializer pendingCommandSerializer = PendingCommandSerializer.getInstance();
            for (PendingCommand pendingCommand : pendingCommands) {
                ContentValues cv = new ContentValues();
                cv.put("command", pendingCommand.getCommandName());
                cv.put("data", pendingCommandSerializer.serialize(pendingCommand));
                db.insert("pending_commands", "command", cv);
            }
        }
    }

    private static boolean columnExists(SQLiteDatabase db, String table, String columnName) {
        Cursor columnCursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);
        boolean foundColumn = false;
        while (columnCursor.moveToNext()) {
            String currentColumnName = columnCursor.getString(1);
            if (currentColumnName.equals(columnName)) {
                foundColumn = true;
                break;
            }
        }
        columnCursor.close();
        return foundColumn;
    }

    @VisibleForTesting
    static PendingCommand migratePendingCommand(OldPendingCommand oldPendingCommand) {
        switch (oldPendingCommand.command) {
            case PENDING_COMMAND_APPEND: {
                return migrateCommandAppend(oldPendingCommand);
            }
            case PENDING_COMMAND_SET_FLAG_BULK: {
                return migrateCommandSetFlagBulk(oldPendingCommand);
            }
            case PENDING_COMMAND_SET_FLAG: {
                return migrateCommandSetFlag(oldPendingCommand);
            }
            case PENDING_COMMAND_MARK_ALL_AS_READ: {
                return migrateCommandMarkAllAsRead(oldPendingCommand);
            }
            case PENDING_COMMAND_MOVE_OR_COPY_BULK: {
                return migrateCommandMoveOrCopyBulk(oldPendingCommand);
            }
            case PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW: {
                return migrateCommandMoveOrCopyBulkNew(oldPendingCommand);
            }
            case PENDING_COMMAND_MOVE_OR_COPY: {
                return migrateCommandMoveOrCopy(oldPendingCommand);
            }
            case PENDING_COMMAND_EMPTY_TRASH: {
                return migrateCommandEmptyTrash();
            }
            case PENDING_COMMAND_EXPUNGE: {
                return migrateCommandExpunge(oldPendingCommand);
            }
            default: {
                throw new IllegalArgumentException("Tried to migrate unknown pending command!");
            }
        }
    }

    private static PendingCommand migrateCommandExpunge(OldPendingCommand command) {
        String folder = command.arguments[0];
        return PendingExpunge.create(folder);
    }

    private static PendingCommand migrateCommandEmptyTrash() {
        return PendingEmptyTrash.create();
    }

    private static PendingCommand migrateCommandMoveOrCopy(OldPendingCommand command) {
        String srcFolder = command.arguments[0];
        String uid = command.arguments[1];
        String destFolder = command.arguments[2];
        boolean isCopy = Boolean.parseBoolean(command.arguments[3]);

        return PendingMoveOrCopy.create(srcFolder, destFolder, isCopy, singletonList(uid));
    }

    private static PendingCommand migrateCommandMoveOrCopyBulkNew(OldPendingCommand command) {
        String srcFolder = command.arguments[0];
        String destFolder = command.arguments[1];
        boolean isCopy = Boolean.parseBoolean(command.arguments[2]);
        boolean hasNewUids = Boolean.parseBoolean(command.arguments[3]);

        if (hasNewUids) {
            Map<String, String> uidMap = new HashMap<>();
            int offset = (command.arguments.length - 4) / 2;
            for (int i = 4; i < 4 + offset; i++) {
                uidMap.put(command.arguments[i], command.arguments[i + offset]);
            }

            return PendingMoveOrCopy.create(srcFolder, destFolder, isCopy, uidMap);
        } else {
            List<String> uids = new ArrayList<>(command.arguments.length - 4);
            uids.addAll(Arrays.asList(command.arguments).subList(4, command.arguments.length));

            return PendingMoveOrCopy.create(srcFolder, destFolder, isCopy, uids);
        }
    }

    private static PendingCommand migrateCommandMoveOrCopyBulk(OldPendingCommand command) {
        int len = command.arguments.length;

        OldPendingCommand newCommand = new OldPendingCommand();
        newCommand.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW;
        newCommand.arguments = new String[len + 1];
        newCommand.arguments[0] = command.arguments[0];
        newCommand.arguments[1] = command.arguments[1];
        newCommand.arguments[2] = command.arguments[2];
        newCommand.arguments[3] = Boolean.toString(false);
        System.arraycopy(command.arguments, 3, newCommand.arguments, 4, len - 3);

        return migratePendingCommand(newCommand);
    }

    private static PendingCommand migrateCommandMarkAllAsRead(OldPendingCommand command) {
        return PendingMarkAllAsRead.create(command.arguments[0]);
    }

    private static PendingCommand migrateCommandSetFlag(OldPendingCommand command) {
        String folder = command.arguments[0];
        String uid = command.arguments[1];
        boolean newState = Boolean.parseBoolean(command.arguments[2]);
        Flag flag = Flag.valueOf(command.arguments[3]);

        return PendingSetFlag.create(folder, newState, flag, singletonList(uid));
    }

    private static PendingCommand migrateCommandSetFlagBulk(OldPendingCommand command) {
        String folder = command.arguments[0];
        boolean newState = Boolean.parseBoolean(command.arguments[1]);
        Flag flag = Flag.valueOf(command.arguments[2]);

        List<String> uids = new ArrayList<>(command.arguments.length - 3);
        uids.addAll(Arrays.asList(command.arguments).subList(3, command.arguments.length));

        return PendingSetFlag.create(folder, newState, flag, uids);
    }

    private static PendingCommand migrateCommandAppend(OldPendingCommand command) {
        String folder = command.arguments[0];
        String uid = command.arguments[1];
        return PendingAppend.create(folder, uid);
    }

    private static List<OldPendingCommand> getPendingCommands(SQLiteDatabase db) {
        Cursor cursor = null;
        try {
            cursor = db.query("pending_commands",
                    new String[] { "id", "command", "arguments" }, null, null, null, null, "id ASC");
            List<OldPendingCommand> commands = new ArrayList<>();
            while (cursor.moveToNext()) {
                OldPendingCommand command = new OldPendingCommand();
                command.command = cursor.getString(1);
                String arguments = cursor.getString(2);
                command.arguments = arguments.split(",");
                for (int i = 0; i < command.arguments.length; i++) {
                    command.arguments[i] = fastUrlDecode(command.arguments[i]);
                }
                commands.add(command);
            }
            return commands;
        } finally {
            Utility.closeQuietly(cursor);
        }
    }

    private static String fastUrlDecode(String s) {
        byte[] bytes = s.getBytes(Charset.forName("UTF-8"));
        byte ch;
        int length = 0;
        for (int i = 0, count = bytes.length; i < count; i++) {
            ch = bytes[i];
            if (ch == '%') {
                int h = (bytes[i + 1] - '0');
                int l = (bytes[i + 2] - '0');
                if (h > 9) {
                    h -= 7;
                }
                if (l > 9) {
                    l -= 7;
                }
                bytes[length] = (byte) ((h << 4) | l);
                i += 2;
            } else if (ch == '+') {
                bytes[length] = ' ';
            } else {
                bytes[length] = bytes[i];
            }
            length++;
        }
        return new String(bytes, 0, length, Charset.forName("UTF-8"));
    }


    @VisibleForTesting
    static class OldPendingCommand {
        public String command;
        public String[] arguments;
    }
}
