import { noop } from "../../core/function/noop.js";
import { stringify } from "../../core/json/JsonUtils.js";
import { Option } from "./Option.js";
import { OptionAbstract } from "./OptionAbstract.js";

function isOption(c) {
    return typeof c.read === "function";
}

export class OptionGroup extends OptionAbstract {

    /**
     *
     * @type {Array<OptionGroup|Option>}
     */
    children = [];

    /**
     *
     * @param {string[]} path
     * @returns {OptionGroup|Option}
     * @throws when path could not be resolved
     */
    resolve(path) {
        let n = this;

        for (let i = 0; i < path.length; i++) {
            const id = path[i];

            const current = n;

            n = current.children.find(c => c.id === id);

            if (n === undefined) {
                throw new Error(`Failed to resolve path at '${id}'[${i}], in path: [${path.join(', ')}], available options:[${current.children.map(c => c.id).join(', ')}]`);
            }
        }

        return n;
    }

    /**
     *
     * @param {Option|OptionGroup} el
     * @returns {boolean}
     */
    addChild(el) {
        if (this.getChildById(el.id) !== undefined) {
            console.error(`Option '${el.id}' already exists, new option is now added`);
            return false;
        }

        this.children.push(el);
        el.parent = this;

        return true;
    }

    /**
     *
     * @param {string} id
     * @returns {undefined|OptionGroup|Option}
     */
    getChildById(id) {
        return this.children.find(c => c.id === id);
    }

    /**
     *
     * @param id
     * @param read
     * @param write
     * @param settings
     * @returns {OptionGroup}
     */
    add(id, read, write, settings) {
        let option = new Option(id, read, write, settings);
        this.addChild(option);
        return this;
    }

    /**
     *
     * @param id
     * @returns {OptionGroup}
     */
    addGroup(id) {
        let group = new OptionGroup(id);
        this.addChild(group);
        return group;
    }

    toJSON() {
        const result = {};
        this.children.forEach((c) => {
            if (c.isTransient) {
                //skip transient options
                return;
            }

            result[c.id] = c.toJSON();
        });

        return result;
    }

    fromJSON(json) {
        this.children.forEach(c => {
            if (json.hasOwnProperty(c.id)) {
                c.fromJSON(json[c.id]);
            }
        });
    }

    /**
     *
     * @param {function(Option)} visitor
     * @param {*} [thisArg]
     */
    traverseOptions(visitor, thisArg) {
        this.children.forEach(function (c) {
            if (isOption(c)) {

                visitor.call(thisArg, c);

            } else {
                c.traverseOptions(visitor, thisArg);
            }
        })
    }

    /**
     *
     * @param {string} path
     * @param {Storage} storage
     * @returns {Promise}
     */
    attachToStorage(path, storage) {
        const group = this;

        const key = path;

        const p = new Promise(function (resolve, reject) {
            storage.load(key, resolve, reject);
        });


        function store() {
            const json = group.toJSON();

            const value = stringify(json);

            storage.store(key, value, noop, console.error, noop);
        }

        return p
            .then((loaded) => {
                if (loaded === undefined) {
                    // no loaded data, assume options are loaded for hte first time
                    return;
                }

                if (typeof loaded !== "string") {
                    console.error(`expected loaded options to be a string, instead was '${typeof loaded}'`);
                    return;
                }

                group.fromJSON(JSON.parse(loaded));
            })
            .finally(() => {

                group.traverseOptions(op => op.on.written.add(store));
            });

    }
}


/**
 * @readonly
 * @type {boolean}
 */
OptionGroup.prototype.isOptionGroup = true;
