import { assert } from "../../../../assert.js";
import { combine_hash } from "../../../../collection/array/combine_hash.js";
import DataType from "../../../../parser/simple/DataType.js";
import { computeStringHash } from "../../../../primitives/strings/computeStringHash.js";
import { ReactiveExpression } from "../ReactiveExpression.js";


/**
 * Represents a named variable
 * @extends ReactiveExpression
 */
export class ReactiveReference extends ReactiveExpression {

    /**
     *
     * @param {string} [name]
     */
    constructor(name = "") {
        super();

        assert.isString(name, 'name');

        /**
         *
         * @type {string}
         */
        this.name = name;

        /**
         *
         * @type {ObservedBoolean|Vector1|ReactiveExpression}
         */
        this.source = null;
    }

    /**
     *
     * @param {ReactiveReference} other
     */
    copy(other) {
        this.name = other.name;

        this.disconnect();

        if (other.source !== null) {
            this.connect(other.source);
        }
    }

    /**
     *
     * @return {ReactiveReference}
     */
    clone() {
        const r = new ReactiveReference();

        r.copy(this);

        return r;
    }

    update(v, oldV) {
        this.onChanged.send2(v, oldV);
    }

    /**
     * Connect to a reactive source and propagate change notifications up the tree.
     * see {@link disconnect} for the inverse.
     * @template T
     * @param {{getValue():T, onChanged: Signal<T>}} source
     */
    connect(source) {
        assert.notNull(source, 'source');
        assert.defined(source, 'source');
        assert.isObject(source.onChanged, 'source.onChanged');
        assert.isFunction(source.onChanged.add, 'source.onChanged.add');

        if (this.source !== null) {
            // some source is already connected, likely an error

            if (this.source === source) {
                // already connected to this source, do nothing
                return;
            }

            throw new Error('source is already set. Already connected');
        }

        this.source = source;

        source.onChanged.add(this.update, this);
    }

    /**
     * Stop observing the current source and reacting to changes.
     * see {@link connect} for the inverse.
     */
    disconnect() {
        if (this.source === null) {
            return;
        }

        const removed_handler = this.source.onChanged.remove(this.update, this);

        assert.ok(removed_handler, 'handler was not found on source.onChanged');

        assert.equal(this.source.onChanged.contains(this.update, this), false, 'a copy handler is still bound to source.onChanged');

        this.source = null;
    }

    equals(other) {
        return other.isReference
            && this.name === other.name
            && super.equals(other)
            ;
    }

    toCode() {
        return this.name;
    }


    hash() {
        return combine_hash(
            computeStringHash(this.name),
            super.hash()
        );
    }

    evaluate(scope) {
        const v = scope[this.name];

        assert.defined(v, `value 'scope.${this.name}' is not found`);

        return v;
    }

    getValue() {
        return this.source.getValue();
    }


    /**
     *
     * @param {ObservedBoolean|Vector1|ObservedInteger} source
     * @param {string} [name]
     * @returns {ReactiveReference}
     */
    static from(source, name = 'undefined') {
        const r = new ReactiveReference(name);

        r.connect(source);

        //copy source data type
        if (source.isObservedBoolean) {
            r.dataType = DataType.Boolean;
        } else {
            r.dataType = DataType.Number;
        }

        return r;
    }

}

/**
 * @readonly
 * @type {boolean}
 */
ReactiveReference.prototype.isTerminal = true;

/**
 * @readonly
 * @type {boolean}
 */
ReactiveReference.prototype.isReference = true;
