import { FaceResult } from "@geenee/bodyprocessors";
import { FaceRenderer, HeadFitPlugin, PatchPartPlugin,
    OccluderMaterial } from "@geenee/bodyrenderers-babylon";
import { MaskUploadPlugin, MaskSmoothPlugin, MaskUpscalePlugin,
    MaskDilationPlugin } from "@geenee/bodyrenderers-common"
import { Scene } from "@babylonjs/core/scene";
import { Mesh } from "@babylonjs/core/Meshes/mesh";
import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
import { PointLight } from "@babylonjs/core/Lights/pointLight";
import { Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector";
import { Color3 } from "@babylonjs/core/Maths/math.color";
import { LoadAssetContainerAsync } from "@babylonjs/core/Loading/sceneLoader";
import { ImageProcessingConfiguration }
    from "@babylonjs/core/Materials/imageProcessingConfiguration";
import "@babylonjs/core/Materials/Textures/Loaders/envTextureLoader";
import "@babylonjs/loaders/glTF/2.0";

export class HatRenderer extends FaceRenderer {
    // Scene
    protected hat?: TransformNode;
    protected patcher: PatchPartPlugin;
    // Mouth O-shape
    protected mouthOpenness = 0;
    protected mouthOpen = false;
    protected textModel?: Mesh;

    // Constructor
    constructor(container: HTMLElement, mode?: "fit" | "crop") {
        super(container, mode);
        // Mask processing
        this.addPlugin(new MaskUploadPlugin);
        this.addPlugin(new MaskSmoothPlugin(0.5));
        this.addPlugin(new MaskUpscalePlugin(0.49, 2));
        this.addPlugin(new MaskSmoothPlugin(3));
        this.addPlugin(new MaskDilationPlugin(3));
        // Patcher
        this.patcher = new PatchPartPlugin(0.45, 512);
        this.addPlugin(this.patcher);
    }

    // Load assets and setup scene
    async load() {
        if (this.loaded || !this.scene)
            return;
        await this.setupScene(this.scene);
        await super.load();
    }

    // Setup scene
    protected async setupScene(scene: Scene) {
        // Hat
        const hatGltf = await LoadAssetContainerAsync(
            "hat.glb", scene, { pluginExtension: ".glb" });
        hatGltf.addAllToScene();
        this.hat = this.scene?.getTransformNodeByName("HeadTrack") || undefined;
        if (this.hat) {
            const fitter = new HeadFitPlugin(true);
            fitter.setNode(this.hat);
            this.addPlugin(fitter);
        }
        // Occluder
        const head = this.scene?.getMeshByName("HeadOccluder") || undefined;
        if (head)
            head.material = new OccluderMaterial("OccluderMaterial", scene);
        const cloth = hatGltf.meshes.filter((m) => /object/i.test(m.name));
        this.patcher.setParts(cloth, head ? [head] : []);
        // Lightning
        const light = new PointLight("pointLight",
            new Vector3(0, 0, 0), scene);
        light.intensity = 0.75;
        light.diffuse = new Color3(1, 1, 1);
        light.specular = new Color3(1, 1, 1);
        scene.imageProcessingConfiguration.contrast = 1.4;
        scene.imageProcessingConfiguration.exposure = 1.2;
        scene.imageProcessingConfiguration.toneMappingEnabled = true;
        scene.imageProcessingConfiguration.toneMappingType =
            ImageProcessingConfiguration.TONEMAPPING_KHR_PBR_NEUTRAL;
        scene.environmentTexture = new CubeTexture("environment.env", scene);
        // Text model
        const textGltf = await LoadAssetContainerAsync(
            "text.glb", scene, { pluginExtension: ".glb" });
        this.textModel = textGltf.createRootMesh();
        textGltf.addAllToScene();
    }

    // Update
    async update(result: FaceResult, stream: HTMLCanvasElement) {
        // Analyze face keypoints to detect open mouth
        const { transform = undefined, metric = undefined } = result.faces[0] || {};
        if (!metric) {
            this.mouthOpenness = 0;
            this.mouthOpen = false;
            return super.update(result, stream);
        }
        const left = new Vector3(...metric[78]);
        const right = new Vector3(...metric[308]);
        const top = new Vector3(...metric[13]);
        const bottom = new Vector3(...metric[14]);
        // Openness is ratio between height and width
        // Add hysteresis when changing mouth state
        this.mouthOpenness = top.subtract(bottom).length() / left.subtract(right).length();
        if (this.mouthOpenness > 0.6)
            this.mouthOpen = true;
        if (this.mouthOpenness < 0.5)
            this.mouthOpen = false;
        // Position text model
        const { textModel } = this;
        if (textModel && transform) {
            // Align model with mesh
            textModel.rotationQuaternion = new Quaternion(...transform.rotation);
            textModel.position.set(...transform.translation);
            textModel.scaling.setAll(transform.scale);;
            textModel.setEnabled(this.mouthOpen);
        }
        await super.update(result, stream);
    }
}
