import { assert } from "../../../core/assert.js";
import { IllegalStateException } from "../../../core/fsm/exceptions/IllegalStateException.js";
import { objectKeyByValue } from "../../../core/model/object/objectKeyByValue.js";
import ObservedEnum from "../../../core/model/ObservedEnum.js";
import Task from "../../../core/process/task/Task.js";
import TaskGroup from "../../../core/process/task/TaskGroup.js";
import { GameAssetType } from "../../asset/GameAssetType.js";
import { StaticKnowledgeDataTableDescriptor } from "./StaticKnowledgeDataTableDescriptor.js";

/**
 * @readonly
 * @enum {number}
 */
export const StaticKnowledgeState = {
    Initial: 0,
    Loading: 1,
    Loaded: 2,
    Failed: 3
};

export class StaticKnowledgeDatabase {

    /**
     *
     * @type {ObservedEnum<StaticKnowledgeState>}
     */
    __state = new ObservedEnum(StaticKnowledgeState.Initial, StaticKnowledgeState);


    /**
     *
     * @type {StaticKnowledgeDataTableDescriptor[]}
     * @private
     */
    __specs = [];

    /**
     *
     * @type {boolean}
     */
    #validation_enabled = false;

    /**
     *
     * @param {boolean} v
     */
    set validation_enabled(v) {
        this.#validation_enabled = v;
    }


    /**
     * Reset database, drop all table data
     */
    reset() {
        this.__state.set(StaticKnowledgeState.Initial);

        const tableDescriptors = this.__specs;
        const n = tableDescriptors.length;
        for (let i = 0; i < n; i++) {
            const descriptor = tableDescriptors[i];

            descriptor.table.reset();
        }
    }

    /**
     *
     * @param {string} id
     * @param {string} path
     * @param {StaticKnowledgeDataTable} table
     */
    add(id, path, table) {
        assert.defined(table,'table');
        assert.notNull(table,'table');

        const state = this.__state.getValue();

        if (state !== StaticKnowledgeState.Initial) {
            throw new IllegalStateException(`Adding new tables is only allowed in Initial state, current state is ${objectKeyByValue(StaticKnowledgeState, state)}`);
        }

        const existing = this.getDescriptorById(id);
        if (existing !== undefined) {

            if (existing.id === id && existing.source === path && Object.getPrototypeOf(existing.table) === Object.getPrototypeOf(table)) {
                // attempting to add the same thing

                console.warn(`Attempting to ad the same table again, ignoring. id='${id}', path='${path}' (table prototype matching)`);

                return;
            }

            throw new Error(`Table '${id}' already exists`);
        }

        const descriptor = StaticKnowledgeDataTableDescriptor.from(id, path, table);

        this.__specs.push(descriptor);

        //expose table accessor
        this[id] = table;
    }

    /**
     * @template T
     * @param {string} id
     * @returns {StaticKnowledgeDataTableDescriptor|undefined}
     */
    getDescriptorById(id) {

        const descriptors = this.__specs;
        const n = descriptors.length;
        for (let i = 0; i < n; i++) {
            /**
             *
             * @type {StaticKnowledgeDataTableDescriptor}
             */
            const descriptor = descriptors[i];

            if (descriptor.id === id) {
                return descriptor;
            }
        }

    }

    /**
     * @template T
     * @param {string} id
     * @returns {StaticKnowledgeDataTable<T>|undefined}
     */
    getTable(id) {
        const descriptor = this.getDescriptorById(id);

        if (descriptor !== undefined) {
            return descriptor.table;
        }

        return undefined;
    }


    /**
     *
     * @returns {Promise}
     */
    promise() {
        const state = this.__state;

        return new Promise(function (resolve, reject) {
            function attemptResolution(value) {
                if (value === StaticKnowledgeState.Loaded) {
                    return resolve();
                } else if (value === StaticKnowledgeState.Failed) {
                    return reject('Failed to load Static Knowledge');
                }
            }

            state.process(attemptResolution);
        });
    }

    /**
     *
     * @param {ConcurrentExecutor} executor
     * @return {Promise}
     */
    validate(executor) {

        const tasks = this.__specs.map(d => {
            const table = d.table;

            const task = table.validate(this, console.error);

            return task;
        });

        const taskGroup = new TaskGroup(tasks, 'Database Validation');

        const promise = Task.promise(taskGroup);

        executor.runGroup(taskGroup);

        return promise;
    }


    /**
     *
     * @param {StaticKnowledgeDataTableDescriptor} descriptor
     * @param {AssetManager} assetManager
     * @param {ConcurrentExecutor} executor
     * @private
     */
    async __link_table(descriptor, assetManager, executor) {
        /**
         *
         * @type {StaticKnowledgeDataTable}
         */
        const t = descriptor.table;

        await t.link(this, assetManager, executor);
    }

    /**
     *
     * @param {AssetManager} am
     * @param {ConcurrentExecutor} executor
     * @returns {Promise}
     */
    link(am, executor) {
        const promises = this.__specs.map(d => {

            const promise = this.__link_table(d, am, executor);

            promise.catch((e) => {
                /**
                 *
                 * @type {StaticKnowledgeDataTable}
                 */
                const t = d.table;

                console.error('Failed to link table:', t, e);
            });
        });

        return Promise.all([
            promises
        ]);
    }

    /**
     *
     * @private
     * @param {StaticKnowledgeDataTableDescriptor} descriptor
     * @param {AssetManager} assetManager
     * @param {ConcurrentExecutor} executor
     * @returns {Promise<StaticKnowledgeDataTable>}
     */
    async __load_table(descriptor, assetManager, executor) {
        const path = descriptor.source;
        const table = descriptor.table;

        if (table.isStaticKnowledgeDataTable !== true) {
            throw new Error('Illegal argument, table.isStaticKnowledgeDataTable !== true');
        }

        const asset = await assetManager.promise(path, GameAssetType.JSON);

        const data = asset.create();

        await table.load(data, executor);

        return table;
    }

    /**
     * Load a table at runtime
     * @param {StaticKnowledgeDataTableDescriptor} descriptor
     * @param {AssetManager} assetManager
     * @param {ConcurrentExecutor} executor
     * @private
     */
    async __hot_load(descriptor, assetManager, executor) {
        await this.__load_table(descriptor, assetManager, executor);

        await this.__link_table(descriptor, assetManager, executor);
    }

    /**
     *
     * @param {AssetManager} am
     * @param {ConcurrentExecutor} executor
     * @returns {Promise}
     */
    load(am, executor) {
        if (this.__state.getValue() !== StaticKnowledgeState.Initial) {
            throw new Error('Illegal state');
        }

        this.__state.set(StaticKnowledgeState.Loading);

        const self = this;

        const promises = [];

        const n = this.__specs.length;
        for (let i = 0; i < n; i++) {
            const descriptor = this.__specs[i];

            const p = this.__load_table(descriptor, am, executor);

            promises.push(p);
        }

        return Promise.all(promises).then(() => {
            let p = this.link(am, executor);

            if (this.#validation_enabled) {
                p = p.then(() => this.validate(executor));
            }

            return p;
        }).then(function () {
            self.__state.set(StaticKnowledgeState.Loaded);
        }, function (error) {

            console.error('Failed to load static knowledge', error);
            self.__state.set(StaticKnowledgeState.Failed);
        });
    }
}

StaticKnowledgeDatabase.prototype.isStaticKnowledgeDatabase = true;
