// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

define([
    'jquery',
    '/common/common-interface.js',
    '/common/common-util.js',
    '/common/hyperscript.js',
    '/customize/messages.js',
    '/components/nthen/index.js',
    //'/components/chainpad-json-validator/json-ot.js',

    '/components/chainpad/chainpad.dist.js',
], function ($, UI, Util, h, Messages, nThen, ChainPad /* JsonOT */) {
    //var ChainPad = window.ChainPad;
    var History = {};

    History.create = function (common, config) {
        if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
        if (History.loading) { return void console.error("History is already being loaded..."); }
        if (History.state) { return void console.error("Already loaded"); }
        History.loading = true;
        History.state = true;
        var $toolbar = config.$toolbar;
        var $hist = $toolbar.find('.cp-toolbar-history');
        $hist.addClass('cp-history-init');

        if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
            throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
        }

        var getStates = function (rt) {
            var states = [];
            var b = rt.getAuthBlock();
            if (b) { states.unshift(b); }
            while (b.getParent()) {
                b = b.getParent();
                states.unshift(b);
            }
            return states;
        };

        var createRealtime = function (config) {
            return ChainPad.create({
                userName: 'history',
                validateContent: function (content) {
                    try {
                        JSON.parse(content);
                        return true;
                    } catch (e) {
                        console.log('Failed to parse, rejecting patch');
                        return false;
                    }
                },
                initialState: '',
                logLevel: config.debug ? 2 : 0,
                noPrune: true
            });
        };

        var fillChainPad = function (realtime, messages) {
            messages.forEach(function (m) {
                realtime.message(m);
            });
        };

        var realtime;
        var states = [];
        var patchWidth = 0;
        var c = 0;//states.length - 1;

        var getIndex = function (i) {
            return states.length - 1 + i;
        };
        var getRank = function (idx) {
            return idx - states.length + 1;
        };
        // Get the author or group of author linked to a state
        var getAuthor = function (idx, semantic) {
            if (semantic === 1 || !config.extractMetadata) {
                return states[idx].author;
            }
            try {
                var val = JSON.parse(states[idx].getContent().doc);
                var md = config.extractMetadata(val);
                var users = Object.keys(md.users || {}).sort();
                return users.join();
            } catch (e) {
                console.error(e);
                return states[idx].author;
            }
        };

        var bar = h('span.cp-history-timeline-bar');
        var onResize = function () {
            var $bar = $(bar);
            if (!$bar.width() || !$bar.length) { return; }
            var widthPx = patchWidth * $bar.width() / 100;
            $hist.removeClass('cp-smallpatch');
            $bar.find('.cp-history-snapshot').css('margin-left', "");
            var $pos = $hist.find('.cp-history-timeline-pos');
            $pos.css('margin-left', "");
            if (widthPx < 18) {
                $hist.addClass('cp-smallpatch');
                $bar.find('.cp-history-snapshot').css('margin-left', (widthPx/2-2)+"px");
                $pos.css('margin-left', (widthPx/2-2)+"px");
            }
        };

        // Refresh the timeline UI with the block states
        var refreshBar = function (snapshotsOnly) {
            var $pos = $hist.find('.cp-history-timeline-pos');
            var $bar = $(bar);
            var users = {
                list: [],
                author: '',
                el: undefined,
                i: 0
            };
            var user = {
                list: [],
                author: '',
                el: undefined,
                i: 0
            };

            var snapshotsData = {};
            var snapshots = [];
            if (config.getLastMetadata) {
                try {
                    var md = config.getLastMetadata();
                    if (md.snapshots) {
                        snapshotsData = md.snapshots;
                        snapshots = Object.keys(md.snapshots);
                    }
                } catch (e) { console.error(e); }
            }

            var max = states.length - 1;
            var snapshotsEl = [];
            patchWidth = 100 / max;

            // Check if we need a new block on the index i for the "obj" type (user or users)
            var check = function (obj, author, i) {
                if (snapshotsOnly) { return; }
                if (obj.author !== author) {
                    obj.author = author;
                    if (obj.el) {
                        $(obj.el).css('width', (100*(i - obj.i)/max)+'%');
                    }
                    obj.el = h('span.cp-history-bar-el');
                    obj.list.push(obj.el);
                    obj.i = i;
                }
            };

            var hash;
            for (var i = 1; i < states.length; i++) {
                hash = states[i].serverHash;
                if (snapshots.indexOf(hash) !== -1) {
                    snapshotsEl.push(h('div.cp-history-snapshot', {
                        style: 'width:'+patchWidth+'%;left:'+(patchWidth * (i-1))+'%;',
                        title: snapshotsData[hash].title
                    }, h('i.fa.fa-camera')));
                }
                if (config.drive) {
                    // Display only one bar, split by patch
                    check(user, i, i);
                } else {
                    // Display two bars, split by author(s)
                    check(user, getAuthor(i, 1), i);
                    check(users, getAuthor(i, 2), i);
                }
            }

            if (snapshotsOnly) {
                // We only want to redraw the snapshots
                $bar.find('.cp-history-snapshots').html('').append([
                    $pos,
                    snapshotsEl
                ]);
            } else {
                $(user.el).css('width', (100*(max + 1 - user.i)/max)+'%');
                if (!config.drive) {
                    $(users.el).css('width', (100*(max + 1 - users.i)/max)+'%');
                }

                $bar.html('').append([
                    h('span.cp-history-timeline-users', users.list),
                    h('span.cp-history-timeline-user', user.list),
                    h('div.cp-history-snapshots', [
                        $pos[0],
                        snapshotsEl
                    ]),
                ]);
            }

            onResize();
        };

        var allMessages = [];
        var lastKnownHash;
        var isComplete = false;
        var loadMoreHistory = function (config, common, cb) {
            if (isComplete) { return void cb ('EFULL'); }
            var realtime = createRealtime(config);
            var sframeChan = common.getSframeChannel();

            sframeChan.query('Q_GET_HISTORY_RANGE', {
                lastKnownHash: lastKnownHash,
                sharedFolder: config.sharedFolder
            }, function (err, data) {
                if (err) { return void console.error(err); }
                if (data && data.error) {
                    return void cb(data.error);
                }
                if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
                lastKnownHash = data.lastKnownHash;
                isComplete = data.isFull;
                var messages = (data.messages || []).map(function (obj) {
                    return obj;
                });

                // We're supposed to receive 2 checkpoints. If the result is only ONE message
                // and this message is a checkpoint, it means it's the last message of the history
                // (and this is a trimmed history)
                if (messages.length === 1) {
                    var parsed = JSON.parse(messages[0].msg);
                    if (parsed[0] === 4) {
                        isComplete = true;
                    }
                }
                if (config.debug) { console.log(data.messages); }
                Array.prototype.unshift.apply(allMessages, messages); // Destructive concat
                fillChainPad(realtime, allMessages);
                cb (null, realtime, data.isFull);
            });
        };

        // config.setHistory(bool, bool)
        // - bool1: history value
        // - bool2: reset old content?
        var render = function (val) {
            if (typeof val === "undefined") { return; }
            try {
                config.applyVal(val);
            } catch (e) {
                // Probably a parse error
                console.error(e);
            }
        };
        var onClose = function () {
            config.setHistory(false, true);
        };

        var onRevert = function () {
            // Before we can restore the current version, we need to update metadataMgr
            // so that it will uses the snapshots from the realtime version!
            // Restoring the snapshots to their old version would go against the
            // goal of having snapshots
            if (config.getLastMetadata) {
                var metadataMgr = common.getMetadataMgr();
                var lastMd = config.getLastMetadata() || {};
                var _snapshots = lastMd.snapshots;
                var _users = lastMd.users;
                var md = Util.clone(metadataMgr.getMetadata());
                md.snapshots = _snapshots;
                md.users = _users;
                metadataMgr.updateMetadata(md);
            }

            // And now we can properly restore the content
            var closed = config.setHistory(false, false);
            if (!closed) {
                return void UI.alert(Messages.history_cantRestore);
            }
            config.onLocal();
            config.onRemote();
            return true;
        };

        config.setHistory(true);

        var $bottom = $toolbar.find('.cp-toolbar-bottom');
        var $cke = $toolbar.find('.cke_toolbox_main');

        $hist.html('').css('display', 'flex');
        $bottom.hide();
        $cke.hide();

        UI.spinner($hist).get().show();

        let closeAll = () => {
            History.state = false;
            $hist.hide();
            $bottom.show();
            $cke.show();
            $(window).trigger('resize');
        };

        var update = function (newRt) {
            realtime = newRt;
            if (!realtime) { return []; }
            states = getStates(realtime);
            refreshBar();
            return states;
        };

        var $loadMore, $time, get;

        // Get the content of the selected version, and change the version number
        var loading = false;
        var loadMore = function (cb) {
            if (loading) { return; }
            loading = true;
            $loadMore.find('.fa-ellipsis-h').hide();
            $loadMore.find('.fa-refresh').show();

            loadMoreHistory(config, common, function (err, newRt, isFull) {
                if (err === 'EFULL') {
                    $loadMore.off('click').hide();
                    get(c);
                    return;
                }
                loading = false;
                if (err) { return void console.error(err); }
                update(newRt);
                $loadMore.find('.fa-ellipsis-h').show();
                $loadMore.find('.fa-refresh').hide();
                get(c);
                if (isFull) {
                    $loadMore.off('click').hide();
                }
                if (cb) { cb(); }
            });
        };

        // semantic === 1 : group by user
        // semantic === 2 : group by "group of users"
        get = function (i, blockOnly, semantic) {
            i = parseInt(i);
            if (isNaN(i)) { return; }
            if (i > 0) { i = 0; }
            if (i < -(states.length - 2)) { i = -(states.length - 2); }

            var idx = getIndex(i);
            if (semantic && i !== c) {
                // If semantic is true, jump to the next patch from a different netflux ID
                var author = getAuthor(idx, semantic);
                var forward = i > c;
                for (var j = idx; (j > 0 && j < states.length ); (forward ? j++ : j--)) {
                    if (author !== getAuthor(j, semantic)) {
                        break;
                    }
                    idx = j;
                    i = getRank(idx);
                }
            }

            if (i <= -(states.length - 11)) {
                loadMore();
            }

            if (blockOnly) { return states[idx]; }

            var val = states[idx].getContent().doc;
            c = i;
            $hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
                .prop('disabled', '');
            if (c === -(states.length-1)) {
                $hist.find('.cp-toolbar-history-previous').prop('disabled', 'disabled');
            }
            if (c === 0) {
                $hist.find('.cp-toolbar-history-next').prop('disabled', 'disabled');
            }
            var $pos = $hist.find('.cp-history-timeline-pos');
            var p = 100 * (1 - (-(c - 1) / (states.length-1)));
            $pos.css('left', p+'%');
            $pos.css('width', patchWidth+'%');

            // Display the version when the full history is loaded
            // Note: the first version is always empty and probably can't be displayed, so
            // we can consider we have only states.length - 1 versions
            var time = states[idx].time;
            if (time) {
                $time.text(new Date(time).toLocaleString());
            } else { $time.text(''); }

            if (config.debug) {
                console.log(states[idx]);
                var ops = states[idx] && states[idx].getPatch() && states[idx].getPatch().operations;
                if (Array.isArray(ops)) {
                    ops.forEach(function (op) { console.log(op); });
                }
            }

            return val || '';
        };

        /*
        var getNext = function (step) {
            return typeof step === "number" ? get(c + step) : get(c + 1);
        };
        var getPrevious = function (step) {
            return typeof step === "number" ? get(c - step) : get(c - 1);
        };
        */

        var makeSnapshot = function (title, $input) {
            var idx = getIndex(c);
            if (!config.getLastMetadata || !config.setLastMetadata) { return; }
            try {
                var block = states[idx];
                var hash = block.serverHash;
                var md = config.getLastMetadata();
                md.snapshots = md.snapshots || {};
                if (md.snapshots[hash]) { return; }
                md.snapshots[hash] = {
                    title: title,
                    time: block.time ? (+new Date(block.time)) : +new Date()
                };
                var sent = config.setLastMetadata(md);
                if (!sent) { return void UI.alert(Messages.snapshots_cantMake); }
                $input.val('');
                refreshBar();
            } catch (e) {
                console.error(e);
            }
        };

        // Create the history toolbar
        var display = function () {
            $hist.html('');
            $hist.removeClass('cp-history-init');

            var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_fastPrev }, [
                h('i.fa.fa-step-backward'),
                h('i.fa.fa-users')
            ]);
            var userPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_userPrev }, [
                h('i.fa.fa-step-backward'),
                h('i.fa.fa-user')
            ]);
            var prev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
                h('i.fa.fa-step-backward')
            ]);
            var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_fastNext }, [
                h('i.fa.fa-users'),
                h('i.fa.fa-step-forward'),
            ]);
            var userNext = h('button.cp-toolbar-history-next', { title: Messages.history_userNext }, [
                h('i.fa.fa-user'),
                h('i.fa.fa-step-forward'),
            ]);
            var next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
                h('i.fa.fa-step-forward')
            ]);
            if (config.drive) {
                fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
                    h('i.fa.fa-fast-forward'),
                ]);
                fastPrev = h('button.cp-toolbar-history-previous', {title: Messages.history_prev}, [
                    h('i.fa.fa-fast-backward'),
                ]);
            }

            var $fastPrev = $(fastPrev);
            var $userPrev = $(userPrev);
            var $prev = $(prev);
            var $fastNext = $(fastNext);
            var $userNext = $(userNext);
            var $next = $(next);

            var _loadMore = h('button.cp-toolbar-history-loadmore', { title: Messages.history_loadMore }, [
                h('i.fa.fa-ellipsis-h'),
                h('i.fa.fa-refresh.fa-spin.fa-3x.fa-fw', { style: 'display: none;' })
            ]);

            var pos = h('span.cp-history-timeline-pos.fa.fa-caret-down');
            var time = h('div.cp-history-timeline-time');
            $time = $(time);
            var timeline = h('div.cp-toolbar-history-timeline', [
                h('div.cp-history-timeline-line', [
                    h('span.cp-history-timeline-legend', [
                        h('i.fa.fa-users'),
                        h('i.fa.fa-user')
                    ]),
                    h('span.cp-history-timeline-loadmore', _loadMore),
                    h('span.cp-history-timeline-container', [
                        bar
                    ])
                ]),
                h('div.cp-history-timeline-actions', [
                    h('span.cp-history-timeline-prev', [
                        fastPrev,
                        config.drive ? undefined : userPrev,
                        prev,
                    ]),
                    time,
                    h('span.cp-history-timeline-next', [
                        next,
                        config.drive ? undefined : userNext,
                        fastNext
                    ])
                ])
            ]);

            var snapshot = h('button', {
                title: Messages.snapshots_new,
            }, [
                h('i.fa.fa-camera')
            ]);
            var share = h('button', { title: Messages.history_shareTitle }, [
                h('i.fa.fa-shhare-alt'),
                h('span', Messages.shareButton)
            ]);
            var restoreTitle = config.drive ? Messages.history_restoreDriveTitle
                                   : Messages.history_restoreTitle;
            var restore = h('button', {
                title: restoreTitle,
            }, [
                h('i.fa.fa-check'),
                h('span', Messages.history_restore)
            ]);
            var close = h('button', { title: Messages.history_closeTitle }, [
                h('i.fa.fa-times'),
                h('span', Messages.history_close)
            ]);
            var actions = h('div.cp-toolbar-history-actions', [
                h('span.cp-history-actions-first', [
                    snapshot,
                    share
                ]),
                h('span.cp-history-actions-last', [
                    restore,
                    close
                ])
            ]);

            if (History.readOnly) {
                snapshot.disabled = true;
                restore.disabled = true;
            }
            if (config.drive) {
                $hist.addClass('cp-history-drive');
                $(snapshot).hide();
                $(share).hide();
            }

            $hist.append([timeline, actions]);
            onResize();
            $(window).on('resize', onResize);

            var $bar = $(bar);
            $bar.find('.cp-history-snapshots').append(pos);
            $bar.click(function (e) {
                e.stopPropagation();
                var $t = $(e.target);
                if ($t.closest('.cp-history-snapshot').length) {
                    $t = $t.closest('.cp-history-snapshot');
                }
                var isEl = $t.is('.cp-history-snapshot');
                if (!$t.is('.cp-history-snapshots') && !isEl) { return; }
                var x = e.offsetX;
                if (isEl) {
                    x += $t.position().left;
                }
                var p = x / $bar.width();
                var v = 1-Math.ceil((states.length - 1) * (1 - p));
                render(get(v));
            });
            $loadMore = $(_loadMore).click(function () {
                loadMore(function () {
                    get(c);
                });
            });

            var onKeyDown, onKeyUp;
            var closeUI = function () {
                closeAll();
                $(window).off('keydown', onKeyDown);
                $(window).off('keyup', onKeyUp);
            };

            // Version buttons
            $prev.click(function () { render(get(c - 1)); });
            $next.click(function () { render(get(c + 1)); });
            if (config.drive) {
                $fastPrev.click(function () { render(get(c - 10)); });
                $fastNext.click(function () { render(get(c + 10)); });
                $userPrev.click(function () { render(get(c - 10)); });
                $userNext.click(function () { render(get(c + 10)); });
            } else {
                $userPrev.click(function () { render(get(c - 1, false, 1)); });
                $userNext.click(function () { render(get(c + 1, false, 1)); });
                $fastPrev.click(function () { render(get(c - 1, false, 2)); });
                $fastNext.click(function () { render(get(c + 1, false, 2)); });
            }
            onKeyDown = function (e) {
                var p = function () { e.preventDefault(); };
                if (e.which === 39) { p(); return $next.click(); } // Right
                if (e.which === 37) { p(); return $prev.click(); } // Left
                if (e.which === 38) { p(); return $userNext.click(); } // Up
                if (e.which === 40) { p(); return $userPrev.click(); } // Down
                if (e.which === 33) { p(); return $fastNext.click(); } // PageUp
                if (e.which === 34) { p(); return $fastPrev.click(); } // PageUp
                if (e.which === 27) { p(); $(close).click(); }
            };
            onKeyUp = function (e) { e.stopPropagation(); };
            $(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();

            // Snapshots
            $(snapshot).click(function () {
                var input = h('input', {
                    placeholder: Messages.snapshots_placeholder
                });
                var $input = $(input);
                var content = h('div', [
                    h('h5', Messages.snapshots_new),
                    input
                ]);

                var buttons = [{
                    className: 'cancel',
                    name: Messages.filePicker_close,
                    onClick: function () {},
                    keys: [27],
                }, {
                    className: 'primary',
                    iconClass: '.fa.fa-camera',
                    name: Messages.snapshots_new,
                    onClick: function () {
                        var val = $input.val();
                        if (!val) { return true; }
                        makeSnapshot(val, $input);
                    },
                    keys: [],
                }];

                UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
                setTimeout(function () {
                    $input.focus();
                });
            });

            // Share
            $(share).click(function () {
                var block = get(c, true);
                common.getSframeChannel().event('EV_SHARE_OPEN', {
                    versionHash: block.serverHash,
                    //title: title
                });
            });

            // Close & restore buttons
            $(close).click(function () {
                states = [];
                onClose();
                closeUI();
                UI.clearTooltipsDelay();
            });
            $(restore).click(function () {
                var restorePrompt = config.drive ? Messages.history_restoreDrivePrompt
                                                 : Messages.history_restorePrompt;
                UI.confirm(restorePrompt, function (yes) {
                    if (!yes) { return; }
                    var done = onRevert();
                    if (done) {
                        closeUI();
                        var restoreDone = config.drive ? Messages.history_restoreDriveDone
                                                       : Messages.history_restoreDone;
                        UI.log(restoreDone);
                    }
                });
            });

            // Display the latest content
            render(get(c));
            $(window).trigger('resize');
        };

        if (config.onOpen) {
            config.onOpen();
        }

        // Load all the history messages into a new chainpad object
        loadMoreHistory(config, common, function (err, newRt, isFull) {
            History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;
            History.loading = false;
            if (err) {
                console.error(err);
                UI.warn(`${Messages.error}: ${err}`);
                closeAll();
                return;
            }
            update(newRt);
            display();
            if (isFull) {
                $loadMore.off('click').hide();
            }
        });
    };

    return History;
});


