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

define(['json.sortify'], function (Sortify) {
    var UNINIT = 'uninitialized';
    var create = function (sframeChan) {
        var meta = UNINIT;
        var members = [];
        var metadataObj = UNINIT;
        // This object reflects the metadata which is in the document at this moment.
        // Normally when a person leaves the pad, everybody sees them leave and updates
        // their metadata, this causes everyone to fight to change the document and
        // operational transform doesn't like it. So this is a lazy object which is
        // only updated either:
        // 1. On changes to the metadata that come in from someone else
        // 2. On changes connects, disconnects or changes to your own metadata
        var metadataLazyObj = UNINIT;
        var priv = {};
        var dirty = true;
        var history = false;
        var degraded = 1; // truthy value but not true: we don't know yet if we're degraded
        var changeHandlers = [];
        var lazyChangeHandlers = [];
        var titleChangeHandlers = [];

        // When someone leaves the document, their metadata is removed from our metadataObj
        // but it is not removed instantly from the chainpad document metadata. This is
        // the result of the lazy object: if we had to remove the metadata instantly, all
        // the remaining members would try to push a patch to do it, and it could create
        // conflicts. Their metadata is instead removed from the chainpad doc only when
        // someone calls onLocal to make another change.
        // The leaving user is not visible in the userlist UI because we filter it using
        // the list of "members" (netflux ID currently online).
        // Our Problem:
        // With the addition of shared workers, a user can leave and join back with the same
        // netflux ID (just reload the pad). If nobody has made any change in the mean time,
        // their metadata will still be in the document, but they won't be in our metadataObj.
        // This causes the presence of a "viewer" instead of an editor, because they don't
        // have user data.
        // To fix this problem, the metadata manager can request "syncs" from a chainpad app,
        // and the app should trigger a "metadataMgr.updateMetadata(data)" in the handler.
        // See "metadataMgr.onRequestSync" in sframe-app-framework for an example.
        var syncHandlers = [];

        var rememberedTitle;

        var checkUpdate = function (lazy) {
            if (!dirty) { return; }
            if (meta === UNINIT) { throw new Error(); }
            if (metadataObj === UNINIT) {
                metadataObj = {
                    defaultTitle: meta.doc.defaultTitle,
                    //title: meta.doc.defaultTitle,
                    type: meta.doc.type,
                    users: {},
                };
                metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
            }
            if (!metadataObj.users) { metadataObj.users = {}; }
            if (!metadataLazyObj.users) { metadataLazyObj.users = {}; }

            if (!metadataObj.type) { metadataObj.type = meta.doc.type; }
            if (!metadataLazyObj.type) { metadataLazyObj.type = meta.doc.type; }

            var mdo = {};
            // We don't want to add our user data to the object multiple times.
            Object.keys(metadataObj.users).forEach(function (x) {
                if (members.indexOf(x) === -1 && !history) { return; }
                mdo[x] = metadataObj.users[x];
            });
            if (!priv.readOnly && !degraded) {
                mdo[meta.user.netfluxId] = meta.user;
            }
            metadataObj.users = mdo;

            // Clean old data
            delete metadataObj.authors;
            delete metadataLazyObj.authors;

            // Always update the userlist in the lazy object, otherwise it may be outdated
            // and metadataMgr.updateMetadata() won't do anything, and so we won't push events
            // to the userlist UI ==> phantom viewers
            var lazyUserStr = Sortify(metadataLazyObj.users[meta.user.netfluxId]);
            dirty = false;
            if (lazy || lazyUserStr !== Sortify(meta.user)) {
                metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
                lazyChangeHandlers.forEach(function (f) { f(); });
            } else {
                metadataLazyObj.users = JSON.parse(JSON.stringify(mdo));
            }

            if (metadataObj.title !== rememberedTitle) {
                rememberedTitle = metadataObj.title;
                titleChangeHandlers.forEach(function (f) {
                    f(metadataObj.title, metadataObj.defaultTitle);
                });
            }

            changeHandlers.forEach(function (f) { f(); });
        };
        var change = function (lazy) {
            dirty = true;
            setTimeout(function () {
                checkUpdate(lazy);
            });
        };

        var netfluxId;
        var isReady = false;
        var readyHandlers = [];
        sframeChan.on('EV_METADATA_UPDATE', function (ev) {
            meta = ev;
            if (ev.priv) {
                priv = ev.priv;
            }
            if (netfluxId) {
                meta.user.netfluxId = netfluxId;
            }
            if (!isReady) {
                isReady = true;
                readyHandlers.forEach(function (f) { f(); });
            }
            change(true);
        });
        sframeChan.on('EV_RT_CONNECT', function (ev) {
            netfluxId = ev.myID;
            members = ev.members;
            if (!meta.user) { return; }
            meta.user.netfluxId = netfluxId;
            change(true);
        });
        sframeChan.on('EV_RT_JOIN', function (ev) {
            var idx = members.indexOf(ev);
            if (idx !== -1) { console.log('Error: ' + ev + ' is already in members'); return; }
            members.push(ev);
            if (!meta.user) { return; }
            change(false);
            syncHandlers.forEach(function (f) { f(); });
        });
        sframeChan.on('EV_RT_LEAVE', function (ev) {
            var idx = members.indexOf(ev);
            if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; }
            members.splice(idx, 1);
            if (!meta.user) { return; }
            change(false);
        });
        sframeChan.on('EV_RT_DISCONNECT', function () {
            members = [];
            if (!meta.user) { return; }
            change(true);
        });
        sframeChan.on('EV_RT_ERROR', function (err) {
            if (err.type !== 'EEXPIRED' && err.type !== 'EDELETED') { return; }
            members = [];
            if (!meta.user) { return; }
            change(true);
        });

        return Object.freeze({
            updateMetadata: function (m) {
                // JSON.parse(JSON.stringify()) reorders the json, so we have to use sortify even
                // if it's on our own computer
                if (!m) { return; }
                if (Sortify(metadataLazyObj) === Sortify(m)) { return; }
                metadataObj = JSON.parse(JSON.stringify(m));
                metadataLazyObj = JSON.parse(JSON.stringify(m));
                change(false);
            },
            refresh : function () {
                change(true);
            },
            updateTitle: function (t) {
                metadataObj.title = t;
                change(true);
            },
            getMetadata: function () {
                checkUpdate(false);
                return Object.freeze(JSON.parse(JSON.stringify(metadataObj)));
            },
            getMetadataLazy: function () {
                return metadataLazyObj;
            },
            setDegraded: function (bool) {
                degraded = bool;
            },
            isDegraded: function () { return degraded; },
            onTitleChange: function (f) { titleChangeHandlers.push(f); },
            onChange: function (f) { changeHandlers.push(f); },
            onChangeLazy: function (f) { lazyChangeHandlers.push(f); },
            onRequestSync: function (f) { syncHandlers.push(f); },
            off: function (name, f) {
                var h = [];
                if (name === 'change') { h = changeHandlers; }
                else if (name === 'lazy') { h = lazyChangeHandlers; }
                else if (name === 'title') { h = titleChangeHandlers; }
                else if (name === 'sync') { h = syncHandlers; }
                var idx = h.indexOf(f);
                if (idx !== -1) { h.splice(idx, 1); }
            },
            isConnected : function () {
                return members.indexOf(meta.user.netfluxId) !== -1;
            },
            getViewers : function () {
                checkUpdate(false);
                var list = members.slice().filter(function (m) { return m.length === 32; });
                return list.length - Object.keys(metadataLazyObj.users).length;
            },
            getChannelMembers: function () { return members.slice(); },
            getPrivateData : function () {
                return priv;
            },
            getUserData : function () {
                return meta.user;
            },
            getNetfluxId : function () {
                return meta.user.netfluxId;
            },
            onReady: function (f) {
                if (isReady) { return void f(); }
                readyHandlers.push(f);
            },
            setHistory: function (bool) {
                history = bool;
            }
        });
    };
    return Object.freeze({ create: create });
});
