import { Box3, Color, Object3D } from 'three';
import DIVEAmbientLight from '../../light/AmbientLight.ts';
import DIVEPointLight from '../../light/PointLight.ts';
import DIVESceneLight from '../../light/SceneLight.ts';
import { DIVEModel } from '../../model/Model.ts';
import { DIVELoadingManager } from '../../loadingmanager/LoadingManager.ts';
import { DIVECommunication } from '../../com/Communication.ts';
import { DIVEPrimitive } from '../../primitive/Primitive.ts';

import { type DIVEScene } from '../Scene.ts';
import { type TransformControls } from 'three/examples/jsm/controls/TransformControls';
import {
    type COMLight,
    type COMModel,
    type COMEntity,
    type COMPrimitive,
    type COMGroup,
} from '../../com/types';
import { type DIVESceneObject } from '../../types';
import { DIVEGroup } from '../../group/Group.ts';

/**
 * A basic scene node to hold grid, floor and all lower level roots.
 *
 * @module
 */

export class DIVERoot extends Object3D {
    readonly isDIVERoot: true = true;

    private loadingManager: DIVELoadingManager;

    constructor() {
        super();
        this.name = 'Root';

        this.loadingManager = new DIVELoadingManager();
    }

    public ComputeSceneBB(): Box3 {
        const bb = new Box3();
        this.traverse((object: Object3D) => {
            if ('isObject3D' in object) {
                bb.expandByObject(object);
            }
        });
        return bb;
    }

    public GetSceneObject<T extends DIVESceneObject>(
        object: Partial<COMEntity> & { id: string },
    ): T | undefined {
        let foundObject: T | undefined;
        this.traverse((object3D) => {
            if (foundObject) return;
            if (object3D.userData.id === object.id) {
                foundObject = object3D as T;
            }
        });
        return foundObject;
    }

    public AddSceneObject(object: COMEntity): void {
        switch (object.entityType) {
            case 'pov': {
                break;
            }
            case 'light': {
                this.updateLight(object as COMLight);
                break;
            }
            case 'model': {
                this.updateModel(object);
                break;
            }
            case 'primitive': {
                this.updatePrimitive(object);
                break;
            }
            case 'group': {
                this.updateGroup(object);
                break;
            }
            default: {
                console.warn(
                    `DIVERoot.AddSceneObject: Unknown entity type: ${object.entityType}`,
                );
            }
        }
    }

    public UpdateSceneObject(
        object: Partial<COMEntity> & { id: string; entityType: string },
    ): void {
        switch (object.entityType) {
            case 'pov': {
                break;
            }
            case 'light': {
                this.updateLight(object as COMLight);
                break;
            }
            case 'model': {
                this.updateModel(object);
                break;
            }
            case 'primitive': {
                this.updatePrimitive(object);
                break;
            }
            case 'group': {
                this.updateGroup(object);
                break;
            }
            default: {
                console.warn(
                    `DIVERoot.UpdateSceneObject: Unknown entity type: ${object.entityType}`,
                );
            }
        }
    }

    public DeleteSceneObject(
        object: Partial<COMEntity> & { id: string; entityType: string },
    ): void {
        switch (object.entityType) {
            case 'pov': {
                break;
            }
            case 'light': {
                this.deleteLight(object);
                break;
            }
            case 'model': {
                this.deleteModel(object);
                break;
            }
            case 'primitive': {
                this.deletePrimitive(object);
                break;
            }
            case 'group': {
                this.deleteGroup(object);
                break;
            }
            default: {
                console.warn(
                    `DIVERoot.DeleteSceneObject: Unknown entity type: ${object.entityType}`,
                );
            }
        }
    }

    public PlaceOnFloor(
        object: Partial<COMEntity> & { id: string; entityType: string },
    ): void {
        switch (object.entityType) {
            case 'pov':
            case 'light': {
                break;
            }
            case 'model':
            case 'primitive': {
                this.placeOnFloor(object);
                break;
            }
            default: {
                console.warn(
                    `DIVERoot.PlaceOnFloor: Unknown entity type: ${object.entityType}`,
                );
            }
        }
    }

    private updateLight(
        light: Partial<COMLight> & {
            id: string;
            entityType: string;
            type: string;
        },
    ): void {
        let sceneObject = this.GetSceneObject(light);
        if (!sceneObject) {
            switch (light.type) {
                case 'scene': {
                    sceneObject = new DIVESceneLight();
                    break;
                }
                case 'ambient': {
                    sceneObject = new DIVEAmbientLight();
                    break;
                }
                case 'point': {
                    sceneObject = new DIVEPointLight();
                    break;
                }
                default: {
                    console.warn(
                        `DIVERoot.updateLight: Unknown light type: ${light.type}`,
                    );
                    return;
                }
            }
            sceneObject.userData.id = light.id;
            this.add(sceneObject);
        }

        if (light.name !== undefined && light.name !== null)
            sceneObject.name = light.name;
        if (light.position !== undefined && light.position !== null)
            sceneObject.position.set(
                light.position.x,
                light.position.y,
                light.position.z,
            );
        if (light.intensity !== undefined && light.intensity !== null)
            (sceneObject as DIVEAmbientLight | DIVEPointLight).SetIntensity(
                light.intensity,
            );
        if (light.enabled !== undefined && light.enabled !== null)
            (sceneObject as DIVEAmbientLight | DIVEPointLight).SetEnabled(
                light.enabled,
            );
        if (light.color !== undefined && light.color !== null)
            (sceneObject as DIVEAmbientLight | DIVEPointLight).SetColor(
                new Color(light.color),
            );
        if (light.visible !== undefined && light.visible !== null)
            (sceneObject as DIVEAmbientLight | DIVEPointLight).visible =
                light.visible;
        if (light.parentId !== undefined)
            this.setParent({ ...light, parentId: light.parentId });
    }

    private updateModel(model: Partial<COMModel> & { id: string }): void {
        let sceneObject = this.GetSceneObject<DIVESceneObject>(model);
        if (!sceneObject) {
            const created = new DIVEModel();
            sceneObject = created;
            sceneObject.userData.id = model.id;
            sceneObject.userData.uri = model.uri;
            this.add(sceneObject);
        }

        if (model.uri !== undefined) {
            this.loadingManager.LoadGLTF(model.uri).then((gltf) => {
                (sceneObject as DIVEModel).SetModel(gltf);
                DIVECommunication.get(model.id!)?.PerformAction(
                    'MODEL_LOADED',
                    { id: model.id! },
                );
            });
        }

        if (model.name !== undefined) sceneObject.name = model.name;
        if (model.position !== undefined)
            (sceneObject as DIVEModel).SetPosition(model.position);
        if (model.rotation !== undefined)
            (sceneObject as DIVEModel).SetRotation(model.rotation);
        if (model.scale !== undefined)
            (sceneObject as DIVEModel).SetScale(model.scale);
        if (model.visible !== undefined)
            (sceneObject as DIVEModel).SetVisibility(model.visible);
        if (model.material !== undefined)
            (sceneObject as DIVEModel).SetMaterial(model.material);
        if (model.parentId !== undefined)
            this.setParent({ ...model, parentId: model.parentId });
    }

    private updatePrimitive(
        primitive: Partial<COMPrimitive> & { id: string },
    ): void {
        let sceneObject = this.GetSceneObject<DIVESceneObject>(primitive);
        if (!sceneObject) {
            const created = new DIVEPrimitive();
            sceneObject = created;
            sceneObject.userData.id = primitive.id;
            this.add(sceneObject);
        }

        if (primitive.name !== undefined) sceneObject.name = primitive.name;
        if (primitive.geometry !== undefined)
            (sceneObject as DIVEPrimitive).SetGeometry(primitive.geometry);
        if (primitive.position !== undefined)
            (sceneObject as DIVEPrimitive).SetPosition(primitive.position);
        if (primitive.rotation !== undefined)
            (sceneObject as DIVEPrimitive).SetRotation(primitive.rotation);
        if (primitive.scale !== undefined)
            (sceneObject as DIVEPrimitive).SetScale(primitive.scale);
        if (primitive.visible !== undefined)
            (sceneObject as DIVEPrimitive).SetVisibility(primitive.visible);
        if (primitive.material !== undefined)
            (sceneObject as DIVEPrimitive).SetMaterial(primitive.material);
        if (primitive.parentId !== undefined)
            this.setParent({ ...primitive, parentId: primitive.parentId });
    }

    private updateGroup(group: Partial<COMGroup> & { id: string }): void {
        let sceneObject = this.GetSceneObject<DIVESceneObject>(group);
        if (!sceneObject) {
            const created = new DIVEGroup();
            sceneObject = created;
            sceneObject.userData.id = group.id;
            this.add(sceneObject);
        }

        if (group.name !== undefined) sceneObject.name = group.name;
        if (group.position !== undefined)
            (sceneObject as DIVEGroup).SetPosition(group.position);
        if (group.rotation !== undefined)
            (sceneObject as DIVEGroup).SetRotation(group.rotation);
        if (group.scale !== undefined)
            (sceneObject as DIVEGroup).SetScale(group.scale);
        if (group.visible !== undefined)
            (sceneObject as DIVEGroup).SetVisibility(group.visible);
        if (group.bbVisible !== undefined)
            (sceneObject as DIVEGroup).SetLinesVisibility(group.bbVisible);
        if (group.parentId !== undefined)
            this.setParent({ ...group, parentId: group.parentId });
    }

    private deleteLight(light: Partial<COMLight> & { id: string }): void {
        const sceneObject = this.GetSceneObject(light);
        if (!sceneObject) {
            console.warn(
                `DIVERoot.deleteLight: Light with id ${light.id} not found`,
            );
            return;
        }

        this.detachTransformControls(sceneObject);

        sceneObject.parent!.remove(sceneObject);
    }

    private deleteModel(model: Partial<COMModel> & { id: string }): void {
        const sceneObject = this.GetSceneObject(model);
        if (!sceneObject) {
            console.warn(
                `DIVERoot.deleteModel: Model with id ${model.id} not found`,
            );
            return;
        }

        this.detachTransformControls(sceneObject);

        sceneObject.parent!.remove(sceneObject);
    }

    private deletePrimitive(
        primitive: Partial<COMPrimitive> & { id: string },
    ): void {
        const sceneObject = this.GetSceneObject(primitive);
        if (!sceneObject) {
            console.warn(
                `DIVERoot.deletePrimitive: Primitive with id ${primitive.id} not found`,
            );
            return;
        }

        this.detachTransformControls(sceneObject);

        sceneObject.parent!.remove(sceneObject);
    }

    private deleteGroup(group: Partial<COMGroup> & { id: string }): void {
        const sceneObject = this.GetSceneObject<DIVEGroup>(group);
        if (!sceneObject) {
            console.warn(
                `DIVERoot.deleteGroup: Group with id ${group.id} not found`,
            );
            return;
        }

        this.detachTransformControls(sceneObject);

        for (let i = sceneObject.members.length - 1; i >= 0; i--) {
            this.attach(sceneObject.members[i]);
        }

        sceneObject.parent!.remove(sceneObject);
    }

    private placeOnFloor(object: Partial<COMEntity> & { id: string }): void {
        const sceneObject = this.GetSceneObject(object);
        if (!sceneObject) return;

        (sceneObject as DIVEModel | DIVEPrimitive).PlaceOnFloor();
    }

    private setParent(
        object: Partial<COMEntity> & { id: string; parentId: string | null },
    ): void {
        const sceneObject = this.GetSceneObject<DIVESceneObject>(object);
        if (!sceneObject) return;

        if (object.parentId !== null) {
            const parent = this.GetSceneObject<DIVESceneObject>({
                id: object.parentId,
            });
            if (!parent) return;

            // attach to new parent (if exists in scene)
            parent.attach(sceneObject);
        } else {
            // attach to root if no parent is found
            this.attach(sceneObject);
        }
    }

    private detachTransformControls(object: Object3D): void {
        // this is only neccessary due to using the old TransformControls instead of the new DIVEGizmo
        this.findScene(object).children.find((object) => {
            if ('isTransformControls' in object) {
                (object as TransformControls).detach();
            }
        });
    }

    private findScene(object: Object3D): DIVEScene {
        if (object.parent !== null) {
            return this.findScene(object.parent);
        }
        return object as DIVEScene;
    }
}
