define([
    '/customize/application_config.js',
    '/common/common-feedback.js',
    '/common/common-hash.js',
    '/common/common-util.js',
    '/common/common-messaging.js',
    '/common/cryptget.js',
    '/common/outer/mailbox.js',
    '/customize/messages.js',
    '/common/common-realtime.js',
    '/bower_components/nthen/index.js',
    '/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Feedback, Hash, Util, Messaging, Crypt, Mailbox, Messages, Realtime, nThen, Crypto) {
    // Start migration check
    // Versions:
    // 1: migrate pad attributes
    // 2: migrate indent settings (codemirror)

    return function (userObject, cb, progress, store) {
        var version = userObject.version || 0;

        nThen(function () {
            // DEPRECATED
            // Migration 1: pad attributes moved to filesData
            var migratePadAttributesToData = function () {
                return true;
            };
            if (version < 1) {
                migratePadAttributesToData();
            }
        }).nThen(function () {
            // Migration 2: global attributes from root to 'settings' subobjects
            var migrateAttributes = function () {
                var drawer = 'cryptpad.userlist-drawer';
                var polls = 'cryptpad.hide_poll_text';
                var indentKey = 'cryptpad.indentUnit';
                var useTabsKey = 'cryptpad.indentWithTabs';
                var settings = userObject.settings = userObject.settings || {};
                if (typeof(userObject[indentKey]) !== "undefined") {
                    settings.codemirror = settings.codemirror || {};
                    settings.codemirror.indentUnit = userObject[indentKey];
                    delete userObject[indentKey];
                }
                if (typeof(userObject[useTabsKey]) !== "undefined") {
                    settings.codemirror = settings.codemirror || {};
                    settings.codemirror.indentWithTabs = userObject[useTabsKey];
                    delete userObject[useTabsKey];
                }
                if (typeof(userObject[drawer]) !== "undefined") {
                    settings.toolbar = settings.toolbar || {};
                    settings.toolbar['userlist-drawer'] = userObject[drawer];
                    delete userObject[drawer];
                }
                if (typeof(userObject[polls]) !== "undefined") {
                    settings.poll = settings.poll || {};
                    settings.poll['hide-text'] = userObject[polls];
                    delete userObject[polls];
                }
            };
            if (version < 2) {
                migrateAttributes();
                Feedback.send('Migrate-2', true);
                userObject.version = version = 2;
            }
        }).nThen(function () {
            // Migration 3: language from localStorage to settings
            var migrateLanguage = function () {
                if (!localStorage.CRYPTPAD_LANG) { return; }
                var l = localStorage.CRYPTPAD_LANG;
                userObject.settings.language = l;
            };
            if (version < 3) {
                migrateLanguage();
                Feedback.send('Migrate-3', true);
                userObject.version = version = 3;
            }
        }).nThen(function () {
            // Migration 4: allowUserFeedback to settings
            var migrateFeedback = function () {
                var settings = userObject.settings = userObject.settings || {};
                if (typeof(userObject['allowUserFeedback']) !== "undefined") {
                    settings.general = settings.general || {};
                    settings.general.allowUserFeedback = userObject['allowUserFeedback'];
                    delete userObject['allowUserFeedback'];
                }
            };
            if (version < 4) {
                migrateFeedback();
                Feedback.send('Migrate-4', true);
                userObject.version = version = 4;
            }
        }).nThen(function () {
            // Migration 5: dates to Number
            var migrateDates = function () {
                var data = userObject.drive && userObject.drive.filesData;
                if (data) {
                    for (var id in data) {
                        if (typeof data[id].ctime !== "number") {
                            data[id].ctime = +new Date(data[id].ctime);
                        }
                        if (typeof data[id].atime !== "number") {
                            data[id].atime = +new Date(data[id].atime);
                        }
                    }
                }
            };
            if (version < 5) {
                migrateDates();
                Feedback.send('Migrate-5', true);
                userObject.version = version = 5;
            }
        }).nThen(function (waitFor) {
            var addChannelId = function () {
                var data = userObject.drive.filesData;
                var el, parsed;
                var n = nThen(function () {});
                var padsLength = Object.keys(data).length;
                Object.keys(data).forEach(function (k, i) {
                    n = n.nThen(function (w) {
                        setTimeout(w(function () {
                            el = data[k];
                            parsed = Hash.parsePadUrl(el.href);
                            if (!el.href) { return; }
                            if (!el.channel) {
                                var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
                                el.channel = secret.channel;
                                progress(6, Math.round(100*i/padsLength));
                                console.log('Adding missing channel in filesData ', el.channel);
                            }
                        }));
                    });
                });
                n.nThen(waitFor(function () {
                    Feedback.send('Migrate-6', true);
                    userObject.version = version = 6;
                }));
            };
            if (version < 6) {
                addChannelId();
            }
        }).nThen(function (waitFor) {
            var addRoHref = function () {
                var data = userObject.drive.filesData;
                var el, parsed;
                var n = nThen(function () {});
                var padsLength = Object.keys(data).length;
                Object.keys(data).forEach(function (k, i) {
                    n = n.nThen(function (w) {
                        setTimeout(w(function () {
                            el = data[k];
                            if (!el.href) {
                                // Already migrated
                                return void progress(7, Math.round(100*i/padsLength));
                            }
                            if (el.href.indexOf('#') === -1) {
                                // Encrypted href: already migrated
                                return void progress(7, Math.round(100*i/padsLength));
                            }
                            parsed = Hash.parsePadUrl(el.href);
                            if (parsed.hashData.type !== "pad") {
                                // No read-only mode for files
                                return void progress(7, Math.round(100*i/padsLength));
                            }
                            if (parsed.hashData.mode === "view") {
                                // This is a read-only pad in our drive
                                el.roHref = el.href;
                                delete el.href;
                                console.log('Move href to roHref in filesData ', el.roHref);
                            } else {
                                var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
                                var hash = Hash.getViewHashFromKeys(secret);
                                if (hash) {
                                    // Version 0 won't have a view hash available
                                    el.roHref = '/' + parsed.type + '/#' + hash;
                                    console.log('Adding missing roHref in filesData ', el.href);
                                }
                            }
                            progress(6, Math.round(100*i/padsLength));
                        }));
                    });
                });
                n.nThen(waitFor(function () {
                    Feedback.send('Migrate-7', true);
                    userObject.version = version = 7;
                }));
            };
            if (version < 7) {
                addRoHref();
            }
        }).nThen(function () {
            // Migration 8: remove duplicate entries in proxy.FS_hashes (list of migrated anon drives)
            var fixDuplicate = function () {
                userObject.FS_hashes = Util.deduplicateString(userObject.FS_hashes || []);
            };
            if (version < 8) {
                fixDuplicate();
                Feedback.send('Migrate-8', true);
                userObject.version = version = 8;
            }
        }).nThen(function () {
            // Migration 9: send our mailbox channel to existing friends
            var migrateFriends = function () {
                var network = store.network;
                var channels = {};
                var ctx = {
                    store: store
                };
                var myData = Messaging.createData(userObject);

                var close = function (chan) {
                    var channel = channels[chan];
                    if (!channel) { return; }
                    try {
                        channel.wc.leave();
                    } catch (e) {}
                    delete channels[chan];
                };

                var onDirectMessage = function (msg, sender) {
                    if (sender !== network.historyKeeper) { return; }
                    var parsed = JSON.parse(msg);

                    // Metadata msg? we don't care
                    if ((parsed.validateKey || parsed.owners) && parsed.channel) { return; }

                    // End of history message, "onReady"
                    if (parsed.channel && channels[parsed.channel]) {
                        // History cleared while we were offline
                        // ==> we asked for an invalid last known hash
                        if (parsed.error && parsed.error === "EINVAL") {
                            var histMsg = ['GET_HISTORY', parsed.channel, {}];
                            network.sendto(network.historyKeeper, JSON.stringify(histMsg))
                              .then(function () {}, function () {});
                            return;
                        }
                        // End of history
                        if (parsed.state && parsed.state === 1) {
                            // Channel is ready and we didn't receive their mailbox channel: send our channel
                            myData.channel = parsed.channel;
                            var updateMsg = ['UPDATE', myData.curvePublic, +new Date(), myData];
                            var cryptMsg = channels[parsed.channel].encrypt(JSON.stringify(updateMsg));
                            channels[parsed.channel].wc.bcast(cryptMsg).then(function () {}, function (err) {
                                console.error("Can't migrate this friend", channels[parsed.channel].friend, err);
                            });
                            close(parsed.channel);
                            return;
                        }
                    } else if (parsed.channel) {
                        return;
                    }

                    // History message: we only care about "UPDATE" messages
                    var chan = parsed[3];
                    if (!chan || !channels[chan]) { return; }
                    var channel = channels[chan];
                    var msgIn = channel.decrypt(parsed[4]);
                    var parsedMsg = JSON.parse(msgIn);
                    if (parsedMsg[0] === 'UPDATE') {
                        if (parsedMsg[1] === myData.curvePublic) { return; }
                        var data = parsedMsg[3];
                        // If it doesn't contain the mailbox channel, ignore the message
                        if (!data.notifications) { return; }
                        // Otherwise we know their channel, we can send them our own
                        channel.friend.notifications = data.notifications;
                        myData.channel = chan;
                        Mailbox.sendTo(ctx, 'UPDATE_DATA', myData, {
                            channel: data.notifications,
                            curvePublic: data.curvePublic
                        }, function (obj) {
                            if (obj && obj.error) { return void console.error(obj); }
                            console.log('friend migrated', channel.friend);
                        });
                        close(chan);
                    }
                };

                network.on('message', function(msg, sender) {
                    try {
                        onDirectMessage(msg, sender);
                    } catch (e) {
                        console.error(e);
                    }
                });

                var friends = userObject.friends || {};
                Object.keys(friends).forEach(function (curve) {
                    if (curve.length !== 44) { return; }
                    var friend = friends[curve];

                    // Check if it is already a "new" friend
                    if (friend.notifications) { return; }

                    /** Old friend:
                     *  1. Open the messenger channel
                     *  2. Check if they sent us their mailbox channel
                     *  3.a. Yes ==> sent them a mail containing our mailbox channel
                     *  3.b. No  ==> post our mailbox data to the messenger channel
                     */
                    network.join(friend.channel).then(function (wc) {
                        var keys = Crypto.Curve.deriveKeys(friend.curvePublic, userObject.curvePrivate);
                        var encryptor = Crypto.Curve.createEncryptor(keys);
                        channels[friend.channel] = {
                            wc: wc,
                            friend: friend,
                            decrypt: encryptor.decrypt,
                            encrypt: encryptor.encrypt
                        };
                        var cfg = {
                            lastKnownHash: friend.lastKnownHash
                        };
                        var msg = ['GET_HISTORY', friend.channel, cfg];
                        network.sendto(network.historyKeeper, JSON.stringify(msg))
                          .then(function () {}, function (err) {
                            console.error("Can't migrate this friend", friend, err);
                        });
                    }, function (err) {
                        console.error("Can't migrate this friend", friend, err);
                    });
                });
            };
            if (version < 9) {
                migrateFriends();
                Feedback.send('Migrate-9', true);
                userObject.version = version = 9;
            }
        }).nThen(function (waitFor) {
            // Migration 10: deprecate todo
            var fixTodo = function () {
                var h = store.proxy.todo;
                if (!h) { return; }
                var next = waitFor(function () {
                    Feedback.send('Migrate-10', true);
                    userObject.version = version = 10;
                });
                var old;
                var opts = {
                    network: store.network,
                    initialState: '{}',
                    metadata: {
                        owners: store.proxy.edPublic ? [store.proxy.edPublic] : []
                    }
                };
                nThen(function (w) {
                    Crypt.get(h, w(function (err, val) {
                        if (err || !val) {
                            w.abort();
                            next();
                            return;
                        }
                        try {
                            old = JSON.parse(val);
                        } catch (e) {} // We will abort in the next step in case of error
                    }), opts);
                }).nThen(function (w) {
                    if (!old || typeof(old) !== "object") {
                        w.abort();
                        next();
                        return;
                    }
                    var k = {
                        content: {
                            data: {
                                "1": {
                                    id: "1",
                                    color: 'color6',
                                    item: [],
                                    title: Messages.kanban_todo
                                },
                                "2": {
                                    id: "2",
                                    color: 'color3',
                                    item: [],
                                    title: Messages.kanban_working
                                },
                                "3": {
                                    id: "3",
                                    color: 'color5',
                                    item: [],
                                    title: Messages.kanban_done
                                },
                            },
                            items: {},
                            list: [1, 2, 3]
                        },
                        metadata: {
                            title: Messages.type.todo,
                            defaultTitle: Messages.type.todo,
                            type: "kanban"
                        }
                    };
                    var i = 4;
                    var items = false;
                    (old.order || []).forEach(function (key) {
                        var data = old.data[key];
                        if (!data || !data.task) { return; }
                        items = true;
                        var column = data.state ? '3' : '1';
                        k.content.data[column].item.push(i);
                        k.content.items[i] = {
                            id: i,
                            title: data.task
                        };
                        i++;
                    });
                    if (!items) {
                        w.abort();
                        next();
                        return;
                    }
                    var newH = Hash.createRandomHash('kanban');
                    var secret = Hash.getSecrets('kanban', newH);
                    var oldSecret = Hash.getSecrets('todo', h);
                    Crypt.put(newH, JSON.stringify(k), w(function (err) {
                        if (err) {
                            w.abort();
                            next();
                            return;
                        }
                        if (store.rpc) {
                            store.rpc.pin([secret.channel], function () {
                                // Try to pin and ignore errors...
                                // Todo won't be available anyway so keep your unpinned kanban
                            });
                            store.rpc.unpin([oldSecret.channel], function () {
                                // Try to unpin and ignore errors...
                            });
                        }
                        var href = Hash.hashToHref(newH, 'kanban');
                        store.manager.addPad(['root'], {
                            title: Messages.type.todo,
                            owners: opts.metadata.owners,
                            channel: secret.channel,
                            href: href,
                            roHref: Hash.hashToHref(Hash.getViewHashFromKeys(secret), 'kanban'),
                            atime: +new Date(),
                            ctime: +new Date()
                        }, w(function (e) {
                            if (e) { return void console.error(e); }
                            delete store.proxy.todo;
                            var myData = Messaging.createData(userObject);
                            var ctx = { store: store };
                            Mailbox.sendTo(ctx, 'MOVE_TODO', {
                                user: myData,
                                href: href,
                            }, {
                                channel: myData.notifications,
                                curvePublic: myData.curvePublic
                            }, function (obj) {
                                if (obj && obj.error) { return void console.error(obj); }
                            });
                        }));
                    }), opts);
                }).nThen(function () {
                    next();
                });
            };
            if (version < 10) {
                fixTodo();
            }
        }).nThen(function (waitFor) {
            if (version >= 11) { return; }
            // Migration 11: alert users of safe links as the new default

            var done = function () {
                Feedback.send('Migrate-11', true);
                userObject.version = version = 11;
            };

            /*  userObject.settings.security.unsafeLinks
                    undefined => the user has never touched it
                    false => the user has explicitly enabled "safe links"
                    true => the user has explicitly disabled "safe links"
            */
            var unsafeLinks = Util.find(userObject, [ 'settings', 'security', 'unsafeLinks' ]);
            if (unsafeLinks !== undefined) { return void done(); }

            var ctx = {
                store: store,
            };
            var myData = Messaging.createData(userObject);
            if (!myData.curvePublic) { return void done(); }

            Mailbox.sendTo(ctx, 'SAFE_LINKS_DEFAULT', {
                user: myData,
            }, {
                channel: myData.notifications,
                curvePublic: myData.curvePublic
            }, waitFor(function (obj) {
                if (obj && obj.error) { return void console.error(obj); }
                done();
            }));
        /*}).nThen(function (waitFor) {
            // Test progress bar in the loading screen
            var i = 0;
            var w = waitFor();
            var it = setInterval(function () {
                i += 5;
                if (i >= 100) { w(); clearInterval(it); i = 100;}
                progress(0, i);
            }, 500);
            progress(0, 0);*/
        }).nThen(function () {
            Realtime.whenRealtimeSyncs(store.realtime, Util.mkAsync(Util.bake(cb)));
        });
    };
});