/**
 * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import { isArray, isObject } from "../types/is.mjs";
import { Node } from "../types/node.mjs";
import { NodeList } from "../types/nodelist.mjs";
import { assembleParts } from "./buildmap.mjs";
import { extend } from "./extend.mjs";

export { buildTree };

/**
 * @private
 * @type {symbol}
 */
const parentSymbol = Symbol("parent");

/**
 * @private
 * @type {symbol}
 */
const rootSymbol = Symbol("root");

/**
 * @typedef {Object} buildTreeOptions
 * @property {array} options.rootReferences=[null, undefined] defines the values for elements without parents
 * @property {Monster.Data~exampleFilterCallback} options.filter filtering of the values
 * @memberOf Monster.Data
 */

/**
 * With the help of the function `buildTree()`, nodes can be easily created from data objects.
 *
 * @param {*} subject
 * @param {string|Monster.Data~exampleSelectorCallback} selector
 * @param {string} idKey
 * @param {string} parentIDKey
 * @param {buildTreeOptions} [options]
 * @return {*}
 * @memberOf Monster.Data
 * @throws {TypeError} value is neither a string nor a function
 * @throws {TypeError} the selector callback must return a map
 * @throws {Error} the object has no value for the specified id
 * @license AGPLv3
 * @since 1.26.0
 */
function buildTree(subject, selector, idKey, parentIDKey, options) {
    const nodes = new Map();

    if (!isObject(options)) {
        options = {};
    }

    options = extend(
        {},
        {
            rootReferences: [null, undefined],
            filter: undefined,
        },
        options,
    );

    const filter = options?.filter;
    let rootReferences = options.rootReferences;
    if (!isArray(rootReferences)) {
        rootReferences = [rootReferences];
    }

    const childMap = assembleParts(subject, selector, filter, function (o, k, m) {
        const key = o?.[idKey];
        let ref = o?.[parentIDKey];
        if (rootReferences.indexOf(ref) !== -1) ref = rootSymbol;

        if (key === undefined) {
            throw new Error("the object has no value for the specified id");
        }

        o[parentSymbol] = ref;

        const node = new Node(o);
        this.has(ref) ? this.get(ref).add(node) : this.set(ref, new NodeList().add(node));
        nodes.set(key, node);
    });

    nodes.forEach((node) => {
        let id = node?.["value"]?.[idKey];

        if (childMap.has(id)) {
            node.childNodes = childMap.get(id);
            childMap.delete(id);
        }
    });

    const list = new NodeList();

    childMap.forEach((s) => {
        if (s instanceof Set) {
            s.forEach((n) => {
                list.add(n);
            });
        }
    });

    return list;
}