import { assert } from "../../../core/assert.js";
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { noop } from "../../../core/function/noop.js";
import DataType from "../../../core/parser/simple/DataType.js";
import { BinaryObjectSerializationAdapter } from "../../ecs/storage/binary/object/BinaryObjectSerializationAdapter.js";
import { Blackboard } from "./Blackboard.js";

export class BlackboardDynamicStorageAdapter {
    constructor() {
        /**
         *
         * @type {Blackboard}
         * @private
         */
        this.__blackboard = null;

        /**
         *
         * @type {Storage}
         * @private
         */
        this.__storage = null;

        /**
         *
         * @type {boolean}
         * @private
         */
        this.__is_linked = false;


        /**
         *
         * @type {string}
         * @private
         */
        this.__storage_key = "";

        /**
         *
         * @type {BinaryObjectSerializationAdapter}
         * @private
         */
        this.__serialization_adapter = new BinaryObjectSerializationAdapter();

        this.__serialization_in_progress = false;
        this.__need_write = false;

        /**
         * "setTimeout" Handle
         * @type {number}
         * @private
         */
        this.__write_timeout_handle = -1;

        /**
         * In milliseconds
         * @type {number}
         * @private
         */
        this.__write_timeout_delay = 200;

        /**
         * Last time write happened
         * @type {number}
         * @private
         */
        this.__last_write_timestamp = 0;
    }

    setStorageKey(value) {
        this.__storage_key = value;
    }

    /**
     * @param {BinarySerializationRegistry} s
     */
    setSerialization(s) {
        assert.defined(s, 's');

        this.__serialization_adapter.initialize(s);
    }

    /**
     * @param {Blackboard} v
     */
    setBlackboard(v) {
        this.__blackboard = v;
    }

    /**
     * @param {Storage} v
     */
    setStorage(v) {
        this.__storage = v;
    }

    /**
     * Read data from storage and populate blackboard accordingly
     */
    async read() {
        const data = await this.__storage.promiseLoadBinary(this.__storage_key);

        this.__blackboard.reset();

        if (data === undefined) {
            // no data
            return;
        }

        const buffer = BinaryBuffer.fromArrayBuffer(data);

        const object = this.__serialization_adapter.deserialize(buffer);

        if (!(object instanceof Blackboard)) {
            throw new Error('Expected blackboard, got something else');
        }

        this.__blackboard.copy(object);
    }

    __handle_serialization_finalized() {
        this.__serialization_in_progress = false;

        if (this.__need_write) {
            // write is required
            this.write();
        }
    }

    /**
     * Write data to storage, this is not guaranteed to result in immediate writing
     */
    write() {
        if (this.__serialization_in_progress) {
            this.__need_write = true;
            return;
        }


        if (this.__write_timeout_handle !== -1) {
            // there is a pending write already
            return;
        }

        const current_time = performance.now();

        const time_since_last_write = current_time - this.__last_write_timestamp;

        const time_until_next_write = this.__write_timeout_delay - time_since_last_write;

        if (time_until_next_write <= 0) {
            // enough time has passed since last write to do an immediate write
            this.__last_write_timestamp = current_time;
            this.__write_immediate();
            return;
        }

        // not enough time has passed since last write, let's schedule another write
        this.__write_timeout_handle = setTimeout(this.__handle_write_timeout.bind(this), time_until_next_write);

    }

    __handle_write_timeout() {
        // clear timeout handle
        this.__write_timeout_handle = -1;

        this.__write_immediate();
    }

    /**
     * Perform write right now
     * @private
     */
    __write_immediate() {

        this.__serialization_in_progress = true;
        this.__need_write = false;

        const buffer = new BinaryBuffer();

        this.__serialization_adapter.serialize(buffer, this.__blackboard, Blackboard.typeName);
        buffer.trim();

        const success = () => {
            this.__handle_serialization_finalized();
        }

        const reject = () => {
            this.__handle_serialization_finalized();
        }

        this.__storage.storeBinary(this.__storage_key, buffer.data, success, reject, noop);
    }

    /**
     *
     * @param {string} name
     * @private
     */
    __watch_property(name) {
        const value = this.__blackboard.acquire(name, DataType.Any);

        value.onChanged.add(this.write, this);

        this.write();
    }

    /**
     *
     * @param {string} name
     * @private
     */
    __unwatch_property(name) {
        const value = this.__blackboard.acquire(name, DataType.Any);

        value.onChanged.remove(this.write, this);
    }

    async link() {
        if (this.__is_linked) {
            return;
        }

        this.__is_linked = true;

        await this.read();

        this.__blackboard.traverse((name, value, type) => {
            this.__watch_property(name);
        });

        this.__blackboard.on.added.add(this.__watch_property, this);
    }

    async unlink() {
        if (!this.__is_linked) {
            return;
        }

        this.__is_linked = false;

        this.__blackboard.traverse((name, value, type) => {
            this.__unwatch_property(name);
        });

        this.__blackboard.on.added.remove(this.__watch_property, this);
    }
}
