/**
 * Supplementary structure to `Graph`, holds edges and neighbour nodes for fast access
 * Used internally by `Graph`
 * @template N
 */
export class NodeContainer {
    /**
     * Node being described
     * @type {N}
     */
    node = null;

    /**
     * Attached edges
     * @type {Edge<N>[]}
     * @private
     */
    __edges = [];

    /**
     *
     * @type {Map<N,number>}
     * Maps neighbour node to number of edges to that node from this one
     * @private
     */
    __neighbors = new Map();

    /**
     * NOTE: this method allocates memory internally
     * @returns {N[]}
     */
    get neighbours() {
        return Array.from(this.__neighbors.keys());
    }

    /**
     *
     * @return {number}
     */
    getEdgeCount() {
        return this.__edges.length;
    }

    /**
     * Do not modify the returned value
     * @return {Edge<N>[]}
     */
    getEdges() {
        return this.__edges;
    }

    /**
     * NOTE: this method allocates memory internally
     * @returns {N[]}
     */
    get inNodes() {
        return this.inEdges.map(e => e.other(this.node));
    }

    /**
     * NOTE: this method allocates memory internally
     * @returns {N[]}
     */
    get outNodes() {
        return this.outEdges.map(e => e.other(this.node));
    }


    /**
     * NOTE: this method allocates memory internally
     * @returns {Edge<N>[]}
     */
    get outEdges() {
        /**
         *
         * @type {Edge<N>[]}
         */
        const result = [];

        this.getOutgoingEdges(result);

        return result;
    }

    /**
     * NOTE: this method allocates memory internally
     * @returns {Edge<N>[]}
     */
    get inEdges() {
        /**
         *
         * @type {Edge<N>[]}
         */
        const result = [];

        this.getIncomingEdges(result);

        return result;
    }


    /**
     *
     * @param {Edge<N>[]} result
     * @returns {number}
     */
    getIncomingEdges(result) {
        const edges = this.__edges;

        const edge_count = edges.length;

        let result_count = 0;

        for (let i = 0; i < edge_count; i++) {
            const edge = edges[i];

            if (edge.isDirectedTowards(this.node)) {

                result.push(edge);
                result_count++;

            }

        }

        return result_count;
    }

    /**
     *
     * @param {Edge<N>[]} result
     * @returns {number}
     */
    getOutgoingEdges(result) {
        const edges = this.__edges;

        const edge_count = edges.length;

        let result_count = 0;

        for (let i = 0; i < edge_count; i++) {
            const edge = edges[i];

            if (edge.isDirectedAwayFrom(this.node)) {

                result.push(edge);
                result_count++;

            }

        }

        return result_count;
    }

    /**
     *
     * @return {number}
     */
    getIncomingEdgeCount() {
        let r = 0;

        const edges = this.__edges;
        const edge_count = edges.length;

        const current_node = this.node;

        for (let i = 0; i < edge_count; i++) {
            const edge = edges[i];

            if (edge.isDirectedTowards(current_node)) {
                r++;
            }
        }

        return r;
    }


    /**
     *
     * @param {N} other
     * @returns {boolean}
     */
    edgeWithNodeExists(other) {

        return this.__neighbors.has(other);

    }

    /**
     *
     * @param {function(Edge<N>)} visitor
     * @param {*} [thisArg]
     * @returns {number}
     */
    traverseEdges(visitor, thisArg) {
        let i = 0;

        const edges = this.__edges;
        const n = edges.length;

        for (; i < n; i++) {
            const edge = edges[i];
            visitor.call(thisArg, edge);
        }

        return i;
    }

    /**
     *
     * @param {N} other
     * @returns {Edge<N>|undefined}
     */
    getAnyEdgeWith(other) {
        if (!this.edgeWithNodeExists(other)) {
            return undefined;
        }


        /**
         *
         * @type {Edge[]}
         */
        const edges = this.__edges;

        const n = edges.length;

        for (let i = 0; i < n; i++) {
            const edge = edges[i];
            if (edge.contains(other)) {
                return edge;
            }
        }

        return undefined;
    }

    /**
     * Undirected edges don't count
     * @param {N} other
     * @returns {Edge<N>|undefined}
     */
    getAnyDirectionEdgeTo(other) {

        /**
         *
         * @type {Edge[]}
         */
        const edges = this.__edges;

        const n = edges.length;

        for (let i = 0; i < n; i++) {
            const edge = edges[i];

            if (
                (edge.second === other && edge.traversableForward())
                || (edge.first === other && edge.traversableBackward())

            ) {
                return edge;
            }
        }

        // not found
        return undefined;
    }

    /**
     *
     * @param {Edge} e
     * @returns {boolean}
     */
    addEdge(e) {
        if (this.__edges.includes(e)) {
            return false;
        }

        const other_node = e.other(this.node);

        this.__edges.push(e);

        let existing_count = this.__neighbors.get(other_node);

        if (existing_count === undefined) {
            existing_count = 0;
        }

        this.__neighbors.set(other_node, existing_count + 1);

        return true;
    }

    /**
     *
     * @param {Edge} e
     * @returns {boolean}
     */
    removeEdge(e) {
        const edges = this.__edges;
        const i = edges.indexOf(e);
        if (i === -1) {
            return false;
        }

        edges.splice(i, 1);

        const other_node = e.other(this.node);

        const existing_count = this.__neighbors.get(other_node);

        if (existing_count <= 1) {
            this.__neighbors.delete(other_node);
        } else {
            this.__neighbors.set(other_node, existing_count - 1);
        }

        return true;
    }
}
