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

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

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

const bmMetaStore = localForage.createInstance({
    name: 'bmMetaStore'
});

const bmDataStore = localForage.createInstance({
    name: 'bmDataStore'
});

const bmRecentStore = localForage.createInstance({
    name: 'bmRecentStore'
});

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

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

        this.recentLast = await bmRecentStore.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;
            }
        }

        this.recentChanged = true;

        this.loadStored();//no await
    }

    //Долгая асинхронная загрузка из хранилища.
    //Хранение в отдельных записях дает относительно
    //нормальное поведение при нескольких вкладках с читалкой в браузере.
    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);
                }
            }
        }

        let key = null;
        len = await bmRecentStore.length();
        for (let i = len - 1; i >= 0; i--) {
            key = await bmRecentStore.key(i);
            if (key) {
                let r = await bmRecentStore.getItem(key);
                if (_.isObject(r) && r.key) {
                    this.recent[r.key] = r;
                }
            } else  {
                await bmRecentStore.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 this.cleanBooks();
        await this.cleanRecentBooks();

        this.recentChanged = true;
        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 = 128*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 = 64*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 bmDataStore.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 bmDataStore.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 setRecentBook(value) {
        const result = this.metaOnly(value);
        result.touchTime = Date.now();
        result.deleted = 0;

        if (this.recent[result.key] && this.recent[result.key].deleted) {
            //восстановим из небытия пользовательские данные
            if (!result.bookPos)
                result.bookPos = this.recent[result.key].bookPos;
            if (!result.bookPosSeen)
                result.bookPosSeen = this.recent[result.key].bookPosSeen;
        }

        this.recent[result.key] = result;

        await bmRecentStore.setItem(result.key, result);

        this.recentLast = result;
        await bmRecentStore.setItem('recent-last', this.recentLast);

        this.recentChanged = true;
        this.emit('recent-changed', result.key);
        return result;
    }

    async getRecentBook(value) {
        let result = this.recent[value.key];
        if (!result) {
            result = await bmRecentStore.getItem(value.key);
            if (result)
                this.recent[value.key] = result;
        }
        return result;
    }

    async delRecentBook(value) {
        this.recent[value.key].deleted = 1;
        await bmRecentStore.setItem(value.key, this.recent[value.key]);

        if (this.recentLast.key == value.key) {
            this.recentLast = null;
            await bmRecentStore.setItem('recent-last', this.recentLast);
        }
        this.emit('recent-deleted', value.key);
        this.emit('recent-changed', value.key);
    }

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

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

        this.sortedRecentCached = null;

        if (isDel)
            this.emit('recent-changed');
        return isDel;
    }

    mostRecentBook() {
        if (this.recentLast) {
            return this.recentLast;
        }
        const oldRecentLast = this.recentLast;

        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;
            }
        }
        this.recentLast = result;
        bmRecentStore.setItem('recent-last', this.recentLast);//no await

        if (this.recentLast !== oldRecentLast)
            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];
        }
        
        //"ленивое" обновление хранилища
        (async() => {
            for (const rec of Object.values(mergedRecent)) {
                if (rec.key) {
                    await bmRecentStore.setItem(rec.key, rec);
                    await utils.sleep(1);
                }
            }
        })();

        this.recent = mergedRecent;

        this.recentLast = null;
        await bmRecentStore.setItem('recent-last', this.recentLast);

        this.recentChanged = true;
        this.emit('set-recent');
        this.emit('recent-changed');
    }

    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();