import { FaceRenderer, HeadFitPlugin, OccluderMaterial,
    isMesh} from "@geenee/bodyrenderers-three";
import { FaceResult } from "@geenee/bodyprocessors";
import { Scene } from "three/src/scenes/Scene";
import { Object3D } from "three/src/core/Object3D";
import { Mesh } from "three/src/objects/Mesh";
import { Group } from "three/src/objects/Group";
import { MeshStandardMaterial } from "three/src/materials/MeshStandardMaterial";
import { PointLight } from "three/src/lights/PointLight";
import { AmbientLight } from "three/src/lights/AmbientLight";
import { Vector3 } from "three/src/math/Vector3";
import { SRGBColorSpace } from "three/src/constants";
import { Quaternion } from "three/src/math/Quaternion";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";

export class HatRenderer extends FaceRenderer {
    // Scene
    protected hat?: Object3D;
    protected head?: Object3D;
    protected light?: PointLight;
    protected ambient?: AmbientLight;
    readonly lightInt = 1;
    readonly ambientInt = 3;
    // Mouth O-shape
    protected mouthOpenness = 0;
    protected mouthOpen = false;
    protected textModel?: Group;

    // Constructor
    constructor(container: HTMLElement, mode?: "fit" | "crop") {
        super(container, mode);
    }

    // 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 new GLTFLoader().loadAsync("hat.glb");
        scene.add(hatGltf.scene);
        this.hat = scene.getObjectByName("HeadTrack");
        if (this.hat) {
            const fitter = new HeadFitPlugin(true);
            fitter.setNode(this.hat);
            this.addPlugin(fitter);
        }
        // Occluder
        this.head = scene.getObjectByName("HeadOccluder");
        if (this.head && isMesh(this.head)) {
            let materials = this.head.material;
            this.head.material = new OccluderMaterial("OccluderMaterial");
            this.head.renderOrder = -1;
            if (!(materials instanceof Array))
                materials = [materials];
            materials.forEach((m) => m.dispose());
        }
        // Lightning
        this.light = new PointLight(0xFFFFFF, this.lightInt);
        this.ambient = new AmbientLight(0xFFFFFF, this.ambientInt);
        this.camera.add(this.light);
        scene.add(this.ambient);
        // Environment
        this.renderer.outputColorSpace = SRGBColorSpace;
        const environment = await new RGBELoader().loadAsync("environment.hdr");
        scene.environment = environment;
        // Text model
        const font = await new FontLoader().loadAsync("font.json");
        const geometry = new TextGeometry("WOW!!!", {
            font: font, size: 0.03, depth: 0.01,
            bevelSize: 0.001, bevelThickness: 0.01,
            bevelSegments: 10, bevelEnabled: true
        });
        const mesh = new Mesh(geometry, [
            new MeshStandardMaterial({
                color: 0x3BDB9B, opacity: 0.85, flatShading: true }),
            new MeshStandardMaterial({
                color: 0x3BDB9B, opacity: 0.85 })
        ]);
        // Center model
        geometry.computeBoundingBox();
        const box = geometry.boundingBox;
        if (box) {
            mesh.position.x = -0.5 * (box.max.x - box.min.x);
            mesh.position.y = -0.5 * (box.max.y - box.min.y) - 0.05;
            mesh.position.z = 0.05;
        }
        this.textModel = new Group();
        this.textModel.visible = false;
        this.textModel.add(mesh);
        this.scene?.add(this.textModel);
    }

    // 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.distanceTo(bottom) / left.distanceTo(right);
        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.setRotationFromQuaternion(
                new Quaternion(...transform.rotation));
            textModel.position.set(...transform.translation);
            textModel.scale.setScalar(transform.scale);
            textModel.visible = this.mouthOpen;
        }
        await super.update(result, stream);
    }
}
