import { Object3D, PlaneGeometry, Scene, TextureLoader, VideoTexture, Mesh, MeshStandardMaterial } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Utils } from "../utils/Utils";
import SceneRendererTJS from "../SceneRendererTJS";

/**
 * Interface to define the ARVideo object used in addVideo.
 * @param play play a video.
 */
interface ARvideo {
    play: () => void;
}

/**
 * Interface to define an Entity.
 * @param name the name of the Entity
 */
interface Entity {
    name: string;
}

/**
 * Interface to define the PlaneGeometry used in the addImage and addVideo functions.
 * @param w  width of the PlaneGeometry.
 * @param h height of the PlaneGeometry.
 * @param ws width number of segments of the PlaneGeometry.
 * @param hs height number of segments of the PlaneGeometry.
 */
interface IPlaneConfig {
    w: number;
    h: number;
    ws: number;
    hs: number;
}

/**
 * This class is responsable to attach Threejs object to the rendering root and pass matrix data to it.
 */
export default class NFTaddTJS {
    private entities: Entity[] = [];
    private names: Array<string>;
    private scene: Scene;
    private target: EventTarget;
    private uuid: string;

    /**
     * The NFTaddTJS constuctor, you need to pass the uuid from the ARnft instance.
     * @param uuid the uuid.
     */
    constructor(uuid: string) {
        this.scene = SceneRendererTJS.getGlobalScene();
        this.target = window || global;
        this.uuid = uuid;
        this.names = [];
    }

    /**
     * The add function will add a mesh to the Renderer root. You need to associate a name of the Entity.
     * @param mesh The mesh to add
     * @param name the name of the Entity associated.
     * @param objVisibility set true or false if the mesh wll stay visible or not after tracking.
     */
    public add(mesh: Object3D, name: string, objVisibility: boolean) {
        this.target.addEventListener("getNFTData-" + this.uuid + "-" + name, (ev: any) => {
            var msg = ev.detail;
            mesh.position.y = ((msg.height / msg.dpi) * 2.54 * 10) / 2.0;
            mesh.position.x = ((msg.width / msg.dpi) * 2.54 * 10) / 2.0;
        });
        const root = new Object3D();
        root.name = "root-" + name;
        this.scene.add(root);
        root.add(mesh);
        this.target.addEventListener("getMatrixGL_RH-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = true;
            mesh.visible = true;
            root.matrixAutoUpdate = false;
            Utils.setMatrix(root.matrix, ev.detail.matrixGL_RH);
        });
        this.target.addEventListener("nftTrackingLost-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = objVisibility;
            mesh.visible = objVisibility;
        });
        this.names.push(name);
        this.entities.push({ name });
    }

    /**
     * The addModel function will add a model to the Renderer root. You need to associate a name of the Entity.
     * @param url url of the model.
     * @param name the name of the Entity associated.
     * @param scale scale of the model.
     * @param objVisibility set true or false if the mesh wll stay visible or not after tracking.
     */
    public addModel(url: string, name: string, scale: number, objVisibility: boolean) {
        const root = new Object3D();
        root.name = "root-" + name;
        this.scene.add(root);
        let model: any;
        /* Load Model */
        const threeGLTFLoader = new GLTFLoader();
        threeGLTFLoader.load(url, (gltf: { scene: Object3D }) => {
            model = gltf.scene;
            model.scale.set(scale, scale, scale);
            model.rotation.x = Math.PI / 2;
            this.target.addEventListener("getNFTData-" + this.uuid + "-" + name, (ev: any) => {
                var msg = ev.detail;
                model.position.y = ((msg.height / msg.dpi) * 2.54 * 10) / 2.0;
                model.position.x = ((msg.width / msg.dpi) * 2.54 * 10) / 2.0;
            });
            root.add(model);
        });
        this.target.addEventListener("getMatrixGL_RH-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = true;
            model.visible = true;
            root.matrixAutoUpdate = false;
            Utils.setMatrix(root.matrix, ev.detail.matrixGL_RH);
        });
        this.target.addEventListener("nftTrackingLost-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = objVisibility;
            model.visible = objVisibility;
        });
        this.names.push(name);
    }

    /**
     * The addModelWithCallback function will add a model to the Renderer root. You need to associate a name of the Entity.
     * You can modify the model rotation, scale and other properties with the callback.
     * @param url url of the model.
     * @param name the name of the Entity associated.
     * @param callback modify the model in the callback.
     * @param objVisibility set true or false if the mesh wll stay visible or not after tracking.
     */
    public addModelWithCallback(url: string, name: string, callback: (gltf: any) => {}, objVisibility: boolean) {
        const root = new Object3D();
        root.name = "root-" + name;
        this.scene.add(root);
        let model: any;
        /* Load Model */
        const threeGLTFLoader = new GLTFLoader();
        threeGLTFLoader.load(url, (gltf: { scene: Object3D }) => {
            model = gltf.scene;
            this.target.addEventListener("getNFTData-" + this.uuid + "-" + name, (ev: any) => {
                var msg = ev.detail;
                model.position.y = ((msg.height / msg.dpi) * 2.54 * 10) / 2.0;
                model.position.x = ((msg.width / msg.dpi) * 2.54 * 10) / 2.0;
            });
            callback(gltf);
            root.add(model);
        });
        this.target.addEventListener("getMatrixGL_RH-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = true;
            model.visible = true;
            root.matrixAutoUpdate = false;
            Utils.setMatrix(root.matrix, ev.detail.matrixGL_RH);
        });
        this.target.addEventListener("nftTrackingLost-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = objVisibility;
            model.visible = objVisibility;
        });
        this.names.push(name);
    }

    /**
     * The addImage function will add an image to the Renderer root. You need to associate a name of the Entity.
     * @param imageUrl url of the image.
     * @param name the name of the Entity associated.
     * @param color color of the background plane.
     * @param scale scale of the plane.
     * @param configs see IPlaneConfig.
     * @param objVisibility set true or false if the mesh wll stay visible or not after tracking.
     */
    public addImage(
        imageUrl: string,
        name: string,
        color: string,
        scale: number,
        configs: IPlaneConfig,
        objVisibility: boolean
    ) {
        const root = new Object3D();
        root.name = "root-" + name;
        this.scene.add(root);
        const planeGeom = new PlaneGeometry(configs.w, configs.h, configs.ws, configs.hs);
        const texture = new TextureLoader().load(imageUrl);
        const material = new MeshStandardMaterial({ color: color, map: texture });
        const plane = new Mesh(planeGeom, material);
        plane.scale.set(scale, scale, scale);
        this.target.addEventListener("getNFTData-" + this.uuid + "-" + name, (ev: any) => {
            var msg = ev.detail;
            plane.position.y = ((msg.height / msg.dpi) * 2.54 * 10) / 2.0;
            plane.position.x = ((msg.width / msg.dpi) * 2.54 * 10) / 2.0;
        });
        root.add(plane);
        this.target.addEventListener("getMatrixGL_RH-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = true;
            plane.visible = true;
            root.matrixAutoUpdate = false;
            Utils.setMatrix(root.matrix, ev.detail.matrixGL_RH);
        });
        this.target.addEventListener("nftTrackingLost-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = objVisibility;
            plane.visible = objVisibility;
        });
        this.names.push(name);
    }

    /**
     * The addVideo function will add a video to the Renderer root. You need to associate a name of the Entity.
     * @param id the id of the html video element.
     * @param name the name of the Entity associated.
     * @param scale scale of the plane.
     * @param configs see IPlaneConfig.
     * @param objVisibility set true or false if the mesh wll stay visible or not after tracking.
     */
    public addVideo(id: string, name: string, scale: number, configs: IPlaneConfig, objVisibility: boolean) {
        const root = new Object3D();
        root.name = "root-" + name;
        this.scene.add(root);
        const ARVideo: HTMLVideoElement = document.getElementById(id) as HTMLVideoElement;
        const texture = new VideoTexture(ARVideo as HTMLVideoElement);
        const mat = new MeshStandardMaterial({ color: 0xbbbbff, map: texture });
        const planeGeom = new PlaneGeometry(configs.w, configs.h, configs.ws, configs.hs);
        const plane = new Mesh(planeGeom, mat);
        plane.scale.set(scale, scale, scale);
        this.target.addEventListener("getNFTData-" + this.uuid + "-" + name, (ev: any) => {
            var msg = ev.detail;
            plane.position.y = ((msg.height / msg.dpi) * 2.54 * 10) / 2.0;
            plane.position.x = ((msg.width / msg.dpi) * 2.54 * 10) / 2.0;
        });
        root.add(plane);
        this.target.addEventListener("getMatrixGL_RH-" + this.uuid + "-" + name, (ev: any) => {
            setTimeout(() => {
                ARVideo.play();
                root.visible = true;
                plane.visible = true;
                root.matrixAutoUpdate = false;
            }, 100);
            Utils.setMatrix(root.matrix, ev.detail.matrixGL_RH);
        });
        this.target.addEventListener("nftTrackingLost-" + this.uuid + "-" + name, (ev: any) => {
            root.visible = objVisibility;
            plane.visible = objVisibility;
            ARVideo.pause();
        });
        this.names.push(name);
    }

    /**
     * You can get the names of the entities used in your project.
     * @returns the names of the entities
     */
    public getNames() {
        return this.names;
    }
}
