import localForage from 'localforage';
import _ from 'lodash';

import * as utils from '../../../share/utils';
import BookParser from './BookParser';

const maxDataSize = 500*1024*1024;//compressed bytes

//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
    name: 'bmMetaStore'
});

//локальный кэш самих книг, ограничение maxDataSize
const bmDataStore = localForage.createInstance({
    name: 'bmDataStore'
});

//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
    name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
    name: 'bmRecentStoreNew'
});

class BookManager {
    async init(settings) {
        this.loaded = false;
        this.settings = settings;

        this.eventListeners = [];
        this.books = {};

        this.recent = {};
        this.saveRecent = _.debounce(() => {
            bmRecentStoreNew.setItem('recent', this.recent);
        }, 300, {maxWait: 800});

        this.saveRecentItem = _.debounce(() => {
            bmRecentStoreNew.setItem('recent-item', this.recentItem);
            this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
            bmRecentStoreNew.setItem('rev', this.recentRev);
        }, 200, {maxWait: 300});

        //загрузка bmRecentStore
        this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
        if (this.recentRev) {
            this.recent = await bmRecentStoreNew.getItem('recent');
            if (!this.recent)
                this.recent = {};

            this.recentItem = await bmRecentStoreNew.getItem('recent-item');
            if (this.recentItem)
                this.recent[this.recentItem.key] = this.recentItem;

            this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
            if (this.recentLastKey) {
                const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
                if (_.isObject(meta)) {
                    this.books[meta.key] = meta;
                }
            }

            await this.cleanRecentBooks();

            //TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
            {
                await this.convertFileToDiskPrefix();
                if (this.recentRev > 10)
                    await bmRecentStoreOld.clear();
            }
        } else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
            this.recentLast = await bmRecentStoreOld.getItem('recent-last');
            if (this.recentLast) {
                this.recent[this.recentLast.key] = this.recentLast;
                const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
                if (_.isObject(meta)) {
                    this.books[meta.key] = meta;
                }
            }

            let key = null;
            const len = await bmRecentStoreOld.length();
            for (let i = len - 1; i >= 0; i--) {
                key = await bmRecentStoreOld.key(i);
                if (key) {
                    let r = await bmRecentStoreOld.getItem(key);
                    if (_.isObject(r) && r.key) {
                        this.recent[r.key] = r;
                    }
                } else  {
                    await bmRecentStoreOld.removeItem(key);
                }
            }

            //размножение для дебага
            /*if (key) {
                for (let i = 0; i < 1000; i++) {
                    const k = this.keyFromUrl(i.toString());
                    this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
                }
            }*/

            await bmRecentStoreNew.setItem('recent', this.recent);
            this.recentRev = 1;
            await bmRecentStoreNew.setItem('rev', this.recentRev);
        }

        this.recentChanged = true;

        this.loadStored();//no await
    }

    //Ленивая асинхронная загрузка bmMetaStore
    async loadStored() {
        //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
        await utils.sleep(2000);

        let len = await bmMetaStore.length();
        for (let i = len - 1; i >= 0; i--) {
            const key = await bmMetaStore.key(i);
            const keySplit = key.split('-');

            if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
                let meta = await bmMetaStore.getItem(key);

                if (_.isObject(meta)) {
                    //уже может быть распарсена книга
                    const oldBook = this.books[meta.key];
                    this.books[meta.key] = meta;

                    if (oldBook && oldBook.parsed) {
                        this.books[meta.key].parsed = oldBook.parsed;
                    }
                } else {
                    await bmMetaStore.removeItem(key);
                }
            }
        }

        await this.cleanBooks();
        this.loaded = true;
        this.emit('load-stored-finish');
    }

    async cleanBooks() {
        while (1) {// eslint-disable-line no-constant-condition
            let size = 0;
            let min = Date.now();
            let toDel = null;
            for (let key in this.books) {
                let book = this.books[key];
                const bookLength = (book.length ? book.length : 0);
                size += (book.dataCompressedLength ? book.dataCompressedLength : bookLength);

                if (book.addTime < min) {
                    toDel = book;
                    min = book.addTime;
                }
            }

            if (size > maxDataSize && toDel) {
                await this.delBook(toDel);
            } else {
                break;
            }
        }
    }

    async deflateWithProgress(data, callback) {
        const chunkSize = 512*1024;
        const deflator = new utils.pako.Deflate({level: 5});

        let chunkTotal = 1 + Math.floor(data.length/chunkSize);
        let chunkNum = 0;
        let perc = 0;
        let prevPerc = 0;

        for (var i = 0; i < data.length; i += chunkSize) {
            if ((i + chunkSize) >= data.length) {
                deflator.push(data.substring(i, i + chunkSize), true);
            } else {
                deflator.push(data.substring(i, i + chunkSize), false);
            }
            chunkNum++;

            perc = Math.round(chunkNum/chunkTotal*100);
            if (perc != prevPerc) {
                callback(perc);
                await utils.sleep(1);
                prevPerc = perc;
            }
        }

        if (deflator.err) {
            throw new Error(deflator.msg);
        }
        
        callback(100);

        return deflator.result;
    }

    async inflateWithProgress(data, callback) {
        const chunkSize = 512*1024;
        const inflator = new utils.pako.Inflate({to: 'string'});

        let chunkTotal = 1 + Math.floor(data.length/chunkSize);
        let chunkNum = 0;
        let perc = 0;
        let prevPerc = 0;

        for (var i = 0; i < data.length; i += chunkSize) {
            if ((i + chunkSize) >= data.length) {
                inflator.push(data.subarray(i, i + chunkSize), true);
            } else {
                inflator.push(data.subarray(i, i + chunkSize), false);
            }
            chunkNum++;

            perc = Math.round(chunkNum/chunkTotal*100);
            if (perc != prevPerc) {
                callback(perc);
                await utils.sleep(1);
                prevPerc = perc;
            }
        }

        if (inflator.err) {
            throw new Error(inflator.msg);
        }
        
        callback(100);

        return inflator.result;
    }

    async addBook(newBook, callback) {        
        let meta = {url: newBook.url, path: newBook.path};
        meta.key = this.keyFromUrl(meta.url);
        meta.addTime = Date.now();

        const cb = (perc) => {
            const p = Math.round(30*perc/100);
            callback(p);
        };

        const cb2 = (perc) => {
            const p = Math.round(30 + 65*perc/100);
            callback(p);
        };

        const result = await this.parseBook(meta, newBook.data, cb);
        result.dataCompressed = true;

        let data = newBook.data;
        if (result.dataCompressed) {
            //data = utils.pako.deflate(data, {level: 5});
            data = await this.deflateWithProgress(data, cb2);
            result.dataCompressedLength = data.byteLength;
        }
        callback(95);

        this.books[meta.key] = result;

        await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
        await bmDataStore.setItem(`bmData-${meta.key}`, data);

        callback(100);
        return result;
    }

    async hasBookParsed(meta) {
        if (!this.books) 
            return false;
        if (!meta.url)
            return false;
        if (!meta.key)
            meta.key = this.keyFromUrl(meta.url);

        let book = this.books[meta.key];

        if (!book && !this.loaded) {
            book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
            if (book)
                this.books[meta.key] = book;
        }

        return !!(book && book.parsed);
    }

    async getBook(meta, callback) {
        let result = undefined;
        if (!meta.key)
            meta.key = this.keyFromUrl(meta.url);

        result = this.books[meta.key];

        if (!result) {
            result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
            if (result)
                this.books[meta.key] = result;
        }

        //Если файл на сервере изменился, считаем, что в кеше его нету
        if (meta.path && result && meta.path != result.path) {
            return;
        }

        if (result && !result.parsed) {
            let data = await bmDataStore.getItem(`bmData-${meta.key}`);
            callback(5);
            await utils.sleep(10);

            let cb = (perc) => {
                const p = 5 + Math.round(15*perc/100);
                callback(p);
            };

            if (result.dataCompressed) {
                try {
                    //data = utils.pako.inflate(data, {to: 'string'});
                    data = await this.inflateWithProgress(data, cb);
                } catch (e) {
                    this.delBook(meta);
                    throw e;
                }
            }
            callback(20);

            cb = (perc) => {
                const p = 20 + Math.round(80*perc/100);
                callback(p);
            };

            result = await this.parseBook(result, data, cb);
            this.books[meta.key] = result;
        }

        return result;
    }

    async delBook(meta) {
        await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
        await bmDataStore.removeItem(`bmData-${meta.key}`);

        delete this.books[meta.key];
    }

    async parseBook(meta, data, callback) {
        const parsed = new BookParser(this.settings);

        const parsedMeta = await parsed.parse(data, callback);
        const result = Object.assign({}, meta, parsedMeta, {
            length: data.length,
            textLength: parsed.textLength,
            parsed
        });

        return result;
    }

    metaOnly(book) {
        let result = Object.assign({}, book);
        delete result.parsed;
        return result;
    }

    keyFromUrl(url) {
        return utils.stringToHex(url);
    }

    //-- recent --------------------------------------------------------------
    async recentSetItem(item = null, skipCheck = false) {
        const rev = await bmRecentStoreNew.getItem('rev');
        if (rev != this.recentRev && !skipCheck) {
            const newRecent = await bmRecentStoreNew.getItem('recent');
            Object.assign(this.recent, newRecent);
            this.recentItem = await bmRecentStoreNew.getItem('recent-item');
            this.recentRev = rev;
        }

        const prevKey = (this.recentItem ? this.recentItem.key : '');
        if (item) {
            this.recent[item.key] = item;
            this.recentItem = item;
        } else {
            this.recentItem = null;
        }

        this.saveRecentItem();

        if (!item || prevKey != item.key) {
            this.saveRecent();
        }

        this.recentChanged = true;

        if (item) {
            this.emit('recent-changed', item.key);
        } else {
            this.emit('recent-changed');
        }
    }

    async recentSetLastKey(key) {
        this.recentLastKey = key;
        await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
    }

    async setRecentBook(value) {
        let result = this.metaOnly(value);
        result.touchTime = Date.now();
        result.deleted = 0;

        if (this.recent[result.key]) {
            result = Object.assign({}, this.recent[result.key], result);
        }

        await this.recentSetLastKey(result.key);
        await this.recentSetItem(result);
        return result;
    }

    async getRecentBook(value) {
        return this.recent[value.key];
    }

    async delRecentBook(value) {
        const item = this.recent[value.key];
        item.deleted = 1;

        if (this.recentLastKey == value.key) {
            await this.recentSetLastKey(null);
        }

        await this.recentSetItem(item);
        this.emit('recent-deleted', value.key);
    }

    async cleanRecentBooks() {
        const sorted = this.getSortedRecent();

        let isDel = false;
        for (let i = 1000; i < sorted.length; i++) {
            delete this.recent[sorted[i].key];
            isDel = true;
        }

        this.sortedRecentCached = null;

        if (isDel)
            await this.recentSetItem();
        return isDel;
    }

    async convertFileToDiskPrefix() {
        let isConverted = false;

        const newRecent = {};
        for (let key of Object.keys(this.recent)) {
            let newKey = key;
            let newUrl = this.recent[key].url;

            if (newKey.indexOf('66696c65') == 0) {
                newKey = newKey.replace(/^66696c65/, '6469736b');
                if (newUrl)
                    newUrl = newUrl.replace(/^file/, 'disk');
                isConverted = true;
            }

            newRecent[newKey] = this.recent[key];
            newRecent[newKey].key = newKey;
            if (newUrl)
                newRecent[newKey].url = newUrl;
        }
        if (isConverted) {
            this.recent = newRecent;
            await this.recentSetItem(null, true);
        }
        return isConverted;
    }

    mostRecentBook() {
        if (this.recentLastKey) {
            return this.recent[this.recentLastKey];
        }
        const oldKey = this.recentLastKey;

        let max = 0;
        let result = null;
        for (let key in this.recent) {
            const book = this.recent[key];
            if (!book.deleted && book.touchTime > max) {
                max = book.touchTime;
                result = book;
            }
        }
        
        const newRecentLastKey = (result ? result.key : null);
        this.recentSetLastKey(newRecentLastKey);//no await

        if (newRecentLastKey !== oldKey)
            this.emit('recent-changed');

        return result;
    }

    getSortedRecent() {
        if (!this.recentChanged && this.sortedRecentCached) {
            return this.sortedRecentCached;
        }

        let result = Object.values(this.recent);

        result.sort((a, b) => b.touchTime - a.touchTime);

        this.sortedRecentCached = result;
        this.recentChanged = false;
        return result;
    }

    async setRecent(value) {
        const mergedRecent = _.cloneDeep(this.recent);

        Object.assign(mergedRecent, value);

        //подстраховка от hotReload
        for (let i of Object.keys(mergedRecent)) {
            if (!mergedRecent[i].key || mergedRecent[i].key !== i)
                delete mergedRecent[i];
        }
        
        this.recent = mergedRecent;

        await this.recentSetLastKey(null);
        await this.recentSetItem(null, true);

        this.emit('set-recent');
    }

    addEventListener(listener) {
        if (this.eventListeners.indexOf(listener) < 0)
            this.eventListeners.push(listener);
    }

    removeEventListener(listener) {
        const i = this.eventListeners.indexOf(listener);
        if (i >= 0)
            this.eventListeners.splice(i, 1);
    }

    emit(eventName, value) {
        if (this.eventListeners) {
            for (const listener of this.eventListeners) {
                //console.log(eventName);
                listener(eventName, value);
            }
        }
    }

}

export default new BookManager();