import { assert } from "../../assert.js";

import { noop } from "../../function/noop.js";
import { objectKeyByValue } from "../object/objectKeyByValue.js";
import { PortDirection } from "./node/PortDirection.js";

/**
 *
 * @type {number}
 */
let id_counter = 0;

export class Connection {
    /**
     * @readonly
     * @type {number}
     */
    id = id_counter++;

    /**
     *
     * @type {NodeInstancePortReference}
     */
    source = null;

    /**
     *
     * @type {NodeInstancePortReference}
     */
    target = null;

    /**
     *
     * @param {function(string):*} [problem_consumer]
     * @returns {boolean} true if connection is valid
     */
    validate(problem_consumer = noop) {
        assert.isFunction(problem_consumer, "problem_consumer");

        const source = this.source;
        const target = this.target;

        let result = true;

        if (source === null) {
            problem_consumer(`Source port is not set`);

            result = false;
        } else if (source.port.direction !== PortDirection.Out) {
            problem_consumer(`Source port must be directed OUT, instead was ${objectKeyByValue(PortDirection, source.port.direction)}`);

            result = false;
        }

        if (target === null) {
            problem_consumer(`Target port is not set`);

            result = false;
        } else if (target.port.direction !== PortDirection.In) {
            problem_consumer(`Target port must be directed IN, instead was ${objectKeyByValue(PortDirection, target.port.direction)}`);

            result = false;
        }

        if (
            (source !== null && target !== null)
            && (source.port.dataType !== target.port.dataType)
        ) {
            problem_consumer(`Source and Target port data types don't match`);

            result = false;
        }

        return result;
    }

    /**
     *
     * @param {NodeInstancePortReference} endpoint
     */
    setSource(endpoint) {
        assert.defined(endpoint, 'endpoint');
        assert.notNull(endpoint, 'endpoint');

        this.source = endpoint;
    }

    /**
     *
     * @param {NodeInstancePortReference} endpoint
     */
    setTarget(endpoint) {
        assert.defined(endpoint, 'endpoint');
        assert.notNull(endpoint, 'endpoint');

        this.target = endpoint;
    }

    /**
     *
     * @param {number} id Id of {@link NodeInstance}
     * @returns {boolean}
     */
    isAttachedToNode(id) {
        assert.isNumber(id, 'id');
        assert.isInteger(id, 'id');

        return this.source?.instance?.id === id
            || this.target?.instance?.id === id;
    }

    /**
     *
     * @param {Connection} other
     * @returns {boolean}
     */
    equals(other) {
        return this.target === other.target
            && this.source === other.source
            ;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        const source_hash = this.source !== null ? this.source.id : 0;
        const target_hash = this.target !== null ? this.target.id : 0;

        // shuffle bits a bit to get better spread
        return ((source_hash << 16) | (source_hash >>> 16))
            ^ (target_hash)
            ;
    }

    toString() {
        return `Connection{ id = ${this.id}, source=${this.source !== null ? this.source.id : 'null'}, target=${this.target !== null ? this.target.id : 'null'} }`
    }
}


/**
 * @readonly
 * @type {boolean}
 */
Connection.prototype.isConnection = true;
