import { assert } from "../../../assert.js";
import { combine_hash } from "../../../collection/array/combine_hash.js";
import { ReactiveExpression } from "./ReactiveExpression.js";

/**
 * @template T
 * @extends {ReactiveExpression<T>}
 */
export class ReactiveBinaryExpression extends ReactiveExpression {

    /**
     *
     * @type {ReactiveExpression|null}
     */
    left = null;

    /**
     *
     * @type {ReactiveExpression|null}
     */
    right = null;

    /**
     *
     * @type {T}
     * @private
     */
    __oldValue = undefined;

    copy(other) {
        assert.equal(other.isReactiveExpression, true, 'other.isReactiveExpression !== true');

        // make sure to disconnect from old children
        this.disconnect();

        if (other.left !== null && other.right !== null) {

            this.connect(
                other.left.clone(),
                other.right.clone()
            );

        }
    }

    equals(other) {
        if (other.isBinaryExpression !== true) {
            return false;
        }

        if (!this.left.equals(other.left)) {
            return false;
        }

        if (!this.right.equals(other.right)) {
            return false;
        }

        return super.equals(other);
    }

    hash() {
        return combine_hash(
            this.left.hash(),
            this.right.hash(),
            super.hash()
        );
    }

    evaluate(scope) {
        const left = this.left.evaluate(scope);
        const right = this.right.evaluate(scope);

        return this.transform(
            left,
            right
        );
    }

    update() {
        const result = this.getValue();

        if (result === this.__oldValue) {
            return;
        }

        this.onChanged.send1(result);

        this.__oldValue = result;
    }

    /**
     *
     * @returns {T|boolean|number}
     */
    transform(left, right) {
        throw new Error('ReactiveBinaryExpression.transform is not overridden');
    }

    traverse(visitor, thisArg) {
        super.traverse(visitor, thisArg);

        this.left.traverse(visitor, thisArg);
        this.right.traverse(visitor, thisArg);
    }

    /**
     *
     * @param left
     * @param right
     */
    connect(left, right) {
        assert.defined(left, 'left');
        assert.notNull(left,  'left');

        assert.defined(right, 'right');
        assert.notNull(right,  'right');

        assert.defined(left.onChanged,  'left');
        assert.defined(right.onChanged,  'right');

        this.left = left;
        this.right = right;

        left.onChanged.add(this.update, this);
        right.onChanged.add(this.update, this);

        //reset the old value
        this.__oldValue = undefined;
    }

    disconnect() {
        if (this.left !== null) {

            this.left.onChanged.remove(this.update, this);
            this.left = null;

        }

        if (this.right !== null) {
            this.right.onChanged.remove(this.update, this);

            this.right = null;
        }
    }

    getValue() {

        const left = this.left;
        assert.notNull(left, 'left'); // not linked?

        const right = this.right;
        assert.notNull(right, 'right'); // not linked?

        const leftValue = left.getValue();
        const rightValue = right.getValue();

        return this.transform(leftValue, rightValue);
    }
}

/**
 * Binary expression is commutative if changing the order of operands does not change the result
 * This property should be overridden in subclasses as necessary
 * Property is used to guide expression transformation
 * @readonly
 * @type {boolean}
 */
ReactiveBinaryExpression.prototype.isCommutative = false;

/**
 * @readonly
 * @type {boolean}
 */
ReactiveBinaryExpression.prototype.isBinaryExpression = true;
