<template>
    <div></div>
</template>

<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';

import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';

export default @Component({
    watch: {
        serverSyncEnabled: function() {
            this.serverSyncEnabledChanged();
        },
        serverStorageKey: function() {
            this.serverStorageKeyChanged(true);
        },
        settings: function() {
            this.debouncedSaveSettings();
        },
        profiles: function() {
            this.saveProfiles();
        },
        currentProfile: function() {
            this.currentProfileChanged(true);
        },
    },
})
class ServerStorage extends Vue {
    created() {
        this.inited = false;
        this.keyInited = false;
        this.commit = this.$store.commit;
        this.prevServerStorageKey = null;
        this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});

        this.debouncedSaveSettings = _.debounce(() => {
            this.saveSettings();
        }, 500);

        this.debouncedSaveRecent = _.debounce((itemKey) => {
            this.saveRecent(itemKey);
        }, 1000);

        this.debouncedNotifySuccess = _.debounce(() => {
            this.success('Данные синхронизированы с сервером');
        }, 1000);

        this.oldProfiles = {};
        this.oldSettings = {};
    }

    async init() {
        try {
            if (!this.serverStorageKey) {
                //генерируем новый ключ
                await this.generateNewServerStorageKey();
            } else {
                await this.serverStorageKeyChanged();
            }
            bookManager.addEventListener(this.bookManagerEvent);
        } finally {
            this.inited = true;
        }
    }

    async bookManagerEvent(eventName, itemKey) {
        if (eventName == 'recent-changed') {
            this.debouncedSaveRecent(itemKey);
        }
    }

    async generateNewServerStorageKey() {
        const key = utils.toBase58(utils.randomArray(32));
        this.commit('reader/setServerStorageKey', key);
        await this.serverStorageKeyChanged(true);
    }

    async serverSyncEnabledChanged() {
        if (this.serverSyncEnabled) {
            this.prevServerStorageKey = null;
            if (!this.serverStorageKey) {
                //генерируем новый ключ
                await this.generateNewServerStorageKey();
            } else {
                await this.serverStorageKeyChanged(true);
            }
        }
    }

    async serverStorageKeyChanged(force) {
        if (this.prevServerStorageKey != this.serverStorageKey) {
            this.prevServerStorageKey = this.serverStorageKey;
            this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
            this.keyInited = true;

            await this.loadProfiles(force);
            this.checkCurrentProfile();
            await this.currentProfileChanged(force);
            await this.loadRecent(force);
            if (force)
                await this.saveRecent();
        }
    }

    async currentProfileChanged(force) {
        if (!this.currentProfile)
            return;

        await this.loadSettings(force);
    }

    get serverSyncEnabled() {
        return this.$store.state.reader.serverSyncEnabled;
    }

    get settings() {
        return this.$store.state.reader.settings;
    }

    get settingsRev() {
        return this.$store.state.reader.settingsRev;
    }

    get serverStorageKey() {
        return this.$store.state.reader.serverStorageKey;
    }

    get profiles() {
        return this.$store.state.reader.profiles;
    }

    get profilesRev() {
        return this.$store.state.reader.profilesRev;
    }

    get currentProfile() {
        return this.$store.state.reader.currentProfile;
    }

    get showServerStorageMessages() {
        return this.settings.showServerStorageMessages;
    }

    checkCurrentProfile() {
        if (!this.profiles[this.currentProfile]) {
            this.commit('reader/setCurrentProfile', '');
        }
    }

    success(message) {
        if (this.showServerStorageMessages)
            this.$notify.success({message});
    }

    warning(message) {
        if (this.showServerStorageMessages)
            this.$notify.warning({message});
    }

    error(message) {
        if (this.showServerStorageMessages)
            this.$notify.error({message});
    }

    async loadSettings(force = false, doNotifySuccess = true) {
        if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
            return;

        const setsId = `settings-${this.currentProfile}`;
        const oldRev = this.settingsRev[setsId] || 0;
        //проверим ревизию на сервере
        if (!force) {
            try {
                const revs = await this.storageCheck({[setsId]: {}});
                if (revs.state == 'success' && revs.items[setsId].rev == oldRev) {
                    return;
                }
            } catch(e) {
                this.error(`Ошибка соединения с сервером: ${e.message}`);
                return;
            }
        }

        let sets = null;
        try {
            sets = await this.storageGet({[setsId]: {}});
        } catch(e) {
            this.error(`Ошибка соединения с сервером: ${e.message}`);
            return;
        }

        if (sets.state == 'success') {
            sets = sets.items[setsId];

            if (sets.rev == 0)
                sets.data = {};

            this.oldSettings = _.cloneDeep(sets.data);
            this.commit('reader/setSettings', sets.data);
            this.commit('reader/setSettingsRev', {[setsId]: sets.rev});

            if (doNotifySuccess)
                this.debouncedNotifySuccess();
        } else {
            this.warning(`Неверный ответ сервера: ${sets.state}`);
        }
    }

    async saveSettings() {
        if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
            return;

        const diff = utils.getObjDiff(this.oldSettings, this.settings);
        if (utils.isEmptyObjDiff(diff))
            return;

        this.savingSettings = true;
        try {
            const setsId = `settings-${this.currentProfile}`;
            let result = {state: ''};

            const oldRev = this.settingsRev[setsId] || 0;
            try {
                result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
            } catch(e) {
                this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
            }

            if (result.state == 'reject') {
                await this.loadSettings(true, false);
                this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
            } else if (result.state == 'success') {
                this.oldSettings = _.cloneDeep(this.settings);
                this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
            }
        } finally {
            this.savingSettings = false;
        }
    }

    async loadProfiles(force = false, doNotifySuccess = true) {
        if (!this.keyInited || !this.serverSyncEnabled)
            return;

        const oldRev = this.profilesRev;
        //проверим ревизию на сервере
        if (!force) {
            try {
                const revs = await this.storageCheck({profiles: {}});
                if (revs.state == 'success' && revs.items.profiles.rev == oldRev) {
                    return;
                }
            } catch(e) {
                this.error(`Ошибка соединения с сервером: ${e.message}`);
                return;
            }
        }

        let prof = null;
        try {
            prof = await this.storageGet({profiles: {}});
        } catch(e) {
            this.error(`Ошибка соединения с сервером: ${e.message}`);
            return;
        }

        if (prof.state == 'success') {
            prof = prof.items.profiles;

            if (prof.rev == 0)
                prof.data = {};

            this.oldProfiles = _.cloneDeep(prof.data);
            this.commit('reader/setProfiles', prof.data);
            this.commit('reader/setProfilesRev', prof.rev);
            this.checkCurrentProfile();

            if (doNotifySuccess)
                this.debouncedNotifySuccess();
        } else {
            this.warning(`Неверный ответ сервера: ${prof.state}`);
        }
    }

    async saveProfiles() {
        if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
            return;

        const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
        if (utils.isEmptyObjDiff(diff))
            return;

        //обнуляются профили во время разработки при hotReload, подстраховка
        if (!this.$store.state.reader.allowProfilesSave) {
            console.error('Сохранение профилей не санкционировано');
            return;
        }

        this.savingProfiles = true;
        try {
            let result = {state: ''};
            try {
                result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
            } catch(e) {
                this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
            }

            if (result.state == 'reject') {
                await this.loadProfiles(true, false);
                this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
            } else if (result.state == 'success') {
                this.oldProfiles = _.cloneDeep(this.profiles);
                this.commit('reader/setProfilesRev', this.profilesRev + 1);        
            }
        } finally {
            this.savingProfiles = false;
        }
    }

    async loadRecent(force = false, doNotifySuccess = true) {
        if (!this.keyInited || !this.serverSyncEnabled)
            return;

        const oldRecentRev = bookManager.recentRev;
        const oldRecentDiffRev = bookManager.recentDiffRev;
        //проверим ревизию на сервере
        let revs = null;
        if (!force) {
            try {
                revs = await this.storageCheck({recent: {}, recentDiff: {}});
                if (revs.state == 'success' && revs.items.recent.rev == oldRecentRev &&
                    revs.items.recentDiff.rev == oldRecentDiffRev) {
                    return;
                }
            } catch(e) {
                this.error(`Ошибка соединения с сервером: ${e.message}`);
                return;
            }
        }

        let recent = null;
        if (force || revs.items.recent.rev != oldRecentRev || revs.items.recentDiff.rev != oldRecentDiffRev) {
            try {
                recent = await this.storageGet({recent: {}, recentDiff: {}});
            } catch(e) {
                this.error(`Ошибка соединения с сервером: ${e.message}`);
                return;
            }

            if (recent.state == 'success') {
                let recentDiff = recent.items.recentDiff;
                recent = recent.items.recent;

                if (recent.rev == 0)
                    recent.data = {};

                this.oldRecent = _.cloneDeep(recent.data);
                let newRecent = {};
                if (recentDiff && recentDiff.data) {
                    newRecent = utils.applyObjDiff(recent.data, recentDiff.data);
                    this.recentDiff = _.cloneDeep(recentDiff.data);
                    if (!utils.isObjDiff(this.recentDiff))
                        this.recentDiff = null;
                } else {
                    newRecent = recent.data;
                    this.recentDiff = null;
                }


                if (!bookManager.loaded) {
                    this.warning('Ожидание загрузки списка книг перед синхронизацией');
                    while (!bookManager.loaded) await utils.sleep(100);
                }
                await bookManager.setRecent(newRecent);
                await bookManager.setRecentRev(recent.rev);
                await bookManager.setRecentDiffRev(recentDiff.rev);
            } else {
                this.warning(`Неверный ответ сервера: ${recent.state}`);
            }
        }

        if (doNotifySuccess)
            this.debouncedNotifySuccess();
    }

    async saveRecent(itemKey) {
        if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
            return;

        const bm = bookManager;

        /*if (!bookManager.loaded) {
            this.warning('Функции сохранения на сервер пока недоступны');
            return;
        }*/

        //несколько замудреная инициализация oldRecent
        if (!this.oldRecent) {
            this.oldRecent = _.cloneDeep(bookManager.recent);
        }

        if (bookManager.loaded && !this.oldRecentInited) {
            this.oldRecent = _.cloneDeep(bookManager.recent);
            this.oldRecentInited = true;
        }

        //вычисляем дифф
        let diff = null;
        if (itemKey) {//ускоренное вычисления диффа
            let itemDiff;
            if (this.oldRecent[itemKey]) {
                itemDiff = utils.getObjDiff({[itemKey]: (this.oldRecentInited ? this.oldRecent[itemKey] : {})}, {[itemKey]: bm.recent[itemKey]});
            } else {
                itemDiff = utils.getObjDiff({}, {[itemKey]: bm.recent[itemKey]});
            }
            if (this.recentDiff) {
                diff = this.recentDiff;
                if (itemDiff.change[itemKey])
                    diff.change[itemKey] = itemDiff.change[itemKey];
                if (itemDiff.add[itemKey])
                    diff.add[itemKey] = itemDiff.add[itemKey];
            } else {
                diff = itemDiff;
            }
        } else {//медленное вычисление диффа
            if (this.oldRecentInited) {
                diff = utils.getObjDiff(this.oldRecent, bm.recent);
            } else
                return;
        }

        if (utils.isEmptyObjDiff(diff))
            return;

        //вычисление критерия сохранения целиком
        let forceSaveRecent = JSON.stringify(diff).length > 2000;
        if (!forceSaveRecent && itemKey) {
            if (!this.sameKeyCount)
                this.sameKeyCount = 0;
            if (this.prevItemKey == itemKey)
                this.sameKeyCount++;

            forceSaveRecent = this.sameKeyCount > 5 && (Object.keys(diff.change).length > 1);

            this.sameKeyCount = (!forceSaveRecent ? this.sameKeyCount : 0);
            this.prevItemKey = itemKey;
        }

        this.recentDiff = diff;
        this.savingRecent = true;        
        try {
            if (forceSaveRecent) {//сохраняем recent целиком
                let result = {state: ''};

                try {
                    result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}, recentDiff: {rev: bm.recentDiffRev + 1, data: {}}});
                } catch(e) {
                    this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
                }

                if (result.state == 'reject') {
                    await this.loadRecent(true, false);
                    this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
                } else if (result.state == 'success') {
                    this.oldRecent = _.cloneDeep(bm.recent);
                    this.recentDiff = null;
                    await bm.setRecentRev(bm.recentRev + 1);
                    await bm.setRecentDiffRev(bm.recentDiffRev + 1);
                }
            } else {//сохраняем только дифф
                let result = {state: ''};

                try {
                    result = await this.storageSet({recentDiff: {rev: bm.recentDiffRev + 1, data: this.recentDiff}});
                } catch(e) {
                    this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
                }

                if (result.state == 'reject') {
                    await this.loadRecent(true, false);
                    this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
                } else if (result.state == 'success') {
                    await bm.setRecentDiffRev(bm.recentDiffRev + 1);
                }
            }
        } finally {
            this.savingRecent = false;
        }
    }

    async storageCheck(items) {
        return await this.storageApi('check', items);
    }

    async storageGet(items) {
        return await this.storageApi('get', items);
    }

    async storageSet(items, force) {
        return await this.storageApi('set', items, force);
    }

    async storageApi(action, items, force) {
        const request = {action, items};
        if (force)
            request.force = true;
        const encodedRequest = await this.encodeStorageItems(request);
        return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
    }

    async encodeStorageItems(request) {
        if (!this.hashedStorageKey)
            throw new Error('hashedStorageKey is empty');

        if (!_.isObject(request.items))
            throw new Error('items is not an object');

        let result = Object.assign({}, request);
        let items = {};
        for (const id of Object.keys(request.items)) {
            const item = request.items[id];
            if (request.action == 'set' && !_.isObject(item.data))
                throw new Error('encodeStorageItems: data is not an object');

            let encoded = Object.assign({}, item);

            if (item.data) {
                const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
                let encrypted = null;
                try {
                    encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
                } catch (e) {
                    throw new Error('encrypt failed');
                }
                encoded.data = '1' + utils.toBase64(encrypted);
            }
            items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
        }

        result.items = items;
        return result;
    }

    async decodeStorageItems(response) {
        if (!this.hashedStorageKey)
            throw new Error('hashedStorageKey is empty');

        let result = Object.assign({}, response);
        let items = {};
        if (response.items) {
            if (!_.isObject(response.items))
                throw new Error('items is not an object');

            for (const id of Object.keys(response.items)) {
                const item = response.items[id];
                let decoded = Object.assign({}, item);
                if (item.data) {
                    if (!_.isString(item.data) || !item.data.length)
                        throw new Error('decodeStorageItems: data is not a string');
                    if (item.data[0] !== '1')
                        throw new Error('decodeStorageItems: unknown data format');

                    const a = utils.fromBase64(item.data.substr(1));
                    let decrypted = null;
                    try {
                        decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
                    } catch (e) {
                        throw new Error('decrypt failed');
                    }
                    decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
                }

                const ids = id.split('.');
                if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
                    throw new Error(`decodeStorageItems: bad id - ${id}`);
                items[utils.fromBase58(ids[1])] = decoded;
            }
        }

        result.items = items;
        return result;
    }
}
//-----------------------------------------------------------------------------
</script>
