import { assert } from "../../../core/assert.js";
import { weightedRandomFromArray } from "../../../core/collection/array/weightedRandomFromArray.js";
import Vector2 from "../../../core/geom/Vector2.js";
import { max2 } from "../../../core/math/max2.js";
import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
import { DomSizeObserver } from "../../../view/util/DomSizeObserver.js";
import AnimationTrack from "../../animation/keyed2/AnimationTrack.js";
import AnimationTrackPlayback from "../../animation/keyed2/AnimationTrackPlayback.js";
import { AnimationBehavior } from "../../animation/keyed2/behavior/AnimationBehavior.js";
import TransitionFunctions from "../../animation/TransitionFunctions.js";
import { GameAssetType } from "../../asset/GameAssetType.js";
import { SequenceBehavior } from "../../intelligence/behavior/composite/SequenceBehavior.js";
import { BehaviorComponent } from "../../intelligence/behavior/ecs/BehaviorComponent.js";
import { DieBehavior } from "../../intelligence/behavior/ecs/DieBehavior.js";
import { ActionBehavior } from "../../intelligence/behavior/primitive/ActionBehavior.js";
import { DelayBehavior } from "../../intelligence/behavior/util/DelayBehavior.js";
import { Blackboard } from "../../intelligence/blackboard/Blackboard.js";
import { globalMetrics } from "../../metrics/GlobalMetrics.js";
import { MetricsCategory } from "../../metrics/MetricsCategory.js";
import { Attachment } from "../attachment/Attachment.js";
import { SerializationMetadata } from "../components/SerializationMetadata.js";
import Entity from "../Entity.js";
import { EntityFlags } from "../EntityFlags.js";
import GUIElement from "../gui/GUIElement.js";
import HeadsUpDisplay from "../gui/hud/HeadsUpDisplay.js";
import ViewportPosition from "../gui/position/ViewportPosition.js";
import { AbstractContextSystem } from "../system/AbstractContextSystem.js";
import { SystemEntityContext } from "../system/SystemEntityContext.js";
import { Transform } from "../transform/Transform.js";
import { SpeechBubbleView } from "./SpeechBubbleView.js";
import { Voice } from "./Voice.js";
import { VoiceEvents } from "./VoiceEvents.js";
import { VoiceFlags } from "./VoiceFlags.js";

/**
 * Delay before the user notices the text and begins to read
 * @type {number}
 * @ignore
 */
const TIMING_NOTICE_DELAY = 0.5;

/**
 * Minimum time to read something
 * @type {number}
 * @ignore
 */
const TIMING_MINIMUM_READ_TIME = 1.2;

/**
 *
 * @type {LineDescription[]}
 * @ignore
 */
const temp_lines = [];

class LineWeigher {
    /**
     *
     * @type {number}
     */
    entity = -1;
    /**
     *
     * @type {VoiceSystem}
     */
    system = null;
    /**
     *
     * @type {number}
     */
    time = 0;

    /**
     *
     * @param {LineDescription} line
     * @return {number}
     */
    compute(line) {
        const entity = this.entity;

        const last_spoken = this.system.__global_last_used_times.get(line.id);

        let freshness_score = 0;

        if (last_spoken !== undefined) {
            const time_since_last_spoken = this.time - last_spoken;

            freshness_score += time_since_last_spoken;
        } else {
            // no record of the line being spoken, consider very fresh
            freshness_score = 10000;
        }

        return freshness_score;
    }
}

class Context extends SystemEntityContext {

    /**
     *
     * @type {LineDescription}
     */
    active_line = null;

    /**
     *
     * @type {Entity}
     */
    active_executor = null;

    handleSpeakLineEvent({ id }) {
        this.system.sayLine(this.entity, id, this.components[0]);
    }

    handleSpeakLineSetEvent({ id }) {
        this.system.sayLineFromSet(this.entity, id, this.components[0]);
    }

    link() {
        const dataset = this.getDataset();

        dataset.addEntityEventListener(this.entity, VoiceEvents.SpeakLine, this.handleSpeakLineEvent, this);

        dataset.addEntityEventListener(this.entity, VoiceEvents.SpeakSetLine, this.handleSpeakLineSetEvent, this);
    }

    unlink() {
        const dataset = this.getDataset();

        dataset.removeEntityEventListener(this.entity, VoiceEvents.SpeakLine, this.handleSpeakLineEvent, this);

        dataset.removeEntityEventListener(this.entity, VoiceEvents.SpeakSetLine, this.handleSpeakLineSetEvent, this);
    }
}

const SPEECH_BUBBLE_ANIMATION_INTRO = new AnimationTrack(['alpha']);
SPEECH_BUBBLE_ANIMATION_INTRO.addKey(0, [0]);
SPEECH_BUBBLE_ANIMATION_INTRO.addKey(0.1, [1]);
SPEECH_BUBBLE_ANIMATION_INTRO.addTransition(0, TransitionFunctions.CubicEaseIn);


const SPEECH_BUBBLE_ANIMATION_OUTRO = new AnimationTrack(['alpha']);
SPEECH_BUBBLE_ANIMATION_OUTRO.addKey(0, [1]);
SPEECH_BUBBLE_ANIMATION_OUTRO.addKey(0.2, [0]);
SPEECH_BUBBLE_ANIMATION_OUTRO.addTransition(0, TransitionFunctions.CubicEaseIn);

/**
 * @this View
 * @param {number} alpha
 * @ignore
 */
function bubble_animation_update_function(alpha) {
    this.css({
        opacity: alpha
    });
}

export class VoiceSystem extends AbstractContextSystem {
    /**
     *
     * @param {Engine} engine
     * @param settings
     */
    constructor(engine, settings = {
        font: "unknown.ttf", //path to font file
        font_size: 16
    }) {
        super(Context);

        this.dependencies = [Voice];

        this.components_used = [
            ResourceAccessSpecification.from(GUIElement, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(ViewportPosition, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(HeadsUpDisplay, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(Transform, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(Attachment, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(SerializationMetadata, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(BehaviorComponent, ResourceAccessKind.Create),
        ];


        /**
         *
         * @type {Localization}
         */
        this.localiation = null;

        /**
         *
         * @type {GMLEngine}
         */
        this.gml = null;

        /**
         *
         * @type {LineDescriptionTable}
         */
        this.lines = null;

        /**
         *
         * @type {LineSetDescriptionTable}
         */
        this.sets = null;

        /**
         *
         * @type {Engine}
         */
        this.engine = engine;

        /**
         * When last a line was spoken
         * @type {Map<string, number>}
         * @private
         */
        this.__global_last_used_times = new Map();

        /**
         *
         * @type {LineWeigher}
         * @private
         */
        this.__weigher = new LineWeigher();
        this.__weigher.system = this;


        /**
         *
         * @type {Font}
         * @private
         */
        this.__font = null;

        /**
         *
         * @type {string}
         * @private
         */
        this.__font_path = settings.font;

        /**
         *
         * @private
         */
        this.__font_size = settings.font_size;

        /**
         * Print debug output into console
         * @type {boolean}
         * @private
         */
        this.__debug = false;
    }

    /**
     *
     * @return {number}
     */
    getCurrentTime() {
        return this.engine.ticker.clock.getElapsedTime();
    }

   async startup(entityManager) {

        const engine = this.engine;

        this.localiation = engine.localization
        this.gml = engine.gui.gml;

        const knowledge = engine.staticKnowledge;

        this.lines = knowledge.getTable('voice-lines');
        this.sets = knowledge.getTable('voice-line-sets');

        assert.defined(this.lines, 'lines');
        assert.defined(this.sets, 'sets');

        const p_font_setting = engine.assetManager.promise(this.__font_path, GameAssetType.Font).then(font_asset => {

            this.__font = font_asset.create();
        });

        await p_font_setting;
    }

    /**
     *
     * @param {number} entity
     * @param {string} set_id
     * @param {Voice} voice
     */
    sayLineFromSet(entity, set_id, voice) {
        /**
         *
         * @type {LineSetDescription}
         */
        const set = this.sets.get(set_id);

        if (set === null) {
            console.warn(`Line set '${set_id}' not found in the database`);
            return;
        }

        const collected_count = set.collect(temp_lines, 0);

        this.__weigher.entity = entity;
        this.__weigher.time = this.getCurrentTime();

        const selected_line = weightedRandomFromArray(temp_lines, Math.random, this.__weigher.compute, this.__weigher, collected_count);

        this.sayLine(entity, selected_line.id, voice);
    }

    /**
     *
     * @param {string} text
     * @param {View} view
     * @ignore
     * @private
     */
    __setBubbleSize(text, view) {

        const sizer = new DomSizeObserver();

        sizer.watchView(view);

        sizer.dimensions.size.onChanged.add((x, y) => {
            if (Number.isFinite(x)) {
                view.size.x = x;
            }

            if (Number.isFinite(y)) {
                view.size.y = y;
            }
        });

        const advanceWidth = this.__font.getAdvanceWidth(text, this.__font_size);

        view.size.setSilent(advanceWidth, this.__font_size);

    }

    /**
     *
     * @param {number} entity
     * @param {string} line_id
     * @param {Voice} voice
     */
    sayLine(entity, line_id, voice) {

        const ctx = this.__getEntityContext(entity);

        if (ctx.active_executor !== null && ctx.active_executor.getFlag(EntityFlags.Built)) {
            // terminate currently speech bubble
            ctx.active_executor.destroy();
        }

        /**
         *
         * @type {LineDescription}
         */
        const line = this.lines.get(line_id);

        if (line === null) {
            console.warn(`Line '${line_id}' not found in the database`);
            return;
        }

        // record when the line was spoken
        this.__global_last_used_times.set(line_id, this.getCurrentTime());

        const ecd = this.entityManager.dataset;

        const localiation = this.localiation;

        const localization_key = line.text;
        const localized_line = localiation.getString(localization_key);

        const view = new SpeechBubbleView();

        const gml = this.gml;

        gml.compile(localized_line, view);

        // localized line may contain reference tags, the user will not see/read those, so we also compile line as pure text for estimating reading time
        const line_pure_text = gml.compileAsText(localized_line);

        const display_time_raw = localiation.estimateReadingTime(line_pure_text);

        const display_time = max2(TIMING_MINIMUM_READ_TIME, display_time_raw * line.displayDuration) + TIMING_NOTICE_DELAY;

        if (this.__debug) {
            console.log('Display time:', display_time, line_pure_text);
        }

        voice.setFlag(VoiceFlags.Speaking);

        ecd.sendEvent(entity, VoiceEvents.StartedSpeakingLocalizedLine, {
            text: localized_line,
            key: localization_key
        });

        const transform = new Transform();

        // copy transform from source entity
        const source_transform = ecd.getComponent(entity, Transform);

        if (source_transform !== undefined) {
            transform.copy(source_transform);
        }

        this.__setBubbleSize(line_pure_text, view);

        const entityBuilder = new Entity()
            .add(GUIElement.fromView(view))
            .add(ViewportPosition.fromJSON({ anchor: new Vector2(0.5, 1) }))
            .add(HeadsUpDisplay.fromJSON({}))
            .add(transform)
            .add(Attachment.fromJSON({
                socket: 'Voice',
                parent: entity,
                immediate: true
            }))
            .add(SerializationMetadata.Transient)
            .add(BehaviorComponent.from(SequenceBehavior.from([
                // play intro animation
                new AnimationBehavior(new AnimationTrackPlayback(SPEECH_BUBBLE_ANIMATION_INTRO, bubble_animation_update_function, view)),
                // wait for a certain amount of time
                DelayBehavior.from(display_time),
                // dispatch event and record that like was spoken
                new ActionBehavior(() => {
                    // clear speaking flag
                    voice.clearFlag(VoiceFlags.Speaking);

                    if (ecd.entityExists(entity)) {

                        // notify that the line has ended
                        ecd.sendEvent(entity, VoiceEvents.FinishedSpeakingLine, line_id);

                        // record the fact that line was spoken
                        const bb = ecd.getComponent(entity, Blackboard);

                        if (bb !== undefined) {
                            bb.incrementNumber(`voice.line_spoken.${line_id}.count`);
                        }

                    }
                }),
                //play outro animation
                new AnimationBehavior(new AnimationTrackPlayback(SPEECH_BUBBLE_ANIMATION_OUTRO, bubble_animation_update_function, view)),
                // destroy the entity
                DieBehavior.create()
            ])));

        ctx.active_line = line;
        ctx.active_executor = entityBuilder;

        entityBuilder
            .build(ecd);

        // send metrics
        globalMetrics.record('ecs.system.voice', {
            category: MetricsCategory.System,
            label: line_id
        });
    }
}
