
import Index from "./index.js";
import Document from "./document.js";
import { KeystoreMap, KeystoreSet } from "./keystore.js";
import { is_string } from "./common.js";

const chunk_size_reg = 250000,
      chunk_size_map = 5000,
      chunk_size_ctx = 1000;


/**
 * @param {Map<IntermediateSearchResults>|KeystoreMap<IntermediateSearchResults>} map
 * @param {number=} size
 * @return {Array<Object>}
 */
function map_to_json(map, size = 0) {
    let chunk = [],
        json = [];

    if (size) {
        size = 0 | chunk_size_map * (chunk_size_reg / size);
    }
    for (const item of map.entries()) {
        json.push(item);
        if (json.length === size) {
            chunk.push(json);
            json = [];
        }
    }
    json.length && chunk.push(json);
    return chunk;
}

/**
 * @param {Array<Object>} json
 * @param {Map<IntermediateSearchResults>|KeystoreMap<IntermediateSearchResults>} map
 * @return {Map<IntermediateSearchResults>|KeystoreMap<IntermediateSearchResults>}
 */
function json_to_map(json, map) {
    map || (map = new Map());
    for (let i = 0, entry; i < json.length; i++) {
        entry = json[i];
        map.set(entry[0], entry[1]);
    }
    return (/** @type {Map} */map
    );
}

/**
 * @param {Map<Map<IntermediateSearchResults>>|KeystoreMap<KeystoreMap<IntermediateSearchResults>>} ctx
 * @param {number=} size
 * @return {Array<Object>}
 */
function ctx_to_json(ctx, size = 0) {
    let chunk = [],
        json = [];

    if (size) {
        size = 0 | chunk_size_ctx * (chunk_size_reg / size);
    }
    for (const item of ctx.entries()) {
        const key = item[0],
              value = item[1];

        json.push([key, map_to_json(value)[0] || []]);
        if (json.length === size) {
            chunk.push(json);
            json = [];
        }
    }
    json.length && chunk.push(json);
    return chunk;
}

/**
 * @param {Array<Object>} json
 * @param {Map<Map<IntermediateSearchResults>>|KeystoreMap<KeystoreMap<IntermediateSearchResults>>} ctx
 * @return {Map<Map<IntermediateSearchResults>>|KeystoreMap<KeystoreMap<IntermediateSearchResults>>}
 */
function json_to_ctx(json, ctx) {
    ctx || (ctx = new Map());
    for (let i = 0, entry, map; i < json.length; i++) {
        entry = json[i];
        map = ctx.get(entry[0]);
        ctx.set(entry[0], json_to_map(entry[1], map));
    }
    return ctx;
}

/**
 * @param {
 *   Set<string|number>|
 *   Map<Array<string|number>>|
 *   KeystoreSet<string|number>|
 *   KeystoreMap<Array<string|number>>
 * } reg
 * @return {Array<Array<string|number>>}
 */
function reg_to_json(reg) {
    let chunk = [],
        json = [];

    for (const key of reg.keys()) {
        json.push(key);
        if (json.length === chunk_size_reg) {
            chunk.push(json);
            json = [];
        }
    }
    json.length && chunk.push(json);
    return chunk;
}

/**
 * @param {Array<string|number>} json
 * @param {
 *   Set<string|number>|
 *   Map<Array<string|number>>|
 *   KeystoreSet<string|number>|
 *   KeystoreMap<Array<string|number>>
 * } reg
 * @return {
 *   Set<string|number>|
 *   KeystoreSet<string|number>
 * }
 */
function json_to_reg(json, reg) {
    reg || (reg = new Set());
    for (let i = 0; i < json.length; i++) {
        reg.add(json[i]);
    }
    return (/** @type {Set<string|number>} */reg
    );
}

/**
 *
 * @param {function(string, string):Promise|void} callback
 * @param {string|null|void} field
 * @param {string} key
 * @param {Array|null} chunk
 * @param {number} index_doc
 * @param {number} index_obj
 * @param {number=} index_prt
 * @this {Index|Document}
 * @return {Promise}
 */
function save(callback, field, key, chunk, index_doc, index_obj, index_prt = 0) {
    const is_arr = chunk && chunk.constructor === Array,
          data = is_arr ? chunk.shift() : chunk;

    if (!data) {
        return this.export(callback, field, index_doc, index_obj + 1);
    }

    const res = callback((field ? field + "." : "") + (index_prt + 1) + "." + key, JSON.stringify(data));

    if (res && res.then) {
        const self = this;
        return res.then(function () {
            return save.call(self, callback, field, key, is_arr ? chunk : null, index_doc, index_obj, index_prt + 1);
        });
    }

    return save.call(this, callback, field, key, is_arr ? chunk : null, index_doc, index_obj, index_prt + 1);
}

/**
 * @param {function(string,string):Promise|void} callback
 * @param {!string|null=} _field
 * @param {number=} _index_doc
 * @param {number=} _index_obj
 * @this {Index}
 */

export function exportIndex(callback, _field, _index_doc = 0, _index_obj = 0) {

    let key, chunk;

    switch (_index_obj) {

        case 0:

            key = "reg";
            chunk = reg_to_json(this.reg);
            break;

        case 1:

            key = "cfg";
            chunk = null;
            break;

        case 2:

            key = "map";
            chunk = map_to_json(this.map, this.reg.size);
            break;

        case 3:

            key = "ctx";
            chunk = ctx_to_json(this.ctx, this.reg.size);
            break;

        default:

            return;
    }

    return save.call(this, callback, _field, key, chunk, _index_doc, _index_obj);
}

/**
 * @param {string} key
 * @param {string|Array<Object>=} data
 * @this Index
 */

export function importIndex(key, data) {

    if (!data) {
        return;
    }
    if ("string" == typeof data) {
        data = /** @type {Array<Object>} */JSON.parse( /** @type {string} */data);
    }

    const split = key.split(".");
    if ("json" === split[split.length - 1]) {
        split.pop();
    }
    if (3 === split.length) {
        split.shift();
    }
    key = 1 < split.length ? split[1] : split[0];

    switch (key) {

        case "cfg":

            break;

        case "reg":

            this.fastupdate = !1;
            this.reg = json_to_reg( /** @type {Array<string|number>} */data, this.reg);
            break;

        case "map":

            this.map = json_to_map(data, this.map);
            break;

        case "ctx":

            this.ctx = json_to_ctx(data, this.ctx);
            break;
    }
}

/**
 * @param {function(string,string):Promise|void} callback
 * @param {string|null=} _field
 * @param {number=} _index_doc
 * @param {number=} _index_obj
 * @this {Document}
 */

export function exportDocument(callback, _field, _index_doc = 0, _index_obj = 0) {

    if (_index_doc < this.field.length) {
        const field = this.field[_index_doc],
              idx = this.index.get(field),
              res = idx.export(callback, field, _index_doc, _index_obj = 1);


        if (res && res.then) {
            const self = this;
            return res.then(function () {
                return self.export(callback, field, _index_doc + 1);
            });
        }

        return this.export(callback, field, _index_doc + 1);
    } else {

        let key, chunk;

        switch (_index_obj) {

            case 0:

                key = "reg";
                chunk = reg_to_json(this.reg);
                _field = null;
                break;

            case 1:

                key = "tag";
                chunk = this.tag && ctx_to_json(this.tag, this.reg.size);
                _field = null;
                break;

            case 2:

                key = "doc";
                chunk = this.store && map_to_json(
                /** @type {Map<IntermediateSearchResults>} */this.store);
                _field = null;
                break;

            default:

                return;
        }

        return save.call(this, callback, _field, key,
        /** @type {Array|null} */chunk || null, _index_doc, _index_obj);
    }
}

/**
 * @param {!string} key
 * @param {string|Array<Object>} data
 * @this {Document}
 */

export function importDocument(key, data) {

    const split = key.split(".");
    if ("json" === split[split.length - 1]) {
        split.pop();
    }
    const field = 2 < split.length ? split[0] : "",
          ref = 2 < split.length ? split[2] : split[1];


    if (this.worker && field) {
        return this.index.get(field).import(key);
    }

    if (!data) {
        return;
    }
    if ("string" == typeof data) {
        data = /** @type {Array<Object>} */JSON.parse( /** @type {string} */data);
    }

    if (!field) {

        switch (ref) {

            case "reg":

                this.fastupdate = !1;
                this.reg = json_to_reg( /** @type {Array<string|number>} */data, this.reg);

                for (let i = 0, idx; i < this.field.length; i++) {
                    idx = this.index.get(this.field[i]);
                    idx.fastupdate = !1;
                    idx.reg = this.reg;
                }

                if (this.worker) {
                    const promises = [],
                          self = this;


                    for (const index of this.index.values()) {

                        promises.push(index.import(key));
                    }

                    return Promise.all(promises);
                }

                break;

            case "tag":

                this.tag = json_to_ctx(data, this.tag);
                break;

            case "doc":

                this.store = json_to_map(data, this.store);
                break;

            case "cfg":

                break;

        }
    } else {

        return this.index.get(field).import(ref, data);
    }
}

/*
reg: "1,2,3,4,5,6,7,8,9"
map: "gulliver:1,2,3|4,5,6|7,8,9;"
ctx: "gulliver+travel:1,2,3|4,5,6|7,8,9;"
*/

/**
 * @this {Index}
 * @param {boolean} withFunctionWrapper
 * @return {string}
 */

export function serialize(withFunctionWrapper = !0) {
    let reg = '',
        map = '',
        ctx = '';


    if (this.reg.size) {

        let type;
        for (const key of this.reg.keys()) {
            type || (type = typeof key);
            reg += (reg ? ',' : '') + ("string" === type ? '"' + key + '"' : key);
        }
        reg = 'index.reg=new Set([' + reg + ']);';

        map = parse_map(this.map, type);
        map = "index.map=new Map([" + map + "]);";

        for (const context of this.ctx.entries()) {
            const key_ctx = context[0],
                  value_ctx = context[1];

            let ctx_map = parse_map(value_ctx, type);
            ctx_map = "new Map([" + ctx_map + "])";
            ctx_map = '["' + key_ctx + '",' + ctx_map + ']';
            ctx += (ctx ? ',' : '') + ctx_map;
        }
        ctx = "index.ctx=new Map([" + ctx + "]);";
    }

    return withFunctionWrapper ? "function inject(index){" + reg + map + ctx + "}" : reg + map + ctx;
}

function parse_map(map, type) {
    let result = '';
    for (const item of map.entries()) {
        const key = item[0],
              value = item[1];

        let res = '';
        for (let i = 0, ids; i < value.length; i++) {
            ids = value[i] || [''];
            let str = '';
            for (let j = 0; j < ids.length; j++) {
                str += (str ? ',' : '') + ("string" === type ? '"' + ids[j] + '"' : ids[j]);
            }
            str = '[' + str + ']';
            res += (res ? ',' : '') + str;
        }
        res = '["' + key + '",[' + res + ']]';
        result += (result ? ',' : '') + res;
    }
    return result;
}