import { PoseRenderer, PoseBFitPlugin, OccluderMaterial,
    isMesh } from "@geenee/bodyrenderers-three";
import { OutfitParams } from "@geenee/bodyrenderers-common";
import { PoseResult } from "@geenee/bodyprocessors";
import { Scene } from "three/src/scenes/Scene";
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 { EquirectangularReflectionMapping } from "three/src/constants";
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";

// Renderer
export class AvatarRenderer extends PoseRenderer {
    // Scene
    protected plugin: PoseBFitPlugin;
    protected model?: Group;
    protected light?: PointLight;
    protected ambient?: AmbientLight;
    readonly lightInt: number = 0.75;
    readonly ambientInt: number = 1.25;
    // Hands up
    protected handsUp = false;
    protected textModel?: Group;

    // Constructor
    constructor(
        container: HTMLElement,
        mode?: "fit" | "crop",
        mirror?: boolean,
        protected url = "onesie.glb",
        protected outfit?: OutfitParams) {
        super(container, mode, mirror);
        this.plugin = new PoseBFitPlugin();
        this.addPlugin(this.plugin);
    }

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

    // Setup scene
    protected async setupScene(scene: Scene) {
        // Model
        await this.setModel(this.url);
        // Lightning
        this.light = new PointLight(0xFFFFFF, this.lightInt);
        this.ambient = new AmbientLight(0xFFFFFF, this.ambientInt);
        scene.add(this.light);
        scene.add(this.ambient);
        // Environment
        const environment = await new RGBELoader().loadAsync("environment.hdr");
        environment.mapping = EquirectangularReflectionMapping;
        scene.environment = environment;
        // Text model
        const font = await new FontLoader().loadAsync("font.json");
        const geometry = new TextGeometry("HOORAY!!!", {
            font: font, size: 5,
            bevelSize: 0.3, bevelThickness: 1,
            bevelSegments: 10, bevelEnabled: true
        });
        // Center model
        geometry.scale(0.01, 0.01, 0.01);
        geometry.computeBoundingBox();
        const box = geometry.boundingBox;
        if (box)
            geometry.translate(...box.max.sub(box.min).
                multiplyScalar(-0.5).toArray());
        geometry.rotateY(Math.PI);
        const mesh = new Mesh(geometry, [
            new MeshStandardMaterial({ color: 0x3BDB9B, flatShading: true }),
            new MeshStandardMaterial({ color: 0x3BDB9B })
        ]);
        this.textModel = new Group();
        this.textModel.visible = false;
        this.textModel.add(mesh);
        this.scene?.add(this.textModel);
    }

    // Set model to render
    async setModel(url: string) {
        return this.setOutfit(url, this.outfit);
    }

    // Set outfit to render
    async setOutfit(url: string, outfit?: OutfitParams) {
        if (this.model)
            this.disposeObject(this.model);
        delete this.model;
        this.url = url;
        this.outfit = outfit;
        const gltf = await new GLTFLoader().loadAsync(url);
        const model = gltf.scene;
        this.scene?.add(model);
        this.plugin.setNode(model);
        model.traverse((mesh) => {
            if (!isMesh(mesh))
                return;
            const matchMesh = (matches?: (string | RegExp)[]) => {
                return matches?.some((match) => {
                    return (typeof match === "string") ?
                        mesh.name === match : match.test(mesh.name);
                })
            }
            // Occluder meshes
            if (matchMesh(outfit?.occluders)) {
                let materials = mesh.material;
                mesh.material = new OccluderMaterial("OccluderMaterial");
                mesh.renderOrder = -1;
                if (!(materials instanceof Array))
                    materials = [materials];
                materials.forEach((m) => m.dispose());
            }
            // Hidden meshes
            if (matchMesh(outfit?.hidden)) {
                mesh.visible = false;
                return;
            }
        });
        this.model = model;
    }

    // Update
    async update(result: PoseResult, stream: HTMLCanvasElement) {
        // Analyze pose keypoints to detect hands up
        const pose = result.poses[0];
        if (!pose) {
            this.handsUp = false;
            return super.update(result, stream);
        }
        // Keypoints
        const { points } = pose;
        const hipL = new Vector3(...points.hipL.metric);
        const hipR = new Vector3(...points.hipR.metric);
        const shoulderL = new Vector3(...points.shoulderL.metric);
        const shoulderR = new Vector3(...points.shoulderR.metric);
        const elbowL = new Vector3(...points.elbowL.metric);
        const elbowR = new Vector3(...points.elbowR.metric);
        const wristL = new Vector3(...points.wristL.metric);
        const wristR = new Vector3(...points.wristR.metric);
        // Arm vectors
        const torsoL = shoulderL.clone().sub(hipL).normalize();
        const torsoR = shoulderR.clone().sub(hipR).normalize();
        const armL = elbowL.clone().sub(shoulderL).normalize();
        const armR = elbowR.clone().sub(shoulderR).normalize();
        const foreArmL = wristL.clone().sub(elbowL).normalize();
        const foreArmR = wristR.clone().sub(elbowR).normalize();
        // Dot product of unit vectors gives cos of angle between
        // If vectors are parallel, angle is close to 0, cos to 1
        const armLCos = torsoL.dot(armL);
        const armRCos = torsoR.dot(armR);
        const foreArmLCos = foreArmL.dot(armL);
        const foreArmRCos = foreArmR.dot(armR);
        // Hands are up if all vectors have almost the same direction
        // Add hysteresis when changing mouth state to reduce noise
        const cosMin = Math.min(armLCos, armRCos, foreArmLCos, foreArmRCos);
        if (cosMin > 0.8)
            this.handsUp = true;
        if (cosMin < 0.7)
            this.handsUp = false;
        // Position text model
        const { textModel } = this;
        if (textModel) {
            const position = wristL.clone().lerp(wristR, 0.5);
            textModel.position.copy(position);
            textModel.visible = this.handsUp;
        }
        await super.update(result, stream);
    }
}
