import * as THREE from 'three'
import * as CANNON from 'cannon-es'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import type { ModelGeometry, PhysicsObject } from './types'

export class ModelLoader {
  private loader: GLTFLoader
  private modelGeometry: ModelGeometry | null = null

  constructor() {
    this.loader = new GLTFLoader()
  }

  private extractModelGeometry(gltf: any): ModelGeometry | null {
    const geometries: THREE.BufferGeometry[] = []
    const boundingBox = new THREE.Box3()

    // Update world matrices first
    gltf.scene.updateMatrixWorld(true)

    // Traverse the model to find all mesh geometries
    gltf.scene.traverse((child: any) => {
      if (child instanceof THREE.Mesh && child.geometry) {
        // Apply the mesh's transform to the geometry
        const geometry = child.geometry.clone() as THREE.BufferGeometry
        
        // Only apply matrix if it's not identity
        if (!child.matrixWorld.equals(new THREE.Matrix4())) {
          geometry.applyMatrix4(child.matrixWorld)
        }
        
        geometries.push(geometry)
        
        // Update bounding box
        const positionAttr = geometry.getAttribute('position') as THREE.BufferAttribute
        if (positionAttr) {
          const geometryBoundingBox = new THREE.Box3().setFromBufferAttribute(positionAttr)
          boundingBox.union(geometryBoundingBox)
        }
      }
    })

    if (geometries.length === 0) {
      console.warn('No geometries found in model')
      return null
    }

    console.log(`Found ${geometries.length} geometries to merge`)

    // Merge all geometries
    const combinedGeometry = this.mergeBufferGeometries(geometries)

    // Extract vertices and faces
    const positionAttribute = combinedGeometry.getAttribute('position') as THREE.BufferAttribute
    if (!positionAttribute) {
      console.error('No position attribute found in combined geometry')
      return null
    }

    const vertices: number[] = Array.from(positionAttribute.array as Float32Array)
    
    let indices: number[] = []
    if (combinedGeometry.index) {
      indices = Array.from(combinedGeometry.index.array)
    } else {
      // Generate indices if none exist
      for (let i = 0; i < vertices.length / 3; i++) {
        indices.push(i)
      }
    }

    console.log(`Extracted ${vertices.length / 3} vertices and ${indices.length / 3} triangles`)

    combinedGeometry.dispose()
    
    return {
      vertices,
      indices,
      boundingBox
    }
  }

  private mergeBufferGeometries(geometries: THREE.BufferGeometry[]): THREE.BufferGeometry {
    const merged = new THREE.BufferGeometry()
    const attributes: { [key: string]: THREE.BufferAttribute[] } = {}
    let indexOffset = 0
    const mergedIndices: number[] = []

    // Collect all attributes
    geometries.forEach(geometry => {
      const attributeNames = Object.keys(geometry.attributes)
      attributeNames.forEach(name => {
        if (!attributes[name]) attributes[name] = []
        const attr = geometry.attributes[name]
        if (attr instanceof THREE.BufferAttribute) {
          attributes[name].push(attr)
        }
      })

      // Handle indices
      if (geometry.index) {
        const indices = Array.from(geometry.index.array)
        indices.forEach(index => mergedIndices.push(index + indexOffset))
        indexOffset += geometry.attributes.position.count
      }
    })

    // Merge attributes
    Object.keys(attributes).forEach(name => {
      const attributeArrays = attributes[name]
      const itemSize = attributeArrays[0].itemSize
      const normalized = attributeArrays[0].normalized
      
      let totalCount = 0
      attributeArrays.forEach(attr => totalCount += attr.count)
      
      const mergedArray = new Float32Array(totalCount * itemSize)
      let offset = 0
      
      attributeArrays.forEach(attr => {
        mergedArray.set(attr.array as Float32Array, offset)
        offset += attr.array.length
      })
      
      merged.setAttribute(name, new THREE.BufferAttribute(mergedArray, itemSize, normalized))
    })

    if (mergedIndices.length > 0) {
      merged.setIndex(mergedIndices)
    }

    return merged
  }

  private downsampleGeometry(geometry: ModelGeometry, factor: number = 0.3): ModelGeometry {
    // Simple downsampling by taking every Nth vertex
    const step = Math.max(1, Math.floor(1 / factor))
    const downsampledVertices: number[] = []
    const downsampledIndices: number[] = []
    const vertexMap = new Map<number, number>()
    
    // Downsample vertices
    for (let i = 0; i < geometry.vertices.length; i += step * 3) {
      if (i + 2 < geometry.vertices.length) {
        const originalIndex = i / 3
        const newIndex = downsampledVertices.length / 3
        vertexMap.set(originalIndex, newIndex)
        
        downsampledVertices.push(
          geometry.vertices[i],
          geometry.vertices[i + 1],
          geometry.vertices[i + 2]
        )
      }
    }
    
    // Downsample indices, keeping only triangles where all vertices are kept
    for (let i = 0; i < geometry.indices.length; i += 3) {
      const v1 = geometry.indices[i]
      const v2 = geometry.indices[i + 1]
      const v3 = geometry.indices[i + 2]
      
      if (vertexMap.has(v1) && vertexMap.has(v2) && vertexMap.has(v3)) {
        downsampledIndices.push(
          vertexMap.get(v1)!,
          vertexMap.get(v2)!,
          vertexMap.get(v3)!
        )
      }
    }
    
    return {
      vertices: downsampledVertices,
      indices: downsampledIndices,
      boundingBox: geometry.boundingBox
    }
  }

  private createCollisionShape(geometry: ModelGeometry, scale: number = 1): CANNON.Shape {
    try {
      // For performance, use a simplified approach
      // If the geometry is too complex, fall back to a hull or sphere
      const vertexCount = geometry.vertices.length / 3
      
      if (vertexCount > 100) {
        // For complex meshes, use bounding box approximation
        const size = geometry.boundingBox.getSize(new THREE.Vector3())
        const scaledSize = size.multiplyScalar(scale)
        return new CANNON.Box(new CANNON.Vec3(
          scaledSize.x * 0.5,
          scaledSize.y * 0.5,
          scaledSize.z * 0.5
        ))
      }
      
      // For simpler meshes, try convex hull
      const scaledVertices = geometry.vertices.map(v => v * scale)
      const vertices: CANNON.Vec3[] = []
      
      for (let i = 0; i < scaledVertices.length; i += 3) {
        vertices.push(new CANNON.Vec3(
          scaledVertices[i],
          scaledVertices[i + 1],
          scaledVertices[i + 2]
        ))
      }
      
      // Limit faces to prevent performance issues
      const faces: number[][] = []
      const maxFaces = Math.min(geometry.indices.length / 3, 50)
      
      for (let i = 0; i < maxFaces * 3; i += 3) {
        const v1 = geometry.indices[i]
        const v2 = geometry.indices[i + 1]
        const v3 = geometry.indices[i + 2]
        
        if (v1 < vertices.length && v2 < vertices.length && v3 < vertices.length) {
          faces.push([v1, v2, v3])
        }
      }
      
      if (faces.length > 0) {
        const shape = new CANNON.ConvexPolyhedron({ vertices, faces })
        return shape
      } else {
        throw new Error('No valid faces')
      }
      
    } catch (error) {
      console.warn('Failed to create complex collision shape, falling back to sphere:', error)
      // Fallback to sphere using bounding box
      const size = geometry.boundingBox.getSize(new THREE.Vector3())
      const radius = Math.max(size.x, size.y, size.z) * 0.5 * scale
      return new CANNON.Sphere(Math.max(0.3, radius))
    }
  }

  async loadAndCreateObjects(
    scene: THREE.Scene, 
    world: CANNON.World, 
    physicsObjects: PhysicsObject[]
  ): Promise<void> {
    try {
      const gltf = await this.loader.loadAsync('/models/main_model.glb')
      
      // Extract and downsample the geometry for collision detection
      const originalGeometry = this.extractModelGeometry(gltf)
      if (originalGeometry) {
        this.modelGeometry = this.downsampleGeometry(originalGeometry, 0.3) // 30% of original detail
      } else {
        this.modelGeometry = null
      }
      
      // Create multiple instances of the loaded model
      const numInstances = 30
      
      // Create polished gun metal material
      const gunMetalMaterial = new THREE.MeshStandardMaterial({
        color: 0x2C3E50,
        metalness: 0.9,
        roughness: 0.3,
        envMapIntensity: 1.5
      })
      
      gltf.scene.traverse((child) => {
        if (child instanceof THREE.Mesh) {
          child.castShadow = true
          child.receiveShadow = true
          // Apply the gun metal material to all meshes
          child.material = gunMetalMaterial
        }
      })
      
      for (let i = 0; i < numInstances; i++) {
        // Clone the model
        const modelClone = gltf.scene.clone()
        
        // Apply gun metal material to cloned meshes
        modelClone.traverse((child) => {
          if (child instanceof THREE.Mesh) {
            child.material = gunMetalMaterial.clone()
            child.castShadow = true
            child.receiveShadow = true
          }
        })
        
        // Start objects close to the attraction point for immediate clustering
        const x = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
        const y = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
        const z = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
        modelClone.position.set(x, y, z)
        
        // Random scale variation (40% of current size)
        const scale = (0.5 + Math.random() * 0.5) // Scaled down to 40%
        modelClone.scale.setScalar(scale)
        
        // Create physics body with custom collision shape or fallback to sphere
        let shape: CANNON.Shape
        if (this.modelGeometry) {
          shape = this.createCollisionShape(this.modelGeometry, scale)
        } else {
          // Fallback to sphere if geometry extraction failed
          const radius = Math.max(0.3, 0.8 * scale)
          shape = new CANNON.Sphere(radius)
        }
        
        const body = new CANNON.Body({ 
          mass: 1,
          material: world.defaultMaterial,
          type: CANNON.Body.DYNAMIC
        })
        body.addShape(shape)
        body.position.set(x, y, z)
        
        // Enable rotational dynamics
        body.angularDamping = 0.1 // Small damping for realistic rotation
        body.linearDamping = 0.05 // Small linear damping
        
        // Prevent objects from sleeping during collisions
        body.sleepSpeedLimit = 0.1
        body.sleepTimeLimit = 1
        
        // Add collision event listener with rotational effects
        body.addEventListener('collide', (event: any) => {
          
          // Add some spin on collision for more dynamic movement
          const impactStrength = event.contact?.getImpactVelocityAlongNormal() || 1
          const spinForce = Math.min(impactStrength * 0.5, 2) // Cap the spin force
          
          body.angularVelocity.set(
            body.angularVelocity.x + (Math.random() - 0.5) * spinForce,
            body.angularVelocity.y + (Math.random() - 0.5) * spinForce,
            body.angularVelocity.z + (Math.random() - 0.5) * spinForce
          )
        })
        
        // Add gentle initial velocity and rotation
        body.velocity.set(
          (Math.random() - 0.5) * 1, // Reduced from 3 to 1
          (Math.random() - 0.5) * 1, // Reduced from 3 to 1
          (Math.random() - 0.5) * 1  // Reduced from 3 to 1
        )
        
        // Add initial angular velocity for natural rotation
        body.angularVelocity.set(
          (Math.random() - 0.5) * 2,
          (Math.random() - 0.5) * 2,
          (Math.random() - 0.5) * 2
        )
        
        scene.add(modelClone)
        world.addBody(body)
        
        physicsObjects.push({ mesh: modelClone, body })
      }
      
      console.log('Created', numInstances, 'instances of main_model with custom collision shapes')
      console.log('Total physics objects:', physicsObjects.length)
      
    } catch (error) {
      console.error('Failed to load main_model.glb:', error)
      console.log('Falling back to placeholder objects...')
      this.createFallbackObjects(scene, world, physicsObjects)
    }
  }

  createFallbackObjects(
    scene: THREE.Scene, 
    world: CANNON.World, 
    physicsObjects: PhysicsObject[]
  ): void {
    // Fallback geometric objects if model loading fails
    const geometries = [
      new THREE.BoxGeometry(0.5, 0.5, 0.5),
      new THREE.SphereGeometry(0.3, 16, 16),
      new THREE.CylinderGeometry(0.2, 0.2, 0.6, 12),
    ]

    const materials = [
      new THREE.MeshStandardMaterial({ color: 0x4a9eff, metalness: 0.7, roughness: 0.3 }),
      new THREE.MeshStandardMaterial({ color: 0xff6b4a, metalness: 0.5, roughness: 0.4 }),
      new THREE.MeshStandardMaterial({ color: 0x4aff6b, metalness: 0.8, roughness: 0.2 }),
    ]

    for (let i = 0; i < 15; i++) {
      const geometry = geometries[i % geometries.length]
      const material = materials[i % materials.length]
      const mesh = new THREE.Mesh(geometry, material)
      mesh.castShadow = true
      mesh.receiveShadow = true

      // Start objects close to the attraction point for immediate clustering
      const x = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
      const y = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
      const z = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
      mesh.position.set(x, y, z)

      // Create physics body with rotation enabled
      const shape = new CANNON.Sphere(0.3)
      const body = new CANNON.Body({ 
        mass: 1,
        material: world.defaultMaterial,
        type: CANNON.Body.DYNAMIC
      })
      body.addShape(shape)
      body.position.set(x, y, z)
      
      // Enable rotational dynamics
      body.angularDamping = 0.1
      body.linearDamping = 0.05
      
      body.velocity.set(
        (Math.random() - 0.5) * 1, // Reduced from 2 to 1
        (Math.random() - 0.5) * 1, // Reduced from 2 to 1
        (Math.random() - 0.5) * 1  // Reduced from 2 to 1
      )
      
      // Add initial angular velocity
      body.angularVelocity.set(
        (Math.random() - 0.5) * 2,
        (Math.random() - 0.5) * 2,
        (Math.random() - 0.5) * 2
      )

      scene.add(mesh)
      world.addBody(body)

      physicsObjects.push({ mesh, body })
    }
  }
}
