<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { toast } from 'bulma-toast'

const props = defineProps({
  locators: {
    type: Array,
    default: () => []
  },
  selectedLocator: {
    type: Object,
    default: null
  },
  warehouseId: {
    type: String,
    default: ''
  },
  organizationId: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['selectLocator', 'locatorAdded', 'locatorRemoved'])

const canvasRef = ref(null)
const editMode = ref(false)
const addMode = ref(true)
const previewPosition = ref(null)
const showFloorConfig = ref(false)
const firstPersonMode = ref(false)

// Floor area configuration (session only, not saved)
const floorConfig = ref({
  width: 10,  // X units
  length: 10  // Z units (Y in 3D)
})

// Pending changes (staged for batch save)
const pendingAdds = ref([])
const pendingRemoves = ref([])
const isSaving = ref(false)

let scene, camera, renderer, controls, THREE
let animationId = null
let locatorMeshes = []
let pendingMeshes = []
let hoveredLocator = null
let gridPlane = null
let previewMesh = null
let floorBoundaryLines = null
let xMap = {}, yMap = {}, zMap = {}
let xArray = [], yArray = [], zArray = []
let raycaster = null
let mouse = null
let clock = null
let hoverDirty = false
const spacing = 0.9
const boxSize = 0.8
const POSITION_EPSILON = 0.01
const ORBIT_MIN_DISTANCE = 3
const ORBIT_MAX_DISTANCE = 80
const FP_WALK_SPEED = 5          // world units / second
const FP_EYE_HEIGHT = 1.6
const FP_RADIUS = 0.3            // collision radius around camera
const FP_HEAD_CLEARANCE = FP_EYE_HEIGHT + 0.1

// Predefined camera views (no free rotation — only zoom)
const currentView = ref('iso')
const VIEW_OPTIONS = [
  { id: 'iso',   label: '3D' },
  { id: 'top',   label: 'Top' },
  { id: 'north', label: 'North' },
  { id: 'south', label: 'South' },
  { id: 'east',  label: 'East' },
  { id: 'west',  label: 'West' }
]
let viewTweenId = null

// First-person mode variables
let fpControls = null
const fpMovement = {
  forward: false,
  backward: false,
  left: false,
  right: false
}
const isPointerLocked = ref(false)
let savedCameraPosition = null
let savedCameraRotation = null
let savedControlsTarget = null

const hasChanges = computed(() => {
  return pendingAdds.value.length > 0 || pendingRemoves.value.length > 0
})

const changesSummary = computed(() => {
  const adds = pendingAdds.value.length
  const removes = pendingRemoves.value.length
  if (adds && removes) return `${adds} to add, ${removes} to remove`
  if (adds) return `${adds} to add`
  if (removes) return `${removes} to remove`
  return 'No changes'
})

onMounted(() => {
  if (process.client) {
    initThreeJS()
  }
})

onBeforeUnmount(() => {
  if (animationId) {
    cancelAnimationFrame(animationId)
  }
  if (renderer) {
    renderer.dispose()
  }
})

watch(() => props.locators, () => {
  if (scene) {
    updateVisualization()
  }
}, { deep: true })

watch(editMode, (newVal) => {
  if (controls) {
    controls.enabled = !newVal
  }
  if (newVal && !previewMesh) {
    createPreviewMesh()
  } else if (!newVal && previewMesh) {
    scene.remove(previewMesh)
    previewMesh = null
    cancelChanges()
  }
})

watch(floorConfig, () => {
  if (scene) {
    drawFloorBoundary()
  }
}, { deep: true })

watch(firstPersonMode, (newVal) => {
  if (newVal) {
    enterFirstPersonMode()
  } else {
    exitFirstPersonMode()
  }
})

const initThreeJS = async () => {
  THREE = await import('three').then(m => m.default || m)
  const { OrbitControls } = await import('three/examples/jsm/controls/OrbitControls.js')

  scene = new THREE.Scene()
  scene.background = new THREE.Color(0xf5f5f5)

  const container = canvasRef.value
  const width = container.clientWidth
  const height = container.clientHeight
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
  camera.position.set(15, 15, 15)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(width, height)
  renderer.shadowMap.enabled = true
  container.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableRotate = false
  controls.enablePan = false
  controls.enableZoom = true
  controls.enableDamping = true
  controls.dampingFactor = 0.12
  controls.zoomSpeed = 1.0
  controls.minDistance = ORBIT_MIN_DISTANCE
  controls.maxDistance = ORBIT_MAX_DISTANCE

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
  scene.add(ambientLight)

  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
  directionalLight.position.set(10, 20, 10)
  directionalLight.castShadow = true
  scene.add(directionalLight)

  const gridHelper = new THREE.GridHelper(30, 30, 0x888888, 0xcccccc)
  scene.add(gridHelper)

  // Draw floor boundary
  drawFloorBoundary()

  const planeGeometry = new THREE.PlaneGeometry(100, 100)
  const planeMaterial = new THREE.MeshBasicMaterial({
    visible: false,
    side: THREE.DoubleSide
  })
  gridPlane = new THREE.Mesh(planeGeometry, planeMaterial)
  gridPlane.rotation.x = -Math.PI / 2
  gridPlane.position.y = 0
  scene.add(gridPlane)

  window.addEventListener('resize', onWindowResize)

  raycaster = new THREE.Raycaster()
  mouse = new THREE.Vector2()
  clock = new THREE.Clock()

  setupFirstPersonControls()

  const updateMouseFromEvent = (event) => {
    const rect = renderer.domElement.getBoundingClientRect()
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  }

  renderer.domElement.addEventListener('click', (event) => {
    if (firstPersonMode.value) return
    updateMouseFromEvent(event)
    raycaster.setFromCamera(mouse, camera)

    if (editMode.value) {
      handleEditClick(raycaster)
    } else {
      const intersects = raycaster.intersectObjects(locatorMeshes)
      if (intersects.length > 0) {
        const locator = intersects[0].object.userData.locator
        emit('selectLocator', locator)
      }
    }
  })

  // Just stash the latest mouse position — actual raycasting runs once per frame in animate()
  renderer.domElement.addEventListener('mousemove', (event) => {
    if (firstPersonMode.value) return
    updateMouseFromEvent(event)
    hoverDirty = true
  })

  renderer.domElement.addEventListener('mouseleave', () => {
    if (hoveredLocator) {
      hoveredLocator.material.emissive.setHex(hoveredLocator.userData.originalEmissive)
      hoveredLocator = null
    }
    renderer.domElement.style.cursor = 'default'
    hoverDirty = false
  })

  updateVisualization()
  animate()
}

const processHover = () => {
  if (!raycaster || !mouse) return
  raycaster.setFromCamera(mouse, camera)

  if (editMode.value) {
    handleEditHover(raycaster)
    return
  }

  const intersects = raycaster.intersectObjects(locatorMeshes)
  if (hoveredLocator) {
    hoveredLocator.material.emissive.setHex(hoveredLocator.userData.originalEmissive)
    hoveredLocator = null
    renderer.domElement.style.cursor = 'default'
  }
  if (intersects.length > 0) {
    hoveredLocator = intersects[0].object
    hoveredLocator.material.emissive.setHex(0x444444)
    renderer.domElement.style.cursor = 'pointer'
  }
}

const drawFloorBoundary = () => {
  if (!THREE || !scene) return

  // Remove old boundary
  if (floorBoundaryLines) {
    scene.remove(floorBoundaryLines)
  }

  const width = floorConfig.value.width * spacing
  const length = floorConfig.value.length * spacing

  const points = []
  points.push(new THREE.Vector3(-spacing/2, 0.01, -spacing/2))
  points.push(new THREE.Vector3(width - spacing/2, 0.01, -spacing/2))
  points.push(new THREE.Vector3(width - spacing/2, 0.01, length - spacing/2))
  points.push(new THREE.Vector3(-spacing/2, 0.01, length - spacing/2))
  points.push(new THREE.Vector3(-spacing/2, 0.01, -spacing/2))

  const geometry = new THREE.BufferGeometry().setFromPoints(points)
  const material = new THREE.LineBasicMaterial({
    color: 0x2196F3,
    linewidth: 3
  })

  floorBoundaryLines = new THREE.Line(geometry, material)
  scene.add(floorBoundaryLines)
}

const handleEditClick = (raycaster) => {
  if (addMode.value) {
    // Check if clicking on existing locator to stack
    const allMeshes = [...locatorMeshes, ...pendingMeshes]
    const locatorIntersects = raycaster.intersectObjects(allMeshes)

    if (locatorIntersects.length > 0) {
      // Stack on this locator
      const baseLocator = locatorIntersects[0].object.userData.locator
      stackLocatorOn(baseLocator)
    } else {
      // Click on grid to add at ground level
      const intersects = raycaster.intersectObject(gridPlane)
      if (intersects.length > 0 && previewPosition.value) {
        stageAddLocator(previewPosition.value, 0)
      }
    }
  } else {
    // Remove mode
    const allMeshes = [...locatorMeshes, ...pendingMeshes]
    const intersects = raycaster.intersectObjects(allMeshes)
    if (intersects.length > 0) {
      const locator = intersects[0].object.userData.locator
      stageRemoveLocator(locator)
    }
  }
}

const stackLocatorOn = (baseLocator) => {
  // Find the highest Z level at this X,Y position
  const allAtPosition = [...props.locators, ...pendingAdds.value].filter(loc =>
    loc.x === baseLocator.x && loc.y === baseLocator.y
  )

  const zValues = allAtPosition.map(loc => loc.z).sort()
  const highestZ = zValues.length > 0 ? zValues[zValues.length - 1] : '0'

  // Generate next Z coordinate
  const nextZ = getNextCoordValue(highestZ)

  // Find grid position of base locator
  const baseX = xMap[baseLocator.x] !== undefined ? xMap[baseLocator.x] : 0
  const baseY = yMap[baseLocator.y] !== undefined ? yMap[baseLocator.y] : 0
  const currentZIndex = zMap[highestZ] !== undefined ? zMap[highestZ] : 0
  const nextZIndex = currentZIndex + 1

  const position = {
    x: baseX * spacing,
    y: 0,
    z: baseY * spacing
  }

  const value = `${baseLocator.x}-${baseLocator.y}-${nextZ}`

  const pendingLocator = {
    id: `pending_${Date.now()}_${Math.random()}`,
    value,
    x: baseLocator.x,
    y: baseLocator.y,
    z: nextZ,
    zIndex: nextZIndex,
    position,
    isPending: true,
    isActive: true
  }

  // Update zMap if needed
  if (!zMap[nextZ]) {
    zArray.push(nextZ)
    zArray.sort()
    zMap[nextZ] = nextZIndex
  }

  pendingAdds.value.push(pendingLocator)
  createPendingMesh(pendingLocator, nextZIndex)

  toast({
    message: `Stacked: ${value}`,
    type: 'is-info',
    duration: 1500,
    position: 'bottom-right'
  })
}

const handleEditHover = (raycaster) => {
  if (addMode.value) {
    // Check if hovering over locator (for stacking)
    const allMeshes = [...locatorMeshes, ...pendingMeshes]
    const locatorIntersects = raycaster.intersectObjects(allMeshes)

    if (locatorIntersects.length > 0) {
      // Show stacking preview
      const baseLocator = locatorIntersects[0].object.userData.locator
      const allAtPosition = [...props.locators, ...pendingAdds.value].filter(loc =>
        loc.x === baseLocator.x && loc.y === baseLocator.y
      )
      const height = allAtPosition.length

      if (previewMesh) {
        const baseX = xMap[baseLocator.x] !== undefined ? xMap[baseLocator.x] : 0
        const baseY = yMap[baseLocator.y] !== undefined ? yMap[baseLocator.y] : 0
        previewMesh.position.set(
          baseX * spacing,
          height * spacing + boxSize/2,
          baseY * spacing
        )
        previewMesh.visible = true
        previewMesh.material.color.setHex(0x00ff00) // Green for valid stack
      }
      renderer.domElement.style.cursor = 'pointer'
    } else {
      // Show ground level preview
      const intersects = raycaster.intersectObject(gridPlane)
      if (intersects.length > 0) {
        const point = intersects[0].point
        const snappedPos = snapToGrid(point)

        // Check if within floor boundary
        const isWithinBounds = snappedPos.x >= 0 &&
                               snappedPos.x < floorConfig.value.width * spacing &&
                               snappedPos.z >= 0 &&
                               snappedPos.z < floorConfig.value.length * spacing

        previewPosition.value = snappedPos

        if (previewMesh) {
          previewMesh.position.set(snappedPos.x, snappedPos.y + boxSize/2, snappedPos.z)
          previewMesh.visible = true

          const isOccupied = checkIfPositionOccupied(snappedPos, 0)
          previewMesh.material.color.setHex(
            !isWithinBounds ? 0xff9800 : // Orange for out of bounds
            isOccupied ? 0xff0000 :       // Red for occupied
            0x00ff00                       // Green for valid
          )
        }
        renderer.domElement.style.cursor = 'crosshair'
      }
    }
  } else {
    // Remove mode hover
    const allMeshes = [...locatorMeshes, ...pendingMeshes]
    const intersects = raycaster.intersectObjects(allMeshes)

    if (hoveredLocator) {
      hoveredLocator.material.emissive.setHex(hoveredLocator.userData.originalEmissive)
      hoveredLocator = null
    }

    if (intersects.length > 0) {
      hoveredLocator = intersects[0].object
      hoveredLocator.material.emissive.setHex(0xff0000)
      renderer.domElement.style.cursor = 'pointer'
    } else {
      renderer.domElement.style.cursor = 'crosshair'
    }
  }
}

const snapToGrid = (point) => {
  const gridSize = spacing
  return {
    x: Math.round(point.x / gridSize) * gridSize,
    y: 0,
    z: Math.round(point.z / gridSize) * gridSize
  }
}

const checkIfPositionOccupied = (pos, zLevel) => {
  const existingOccupied = locatorMeshes.some(mesh => {
    const meshPos = mesh.position
    const locator = mesh.userData.locator
    return Math.abs(meshPos.x - pos.x) < POSITION_EPSILON &&
           Math.abs(meshPos.z - pos.z) < POSITION_EPSILON &&
           (zMap[locator.z] === zLevel)
  })

  const pendingOccupied = pendingAdds.value.some(pending => {
    return Math.abs(pending.position.x - pos.x) < POSITION_EPSILON &&
           Math.abs(pending.position.z - pos.z) < POSITION_EPSILON &&
           (pending.zIndex === zLevel || zMap[pending.z] === zLevel)
  })

  return existingOccupied || pendingOccupied
}

const createLabelSprite = (text) => {
  const canvas = document.createElement('canvas')
  canvas.width = 256
  canvas.height = 128
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  ctx.fillStyle = '#000000'
  ctx.font = 'bold 32px Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  ctx.fillText(text, canvas.width / 2, canvas.height / 2)

  const texture = new THREE.CanvasTexture(canvas)
  const spriteMaterial = new THREE.SpriteMaterial({ map: texture, depthTest: false })
  const sprite = new THREE.Sprite(spriteMaterial)
  sprite.scale.set(0.6, 0.3, 1)
  return sprite
}

const createPreviewMesh = () => {
  if (!THREE) return

  const geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize)
  const material = new THREE.MeshStandardMaterial({
    color: 0x00ff00,
    transparent: true,
    opacity: 0.5,
    metalness: 0.3,
    roughness: 0.4
  })

  previewMesh = new THREE.Mesh(geometry, material)
  previewMesh.position.set(0, boxSize/2, 0)
  previewMesh.visible = false
  scene.add(previewMesh)
}

const stageAddLocator = (pos, zLevel = 0) => {
  // Check within bounds
  const isWithinBounds = pos.x >= 0 &&
                         pos.x < floorConfig.value.width * spacing &&
                         pos.z >= 0 &&
                         pos.z < floorConfig.value.length * spacing

  if (!isWithinBounds) {
    toast({
      message: 'Position is outside the configured floor area',
      type: 'is-warning',
      duration: 2000,
      position: 'bottom-right'
    })
    return
  }

  if (checkIfPositionOccupied(pos, zLevel)) {
    toast({
      message: 'A locator already exists at this position',
      type: 'is-warning',
      duration: 2000,
      position: 'bottom-right'
    })
    return
  }

  const gridX = Math.round(pos.x / spacing)
  const gridZ = Math.round(pos.z / spacing)

  let xCoord, yCoord, zCoord

  if (xArray.length > 0) {
    const existingMaxX = Math.max(...Object.values(xMap))
    if (gridX > existingMaxX) {
      const nextVal = getNextCoordValue(xArray[xArray.length - 1])
      xCoord = nextVal
      xArray.push(nextVal)
      xMap[nextVal] = gridX
    } else {
      xCoord = Object.keys(xMap).find(key => xMap[key] === gridX) || generateCoord(gridX, 'X')
    }
  } else {
    xCoord = generateCoord(gridX, 'X')
    xArray.push(xCoord)
    xMap[xCoord] = gridX
  }

  if (yArray.length > 0) {
    const existingMaxY = Math.max(...Object.values(yMap))
    if (gridZ > existingMaxY) {
      const nextVal = getNextCoordValue(yArray[yArray.length - 1])
      yCoord = nextVal
      yArray.push(nextVal)
      yMap[nextVal] = gridZ
    } else {
      yCoord = Object.keys(yMap).find(key => yMap[key] === gridZ) || generateCoord(gridZ, 'Y')
    }
  } else {
    yCoord = generateCoord(gridZ, 'Y')
    yArray.push(yCoord)
    yMap[yCoord] = gridZ
  }

  // Z is for vertical stacking
  if (zArray.length > 0) {
    zCoord = zArray[zLevel] || '1'
  } else {
    zCoord = '1'
    zArray.push(zCoord)
    zMap[zCoord] = 0
  }

  const value = `${xCoord}-${yCoord}-${zCoord}`

  const pendingLocator = {
    id: `pending_${Date.now()}_${Math.random()}`,
    value,
    x: xCoord,
    y: yCoord,
    z: zCoord,
    zIndex: zLevel,
    position: { x: pos.x, y: pos.y, z: pos.z },
    isPending: true,
    isActive: true
  }

  pendingAdds.value.push(pendingLocator)
  createPendingMesh(pendingLocator, zLevel)

  toast({
    message: `Staged: ${value}`,
    type: 'is-info',
    duration: 1500,
    position: 'bottom-right'
  })
}

const stageRemoveLocator = (locator) => {
  if (locator.isPending) {
    const index = pendingAdds.value.findIndex(p => p.id === locator.id)
    if (index > -1) {
      pendingAdds.value.splice(index, 1)
      const meshIndex = pendingMeshes.findIndex(m => m.userData.locator.id === locator.id)
      if (meshIndex > -1) {
        scene.remove(pendingMeshes[meshIndex])
        pendingMeshes.splice(meshIndex, 1)
      }
      toast({
        message: `Unstaged: ${locator.value}`,
        type: 'is-info',
        duration: 1500,
        position: 'bottom-right'
      })
    }
    return
  }

  const alreadyStaged = pendingRemoves.value.some(p => p.id === locator.id)
  if (alreadyStaged) {
    const index = pendingRemoves.value.findIndex(p => p.id === locator.id)
    pendingRemoves.value.splice(index, 1)
    const mesh = locatorMeshes.find(m => m.userData.locator.id === locator.id)
    if (mesh) {
      mesh.material.opacity = 1
      mesh.material.transparent = false
    }
    toast({
      message: `Unstaged removal: ${locator.value}`,
      type: 'is-info',
      duration: 1500,
      position: 'bottom-right'
    })
  } else {
    pendingRemoves.value.push(locator)
    const mesh = locatorMeshes.find(m => m.userData.locator.id === locator.id)
    if (mesh) {
      mesh.material.transparent = true
      mesh.material.opacity = 0.3
    }
    toast({
      message: `Staged removal: ${locator.value}`,
      type: 'is-info',
      duration: 1500,
      position: 'bottom-right'
    })
  }
}

const createPendingMesh = (locator, zLevel) => {
  if (!THREE) return

  const pos = locator.position
  const color = 0xFFEB3B

  const geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize)
  const material = new THREE.MeshStandardMaterial({
    color: color,
    metalness: 0.3,
    roughness: 0.4,
    transparent: true,
    opacity: 0.7
  })

  const mesh = new THREE.Mesh(geometry, material)
  mesh.position.set(pos.x, zLevel * spacing + boxSize/2, pos.z)
  mesh.castShadow = true
  mesh.receiveShadow = true

  mesh.userData.locator = locator
  mesh.userData.originalEmissive = 0x000000

  scene.add(mesh)
  pendingMeshes.push(mesh)

  const sprite = createLabelSprite(locator.value)
  sprite.position.set(pos.x, zLevel * spacing + boxSize + 0.3, pos.z)
  scene.add(sprite)
}

const saveChanges = async () => {
  if (!hasChanges.value) return

  isSaving.value = true

  try {
    for (const locator of pendingRemoves.value) {
      await $fetch(`/api/materials/warehouses/${locator.id}/locator`, {
        method: 'DELETE',
        headers: useRequestHeaders(['cookie'])
      })
    }

    for (const pending of pendingAdds.value) {
      await $fetch(`/api/materials/warehouses/${props.warehouseId}/locator`, {
        method: 'POST',
        headers: useRequestHeaders(['cookie']),
        body: {
          organizationId: props.organizationId,
          isActive: true,
          value: pending.value,
          priorityNo: 50,
          x: pending.x,
          y: pending.y,
          z: pending.z,
          isDefault: false,
          warehouseId: props.warehouseId,
        }
      })
    }

    toast({
      message: `Successfully saved! Added: ${pendingAdds.value.length}, Removed: ${pendingRemoves.value.length}`,
      type: 'is-success',
      duration: 5000,
      position: 'bottom-right'
    })

    pendingAdds.value = []
    pendingRemoves.value = []

    pendingMeshes.forEach(mesh => scene.remove(mesh))
    pendingMeshes = []

    editMode.value = false
    emit('locatorAdded')
  } catch (err) {
    toast({
      message: 'Error saving changes',
      type: 'is-danger',
      duration: 5000,
      position: 'bottom-right'
    })
  }

  isSaving.value = false
}

const cancelChanges = () => {
  pendingAdds.value = []

  pendingMeshes.forEach(mesh => scene.remove(mesh))
  pendingMeshes = []

  pendingRemoves.value.forEach(locator => {
    const mesh = locatorMeshes.find(m => m.userData.locator.id === locator.id)
    if (mesh) {
      mesh.material.opacity = 1
      mesh.material.transparent = false
    }
  })

  pendingRemoves.value = []

  if (!editMode.value) return

  toast({
    message: 'Changes cancelled',
    type: 'is-info',
    duration: 2000,
    position: 'bottom-right'
  })
}

const generateCoord = (index, axis) => {
  if (index < 0) index = 0
  if (axis === 'X') {
    return String.fromCharCode(65 + (index % 26))
  }
  return String(index + 1).padStart(2, '0')
}

const getNextCoordValue = (current) => {
  if (/^\d+$/.test(current)) {
    return String(parseInt(current) + 1).padStart(current.length, '0')
  } else if (/^[A-Z]$/.test(current)) {
    return String.fromCharCode(current.charCodeAt(0) + 1)
  }
  return current + '1'
}

const onWindowResize = () => {
  if (!canvasRef.value || !camera || !renderer) return

  const width = canvasRef.value.clientWidth
  const height = canvasRef.value.clientHeight

  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
}

const getSceneCenter = () => {
  const cx = xArray.length > 1 ? (xArray.length - 1) * spacing / 2 : (floorConfig.value.width * spacing) / 2
  const cz = yArray.length > 1 ? (yArray.length - 1) * spacing / 2 : (floorConfig.value.length * spacing) / 2
  return { cx, cz }
}

const getViewDistance = () => {
  const w = Math.max(floorConfig.value.width, xArray.length, 6) * spacing
  const l = Math.max(floorConfig.value.length, yArray.length, 6) * spacing
  return Math.max(w, l) * 1.3
}

const getViewTarget = (name) => {
  const { cx, cz } = getSceneCenter()
  const d = getViewDistance()
  const look = new THREE.Vector3(cx, 0.5, cz)
  switch (name) {
    // tiny z offset on top prevents camera.up × lookDir degeneracy
    case 'top':   return { pos: new THREE.Vector3(cx, d * 1.2, cz + 0.01), look }
    case 'north': return { pos: new THREE.Vector3(cx, d * 0.3, cz - d),    look }
    case 'south': return { pos: new THREE.Vector3(cx, d * 0.3, cz + d),    look }
    case 'east':  return { pos: new THREE.Vector3(cx + d, d * 0.3, cz),    look }
    case 'west':  return { pos: new THREE.Vector3(cx - d, d * 0.3, cz),    look }
    case 'iso':
    default:      return { pos: new THREE.Vector3(cx + d * 0.7, d * 0.7, cz + d * 0.7), look }
  }
}

const tweenCamera = (toPos, toLook, duration = 450) => {
  if (!camera || !controls) return
  if (viewTweenId) cancelAnimationFrame(viewTweenId)

  const startPos = camera.position.clone()
  const startLook = controls.target.clone()
  const t0 = performance.now()
  const ease = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)  // easeInOutQuad

  const step = () => {
    const t = Math.min(1, (performance.now() - t0) / duration)
    const e = ease(t)
    camera.position.lerpVectors(startPos, toPos, e)
    controls.target.lerpVectors(startLook, toLook, e)
    controls.update()
    if (t < 1) {
      viewTweenId = requestAnimationFrame(step)
    } else {
      viewTweenId = null
    }
  }
  step()
}

const setView = (name, { animate = true } = {}) => {
  if (!camera || !controls || firstPersonMode.value) return
  currentView.value = name
  const { pos, look } = getViewTarget(name)
  if (animate) {
    tweenCamera(pos, look)
  } else {
    camera.position.copy(pos)
    controls.target.copy(look)
    controls.update()
  }
}

const updateVisualization = async () => {
  if (!scene) return

  if (!THREE) {
    THREE = await import('three').then(m => m.default || m)
  }

  locatorMeshes.forEach(mesh => scene.remove(mesh))
  locatorMeshes = []

  scene.children = scene.children.filter(child =>
    !(child instanceof THREE.Sprite) || pendingMeshes.includes(child)
  )

  if (props.locators.length === 0) return

  const xCoords = new Set()
  const yCoords = new Set()
  const zCoords = new Set()

  props.locators.forEach(loc => {
    xCoords.add(loc.x)
    yCoords.add(loc.y)
    zCoords.add(loc.z)
  })

  xArray = Array.from(xCoords).sort()
  yArray = Array.from(yCoords).sort()
  zArray = Array.from(zCoords).sort()

  xMap = {}
  yMap = {}
  zMap = {}

  xArray.forEach((val, idx) => xMap[val] = idx)
  yArray.forEach((val, idx) => yMap[val] = idx)
  zArray.forEach((val, idx) => zMap[val] = idx)

  props.locators.forEach(locator => {
    const xPos = xMap[locator.x] * spacing
    const yPos = zMap[locator.z] * spacing
    const zPos = yMap[locator.y] * spacing

    let color = 0x4CAF50
    if (!locator.isActive) {
      color = 0x9E9E9E
    } else if (locator.isDefault) {
      color = 0x2196F3
    }

    const geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize)
    const material = new THREE.MeshStandardMaterial({
      color: color,
      metalness: 0.3,
      roughness: 0.4
    })

    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.set(xPos, yPos + boxSize/2, zPos)
    mesh.castShadow = true
    mesh.receiveShadow = true

    mesh.userData.locator = locator
    mesh.userData.originalEmissive = 0x000000

    const isStaged = pendingRemoves.value.some(p => p.id === locator.id)
    if (isStaged) {
      mesh.material.transparent = true
      mesh.material.opacity = 0.3
    }

    scene.add(mesh)
    locatorMeshes.push(mesh)

    const sprite = createLabelSprite(locator.value)
    sprite.position.set(xPos, yPos + boxSize + 0.3, zPos)
    scene.add(sprite)
  })

  // Re-frame the current view so the camera tracks the new scene center
  setView(currentView.value, { animate: false })
}

const animate = () => {
  animationId = requestAnimationFrame(animate)

  const delta = clock ? clock.getDelta() : 0

  if (firstPersonMode.value) {
    updateFirstPersonMovement(delta)
  } else if (hoverDirty) {
    processHover()
    hoverDirty = false
  }

  if (controls && controls.enabled) controls.update()
  if (renderer && scene && camera) {
    renderer.render(scene, camera)
  }
}

const setupFirstPersonControls = async () => {
  const { PointerLockControls } = await import('three/examples/jsm/controls/PointerLockControls.js')
  fpControls = new PointerLockControls(camera, renderer.domElement)

  fpControls.addEventListener('lock', () => { isPointerLocked.value = true })
  fpControls.addEventListener('unlock', () => {
    isPointerLocked.value = false
    // Reset keys so we don't keep drifting after unlock
    fpMovement.forward = fpMovement.backward = fpMovement.left = fpMovement.right = false
  })

  const handleKeyDown = (event) => {
    if (!firstPersonMode.value) return
    switch (event.code) {
      case 'KeyW': case 'ArrowUp':    fpMovement.forward = true;  break
      case 'KeyS': case 'ArrowDown':  fpMovement.backward = true; break
      case 'KeyA': case 'ArrowLeft':  fpMovement.left = true;     break
      case 'KeyD': case 'ArrowRight': fpMovement.right = true;    break
    }
  }

  const handleKeyUp = (event) => {
    if (!firstPersonMode.value) return
    switch (event.code) {
      case 'KeyW': case 'ArrowUp':    fpMovement.forward = false;  break
      case 'KeyS': case 'ArrowDown':  fpMovement.backward = false; break
      case 'KeyA': case 'ArrowLeft':  fpMovement.left = false;     break
      case 'KeyD': case 'ArrowRight': fpMovement.right = false;    break
    }
  }

  window.addEventListener('keydown', handleKeyDown)
  window.addEventListener('keyup', handleKeyUp)

  onBeforeUnmount(() => {
    window.removeEventListener('keydown', handleKeyDown)
    window.removeEventListener('keyup', handleKeyUp)
    if (fpControls) fpControls.dispose()
  })
}

const enterFirstPersonMode = () => {
  if (!camera || !controls || !fpControls) return

  savedCameraPosition = camera.position.clone()
  savedCameraRotation = camera.rotation.clone()
  savedControlsTarget = controls.target.clone()

  controls.enabled = false

  const centerX = (xArray.length - 1) * spacing / 2
  const centerZ = (yArray.length - 1) * spacing / 2
  camera.position.set(centerX, FP_EYE_HEIGHT, centerZ + 5)
  camera.lookAt(centerX, FP_EYE_HEIGHT, centerZ)

  try { fpControls.lock() } catch (e) { /* pointer lock requires user gesture; banner prompts retry */ }

  toast({
    message: 'First-person mode: WASD/Arrows to move, mouse to look, ESC to unlock',
    type: 'is-info',
    duration: 4000,
    position: 'bottom-right'
  })
}

const exitFirstPersonMode = () => {
  if (!camera || !controls) return

  if (fpControls && fpControls.isLocked) fpControls.unlock()

  if (savedCameraPosition) {
    camera.position.copy(savedCameraPosition)
    camera.rotation.copy(savedCameraRotation)
    controls.target.copy(savedControlsTarget)
  }

  controls.enabled = true
  controls.update()

  fpMovement.forward = fpMovement.backward = fpMovement.left = fpMovement.right = false
}

// Block camera from entering the XZ footprint of any locator stack at body height
const collidesWithLocator = (pos) => {
  const hit = (mesh) => {
    const dx = Math.abs(pos.x - mesh.position.x)
    const dz = Math.abs(pos.z - mesh.position.z)
    if (dx >= boxSize / 2 + FP_RADIUS || dz >= boxSize / 2 + FP_RADIUS) return false
    const boxBottom = mesh.position.y - boxSize / 2
    const boxTop = mesh.position.y + boxSize / 2
    return boxBottom < FP_HEAD_CLEARANCE && boxTop > 0
  }
  return locatorMeshes.some(hit) || pendingMeshes.some(hit)
}

const updateFirstPersonMovement = (delta) => {
  if (!fpControls || !camera) return

  const step = FP_WALK_SPEED * delta
  const prev = camera.position.clone()

  // PointerLockControls.moveForward/moveRight use the yaw the user is looking at
  if (fpMovement.forward)  fpControls.moveForward(step)
  if (fpMovement.backward) fpControls.moveForward(-step)
  if (fpMovement.right)    fpControls.moveRight(step)
  if (fpMovement.left)     fpControls.moveRight(-step)

  if (collidesWithLocator(camera.position)) {
    camera.position.copy(prev)
  }

  // Soft floor-area clamp — lets the user stay near the configured warehouse footprint
  const pad = spacing * 2
  const floorW = floorConfig.value.width * spacing
  const floorL = floorConfig.value.length * spacing
  camera.position.x = Math.max(-pad, Math.min(floorW + pad, camera.position.x))
  camera.position.z = Math.max(-pad, Math.min(floorL + pad, camera.position.z))
  camera.position.y = FP_EYE_HEIGHT
}

const toggleEditMode = () => {
  editMode.value = !editMode.value
}

const toggleAddRemoveMode = () => {
  addMode.value = !addMode.value
}

const toggleFirstPersonMode = () => {
  if (editMode.value) {
    toast({
      message: 'Please exit edit mode first',
      type: 'is-warning',
      duration: 2000,
      position: 'bottom-right'
    })
    return
  }
  firstPersonMode.value = !firstPersonMode.value
}

defineExpose({
  toggleEditMode,
  editMode,
  saveChanges,
  cancelChanges,
  hasChanges,
  toggleFirstPersonMode,
  firstPersonMode,
  setView,
  currentView
})
</script>

<template>
  <div class="warehouse-3d-container">
    <div ref="canvasRef" class="canvas-container"></div>

    <!-- Edit Mode Controls -->
    <div class="edit-controls" v-if="editMode">
      <div class="edit-mode-banner">
        <div class="d-flex align-itemsCenter gap-10px">
          <strong>✏️ EDIT MODE</strong>
          <button
            @click="toggleAddRemoveMode"
            class="button is-small"
            :class="addMode ? 'is-success' : 'is-danger'"
          >
            {{ addMode ? '➕ Add Mode' : '🗑️ Remove Mode' }}
          </button>
          <button
            @click="showFloorConfig = true"
            class="button is-small is-info"
          >
            <i class="ti ti-settings me-1"></i>
            Floor Area
          </button>
        </div>
        <div class="changes-info">
          <small><strong>{{ changesSummary }}</strong></small>
        </div>
        <div class="d-flex gap-5px">
          <button
            @click="cancelChanges"
            class="button is-small"
            :disabled="!hasChanges || isSaving"
          >
            Cancel
          </button>
          <button
            @click="saveChanges"
            class="button is-small is-primary"
            :disabled="!hasChanges || isSaving"
          >
            {{ isSaving ? 'Saving...' : 'Save All' }}
          </button>
        </div>
      </div>
      <div class="edit-instructions">
        <small v-if="addMode">
          <strong>Add Mode:</strong> Click grid for ground level | Click locator to stack vertically
        </small>
        <small v-else>
          <strong>Remove Mode:</strong> Click locator to mark for removal (click again to undo)
        </small>
      </div>
    </div>

    <!-- Regular Legend -->
    <div class="legend" v-else>
      <div class="legend-item">
        <span class="legend-color" style="background: #4CAF50;"></span>
        <span>Active</span>
      </div>
      <div class="legend-item">
        <span class="legend-color" style="background: #2196F3;"></span>
        <span>Default</span>
      </div>
      <div class="legend-item">
        <span class="legend-color" style="background: #9E9E9E;"></span>
        <span>Inactive</span>
      </div>
    </div>

    <!-- First-person mode banner -->
    <div class="fp-mode-banner" v-if="firstPersonMode">
      <div class="d-flex align-itemsCenter gap-10px">
        <strong>🎮 FIRST-PERSON MODE</strong>
        <div class="fp-controls-hint">
          <small>WASD/Arrows: Move | Mouse: Look | ESC: Unlock mouse</small>
        </div>
      </div>
      <div class="pointer-lock-status" v-if="!isPointerLocked">
        <small>⚠️ Click canvas to activate mouse look</small>
      </div>
    </div>

    <!-- View switcher (fixed presets; zoom via scroll) -->
    <div class="view-switcher" v-if="!firstPersonMode">
      <button
        v-for="v in VIEW_OPTIONS"
        :key="v.id"
        class="view-btn"
        :class="{ 'is-active': currentView === v.id }"
        @click="setView(v.id)"
      >
        {{ v.label }}
      </button>
    </div>

    <div class="controls-info">
      <small v-if="firstPersonMode">
        <strong>First-Person Mode:</strong> Walk through the warehouse like in a game
      </small>
      <small v-else-if="!editMode">
        <strong>View:</strong> {{ currentView.toUpperCase() }} · Scroll to zoom
      </small>
      <small v-else>
        <strong>Edit Mode:</strong> Floor {{ floorConfig.width }}×{{ floorConfig.length }} · Scroll to zoom
      </small>
    </div>

    <!-- Floor Config Modal -->
    <Teleport to="body">
      <div class="modal" :class="{'active': showFloorConfig}" :style="{display: showFloorConfig ? 'block' : 'none'}">
        <div class="modalDialog modalCenter" @click.self="showFloorConfig = false">
          <div class="modalContent" style="max-width: 500px;">
            <div class="modalHeader">
              <h3 class="modalTitle">Configure Floor Area</h3>
              <div class="cursor-pointer" @click="showFloorConfig = false">
                <span class="tf-icon ti ti-x ti-sm me-1"></span>
              </div>
            </div>
            <div class="modalBody">
              <div class="my-5 mx-5">
                <p class="mb-4">
                  Define the available floor area for placing locators. This is for visualization only and not saved.
                </p>

                <label class="label d-block">Width (X-axis)</label>
                <input
                  v-model.number="floorConfig.width"
                  type="number"
                  min="1"
                  max="50"
                  class="input mb-3"
                  placeholder="10"
                />

                <label class="label d-block">Length (Z-axis)</label>
                <input
                  v-model.number="floorConfig.length"
                  type="number"
                  min="1"
                  max="50"
                  class="input mb-3"
                  placeholder="10"
                />

                <div class="notification is-info is-light">
                  <p><strong>Blue boundary</strong> will show the configured area in 3D view.</p>
                  <p class="mt-2">Total capacity: {{ floorConfig.width }} × {{ floorConfig.length }} = {{ floorConfig.width * floorConfig.length }} ground positions</p>
                </div>
              </div>
            </div>
            <div class="modalFooter">
              <div></div>
              <div>
                <button type="button" class="button is-primary" @click="showFloorConfig = false">Done</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.warehouse-3d-container {
  position: relative;
  width: 100%;
  height: 600px;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
}

.canvas-container {
  width: 100%;
  height: 100%;
}

.edit-controls {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
}

.edit-mode-banner {
  background: rgba(255, 193, 7, 0.95);
  padding: 12px 20px;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 15px;
  font-size: 14px;
  color: #000;
}

.changes-info {
  padding: 4px 12px;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 4px;
}

.edit-instructions {
  background: rgba(255, 255, 255, 0.95);
  padding: 8px 12px;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  margin-top: 8px;
  text-align: center;
}

.legend {
  position: absolute;
  top: 10px;
  right: 10px;
  background: rgba(255, 255, 255, 0.95);
  padding: 12px;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 13px;
}

.legend-item:last-child {
  margin-bottom: 0;
}

.legend-color {
  display: inline-block;
  width: 20px;
  height: 20px;
  border-radius: 3px;
  border: 1px solid rgba(0,0,0,0.2);
}

.controls-info {
  position: absolute;
  bottom: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.95);
  padding: 8px 12px;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.fp-mode-banner {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(76, 175, 80, 0.95);
  padding: 12px 20px;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  color: white;
  z-index: 10;
}

.fp-controls-hint {
  background: rgba(255, 255, 255, 0.2);
  padding: 4px 8px;
  border-radius: 4px;
}

.pointer-lock-status {
  margin-top: 8px;
  padding: 4px 8px;
  background: rgba(255, 152, 0, 0.9);
  border-radius: 4px;
  text-align: center;
}

.view-switcher {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 4px;
  padding: 6px;
  background: rgba(255, 255, 255, 0.95);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 9;
}

.view-btn {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 500;
  color: #333;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.view-btn:hover {
  background: #f0f0f0;
}

.view-btn.is-active {
  background: #2196F3;
  color: #fff;
  border-color: #1976D2;
}
</style>
