# Source: GlobalEngine.ts

    import EffectController from "./EffectController";
    import Loop from "./Loop";
    import PipelineManager from "./Pipe/PipelineManager";
    import MeshGeneratorManager from "./MeshGenerators/MeshGeneratorManager";
    import Screen from "./Utility/Screen";
    import * as THREE from "three";
    import Video from "./Video";
    import OnScreen from "./OnScreen";
    import { Tracker } from "./Tracker";
    import Globals from "./Globals";
    import ControllableObjects from "./ControllableObjects";
    import { FrameProvider } from "./FrameProvider";
    import { ModulesManager } from "./ModulesManager";
    import {
        BoundingBox,
        Effect,
        FeatureTemplate,
        FinishTemplate,
    } from "./FeatureTemplate";

    type Dependencies = {
        modulesManager: ModulesManager;
        frameProvider: FrameProvider;
    };

    function CreateRenderer(
        width: number,
        height: number,
        preserveDrawingBuffer: boolean,
    ) {
        const renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: true,
            preserveDrawingBuffer,
        });

        renderer.setSize(width, height);
        renderer.autoClear = false;

        return renderer;
    }

    function CreateLoopAndController(
        modulesManager: ModulesManager,
        frameProvider: FrameProvider,
        renderer: THREE.WebGLRenderer,
    ) {
        const video = new Video();
        const onScreen = new OnScreen();

        const meshGeneratorManager = new MeshGeneratorManager(modulesManager);
        const pipelineManager = new PipelineManager(video.filter, onScreen);
        const controller = new EffectController(
            pipelineManager,
            meshGeneratorManager,
        );

        const loop = new Loop(
            frameProvider,
            renderer,
            pipelineManager,
            meshGeneratorManager,
            video,
            controller,
            onScreen,
        );

        return [loop, controller] as const;
    }

    /**
     * A glorified wrapper around loop and effectController.
     * Only a single instance of it makes sense, and may not be initialized multiple times.
     * This class is be internal, MakeupEngine and CMSMakeupEngine need to be used externally.
     */
    export class GlobalEngine {
        static instance: GlobalEngine | null;

        static Initialize(
            deps: Dependencies,
            screen: { width: number; height: number },
            preserveDrawingBuffer: boolean,
        ) {
            if (GlobalEngine.instance) {
                throw new Error("Already initialized.");
            }

            Tracker.Create(deps.modulesManager);

            Globals.controllableObjects = new ControllableObjects();

            const renderer = CreateRenderer(
                screen.width,
                screen.height,
                preserveDrawingBuffer,
            );
            Screen.SetRenderer(renderer);

            const [loop, effectController] = CreateLoopAndController(
                deps.modulesManager,
                deps.frameProvider,
                renderer,
            );

            GlobalEngine.instance = new GlobalEngine(effectController, loop);
        }

        private constructor(
            private effectController: EffectController,
            private loop: Loop,
        ) {}

        async CreateMakeup(
            featureTemplate: FeatureTemplate,
            finishTemplate: FinishTemplate,
            boundingBox: BoundingBox = [0.0, 0.0, 1.0, 1.0],
        ): Promise<Effect> {
            const effect = await this.effectController.Create(
                featureTemplate,
                finishTemplate,
                boundingBox,
            );
            return effect;
        }

        UpdateMakeup(effect: Effect): Promise<void> {
            return this.effectController.Refresh(effect);
        }

        IsMakeupDisplayed(effect: Effect): boolean {
            return this.effectController.IsMakeupDisplayed(effect);
        }

        DisplayMakeup(effect: Effect): void {
            this.effectController.Display(effect);
        }

        HideMakeup(effect: Effect): void {
            this.effectController.Hide(effect);
        }

        RemoveMakeup(effect: Effect): void {
            this.effectController.Remove(effect);
        }

        IntersectMakeup(effect: Effect, x: number, y: number) {
            const pixelCoords = { x, y };
            return intersectMakeup(effect, this.effectController, pixelCoords);
        }

        onIterationComplete(callback: VoidFunction): void {
            this.loop.onIterationComplete(callback);
        }
    }

    /**
     * Returns the uv area texture coordinates corresponding to position on the output view canvas that displays the makeup effect.
     * @param {Effect} effect - makeup effect
     * @param {EffectController} effectController - the controller that controls the effect
     * @param {{x:number, y: number}} pixelCoords - pixel distance from the left ant top output view canvas edge
     */
    function intersectMakeup(
        effect: Effect,
        effectController: EffectController,
        pixelCoords: { x: number; y: number },
    ) {
        // NOTE: previously, the initial passed width and height were used here if that makes any difference
        // that seems it would have broken if size ever changed.
        const { width, height } = Screen.size;

        // calculate distance in normalized device coordinates where (-1,-1) point is in the lower-left corner and (1,1) in top-right corner
        const x_ndc_space = (2.0 * pixelCoords.x) / width - 1.0; // In range [-1, 1]
        const y_ndc_space = 1.0 - (2.0 * pixelCoords.y) / height; // In range [-1, 1]

        return effectController.Intersect(effect, x_ndc_space, y_ndc_space);
    }