import { assert } from "../../../core/assert.js";
import List from "../../../core/collection/list/List.js";
import { noop } from "../../../core/function/noop.js";
import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
import Entity from "../../ecs/Entity.js";

import { EventType } from "../../ecs/EventType.js";
import GUIElement from "../../ecs/gui/GUIElement.js";
import NotificationLog from "../../notify/NotificationLog.js";


class LogDisplay {
    /**
     *
     * @param {NotificationLog} log
     * @param {function(log:NotificationLog)} initializer
     * @param {function(timeDelta:number)} [updateFunction]
     */
    constructor({
                    log,
                    initializer,
                    updateFunction = noop
                }) {

        this.needsUpdate = updateFunction !== noop;

        this.__update_function = updateFunction;

        this.log = log;
        this.initializer = initializer;

        /**
         * Allows modifying timing of the view emitter by slowing or speeding up the time
         * @type {number}
         */
        this.timeScale = 1;
    }

    /**
     *
     * @param {number} timeDelta
     */
    update(timeDelta) {
        const td = this.timeScale * timeDelta;

        this.__update_function(td);
    }

    initialize() {
        this.initializer(this.log);
    }
}

export class NotificationManager {
    constructor() {
        /**
         *
         * @type {Map<string, NotificationLog>}
         */
        this.channels = new Map();

        /**
         *
         * @type {List<LogDisplay>}
         */
        this.displays = new List();

        /**
         *
         * @type {string}
         * @private
         */
        this.defaultArea = undefined;

        /**
         *
         * @type {EntityComponentDataset|null}
         */
        this.ecd = null;
    }

    /**
     *
     * @param {string} channel
     */
    createChannel(channel) {
        assert.isString(channel, 'channel');

        if (this.channels.has(channel)) {
            throw new Error(`channel '${channel}' already exists`);
        }

        this.channels.set(channel, new NotificationLog());

        if (this.defaultArea === undefined) {
            this.defaultArea = channel;
        }
    }

    /**
     *
     * @param {string} channel
     * @returns {NotificationLog|undefined}
     */
    getChannel(channel) {
        return this.channels.get(channel);
    }

    /**
     *
     * @param {string} channel
     * @returns {LogDisplay[]}
     */
    getDisplays(channel) {
        const log = this.getChannel(channel);

        return this.displays.filter(d => d.log === log);
    }

    /**
     *
     * @param {string} channel
     * @param {function(log:NotificationLog):View} initializer
     * @param {function(timeDelta:number)} [updateFunction]
     */
    addDisplay(channel, initializer, updateFunction) {
        assert.isString(channel, 'channel');
        assert.isFunction(initializer, 'initializer');

        const log = this.channels.get(channel);

        if (log === undefined) {
            throw new Error(`channel '${channel}' doesn't exist`);
        }

        const display = new LogDisplay({ log, initializer, updateFunction });

        this.displays.add(display);

        display.initialize();
    }

    /**
     *
     * @param {string} channel
     * @param {ViewEmitter} viewEmitter
     * @param {String} [grouping]
     */
    addEmitterDisplay(channel, viewEmitter, grouping = null) {
        /**
         *
         * @type {Map<View, Entity>}
         */
        const views = new Map();

        const self = this;

        const managedNotificationChannelClass = `managed-notification-channel-${channel}`;

        function handleEntityRemoved(entity) {
            //find existing entry
            for (const [view, builder] of views) {

                if (builder.id === entity) {
                    //clear from the map
                    views.delete(view);
                    break;
                }

            }
        }

        function viewFactory(log) {

            viewEmitter.objectEmitter.objectFinalizer = function (view) {

                const entity = views.get(view);

                if (entity !== undefined) {
                    entity.removeEventListener(EventType.EntityRemoved, handleEntityRemoved);
                    entity.destroy();

                    //remove from the map
                    views.delete(view);
                }
            };

            log.elements.on.added.add(function (notification) {
                assert.notEqual(notification, null, 'notification is null');
                assert.notEqual(notification, undefined, 'notification is undefined');

                viewEmitter.spawn(notification);
            });


            viewEmitter.on.spanwed.add(function (view) {

                view.addClass(managedNotificationChannelClass);

                const eb = new Entity();

                //prevent serialization of the notification
                eb.add(SerializationMetadata.Transient);

                const ecd = self.ecd;

                if (ecd !== null) {
                    const guiElement = GUIElement.fromView(view);

                    guiElement.group = grouping;


                    eb.addEventListener(EventType.EntityRemoved, handleEntityRemoved);

                    eb.add(guiElement)
                        .build(ecd);


                    //console.warn('Notification View Spawned:', eb);

                    views.set(view, eb);
                }
            });

        }

        function updateFunction(tileDelta) {
            viewEmitter.tick(tileDelta);
        }

        this.addDisplay(channel, viewFactory, updateFunction);
    }

    /**
     *
     * @param {Notification} notification
     * @param {string} [areaId] uses default area when not specified
     */
    addNotification(notification, areaId = this.defaultArea) {
        assert.notEqual(notification, undefined, 'notification is undefined');
        assert.notEqual(notification, null, 'notification is null');

        const channel = this.channels.get(areaId);

        if (channel === undefined) {
            throw new Error(`Area '${areaId}' doesn't exist`);
        }

        channel.addNotification(notification);
    }

    /**
     *
     * @param {number} timeDelta
     */
    tick(timeDelta) {
        const displays = this.displays;
        const n = displays.length;

        for (let i = 0; i < n; i++) {

            const display = displays.get(i);

            if (display.needsUpdate) {
                display.update(timeDelta);
            }
        }
    }
}
