import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import gsap from 'gsap';
import * as THREE from 'three';
import * as CONSTANTS from '../../../types/world/worldConstants';
import { toast } from 'react-toastify';
import * as Types from "../../../types/world/worldTypes";
import { initializeDB, retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils';
import { getCamera } from '../../../services/factoryBuilder/camera/getCameraApi';
import { getFloorAssets } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi';

async function loadInitialFloorItems(
    itemsGroup: Types.RefGroup,
    setFloorItems: Types.setFloorItemSetState,
    setSimulationStates: (paths: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => void
): Promise<void> {
    if (!itemsGroup.current) return;
    let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
    const email = localStorage.getItem('email');
    const organization = (email!.split("@")[1]).split(".")[0];

    const items = await getFloorAssets(organization);
    localStorage.setItem("FloorItems", JSON.stringify(items));
    await initializeDB();

    if (items.message === "floorItems not found") return;

    if (items) {
        const storedFloorItems: Types.EventData[] = items;
        const loader = new GLTFLoader();
        const dracoLoader = new DRACOLoader();

        dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
        loader.setDRACOLoader(dracoLoader);

        let modelsLoaded = 0;
        const modelsToLoad = storedFloorItems.length;

        const camData = await getCamera(organization, localStorage.getItem('userId')!);
        let storedPosition;
        if (camData && camData.position) {
            storedPosition = camData?.position;
        } else {
            storedPosition = new THREE.Vector3(0, 40, 30);
        }
        if (!storedPosition) return;
        const cameraPosition = new THREE.Vector3(storedPosition.x, storedPosition.y, storedPosition.z);

        storedFloorItems.sort((a, b) => {
            const aPosition = new THREE.Vector3(a.position[0], a.position[1], a.position[2]);
            const bPosition = new THREE.Vector3(b.position[0], b.position[1], b.position[2]);
            return cameraPosition.distanceTo(aPosition) - cameraPosition.distanceTo(bPosition);
        });

        for (const item of storedFloorItems) {
            if (!item.modelfileID) return;
            const itemPosition = new THREE.Vector3(item.position[0], item.position[1], item.position[2]);
            let storedPosition;
            if (localStorage.getItem("cameraPosition")) {
                storedPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
            } else {
                storedPosition = new THREE.Vector3(0, 40, 30);
            }

            const cameraPosition = new THREE.Vector3(storedPosition.x, storedPosition.y, storedPosition.z);

            if (cameraPosition.distanceTo(itemPosition) < 50) {
                await new Promise<void>(async (resolve) => {

                    // Check Three.js Cache
                    const cachedModel = THREE.Cache.get(item.modelfileID!);
                    if (cachedModel) {
                        // console.log(`[Cache] Fetching ${item.modelname}`);
                        processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, setFloorItems, setSimulationStates);
                        modelsLoaded++;
                        checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve);
                        return;
                    }

                    // Check IndexedDB
                    const indexedDBModel = await retrieveGLTF(item.modelfileID!);
                    if (indexedDBModel) {
                        // console.log(`[IndexedDB] Fetching ${item.modelname}`);
                        const blobUrl = URL.createObjectURL(indexedDBModel);
                        loader.load(
                            blobUrl,
                            (gltf) => {
                                URL.revokeObjectURL(blobUrl);
                                THREE.Cache.remove(blobUrl);
                                THREE.Cache.add(item.modelfileID!, gltf);
                                processLoadedModel(gltf.scene.clone(), item, itemsGroup, setFloorItems, setSimulationStates);
                                modelsLoaded++;
                                checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve);
                            },
                            undefined,
                            (error) => {
                                toast.error(`[IndexedDB] Error loading ${item.modelname}:`);
                                URL.revokeObjectURL(blobUrl);
                                resolve();
                            }
                        );
                        return;
                    }

                    // Fetch from Backend
                    // console.log(`[Backend] Fetching ${item.modelname}`);
                    const modelUrl = `${url_Backend_dwinzo}/api/v1/AssetFile/${item.modelfileID!}`;
                    loader.load(
                        modelUrl,
                        async (gltf) => {
                            const modelBlob = await fetch(modelUrl).then((res) => res.blob());
                            await storeGLTF(item.modelfileID!, modelBlob);
                            THREE.Cache.add(item.modelfileID!, gltf);
                            processLoadedModel(gltf.scene.clone(), item, itemsGroup, setFloorItems, setSimulationStates);
                            modelsLoaded++;
                            checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve);
                        },
                        undefined,
                        (error) => {
                            toast.error(`[Backend] Error loading ${item.modelname}:`);
                            resolve();
                        }
                    );
                });
            } else {
                // console.log(`Item ${item.modelname} is not near`);
                setFloorItems((prevItems) => [
                    ...(prevItems || []),
                    {
                        modeluuid: item.modeluuid,
                        modelname: item.modelname,
                        position: item.position,
                        rotation: item.rotation,
                        modelfileID: item.modelfileID,
                        isLocked: item.isLocked,
                        isVisible: item.isVisible,
                    },
                ]);

                if (item.eventData || item.modelfileID === '67e3db95c2e8f37134526fb2') {
                    processEventData(item, setSimulationStates);
                }

                modelsLoaded++;
                checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, () => { });
            }
        }

        // Dispose loader after all models
        dracoLoader.dispose();
    }
}


function processLoadedModel(
    gltf: any,
    item: Types.EventData,
    itemsGroup: Types.RefGroup,
    setFloorItems: Types.setFloorItemSetState,
    setSimulationStates: (paths: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => void
) {
    const model = gltf;
    model.uuid = item.modeluuid;
    model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap);
    model.userData = { name: item.modelname, modelId: item.modelfileID, modeluuid: item.modeluuid };
    model.position.set(...item.position);
    model.rotation.set(item.rotation.x, item.rotation.y, item.rotation.z);

    model.traverse((child: any) => {
        if (child.isMesh) {
            // Clone the material to ensure changes are independent
            // child.material = child.material.clone();

            child.castShadow = true;
            child.receiveShadow = true;
        }
    });


    itemsGroup?.current?.add(model);

    setFloorItems((prevItems) => [
        ...(prevItems || []),
        {
            modeluuid: item.modeluuid,
            modelname: item.modelname,
            position: item.position,
            rotation: item.rotation,
            modelfileID: item.modelfileID,
            isLocked: item.isLocked,
            isVisible: item.isVisible,
        },
    ]);

    if (item.eventData || item.modelfileID === '67e3db5ac2e8f37134526f40' || item.modelfileID === '67eb7904c2e8f37134527eae') {
        processEventData(item, setSimulationStates);
    }

    gsap.to(model.position, { y: item.position[1], duration: 1.5, ease: 'power2.out' });
    gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: 'power2.out' });
}

function processEventData(item: Types.EventData, setSimulationStates: any) {

    if (item.eventData?.type === 'Conveyor') {

        const data: any = item.eventData;
        data.modeluuid = item.modeluuid;
        data.modelName = item.modelname;
        data.position = item.position;
        data.rotation = [item.rotation.x, item.rotation.y, item.rotation.z];

        setSimulationStates((prevEvents: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => [
            ...(prevEvents || []),
            data as Types.ConveyorEventsSchema
        ]);

    } else if (item.eventData?.type === 'Vehicle') {

        const data: any = item.eventData;
        data.modeluuid = item.modeluuid;
        data.modelName = item.modelname;
        data.position = item.position;

        setSimulationStates((prevEvents: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => [
            ...(prevEvents || []),
            data as Types.VehicleEventsSchema
        ]);

    } else if (item.modelfileID === '67e3db5ac2e8f37134526f40') {

        const pointUUID = THREE.MathUtils.generateUUID();
        const pointPosition = new THREE.Vector3(0, 1.5, -0.5);

        const staticMachine: Types.StaticMachineEventsSchema = {
            modeluuid: item.modeluuid,
            modelName: item.modelname,
            type: "StaticMachine",
            points: {
                uuid: pointUUID,
                position: [pointPosition.x, pointPosition.y, pointPosition.z],
                actions: { uuid: THREE.MathUtils.generateUUID(), name: 'Action 1', buffer: 'Inherit', material: 'Inherit', isUsed: false },
                triggers: { uuid: THREE.MathUtils.generateUUID(), name: 'Trigger 1', type: 'OnComplete' },
                connections: { source: { modelUUID: item.modeluuid, pointUUID: pointUUID }, targets: [] },
            },
            position: item.position,
            rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
        };

        setSimulationStates((prevEvents: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => [
            ...(prevEvents || []),
            staticMachine as Types.StaticMachineEventsSchema
        ]);

    } else if (item.modelfileID === '67eb7904c2e8f37134527eae') {
        const pointUUID = THREE.MathUtils.generateUUID();
        const pointPosition = new THREE.Vector3(0, 2.75, -0.5);

        const armBot: Types.ArmBotEventsSchema = {
            modeluuid: item.modeluuid,
            modelName: item.modelname,
            type: "ArmBot",
            points: {
                uuid: pointUUID,
                position: [pointPosition.x, pointPosition.y, pointPosition.z],
                actions: { uuid: THREE.MathUtils.generateUUID(), name: 'Action 1', speed: 1, processes: [] },
                triggers: { uuid: THREE.MathUtils.generateUUID(), name: 'Trigger 1', type: 'OnComplete' },
                connections: { source: { modelUUID: item.modeluuid, pointUUID: pointUUID }, targets: [] },
            },
            position: item.position,
            rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
        }

        setSimulationStates((prevEvents: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema)[]) => [
            ...(prevEvents || []),
            armBot as Types.ArmBotEventsSchema
        ]);

    }
}

function checkLoadingCompletion(
    modelsLoaded: number,
    modelsToLoad: number,
    dracoLoader: DRACOLoader,
    resolve: () => void
) {
    if (modelsLoaded === modelsToLoad) {
        toast.success("Models Loaded!");
        dracoLoader.dispose();
    }
    resolve();
}

export default loadInitialFloorItems;