import { assert } from "../../../core/assert.js";
import Signal from "../../../core/events/signal/Signal.js";
import DataType from "../../../core/parser/simple/DataType.js";
import { AbstractBlackboard } from "./AbstractBlackboard.js";
import { BlackboardValue } from "./BlackboardValue.js";
import { make_blackboard_proxy } from "./make_blackboard_proxy.js";

/**
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Blackboard extends AbstractBlackboard {
    constructor() {
        super();

        this.on = {
            /**
             * Property added
             * @type {Signal}
             */
            added: new Signal()
        };

        /**
         *
         * @type {Object<BlackboardValue>}
         */
        this.data = {};

        /**
         * @private
         */
        this.proxy = make_blackboard_proxy(this);
    }

    getKeys() {
        return Reflect.ownKeys(this.data);
    }

    /**
     *
     * @returns {Object}
     */
    getValueProxy() {
        return this.proxy;
    }

    /**
     *
     * @param {string} name
     * @param {DataType} type
     * @return {boolean} true only if entry exists and matches type, or if supplied parameter type is set to "Any"
     */
    contains(name, type) {
        assert.isString(name, 'name');
        assert.enum(type, DataType, 'type');

        const datum = this.data[name];

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

        if (type !== DataType.Any && datum.type !== type) {
            return false;
        }

        return true;
    }

    /**
     *
     * @param {function(name:string, value:*, type: DataType)} visitor
     * @param {*} [thisArg]
     */
    traverse(visitor, thisArg) {
        assert.isFunction(visitor, 'visitor');

        for (let name in this.data) {
            if (!this.data.hasOwnProperty(name)) {
                continue;
            }

            const blackboardValue = this.data[name];

            visitor.call(thisArg, name, blackboardValue.value, blackboardValue.type);
        }
    }

    /**
     *
     * @param {function(name:string, value:*, type: DataType)} visitor
     * @param {RegExp} pattern
     */
    traverseWithPattern(pattern, visitor) {
        assert.defined(pattern, 'pattern');
        assert.ok(pattern instanceof RegExp, 'pattern is not a RegExp');

        this.traverse(function (name, value, type) {
            if (pattern.test(name)) {
                visitor(name, value, type);
            }
        });
    }

    /**
     * @template T
     * @param {string} name
     * @param {DataType} type
     * @param {number|boolean} [initialValue]
     * @returns {T}
     */
    acquire(name, type, initialValue) {
        assert.isString(name, 'name');

        let datum;

        if (this.data.hasOwnProperty(name)) {
            // property exists
            datum = this.data[name];

            if (type !== DataType.Any && datum.type !== type) {
                throw new TypeError(`Value '${name}' exists, but is type(='${datum.type}'), expected type '${type}'`);
            }

        } else {
            //doesn't exist - create it
            datum = new BlackboardValue(type);

            const concrete_value = datum.value;

            if (initialValue !== undefined) {
                concrete_value.set(initialValue);
            }

            this.data[name] = datum;

            this.on.added.send4(name, concrete_value.getValue(), type, this);
        }

        datum.referenceCount++;

        return datum.value;
    }

    /**
     *
     * @param {string} name
     */
    release(name) {
        assert.isString(name, 'name');
        assert.ok(this.data.hasOwnProperty(name), `Attempting to release a value '${name}' that doesn't exist`);

        const datum = this.data[name];

        datum.referenceCount--;

        //todo cleanup value from blackboard if no one is using it
    }

    /**
     * Drop all values
     */
    reset() {
        this.data = {};
    }

    /**
     *
     * NOTE: where variables of same type with same names exist - they are kept and their values are updated
     * @param {Blackboard} other
     */
    copy(other) {
        // perform coalescence

        const this_data = this.data;

        other.traverse((name, value, type) => {

            const variable = this_data[name];

            const other_value = value.getValue();

            if (variable === undefined) {
                this.acquire(name, type, other_value);
            } else if (variable.type !== type) {
                // type mismatch
                delete this_data[name]
                this.acquire(name, type, other_value);
            } else {
                // variable exists and types match
                variable.value.set(other_value);
            }

        });

        // find out which keys should be removed
        const garbage = [];

        for (const dataKey in this_data) {
            const variable = this_data[dataKey];
            if (!other.contains(dataKey, variable.type)) {
                garbage.push(dataKey);
            }
        }

        for (let i = 0; i < garbage.length; i++) {
            const key = garbage[i];

            delete this_data[key];
        }
    }

    /**
     *
     * @returns {Blackboard}
     */
    clone() {
        const r = new Blackboard();

        r.copy(this);

        return r;
    }


    toJSON() {
        const result = {};

        this.traverse((name, value, type) => {
            result[name] = value.toJSON();
        });

        return result;
    }

    fromJSON(json) {
        this.reset();

        for (let propName in json) {
            const value = json[propName];

            const value_type = typeof value;

            if (value_type === 'number') {
                this.acquireNumber(propName, value).set(value);
            } else if (value_type === 'boolean') {
                this.acquireBoolean(propName, value).set(value);
            } else if (value_type === 'string') {
                this.acquireString(propName, value).set(value);
            }

        }
    }

    /**
     *
     * @param {any} json
     * @return {Blackboard}
     */
    static fromJSON(json) {
        const bb = new Blackboard();
        bb.fromJSON(json);
        return bb;
    }
}

Blackboard.typeName = 'Blackboard';

