const isStr = require('./isStr');
const isObj = require('./isObj');
const each = require('./each');
const Emitter = require('./Emitter');
const toArr = require('./toArr');
const unique = require('./unique');
const concat = require('./concat');
const keys = require('./keys');

const fs = require('fs');

exports = Emitter.extend({
    initialize: function FileBlobStore(path, data) {
        this.callSuper(Emitter, 'initialize', arguments);
        this._path = path;
        this._mapPath = path + '.MAP';
        this._lockPath = path + '.LOCK';
        this._data = data || {};

        let storedBlob = Buffer.alloc(0);
        let storedBlobMap = {};
        if (fs.existsSync(path) && fs.existsSync(this._mapPath)) {
            try {
                storedBlob = fs.readFileSync(path);
                storedBlobMap = JSON.parse(fs.readFileSync(this._mapPath));
            } catch (e) {
                storedBlobMap = {};
                storedBlob = Buffer.alloc(0);
            }
        }
        this._storedBlob = storedBlob;
        this._storedBlobMap = storedBlobMap;
    },
    set(key, buf) {
        let data;

        if (isStr(key)) {
            data = {};
            data[key] = buf;
        } else if (isObj(key)) {
            data = key;
        }

        each(data, (buf, key) => {
            const oldBuf = this.get(key);
            this._data[key] = buf;
            this.emit('change', key, buf, oldBuf);
        });
    },
    get(key) {
        if (isStr(key)) {
            return this._get(key);
        }

        const ret = {};
        each(key, val => {
            ret[val] = this._get(val);
        });

        return ret;
    },
    remove(key) {
        key = toArr(key);
        const data = this._data;
        const storedBlobMap = this._storedBlobMap;

        each(key, val => {
            delete data[val];
            delete storedBlobMap[val];
        });
    },
    clear() {
        this._data = {};
        this._storedBlob = Buffer.alloc(0);
        this._storedBlobMap = {};
    },
    each(fn) {
        const allKeys = this._getKeys();

        each(allKeys, key => {
            fn(this._get(key), key);
        });
    },
    save() {
        const dump = this._getDump();
        const blobToStore = Buffer.concat(dump[0]);
        const mapToStore = JSON.stringify(dump[1]);

        let lock = false;
        try {
            fs.writeFileSync(this._lockPath, 'LOCK', { flag: 'wx' });
            lock = true;

            fs.writeFileSync(this._path, blobToStore);
            fs.writeFileSync(this._mapPath, mapToStore);
        } catch (error) {
            if (error.code !== 'EEXIST') {
                throw error;
            }
        } finally {
            if (lock) {
                fs.unlinkSync(this._lockPath);
            }
        }
    },
    _get(key) {
        return this._data[key] || this._getFromStorage(key);
    },
    _getFromStorage(key) {
        if (!this._storedBlobMap[key]) {
            return;
        }

        const [start, end] = this._storedBlobMap[key];
        return this._storedBlob.slice(start, end);
    },
    _getDump() {
        const buffers = [];
        const blobMap = {};
        let curBufStart = 0;

        function dump(buf, key) {
            buffers.push(buf);
            const len = buf.length;
            blobMap[key] = [curBufStart, curBufStart + len];
            curBufStart += len;
        }

        this.each(dump);

        return [buffers, blobMap];
    },
    _getKeys() {
        return unique(concat(keys(this._data), keys(this._storedBlobMap)));
    }
});

module.exports = exports;