import { assert } from "../assert.js";
import Signal from "../events/signal/Signal.js";
import { computeStringHash } from "../primitives/strings/computeStringHash.js";

/**
 * Observable string container.
 * Signals change via {@link onChanged}.
 *
 * @example
 *  const name = new ObservedString("Alice")
 *  name.onChanged((new_name, old_name) => console.log(`Name changed from ${old_name} to ${new_name}!`));
 *  ...
 *  name.set("Barbara"); // will print "Name changed from Alice to Barbara!" in console
 *  name.getValue() === "Barbara"
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
class ObservedString extends String {
    /**
     *
     * @param {string} [value=""]
     */
    constructor(value = "") {
        super();

        assert.isString(value, 'value');

        /**
         *
         * @type {string}
         * @private
         */
        this.__value = value;

        /**
         *
         * @type {Signal<string,string>}
         */
        this.onChanged = new Signal();
    }

    /**
     *
     * @returns {string}
     */
    valueOf() {
        return this.__value;
    }

    /**
     *
     * @returns {string}
     */
    toString() {
        return this.__value;
    }

    /**
     *
     * @param {string} value
     * @returns {ObservedString}
     */
    set(value) {
        assert.isString(value, 'value');

        const oldValue = this.__value;

        if (oldValue !== value) {
            this.__value = value;
            this.onChanged.send2(value, oldValue);
        }

        return this;
    }

    /**
     *
     * @param {ObservedString} other
     */
    copy(other) {
        this.set(other.getValue());
    }

    /**
     * @return {ObservedString}
     */
    clone() {
        const r = new this.constructor();

        r.copy(this);

        return r;
    }

    /**
     *
     * @returns {string}
     */
    getValue() {
        return this.__value;
    }

    /**
     *
     * @param {function(string,string)} f
     * @param {*} [thisArg]
     * @returns {this}
     */
    process(f, thisArg) {
        this.onChanged.add(f, thisArg);

        const v = this.getValue();

        f.call(thisArg, v, v);

        return this;
    }

    /**
     * @deprecated use {@link buffer.writeUTF8String} directly instead
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        buffer.writeUTF8String(this.__value);
    }

    /**
     * @deprecated use {@link buffer.readUTF8String} directly instead
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        const value = buffer.readUTF8String();

        this.set(value);
    }

    /**
     *
     * @param {ObservedString} other
     * @returns {boolean}
     */
    equals(other) {
        return this.__value === other.__value;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return computeStringHash(this.__value);
    }
}

/**
 * Used for optimized "instanceof" check
 * @readonly
 * @type {boolean}
 */
ObservedString.prototype.isObservedString = true;

/**
 * @readonly
 * @type {string}
 */
ObservedString.typeName = "ObservedString";

// JSON methods

ObservedString.prototype.toJSON = ObservedString.prototype.toString;
ObservedString.prototype.fromJSON = ObservedString.prototype.set;

export default ObservedString;
