import { assert } from "../../../assert.js";
import { BitSet } from "../../../binary/BitSet.js";
import { DataType } from "../type/DataType.js";
import { NodeDescription } from "./NodeDescription.js";
import { Port } from "./Port.js";

/**
 *
 * @param {Port} port
 * @return {DataType}
 */
function getPortType(port) {
    return port.dataType;
}

export class NodeRegistry {
    /**
     * ID -> Node mapping
     * @type {Map<number, NodeDescription>}
     */
    nodes = new Map();

    /**
     * ID -> DataType mapping
     * @type {Map<number, DataType>}
     */
    types = new Map();

    /**
     * @returns {NodeDescription[]}
     */
    getNodesAsArray() {
        return Array.from(this.nodes.values());
    }

    /**
     *
     * @param {NodeDescription} node
     * @returns {boolean} true if node was added, false if node already exists
     */
    addNode(node) {
        assert.defined(node, "node");
        assert.notNull(node, "node");
        assert.equal(node.isNodeDescription, true, 'node.isNodeDescription !== true');

        const existing_node = this.nodes.get(node.id);

        if (existing_node !== undefined) {

            if (existing_node !== node && !existing_node.equals(node)) {
                // node with the same ID exists and is not the same node
                throw new Error(`A different node with the same ID ${node.id} is already registered. IDs of all nodes in the registry must be unique`);
            }

            // console.warn(`Node with id ${node.id} already exists`);

            return false;
        }

        //extract types
        node.getPorts().map(getPortType).forEach(this.addType, this);

        this.nodes.set(node.id, node);

        return true;
    }

    /**
     *
     * @param {NodeDescription} node
     * @returns {boolean}
     */
    hasNode(node) {
        assert.defined(node, "node");
        assert.notNull(node, "node");
        assert.equal(node.isNodeDescription, true, 'node.isNodeDescription !== true');

        const existing = this.nodes.get(node.id);

        if (existing === undefined) {
            return false;
        }

        if (existing !== node) {
            return false;
        }

        return true;
    }

    /**
     *
     * @param {NodeDescription} node
     * @returns {boolean}
     */
    removeNode(node) {
        assert.defined(node, "node");
        assert.notNull(node, "node");
        assert.equal(node.isNodeDescription, true, 'node.isNodeDescription !== true');

        if (!this.hasNode(node)) {
            // not found
            return false;
        }

        this.nodes.delete(node.id);

        return true;
    }

    /**
     * Given a class inheriting from NodeDescription, get all registered instances
     * Matching is strict, instances of subclasses are not matched
     * @param {Type<NodeDescription>} Klass
     * @returns {NodeDescription[]}
     */
    getNodesByClass(Klass) {
        const result = [];

        for (const [id, node] of this.nodes) {
            if (node.constructor === Klass) {
                result.push(node);
            }
        }

        return result;
    }

    /**
     *
     * @param {DataType} type
     */
    addType(type) {
        if (this.hasType(type)) {
            return false;
        }

        this.types.set(type.id, type);

        return true;
    }

    /**
     *
     * @param {DataType} type
     * @return {boolean}
     */
    hasType(type) {
        assert.defined(type, 'type');
        assert.notNull(type, 'type');

        return this.types.has(type.id);
    }

    /**
     * @param {number} id
     * @returns {DataType|undefined}
     */
    getTypeById(id) {
        return this.types.get(id);
    }

    /**
     *
     * @returns {number}
     */
    generateTypeId() {
        const ids = new BitSet();

        this.types.forEach((v, id) => ids.set(id, true));

        const id = ids.nextClearBit(0);

        if (id === -1) {
            return 0;
        } else {
            return id;
        }
    }

    /**
     *
     * @param {String} name
     * @returns {DataType}
     */
    createType(name) {
        const dataType = new DataType();

        dataType.name = name;

        dataType.id = this.generateTypeId();

        this.addType(dataType);

        return dataType;
    }

    /**
     *
     * @param {number} id
     * @returns {NodeDescription|undefined}
     */
    getNode(id) {
        return this.nodes.get(id);
    }

    /**
     *
     * @param json
     * @returns {Port}
     */
    parsePort(json) {
        const port = new Port();

        port.dataType = this.types.get(json.dataType);
        port.direction = json.direction;
        port.name = json.name;

        return port;
    }

    /**
     * NOTE: this only works with un-extended node descriptions
     * @param json
     * @returns {NodeDescription}
     */
    parseNode(json) {
        const node = new NodeDescription();

        node.name = json.name;

        const jPorts = json.ports;

        for (const p in jPorts) {
            const jPort = jPorts[p];

            const portId = parseInt(p);

            const port = this.parsePort(jPort);

            port.id = portId;

            node.ports.push(port);
        }

        return node;
    }

    fromJSON(json) {

        const jNodes = json.nodes;
        const jTypes = json.types;

        this.types.clear();
        this.nodes.clear();

        //parse types
        for (const p in jTypes) {
            const id = parseInt(p);

            const dataType = new DataType();

            dataType.id = id;
            dataType.name = jTypes[p];

            this.types.set(id, dataType);
        }

        for (const p in jNodes) {
            const id = parseInt(p);

            const jNode = jNodes[p];

            const node = this.parseNode(jNode);

            node.id = id;

            this.nodes.set(id, node);

        }

    }

    toJSON() {
        //write types
        const types = {};
        for (const [id, type] of this.types) {
            types[id] = type.name;
        }

        const nodes = {};
        for (const [id, node] of this.nodes) {
            const ports = {};

            node.getPorts().forEach(port => {
                ports[port.id] = {
                    dataType: port.dataType.id,
                    direction: port.direction,
                    name: port.name
                };
            });

            nodes[id] = {
                name: node.name,
                ports
            };
        }

        return {
            types,
            nodes
        };
    }
}

/**
 * @readonly
 * @type {boolean}
 */
NodeRegistry.prototype.isNodeRegistry = true;
