import { useRef, useEffect, forwardRef, useMemo, useState } from "react";
import * as THREE from "three";
import { useFrame, useThree, createPortal } from "@react-three/fiber";
import { MaterialFactory } from "../materials/MaterialFactory";
import { useTexturePipeline } from "../hooks/useTexturePipeline";

interface ModelViewerProps {
  object: THREE.Object3D | null;
  sceneObjects: any[];
}

interface MeshMaterialManagerProps {
  mesh: THREE.Mesh;
  sceneObj: any;
  textureCache: React.MutableRefObject<Map<string, THREE.Texture>>;
}

const MeshMaterialManager = ({ mesh, sceneObj, textureCache }: MeshMaterialManagerProps) => {
  useEffect(() => {
    if (!mesh || !sceneObj.materialProps) return;

    const props = sceneObj.materialProps;
    const mappingType = props.mappingType || 'uv';
    const uvScale = props.uvScale || 1;
    const uvTiling = props.uvTiling || [1, 1];
    const uvOffset = props.uvOffset || [0, 0];
    const uvRotation = props.uvRotation || 0;

    const origMat = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as THREE.MeshPhysicalMaterial;

    const baseParams: any = {
      color: origMat.color,
      roughness: origMat.roughness !== undefined ? origMat.roughness : 0.5,
      metalness: origMat.metalness !== undefined ? origMat.metalness : 0,
      map: origMat.map,
      normalMap: origMat.normalMap,
      roughnessMap: origMat.roughnessMap,
      metalnessMap: origMat.metalnessMap,
      aoMap: origMat.aoMap,
      emissiveMap: origMat.emissiveMap,
      emissive: origMat.emissive,
      emissiveIntensity: origMat.emissiveIntensity,
      envMapIntensity: origMat.envMapIntensity,
      alphaMap: origMat.alphaMap,
      transparent: origMat.transparent,
      opacity: origMat.opacity,
      side: origMat.side,
      alphaTest: origMat.alphaTest,
    };

    // Use MaterialFactory to get reusable materials
    let activeMat: any;
    if (mappingType === 'triplanar') {
      activeMat = MaterialFactory.getInstance().getTriplanarMaterial(
        baseParams,
        mesh.uuid
      );
      
      activeMat.triplanarUniforms.uTriplanarScale.value = uvScale;
      activeMat.triplanarUniforms.uBlendSharpness.value = 5.0; // Default or from props
    } else {
      activeMat = MaterialFactory.getInstance().getStandardUVMaterial(
        baseParams,
        mesh.uuid
      );

      let modeIndex = 0;
      if (mappingType === 'planar') modeIndex = 1;
      else if (mappingType === 'box') modeIndex = 2;
      else if (mappingType === 'cylinder') modeIndex = 3;

      activeMat.uvUniforms.uMappingMode.value = modeIndex;
      activeMat.updateUVTransform(uvOffset, [uvTiling[0] * uvScale, uvTiling[1] * uvScale], uvRotation * (Math.PI / 180));
    }

    // Apply basic props
    if (props.color) activeMat.color.set(props.color);
    if (props.roughness !== undefined) activeMat.roughness = props.roughness;
    if (props.metalness !== undefined) activeMat.metalness = props.metalness;
    if (props.opacity !== undefined) activeMat.opacity = props.opacity;
    if (props.transmission !== undefined) activeMat.transmission = props.transmission;
    activeMat.transparent = (props.transmission > 0) || (props.opacity < 1) || !!props.alphaMap;

    if (Array.isArray(mesh.material)) {
      mesh.material = mesh.material.map(() => activeMat);
    } else {
      mesh.material = activeMat;
    }

    // Textures
    const textureTypes: ('map' | 'normalMap' | 'specularMap' | 'alphaMap')[] = ['map', 'normalMap', 'specularMap', 'alphaMap'];
    textureTypes.forEach(type => {
      const url = props[type];
      if (url) {
        if (!activeMat[type] || activeMat[type]!.name !== url) {
          if (textureCache.current.has(url)) {
            activeMat[type] = textureCache.current.get(url)!;
            if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = activeMat[type];
            activeMat.needsUpdate = true;
          } else {
            const loader = new THREE.TextureLoader();
            loader.load(url, (tex) => {
              tex.name = url;
              if (type === 'map') tex.colorSpace = THREE.SRGBColorSpace;
              tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
              textureCache.current.set(url, tex);
              activeMat[type] = tex;
              if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = tex;
              activeMat.needsUpdate = true;
            });
          }
        }
      } else if (url === null && activeMat[type]) {
        activeMat[type] = null;
        if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = null;
        activeMat.needsUpdate = true;
      }
    });

    mesh.visible = sceneObj.visible;
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    mesh.frustumCulled = false;

    // Sync individual mesh transform
    if (sceneObj.transformProps) {
      const { position, rotation, scale } = sceneObj.transformProps;
      mesh.position.set(position[0], position[1], position[2]);
      mesh.rotation.set(rotation[0], rotation[1], rotation[2]);
      mesh.scale.set(scale[0], scale[1], scale[2]);
    }

  }, [sceneObj, mesh]);

  return null;
};

const FillMesh = ({ fillProps, originalMesh }: { fillProps: any, originalMesh: THREE.Mesh }) => {
  const materialRef = useRef<any>(null);
  const [clippingPlane] = useState(() => new THREE.Plane(new THREE.Vector3(0, -1, 0), 0));
  const { size: winSize } = useThree();
  
  // Clone geometry and fix interpolation
  const geometry = useMemo(() => {
    const geo = originalMesh.geometry.clone();
    geo.computeVertexNormals();
    return geo;
  }, [originalMesh.geometry]);

  // Update clipping plane based on height
  useEffect(() => {
    geometry.computeBoundingBox();
    const box = geometry.boundingBox || new THREE.Box3().setFromObject(originalMesh);
    const minY = box.min.y;
    const maxY = box.max.y;
    // height is 0 to 1
    const clipY = minY + (maxY - minY) * (fillProps.height ?? 0.5);
    // Since this plane is applied in world space by Three.js by default, 
    // wait, localClippingEnabled = true means the plane is in LOCAL space!
    // If it's local space, clipY is exact.
    clippingPlane.constant = clipY;
  }, [geometry, fillProps.height, originalMesh]);

  useFrame((state) => {
    if (materialRef.current && fillProps.type === 'liquid') {
      const mat = materialRef.current;
      if (mat.uTime !== undefined) mat.uTime = state.clock.elapsedTime;
      if (mat.uResolution && mat.uResolution.set) {
        mat.uResolution.set(winSize.width || 800, winSize.height || 600);
      }
    }
  });

  const fillNode = (
    <mesh geometry={geometry} scale={[0.995, 0.995, 0.995]}>
      {fillProps.type === 'liquid' ? (
        // @ts-ignore
        <liquidMaterialImpl 
          ref={materialRef}
          uColor={new THREE.Color(fillProps.color)}
          transparent
          side={THREE.DoubleSide}
          clippingPlanes={[clippingPlane]}
          clipping={true}
        />
      ) : (
        <meshPhysicalMaterial 
          color={fillProps.color}
          side={THREE.DoubleSide}
          clippingPlanes={[clippingPlane]}
          roughness={0.2}
          clearcoat={0.5}
        />
      )}
    </mesh>
  );

  return createPortal(fillNode, originalMesh);
};

export const ModelViewer = forwardRef<THREE.Group, ModelViewerProps>(({ object, sceneObjects }, ref) => {
  const internalRef = useRef<THREE.Group>(null);
  const groupRef = (ref as any) || internalRef;
  const textureCache = useRef<Map<string, THREE.Texture>>(new Map());
  const meshCache = useRef<Map<string, THREE.Mesh>>(new Map());
  
  useTexturePipeline(object);

  // Cleanup textures on unmount
  useEffect(() => {
    return () => {
      textureCache.current.forEach(tex => tex.dispose());
      textureCache.current.clear();
      meshCache.current.clear();
    };
  }, []);

  // Build mesh cache when object changes
  useEffect(() => {
    meshCache.current.clear();
    if (object) {
      let mIdx = 0;
      object.traverse((child) => {
        if (child instanceof THREE.Mesh) {
          const stableId = `mesh-${mIdx}`;
          meshCache.current.set(stableId, child);
          mIdx++;
        }
      });
    }
  }, [object]);

  // Main transform update for the core model
  useEffect(() => {
    if (object && sceneObjects) {
      const mainModel = sceneObjects.find(obj => obj.type === 'model');
      if (mainModel && mainModel.transformProps && groupRef.current) {
        const { position: p, rotation: r, scale: s } = mainModel.transformProps;
        groupRef.current.position.set(p[0], p[1], p[2]);
        groupRef.current.rotation.set(r[0], r[1], r[2]);
        groupRef.current.scale.set(s[0], s[1], s[2]);
      }
    }
  }, [object, sceneObjects]);

  if (!object) return null;

  const meshObjects = sceneObjects.filter(obj => obj.type === 'mesh');

  return (
    <group ref={groupRef}>
      <primitive object={object} />
      {meshObjects.map(obj => {
        const mesh = meshCache.current.get(obj.id);
        if (!mesh) return null;
        return (
          <group key={obj.id}>
            <MeshMaterialManager 
              mesh={mesh} 
              sceneObj={obj} 
              textureCache={textureCache} 
            />
            {obj.fillProps?.enabled && (
              <FillMesh fillProps={obj.fillProps} originalMesh={mesh} />
            )}
          </group>
        );
      })}
    </group>
  );
});
