import { assert } from "../../core/assert.js";
import Vector2 from "../../core/geom/Vector2.js";
import { makeCubicCurve } from "../../core/math/spline/makeCubicCurve.js";
import ObservedBoolean from "../../core/model/ObservedBoolean.js";
import { ReactiveAnd } from "../../core/model/reactive/model/logic/ReactiveAnd.js";
import { ReactiveReference } from "../../core/model/reactive/model/terminal/ReactiveReference.js";
import { AchievementNotificationView } from "../../view/game/achievements/AchievementNotificationView.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 { SerializationMetadata } from "../ecs/components/SerializationMetadata.js";
import Entity from "../ecs/Entity.js";
import GUIElement from "../ecs/gui/GUIElement.js";
import ViewportPosition from "../ecs/gui/position/ViewportPosition.js";
import { Transform } from "../ecs/transform/Transform.js";
import { SequenceBehavior } from "../intelligence/behavior/composite/SequenceBehavior.js";
import { BehaviorComponent } from "../intelligence/behavior/ecs/BehaviorComponent.js";
import { ClockChannelType } from "../intelligence/behavior/ecs/ClockChannelType.js";
import { DieBehavior } from "../intelligence/behavior/ecs/DieBehavior.js";
import { logger } from "../logging/GlobalLogger.js";
import { globalMetrics } from "../metrics/GlobalMetrics.js";
import { MetricsCategory } from "../metrics/MetricsCategory.js";
import { EnginePlugin } from "../plugin/EnginePlugin.js";
import { SoundEmitter } from "../sound/ecs/emitter/SoundEmitter.js";
import { SoundEmitterChannels } from "../sound/ecs/emitter/SoundEmitterSystem.js";
import { Achievement } from "./Achievement.js";


const SLOW_CUBIC = makeCubicCurve(0.04, 0.4, 0.9, 0.99);
const OVERSHOT_CUBIC_0 = makeCubicCurve(0.04, 0.4, 1.8, 0.99);

/**
 *
 * @param {String} id
 * @returns {string}
 */
function computeAchievementBlackboardFlag(id) {
    return `system.achievement.${id}.completed`;
}

export class AchievementManager extends EnginePlugin {
    constructor() {
        super();
        this.id = 'achievements';

        /**
         *
         * @type {AchievementGateway|null}
         */
        this.gateway = null;

        /**
         *
         * @type {Achievement[]}
         */
        this.entries = [];


        /**
         *
         * @type {AssetManager|null}
         */
        this.assetManager = null;

        /**
         *
         * @type {Blackboard|null}
         */
        this.blackboard = null;

        /**
         *
         * @type {EntityManager|null}
         */
        this.entityManager = null;

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

        /**
         * @readonly
         * @type {ObservedBoolean}
         */
        this.isStarted = new ObservedBoolean(false);


        /**
         * @readonly
         * @type {ObservedBoolean}
         */
        this.isGatewayInitialized = new ObservedBoolean(false);

        /**
         * @readonly
         * @type {ObservedBoolean}
         */
        this.isBlackboardAttached = new ObservedBoolean(false);

        /**
         * @readonly
         * @type {ReactiveAnd}
         */
        this.isActive = ReactiveAnd.from(
            ReactiveReference.from(this.isStarted, 'started'),
            ReactiveReference.from(this.isBlackboardAttached, 'blackboardAttached')
        );

        this.isActive.onChanged.add(v => {
            if (v) {
                this.activate();
            } else {
                this.deactivate();
            }
        });

        this.handlers = {};
    }

    /**
     *
     * @param {String} id
     * @return {Achievement|undefined}
     */
    getAchievementById(id) {
        return this.entries.find(a => a.id === id);
    }


    /**
     *
     * @param {String} id
     */
    unlock(id) {
        assert.isString(id, 'id');

        const key = computeAchievementBlackboardFlag(id);
        const value = this.blackboard.acquireBoolean(key, false);

        value.set(true);

        //release value
        this.blackboard.release(key);

        this.gateway.unlock(id);

        const achievement = this.getAchievementById(id);

        if (achievement === undefined) {
            logger.warn(`Attempting to unlock achievement ${id}. Achievement not found`);
        }

        if (achievement !== undefined && this.isGatewayInitialized.getValue()) {
            this.deactivateEntry(achievement);

            this.present(achievement);
        }

        globalMetrics.record("achievement", {
            category: MetricsCategory.Progression,
            label: id
        });
    }

    /**
     * @private
     * @param {Achievement} entry
     */
    activateEntry(entry) {
        entry.trigger.link(this.blackboard);

        const handler = (v) => {
            if (v) {
                this.unlock(entry.id);
            }
        };


        this.handlers[entry.id] = handler;

        entry.trigger.getExpression()
            .process(handler);
    }

    /**
     * @private
     * @param {Achievement} entry
     */
    deactivateEntry(entry) {
        entry.trigger.unlink();

        const handler = this.handlers[entry.id];

        if (handler !== undefined) {

            entry.trigger.getExpression()
                .onChanged.remove(handler);
        }
    }


    /**
     * @private
     */
    activate() {
        this.entries.forEach(a => {

            if (!a.enabled) {
                //ignore
                return;
            }

            this.activateEntry(a);
        });
    }

    /**
     * @private
     */
    deactivate() {

        this.entries.forEach(a => {
            this.deactivateEntry(a);
        });

    }

    async initialize(engine) {

        this.assetManager = engine.assetManager;
        this.gateway = engine.platform.getAchievementGateway();
        this.entityManager = engine.entityManager;
        this.localization = engine.localization;
    }

    /**
     *
     * @param {AssetManager} assetManager
     */
    async loadDefinitions(assetManager) {
        const asset = await assetManager.promise("data/database/achievements/data.json", GameAssetType.JSON)

        const json = asset.create();

        json.forEach(def => {
            const achievement = new Achievement();

            achievement.fromJSON(def);

            this.entries.push(achievement);
        });

    }

    /**
     * Visually present an achievement
     * @param {Achievement} achievement
     */
    present(achievement) {
        const ecd = this.entityManager.dataset;

        if (ecd === null) {
            //no ECD, skip
            return;
        }

        const localization = this.localization;

        const achievementView = new AchievementNotificationView({ achievement, localization });

        achievementView.size.x = 460;
        achievementView.size.y = 58;

        const viewportPosition = ViewportPosition.fromJSON({
            position: new Vector2(0.5, 0)
        });

        viewportPosition.anchor.set(0.5, 0.5);


        const aEntry = new AnimationTrack(['scale', 'alpha', 'v']);
        aEntry.addKey(0, [1.3, 0.2, 0]);
        aEntry.addKey(0.4, [1, 1, 0.8]);
        aEntry.addTransition(0, OVERSHOT_CUBIC_0);

        const aMain = new AnimationTrack(["value"]);
        aMain.addKey(0, [0]);
        aMain.addKey(8.3, [1]);
        aMain.addTransition(0, TransitionFunctions.Linear);

        const aExit = new AnimationTrack(['scale', 'alpha', 'v']);
        aExit.addKey(0, [1, 1, 0]);
        aExit.addKey(1, [1, 0, 1]);
        aExit.addTransition(0, SLOW_CUBIC);

        const builder = new Entity();

        const sequenceBehavior = SequenceBehavior.from([
            new AnimationBehavior(new AnimationTrackPlayback(aEntry, (scale, alpha, v) => {
                achievementView.scale.setScalar(scale);
                achievementView.css({
                    opacity: alpha
                });
            })),
            new AnimationBehavior(new AnimationTrackPlayback(aMain, (value) => {
            })),
            new AnimationBehavior(new AnimationTrackPlayback(aExit, (scale, alpha, v) => {
                achievementView.scale.setScalar(scale);
                achievementView.css({
                    opacity: alpha
                });
            })),
            DieBehavior.create()
        ]);

        const cBehavior = BehaviorComponent.from(sequenceBehavior);

        //use system clock for behavior
        cBehavior.clock = ClockChannelType.Simulation;

        const guiElement = GUIElement.fromView(achievementView);

        guiElement.group = "ui-managed-achievements";

        const soundEmitter = SoundEmitter.fromJSON({
            tracks: [
                {
                    url: "data/sounds/effects/Magic_Game_Essentials/Magic_Airy_Alert.wav",
                    startWhenReady: true
                }
            ],
            isPositioned: false,
            volume: 1,
            loop: false,
            channel: SoundEmitterChannels.Effects
        });

        //prevent achievement message from being serialized in game save

        builder
            .add(SerializationMetadata.Transient)
            .add(new Transform())
            .add(soundEmitter)
            .add(cBehavior)
            .add(guiElement)
            .add(viewportPosition)
            .build(ecd);
    }

    /**
     *
     * @param {Blackboard} blackboard
     */
    attachBlackboard(blackboard) {
        if (this.blackboard !== null) {
            throw new Error('Blackboard already attached');
        }

        this.blackboard = blackboard;
        this.isBlackboardAttached.set(true);
    }

    async initializeGateway() {
        /**
         *
         * @type {string[]}
         */
        const unlockedIds = await this.gateway.getUnlocked();

        //de-activate unlocked achievements
        const unlockedAchievements = this.entries.filter(a => unlockedIds.includes(a.id));

        unlockedAchievements.forEach(a => a.enabled = false);


        //try to read unlocked achievements from blackboard
        if (this.isBlackboardAttached.getValue()) {

            this.entries.forEach(achievement => {
                const id = achievement.id;

                const blackboardKey = computeAchievementBlackboardFlag(id);

                const blackboardValue = this.blackboard.acquireBoolean(blackboardKey, false);

                if (!unlockedIds.includes(id)) {
                    // gateway doesn't have this achievement unlocked yet

                    // check blackboard

                    if (blackboardValue.getValue()) {
                        // unlocked in the blackboard, communicate to the gateway
                        this.gateway.unlock(id);

                        //disable achievement tracking
                        achievement.enabled = false;
                    }


                } else {

                    //not marker as unlocked on gateway
                    this.deactivateEntry(achievement);

                }

                //release the value
                this.blackboard.release(blackboardKey);
            });

        }

        this.isGatewayInitialized.set(true);

    }

    async startup() {
        assert.equal()

        //load achievement definitions
        await this.loadDefinitions(this.assetManager);

        this.initializeGateway();

        this.isStarted.set(true);
    }

    async shutdown() {

    }
}
