import { assert } from "../../../assert.js";
import { abstractJSONDeserializer } from "../../../json/abstractJSONDeserializer.js";
import { NodeInstance } from "../node/NodeInstance.js";
import { NodeRegistry } from "../node/NodeRegistry.js";

/**
 *
 * @param {NodeInstance} node
 * @param {*} json
 * @param {Map<string, function>} deserializers
 */
function nodeParametersFromJSON(node, json, deserializers) {

    for (const key in json) {

        const jParameter = json[key];

        const value = abstractJSONDeserializer(jParameter, deserializers);

        node.setParameterValue(key, value);

    }

}

/**
 *
 * @param {[]} j_descriptions
 * @param {Map<string,function>} deserializers
 * @param {NodeRegistry} node_registry
 * @param {boolean} allow_node_id_collisions
 * @returns {Promise<NodeDescription[]>}
 */
async function deserializeNodeDescriptors(j_descriptions, deserializers, node_registry, allow_node_id_collisions) {
    /**
     *
     * @type {NodeDescription[]}
     */
    const result = [];

    /**
     *
     * @type {{index:number, promise:Promise<NodeDescription>}[]}
     */
    const deferred = [];

    /**
     *
     * @type {NodeDescription[]}
     */
    const deserialized = [];


    const node_description_count = j_descriptions.length;

    // parse node result
    for (let i = 0; i < node_description_count; i++) {
        const jDescription = j_descriptions[i];

        const result = abstractJSONDeserializer(jDescription, deserializers);

        if (result instanceof Promise) {
            deferred.push({
                index: i,
                promise: result
            });
        } else {
            deserialized[i] = result;
        }

    }

    if (deferred.length > 0) {
        const resolved = await Promise.all(deferred.map(m => m.promise));

        for (let i = 0; i < deferred.length; i++) {
            const node_description = resolved[i];

            const spec = deferred[i];

            deserialized[spec.index] = node_description;
        }
    }

    for (let i = 0; i < node_description_count; i++) {

        const loaded_node = deserialized[i];

        if (
            typeof loaded_node !== "object"
            || loaded_node.isNodeDescription !== true
        ) {
            const jDescription = j_descriptions[i];

            throw new Error(`Deserialized object was expected to be a NodeDescription, instead got something else. Source JSON[${i}]: ${jDescription}`);
        }

        const existing_node = node_registry.getNode(loaded_node.id);

        if (existing_node === undefined) {

            result[i] = loaded_node;

            // make sure to register the node
            node_registry.addNode(loaded_node);

        } else {

            if (!existing_node.equals(loaded_node)) {

                // collision exists

                if (allow_node_id_collisions) {
                    result[i] = loaded_node;
                } else {
                    // illegal collision
                    throw new Error(`Loaded node does not match the one in the registry. Loaded: ${loaded_node}, Existing: ${existing_node}`);
                }

            } else {

                // re-use node from registry
                result[i] = existing_node;
            }
        }
    }
    return result;
}

/**
 *
 * @param {NodeGraph} graph
 * @param {{nodes?:[], connections?:[], descriptions?:[]}} json
 * @param {NodeRegistry} [node_registry]
 * @param {Map<string, function>} [deserializers]
 * @param {boolean} [allow_node_id_collisions]
 */
export async function deserializeNodeGraphFromJSON({
                                                       graph,
                                                       json,
                                                       node_registry = new NodeRegistry(),
                                                       deserializers = new Map(),
                                                       allow_node_id_collisions = false
                                                   }) {

    const j_nodes = json.nodes ?? [];
    const j_connections = json.connections ?? [];
    const j_descriptions = json.descriptions ?? [];

    assert.isArray(j_nodes, 'json.nodes');
    assert.isArray(j_connections, 'json.connections');
    assert.isArray(j_descriptions, 'json.descriptions');

    graph.reset();

    const descriptions = await deserializeNodeDescriptors(
        j_descriptions,
        deserializers,
        node_registry,
        allow_node_id_collisions
    );

    // parse nodes
    const node_count = j_nodes.length;

    for (let i = 0; i < node_count; i++) {
        const jNode = j_nodes[i];

        const node_id = jNode.id;

        assert.isNonNegativeInteger(node_id, 'node.id');

        const description_index = jNode.description;

        const jParameters = jNode.parameters;

        // get node description
        const node_description = descriptions[description_index];

        if (node_description === undefined) {
            throw new Error(`Index ${description_index} not found in .description`);
        }

        const nodeInstance = new NodeInstance();

        nodeInstance.id = node_id;
        nodeInstance.setDescription(node_description);

        nodeParametersFromJSON(nodeInstance, jParameters, deserializers);

        graph.addNode(nodeInstance);
    }

    // parse connections
    const connection_count = j_connections.length;

    for (let i = 0; i < connection_count; i++) {
        const jConnection = j_connections[i];

        const jSource = jConnection.source;
        const jTarget = jConnection.target;

        graph.createConnection(jSource.instance, jSource.port, jTarget.instance, jTarget.port);
    }
}
