import {
    BoxGeometry,
    BufferAttribute,
    BufferGeometry,
    Color,
    ConeGeometry,
    CylinderGeometry,
    Mesh,
    MeshStandardMaterial,
    Raycaster,
    SphereGeometry,
    Vector3,
} from 'three';
import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask';
import { findSceneRecursive } from '../helper/findSceneRecursive/findSceneRecursive';
import { DIVENode } from '../node/Node';
import { type COMGeometry, type COMMaterial } from '../com/types';
import { DIVECommunication } from '../com/Communication';

/**
 * A basic model class.
 *
 * It does calculate it's own bounding box which is used for positioning on the floor.
 *
 * Can be moved and selected.
 *
 * @module
 */
export class DIVEPrimitive extends DIVENode {
    readonly isDIVEPrimitive: true = true;

    private _mesh: Mesh;

    constructor() {
        super();

        this._mesh = new Mesh();
        this._mesh.layers.mask = PRODUCT_LAYER_MASK;
        this._mesh.castShadow = true;
        this._mesh.receiveShadow = true;
        this._mesh.material = new MeshStandardMaterial();
        this.add(this._mesh);
    }

    public SetGeometry(geometry: COMGeometry): void {
        const geo = this.assembleGeometry(geometry);
        if (!geo) return;

        this._mesh.geometry = geo;
        this._boundingBox.setFromObject(this._mesh);
    }

    public SetMaterial(material: Partial<COMMaterial>): void {
        const primitiveMaterial = this._mesh.material as MeshStandardMaterial;

        if (material.vertexColors !== undefined) {
            primitiveMaterial.vertexColors = material.vertexColors;
        }

        // apply color if supplied
        if (material.color !== undefined) {
            primitiveMaterial.color = new Color(material.color);
        }

        // apply albedo map if supplied
        if (material.map !== undefined) {
            primitiveMaterial.map = material.map;
        }

        // apply normal map
        if (material.normalMap !== undefined) {
            primitiveMaterial.normalMap = material.normalMap;
        }

        // set roughness value
        // if supplied, apply roughness map
        // if we applied a roughness map, set roughness to 1.0
        if (material.roughness !== undefined) {
            primitiveMaterial.roughness = material.roughness;
        }

        if (material.roughnessMap !== undefined) {
            primitiveMaterial.roughnessMap = material.roughnessMap;

            if (primitiveMaterial.roughnessMap) {
                primitiveMaterial.roughness = 1.0;
            }
        }

        // set metalness value
        // if supplied, apply metalness map
        // if we applied a metalness map, set metalness to 1.0
        if (material.metalness !== undefined) {
            primitiveMaterial.metalness = material.metalness;
        }

        if (material.metalnessMap !== undefined) {
            primitiveMaterial.metalnessMap = material.metalnessMap;

            if (primitiveMaterial.metalnessMap) {
                primitiveMaterial.metalness = 0.0;
            }
        }

        // if the mesh is already set, update the material
        if (this._mesh) this._mesh.material = primitiveMaterial;
    }

    public PlaceOnFloor(): void {
        // calculate and temporary save world position
        const worldPos = this.getWorldPosition(this._positionWorldBuffer);
        const oldWorldPos = worldPos.clone();

        // compute the bounding box
        this._mesh?.geometry?.computeBoundingBox();
        const meshBB = this._mesh?.geometry?.boundingBox;

        // subtract the bounding box min y axis value from the world position y value
        if (!meshBB || !this._mesh) return;
        worldPos.y = worldPos.y - this._mesh.localToWorld(meshBB.min.clone()).y;

        // skip any action when the position did not change
        if (worldPos.y === oldWorldPos.y) return;

        DIVECommunication.get(this.userData.id)?.PerformAction(
            'UPDATE_OBJECT',
            {
                id: this.userData.id,
                position: worldPos,
                rotation: this.rotation,
                scale: this.scale,
            },
        );
    }

    public DropIt(): void {
        if (!this.parent) {
            console.warn(
                'DIVEPrimitive: DropIt() called on a model that is not in the scene.',
                this,
            );
            return;
        }

        // calculate the bottom center of the bounding box
        const bottomY = this._boundingBox.min.y * this.scale.y;
        const bbBottomCenter = this.localToWorld(
            this._boundingBox.getCenter(new Vector3()).multiply(this.scale),
        );
        bbBottomCenter.y = bottomY + this.position.y;

        // set up raycaster and raycast all scene objects (product layer)
        const raycaster = new Raycaster(bbBottomCenter, new Vector3(0, -1, 0));
        raycaster.layers.mask = PRODUCT_LAYER_MASK;
        const intersections = raycaster.intersectObjects(
            findSceneRecursive(this).Root.children,
            true,
        );

        // if we hit something, move the model to the top on the hit object's bounding box
        if (intersections.length > 0) {
            const mesh = intersections[0].object as Mesh;
            mesh.geometry.computeBoundingBox();
            const meshBB = mesh.geometry.boundingBox!;
            const worldPos = mesh.localToWorld(meshBB.max.clone());

            const oldPos = this.position.clone();
            const newPos = this.position
                .clone()
                .setY(worldPos.y)
                .sub(new Vector3(0, bottomY, 0));
            this.position.copy(newPos);

            // if the position changed, update the object in communication
            if (this.position.y === oldPos.y) return;

            this.onMove();
        }
    }

    private assembleGeometry(geometry: COMGeometry): BufferGeometry | null {
        // reset material to smooth shading
        (this._mesh.material as MeshStandardMaterial).flatShading = false;

        switch (geometry.name.toLowerCase()) {
            case 'cylinder':
                return this.createCylinderGeometry(geometry);
            case 'sphere':
                return this.createSphereGeometry(geometry);
            case 'pyramid':
                // set material to flat shading for pyramid
                (this._mesh.material as MeshStandardMaterial).flatShading =
                    true;
                return this.createPyramidGeometry(geometry);
            case 'cube':
            case 'box':
                return this.createBoxGeometry(geometry);
            case 'cone':
                return this.createConeGeometry(geometry);
            case 'wall':
                return this.createWallGeometry(geometry);
            case 'plane':
                return this.createPlaneGeometry(geometry);
            default: {
                console.warn(
                    'DIVEPrimitive.assembleGeometry: Invalid geometry type:',
                    geometry.name.toLowerCase(),
                );
                return null;
            }
        }
    }

    private createCylinderGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new CylinderGeometry(
            geometry.width / 2,
            geometry.width / 2,
            geometry.height,
            64,
        );
        geo.translate(0, geometry.height / 2, 0);
        return geo;
    }

    private createSphereGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new SphereGeometry(geometry.width / 2, 256, 256);
        return geo;
    }

    private createPyramidGeometry(geometry: COMGeometry): BufferGeometry {
        // prettier-multiline-arrays-next-line-pattern: 3
        const vertices = new Float32Array([
            -geometry.width / 2, 0, -geometry.depth / 2, // 0
            geometry.width / 2, 0, -geometry.depth / 2, // 1
            geometry.width / 2, 0, geometry.depth / 2, // 2
            -geometry.width / 2, 0, geometry.depth / 2, // 3
            0, geometry.height, 0,
        ]);

        // prettier-multiline-arrays-next-line-pattern: 3
        const indices = new Uint16Array([
            0, 1, 2,
            0, 2, 3,
            0, 4, 1,
            1, 4, 2,
            2, 4, 3,
            3, 4, 0,
        ]);

        const geometryBuffer = new BufferGeometry();
        geometryBuffer.setAttribute(
            'position',
            new BufferAttribute(vertices, 3),
        );
        geometryBuffer.setIndex(new BufferAttribute(indices, 1));
        geometryBuffer.computeVertexNormals();

        geometryBuffer.computeBoundingBox();
        geometryBuffer.computeBoundingSphere();
        return geometryBuffer;
    }

    private createBoxGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new BoxGeometry(
            geometry.width,
            geometry.height,
            geometry.depth,
        );
        geo.translate(0, geometry.height / 2, 0);
        return geo;
    }

    private createConeGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new ConeGeometry(geometry.width / 2, geometry.height, 256);
        geo.translate(0, geometry.height / 2, 0);
        return geo;
    }

    private createWallGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new BoxGeometry(
            geometry.width,
            geometry.height,
            geometry.depth || 0.05,
            16,
        );
        geo.translate(0, geometry.height / 2, 0);
        return geo;
    }

    private createPlaneGeometry(geometry: COMGeometry): BufferGeometry {
        const geo = new BoxGeometry(
            geometry.width,
            geometry.height,
            geometry.depth,
        );
        geo.translate(0, geometry.height / 2, 0);
        return geo;
    }
}
