import * as THREE from 'three'
import type { BitBybitViewportAdapter, MeshAsset, SubshapeRef, ViewportInteractionHandlers, ViewportMeshAsset } from './types'

type ScreenRect = { left: number; top: number; right: number; bottom: number }

export const resolveScreenBoxSelection = (candidates: Array<{ objectId: string; bounds: ScreenRect }>, selection: ScreenRect, mode: 'window' | 'crossing'): string[] => candidates.filter(({ bounds }) => mode === 'window'
  ? bounds.left >= selection.left && bounds.right <= selection.right && bounds.top >= selection.top && bounds.bottom <= selection.bottom
  : bounds.right >= selection.left && bounds.left <= selection.right && bounds.bottom >= selection.top && bounds.top <= selection.bottom).map(({ objectId }) => objectId)

export const resolveMeshSubshape = (mesh: MeshAsset, triangleIndex: number): SubshapeRef | null => {
  if (!Number.isSafeInteger(triangleIndex) || triangleIndex < 0) return null
  const range = mesh.subshapeRanges?.find((entry) => triangleIndex >= entry.startTriangle && triangleIndex < entry.startTriangle + entry.triangleCount)
  return range ? { ...range.ref, candidates: range.ref.candidates ? [...range.ref.candidates] : undefined } : null
}

/** Internal renderer boundary. React receives only the adapter contract. */
export class ThreeViewportAdapter implements BitBybitViewportAdapter {
  private host: HTMLElement | null = null
  private renderer: THREE.WebGLRenderer | null = null
  private scene: THREE.Scene | null = null
  private camera: THREE.PerspectiveCamera | null = null
  private subshapeHighlight: THREE.Mesh | THREE.Line | THREE.Points | null = null
  private toolpath: THREE.Group | null = null
  private toolpathSource: Array<Array<[number, number, number]>> = []
  private readonly objectMeshes = new Map<string, { asset: MeshAsset; mesh: THREE.Mesh }>()
  private interactionHandlers: ViewportInteractionHandlers = {}
  private hoveredSubshapeId = ''
  private selectedObjectId = ''
  private selectedObjectIds = new Set<string>()
  private selectionFlashObjectId = ''
  private selectionFlashUntil = 0
  private boxDrag: { startX: number; startY: number; pointerId: number; additive: boolean } | null = null
  private selectionBox: HTMLDivElement | null = null
  private suppressNextClick = false
  private readonly raycaster = new THREE.Raycaster()
  private readonly pointer = new THREE.Vector2()
  private viewTarget = new THREE.Vector3(0, 0.8, 0)
  private drag: { x: number; y: number; mode: 'pan' | 'orbit'; pointerId: number } | null = null
  private frame = 0

  private readonly handleWheel = (event: WheelEvent) => {
    event.preventDefault()
    this.zoomBy(Math.exp(Math.sign(event.deltaY) * 0.12))
  }

  private readonly handlePointerDown = (event: PointerEvent) => {
    const additive = event.shiftKey || event.ctrlKey || event.metaKey
    if (event.button === 0 && (additive || !this.pickHit(event.clientX, event.clientY))) {
      event.preventDefault()
      this.boxDrag = { startX: event.clientX, startY: event.clientY, pointerId: event.pointerId, additive }
      this.showSelectionBox(event.clientX, event.clientY)
      this.renderer?.domElement.setPointerCapture(event.pointerId)
      return
    }
    if (event.button !== 1 && !(this.drag && (event.button === 0 || event.button === 2))) return
    event.preventDefault()
    const mode = this.drag && (event.button === 0 || event.button === 2) ? 'orbit' : 'pan'
    this.drag = { x: event.clientX, y: event.clientY, mode, pointerId: event.pointerId }
    this.renderer?.domElement.setPointerCapture(event.pointerId)
  }

  private readonly handlePointerMove = (event: PointerEvent) => {
    if (this.boxDrag) {
      this.showSelectionBox(event.clientX, event.clientY)
      return
    }
    if (!this.drag) {
      if (event.buttons === 0) this.updatePreselection(event)
      return
    }
    if (!this.camera || (event.buttons & 4) === 0) return
    const deltaX = event.clientX - this.drag.x
    const deltaY = event.clientY - this.drag.y
    this.drag.x = event.clientX
    this.drag.y = event.clientY
    this.drag.mode = (event.buttons & 3) !== 0 ? 'orbit' : 'pan'
    if (this.drag.mode === 'orbit') {
      const offset = this.camera.position.clone().sub(this.viewTarget)
      const spherical = new THREE.Spherical().setFromVector3(offset)
      spherical.theta -= deltaX * 0.006
      spherical.phi = THREE.MathUtils.clamp(spherical.phi - deltaY * 0.006, 0.04, Math.PI - 0.04)
      this.camera.position.copy(this.viewTarget).add(new THREE.Vector3().setFromSpherical(spherical))
      this.camera.up.set(0, 1, 0)
    } else {
      const distance = this.camera.position.distanceTo(this.viewTarget)
      const scale = distance * 0.0014
      const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 0)
      const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 1)
      const translation = right.multiplyScalar(-deltaX * scale).add(up.multiplyScalar(deltaY * scale))
      this.camera.position.add(translation)
      this.viewTarget.add(translation)
    }
    this.camera.lookAt(this.viewTarget)
  }

  private readonly handlePointerUp = (event: PointerEvent) => {
    if (this.boxDrag) {
      const box = this.boxDrag
      const width = Math.abs(event.clientX - box.startX)
      const height = Math.abs(event.clientY - box.startY)
      if (this.renderer?.domElement.hasPointerCapture(box.pointerId)) this.renderer.domElement.releasePointerCapture(box.pointerId)
      this.boxDrag = null
      this.clearSelectionBox()
      if (width >= 4 && height >= 4) {
        this.suppressNextClick = true
        const mode = event.clientX >= box.startX ? 'window' : 'crossing'
        const objectIds = this.pickObjectsInBox(box.startX, box.startY, event.clientX, event.clientY, mode)
        if (this.renderer) {
          this.renderer.domElement.dataset.boxSelectionMode = mode
          this.renderer.domElement.dataset.boxSelectionObjectIds = objectIds.join(',')
        }
        this.interactionHandlers.onBoxSelect?.({ objectIds, additive: box.additive, mode })
      }
      return
    }
    if (!this.drag) return
    if ((event.buttons & 4) !== 0) {
      this.drag.mode = 'pan'
      this.drag.x = event.clientX
      this.drag.y = event.clientY
      return
    }
    if (this.renderer?.domElement.hasPointerCapture(this.drag.pointerId)) this.renderer.domElement.releasePointerCapture(this.drag.pointerId)
    this.drag = null
  }

  private readonly handleContextMenu = (event: MouseEvent) => {
    if (this.drag) event.preventDefault()
  }

  private readonly handleClick = (event: MouseEvent) => {
    if (this.suppressNextClick) {
      this.suppressNextClick = false
      event.preventDefault()
      event.stopPropagation()
      return
    }
    const hit = this.pickHit(event.clientX, event.clientY)
    const additive = event.shiftKey || event.ctrlKey || event.metaKey
    if (hit && additive) {
      event.stopPropagation()
      this.interactionHandlers.onObjectClick?.(hit.objectId, { additive: true })
      return
    }
    const picked = hit ? this.pickSubshape(event.clientX, event.clientY, hit) : null
    if (picked) {
      event.stopPropagation()
      this.interactionHandlers.onSubshapeClick?.({ kind: picked.ref.kind, persistentId: picked.ref.persistentId, objectId: picked.objectId })
    } else if (hit) {
      event.stopPropagation()
      this.interactionHandlers.onObjectClick?.(hit.objectId, { additive: false })
    }
  }

  private readonly handlePointerLeave = () => this.updateHoveredSubshape(null)

  private showSelectionBox(clientX: number, clientY: number) {
    if (!this.boxDrag) return
    if (!this.selectionBox) {
      this.selectionBox = document.createElement('div')
      Object.assign(this.selectionBox.style, { position: 'fixed', pointerEvents: 'none', zIndex: '1000' })
      document.body.appendChild(this.selectionBox)
    }
    const windowMode = clientX >= this.boxDrag.startX
    this.selectionBox.style.border = `1px ${windowMode ? 'solid #74a7ff' : 'dashed #6ee7d8'}`
    this.selectionBox.style.background = windowMode ? 'rgba(116, 167, 255, 0.12)' : 'rgba(110, 231, 216, 0.12)'
    const left = Math.min(this.boxDrag.startX, clientX)
    const top = Math.min(this.boxDrag.startY, clientY)
    this.selectionBox.style.left = `${left}px`
    this.selectionBox.style.top = `${top}px`
    this.selectionBox.style.width = `${Math.abs(clientX - this.boxDrag.startX)}px`
    this.selectionBox.style.height = `${Math.abs(clientY - this.boxDrag.startY)}px`
  }

  private clearSelectionBox() {
    this.selectionBox?.remove()
    this.selectionBox = null
  }

  private pickObjectsInBox(startX: number, startY: number, endX: number, endY: number, mode: 'window' | 'crossing') {
    if (!this.renderer || !this.camera) return []
    const viewport = this.renderer.domElement.getBoundingClientRect()
    const selection = { left: Math.min(startX, endX), top: Math.min(startY, endY), right: Math.max(startX, endX), bottom: Math.max(startY, endY) }
    this.camera.updateMatrixWorld()
    const candidates = [...this.objectMeshes.entries()].flatMap(([objectId, entry]) => {
      entry.mesh.updateMatrixWorld()
      if (!entry.mesh.geometry.boundingBox) entry.mesh.geometry.computeBoundingBox()
      const bounds = entry.mesh.geometry.boundingBox
      if (!bounds || bounds.isEmpty()) return []
      const points = [
        [bounds.min.x, bounds.min.y, bounds.min.z], [bounds.min.x, bounds.min.y, bounds.max.z],
        [bounds.min.x, bounds.max.y, bounds.min.z], [bounds.min.x, bounds.max.y, bounds.max.z],
        [bounds.max.x, bounds.min.y, bounds.min.z], [bounds.max.x, bounds.min.y, bounds.max.z],
        [bounds.max.x, bounds.max.y, bounds.min.z], [bounds.max.x, bounds.max.y, bounds.max.z],
      ].map(([x, y, z]) => new THREE.Vector3(x, y, z).applyMatrix4(entry.mesh.matrixWorld).project(this.camera as THREE.Camera))
      if (!points.some((point) => Number.isFinite(point.x) && Number.isFinite(point.y) && point.z >= -1 && point.z <= 1)) return []
      const xs = points.map((point) => viewport.left + (point.x + 1) * 0.5 * viewport.width)
      const ys = points.map((point) => viewport.top + (1 - point.y) * 0.5 * viewport.height)
      return [{ objectId, bounds: { left: Math.min(...xs), top: Math.min(...ys), right: Math.max(...xs), bottom: Math.max(...ys) } }]
    })
    return resolveScreenBoxSelection(candidates, selection, mode)
  }

  private pickHit(clientX: number, clientY: number) {
    if (!this.renderer || !this.camera || this.objectMeshes.size === 0) return null
    const bounds = this.renderer.domElement.getBoundingClientRect()
    if (bounds.width <= 0 || bounds.height <= 0) return null
    this.pointer.set((clientX - bounds.left) / bounds.width * 2 - 1, -((clientY - bounds.top) / bounds.height) * 2 + 1)
    this.raycaster.setFromCamera(this.pointer, this.camera)
    const hit = this.raycaster.intersectObjects([...this.objectMeshes.values()].map((entry) => entry.mesh), false)[0]
    const faceIndex = hit?.faceIndex
    if (!hit || !(hit.object instanceof THREE.Mesh)) return null
    const entry = [...this.objectMeshes.entries()].find(([, candidate]) => candidate.mesh === hit.object)
    if (!entry) return null
    const projectedHit = hit.point.clone().project(this.camera)
    return { bounds, hitDepth: projectedHit.z, faceIndex: typeof faceIndex === 'number' && Number.isSafeInteger(faceIndex) ? faceIndex : -1, objectId: entry[0], asset: entry[1].asset, mesh: entry[1].mesh }
  }

  private projectPoint(point: [number, number, number], bounds: DOMRect, mesh: THREE.Mesh) {
    if (!this.camera) return null
    const projected = new THREE.Vector3(...point).applyMatrix4(mesh.matrixWorld).project(this.camera)
    if (!Number.isFinite(projected.x) || !Number.isFinite(projected.y) || !Number.isFinite(projected.z)) return null
    return {
      x: bounds.left + (projected.x + 1) * 0.5 * bounds.width,
      y: bounds.top + (1 - projected.y) * 0.5 * bounds.height,
      depth: projected.z,
    }
  }

  private pickSubshape(clientX: number, clientY: number, hit = this.pickHit(clientX, clientY)): { ref: SubshapeRef; objectId: string; asset: MeshAsset; mesh: THREE.Mesh } | null {
    if (!hit) return null
    const pointer = { x: clientX, y: clientY }
    const distanceToSegment = (point: { x: number; y: number }, start: { x: number; y: number }, end: { x: number; y: number }) => {
      const dx = end.x - start.x
      const dy = end.y - start.y
      const lengthSquared = dx * dx + dy * dy
      if (lengthSquared <= 1e-9) return Math.hypot(point.x - start.x, point.y - start.y)
      const factor = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared))
      return Math.hypot(point.x - (start.x + factor * dx), point.y - (start.y + factor * dy))
    }
    const vertices = (hit.asset.subshapeVertices ?? []).flatMap((candidate) => {
      const projected = this.projectPoint(candidate.position, hit.bounds, hit.mesh)
      return projected && projected.depth <= hit.hitDepth + 0.08 ? [{ ref: candidate.ref, distance: Math.hypot(pointer.x - projected.x, pointer.y - projected.y) }] : []
    }).sort((left, right) => left.distance - right.distance)
    if (vertices[0] && vertices[0].distance <= 10) return { ref: vertices[0].ref, objectId: hit.objectId, asset: hit.asset, mesh: hit.mesh }
    const edges = (hit.asset.subshapeEdges ?? []).flatMap((candidate) => {
      const start = this.projectPoint(candidate.start, hit.bounds, hit.mesh)
      const end = this.projectPoint(candidate.end, hit.bounds, hit.mesh)
      if (!start || !end || Math.min(start.depth, end.depth) > hit.hitDepth + 0.08) return []
      return [{ ref: candidate.ref, distance: distanceToSegment(pointer, start, end) }]
    }).sort((left, right) => left.distance - right.distance)
    if (edges[0] && edges[0].distance <= 8) return { ref: edges[0].ref, objectId: hit.objectId, asset: hit.asset, mesh: hit.mesh }
    const ref = resolveMeshSubshape(hit.asset, hit.faceIndex)
    return ref ? { ref, objectId: hit.objectId, asset: hit.asset, mesh: hit.mesh } : null
  }

  private updatePreselection(event: PointerEvent) {
    const picked = this.pickSubshape(event.clientX, event.clientY)
    this.updateHoveredSubshape(picked)
  }

  private updateHoveredSubshape(picked: { ref: SubshapeRef; objectId: string; asset: MeshAsset; mesh: THREE.Mesh } | null) {
    const nextId = picked ? `${picked.objectId}:${picked.ref.kind}:${picked.ref.persistentId}` : ''
    if (nextId === this.hoveredSubshapeId) return
    this.hoveredSubshapeId = nextId
    this.renderSubshapeHighlight(picked)
    this.interactionHandlers.onSubshapeHover?.(picked ? { kind: picked.ref.kind, persistentId: picked.ref.persistentId, objectId: picked.objectId } : null)
  }

  private clearSubshapeHighlight() {
    if (!this.subshapeHighlight) return
    this.subshapeHighlight.geometry.dispose()
    if (this.subshapeHighlight.material instanceof THREE.Material) this.subshapeHighlight.material.dispose()
    this.scene?.remove(this.subshapeHighlight)
    this.subshapeHighlight = null
  }

  private renderSubshapeHighlight(picked: { ref: SubshapeRef; objectId: string; asset: MeshAsset; mesh: THREE.Mesh } | null) {
    this.clearSubshapeHighlight()
    if (!picked || !this.scene) return
    const { ref, asset, mesh } = picked
    if (ref.kind === 'edge') {
      const edges = asset.subshapeEdges?.filter((entry) => entry.ref.persistentId === ref.persistentId) ?? []
      if (edges.length === 0) return
      const geometry = new THREE.BufferGeometry().setFromPoints(edges.flatMap((edge) => [new THREE.Vector3(...edge.start), new THREE.Vector3(...edge.end)]))
      const material = new THREE.LineBasicMaterial({ color: 0xffd05a, transparent: true, opacity: 0.95, depthTest: false })
      this.subshapeHighlight = new THREE.LineSegments(geometry, material)
    } else if (ref.kind === 'vertex') {
      const vertex = asset.subshapeVertices?.find((entry) => entry.ref.persistentId === ref.persistentId)
      if (!vertex) return
      const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(...vertex.position)])
      const material = new THREE.PointsMaterial({ color: 0xffd05a, size: 12, sizeAttenuation: false, depthTest: false })
      this.subshapeHighlight = new THREE.Points(geometry, material)
    } else {
      const range = asset.subshapeRanges?.find((entry) => entry.ref.kind === ref.kind && entry.ref.persistentId === ref.persistentId)
      if (!range) return
      const source = mesh.geometry
      const sourceIndex = source.getIndex()
      if (!sourceIndex) return
      const start = range.startTriangle * 3
      const count = range.triangleCount * 3
      const indices = Array.from({ length: count }, (_, offset) => sourceIndex.getX(start + offset))
      const geometry = new THREE.BufferGeometry()
      const positions = source.getAttribute('position')
      const normals = source.getAttribute('normal')
      geometry.setAttribute('position', positions.clone())
      if (normals) geometry.setAttribute('normal', normals.clone())
      geometry.setIndex(indices)
      const material = new THREE.MeshBasicMaterial({ color: 0xffd05a, transparent: true, opacity: 0.48, depthTest: false, side: THREE.DoubleSide })
      this.subshapeHighlight = new THREE.Mesh(geometry, material)
    }
    this.subshapeHighlight.position.copy(mesh.position)
    this.subshapeHighlight.rotation.copy(mesh.rotation)
    this.subshapeHighlight.scale.copy(mesh.scale)
    this.subshapeHighlight.renderOrder = 20
    this.scene.add(this.subshapeHighlight)
  }

  mount(host: HTMLElement) {
    this.host = host
    this.scene = new THREE.Scene()
    this.scene.background = new THREE.Color(0x0e1417)
    this.camera = new THREE.PerspectiveCamera(38, 1, 0.1, 1000)
    this.camera.position.set(3.8, 3.1, 5.2)
    this.camera.lookAt(this.viewTarget)
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' })
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
    this.renderer.setSize(host.clientWidth || 1, host.clientHeight || 1, false)
    this.renderer.domElement.dataset.geometrySource = 'three-fallback'
    this.renderer.domElement.style.touchAction = 'none'
    this.renderer.domElement.addEventListener('wheel', this.handleWheel, { passive: false })
    this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown)
    this.renderer.domElement.addEventListener('pointermove', this.handlePointerMove)
    this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp)
    this.renderer.domElement.addEventListener('pointercancel', this.handlePointerUp)
    this.renderer.domElement.addEventListener('pointerleave', this.handlePointerLeave)
    this.renderer.domElement.addEventListener('click', this.handleClick)
    this.renderer.domElement.addEventListener('contextmenu', this.handleContextMenu)
    host.appendChild(this.renderer.domElement)
    const grid = new THREE.GridHelper(8, 16, 0x31555a, 0x1e3439)
    grid.rotation.x = 0
    this.scene.add(grid)
    this.scene.add(new THREE.HemisphereLight(0xb8eeee, 0x142125, 2.1))
    const key = new THREE.DirectionalLight(0xffffff, 2.4)
    key.position.set(4, 6, 4)
    this.scene.add(key)
    const render = () => {
      if (!this.renderer || !this.scene || !this.camera) return
      this.updateSelectionFlash()
      this.renderer.render(this.scene, this.camera)
      this.frame = requestAnimationFrame(render)
    }
    this.resize(host.clientWidth || 1, host.clientHeight || 1)
    render()
  }

  setMesh(mesh: MeshAsset | null) {
    this.setMeshes(mesh ? [{ objectId: this.selectedObjectId || mesh.shapeId, mesh }] : [])
  }

  private clearObjectMeshes() {
    for (const { mesh } of this.objectMeshes.values()) {
      mesh.geometry.dispose()
      if (mesh.material instanceof THREE.Material) mesh.material.dispose()
      this.scene?.remove(mesh)
    }
    this.objectMeshes.clear()
  }

  setMeshes(entries: ViewportMeshAsset[]) {
    if (!this.scene) return
    this.clearSubshapeHighlight()
    this.updateHoveredSubshape(null)
    this.clearObjectMeshes()
    const uniqueEntries = [...new Map(entries.filter((entry) => entry.objectId).map((entry) => [entry.objectId, entry])).values()]
    const verticalOffset = uniqueEntries.length > 0 ? -Math.min(...uniqueEntries.map((entry) => entry.mesh.bounds.min[1])) : 0
    for (const { objectId, mesh: asset } of uniqueEntries) {
      const geometry = new THREE.BufferGeometry()
      geometry.setAttribute('position', new THREE.BufferAttribute(asset.positions, 3))
      geometry.setAttribute('normal', new THREE.BufferAttribute(asset.normals, 3))
      geometry.setIndex(new THREE.BufferAttribute(asset.indices, 1))
      if (!asset.normals.some((component) => component !== 0)) geometry.computeVertexNormals()
      geometry.computeBoundingBox()
      geometry.computeBoundingSphere()
      const material = new THREE.MeshStandardMaterial({ color: 0x579a9c, roughness: 0.62, metalness: 0.1 })
      const objectMesh = new THREE.Mesh(geometry, material)
      objectMesh.name = objectId
      objectMesh.position.set(0, verticalOffset, 0)
      this.scene.add(objectMesh)
      this.objectMeshes.set(objectId, { asset, mesh: objectMesh })
    }
    this.applySelectionMaterials()
    this.renderToolpath()
    if (uniqueEntries.length > 0) this.fitAll()
    if (uniqueEntries.length === 0) {
      if (this.renderer) {
        this.renderer.domElement.dataset.geometrySource = 'none'
        delete this.renderer.domElement.dataset.triangleCount
        this.renderer.domElement.dataset.objectCount = '0'
        this.renderer.domElement.dataset.topologySource = 'none'
        this.renderer.domElement.dataset.faceTopologySource = 'none'
        this.renderer.domElement.dataset.edgeTopologySource = 'none'
        this.renderer.domElement.dataset.vertexTopologySource = 'none'
        this.renderer.domElement.dataset.subshapeEdgeCount = '0'
        this.renderer.domElement.dataset.subshapeVertexCount = '0'
      }
      return
    }
    if (this.renderer) {
      this.renderer.domElement.dataset.geometrySource = 'bitbybit-occt'
      this.renderer.domElement.dataset.triangleCount = String(uniqueEntries.reduce((sum, entry) => sum + entry.mesh.indices.length / 3, 0))
      this.renderer.domElement.dataset.objectCount = String(uniqueEntries.length)
      const edgeRefs = new Set(uniqueEntries.flatMap((entry) => (entry.mesh.subshapeEdges ?? []).map((edge) => edge.ref.persistentId)))
      const vertexRefs = new Set(uniqueEntries.flatMap((entry) => (entry.mesh.subshapeVertices ?? []).map((vertex) => vertex.ref.persistentId)))
      const analyticFaces = uniqueEntries.every((entry) => (entry.mesh.subshapeRanges ?? []).every((range) => range.ref.signature?.includes('surface=')))
      const analyticEdges = uniqueEntries.every((entry) => (entry.mesh.subshapeEdges ?? []).every((edge) => edge.ref.signature?.includes('curve=')))
      const analyticVertices = uniqueEntries.every((entry) => (entry.mesh.subshapeVertices ?? []).every((vertex) => vertex.ref.signature?.startsWith('vertex|degree=')))
      this.renderer.domElement.dataset.topologySource = analyticFaces && analyticEdges && analyticVertices ? 'occt-analytic' : 'mesh-fallback'
      this.renderer.domElement.dataset.faceTopologySource = analyticFaces ? 'occt-analytic' : 'mesh-fallback'
      this.renderer.domElement.dataset.edgeTopologySource = analyticEdges ? 'occt-analytic' : 'mesh-fallback'
      this.renderer.domElement.dataset.vertexTopologySource = analyticVertices ? 'occt-analytic' : 'mesh-fallback'
      this.renderer.domElement.dataset.subshapeEdgeCount = String(edgeRefs.size)
      this.renderer.domElement.dataset.subshapeVertexCount = String(vertexRefs.size)
    }
  }

  private clearToolpath() {
    if (!this.toolpath) return
    for (const child of this.toolpath.children) {
      if (child instanceof THREE.Line) {
        child.geometry.dispose()
        if (child.material instanceof THREE.Material) child.material.dispose()
      }
    }
    this.toolpath.clear()
  }

  private renderToolpath() {
    if (!this.scene) return
    if (!this.toolpath) {
      this.toolpath = new THREE.Group()
      this.toolpath.name = 'CAM Toolpath'
      this.scene.add(this.toolpath)
    }
    this.clearToolpath()
    const points = this.toolpathSource.flat()
    const targetMesh = this.objectMeshes.get(this.selectedObjectId)?.mesh ?? this.objectMeshes.values().next().value?.mesh
    if (points.length === 0 || !targetMesh) {
      if (this.renderer) {
        this.renderer.domElement.dataset.toolpathPoints = '0'
        this.renderer.domElement.dataset.toolpathOperations = '0'
      }
      return
    }
    const sourceMin = [Math.min(...points.map((entry) => entry[0])), Math.min(...points.map((entry) => entry[1])), Math.min(...points.map((entry) => entry[2]))]
    const sourceMax = [Math.max(...points.map((entry) => entry[0])), Math.max(...points.map((entry) => entry[1])), Math.max(...points.map((entry) => entry[2]))]
    const target = new THREE.Box3().setFromObject(targetMesh)
    const targetSize = target.getSize(new THREE.Vector3())
    const map = (value: number, axis: number, min: number, size: number) => {
      const span = sourceMax[axis] - sourceMin[axis]
      return span <= 1e-9 ? min + size * 0.5 : min + (value - sourceMin[axis]) / span * size
    }
    const colors = [0xffd05a, 0x6ee7d8, 0xf79ac0, 0xa7d47b]
    this.toolpathSource.forEach((path, index) => {
      if (path.length < 2) return
      const positions = path.map(([x, y, z]) => new THREE.Vector3(map(x, 0, target.min.x, targetSize.x), map(z, 2, target.min.y, targetSize.y) + 0.015, map(y, 1, target.min.z, targetSize.z)))
      const geometry = new THREE.BufferGeometry().setFromPoints(positions)
      const material = new THREE.LineBasicMaterial({ color: colors[index % colors.length], depthTest: false, transparent: true, opacity: 0.96 })
      const line = new THREE.Line(geometry, material)
      line.renderOrder = 10
      this.toolpath?.add(line)
    })
    if (this.renderer) {
      this.renderer.domElement.dataset.toolpathPoints = String(points.length)
      this.renderer.domElement.dataset.toolpathOperations = String(this.toolpathSource.length)
    }
  }

  setToolpath(paths: Array<Array<[number, number, number]>>) {
    this.toolpathSource = paths.map((path) => path.map((entry) => [...entry] as [number, number, number]))
    this.renderToolpath()
  }

  setInteractionHandlers(handlers: ViewportInteractionHandlers) {
    this.interactionHandlers = { ...handlers }
  }

  resize(width: number, height: number, devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2)) {
    if (!this.renderer || !this.camera) return
    this.camera.aspect = Math.max(width, 1) / Math.max(height, 1)
    this.camera.updateProjectionMatrix()
    this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
    this.renderer.setSize(Math.max(width, 1), Math.max(height, 1), false)
  }

  setSelection(objectId: string) {
    this.setSelectedObjects(objectId ? [objectId] : [])
  }

  setSelectedObjects(objectIds: string[]) {
    const uniqueIds = [...new Set(objectIds)]
    this.selectedObjectId = uniqueIds[0] ?? ''
    this.selectedObjectIds = new Set(uniqueIds)
    this.applySelectionMaterials()
  }

  private applySelectionMaterials() {
    for (const [objectId, entry] of this.objectMeshes) {
      if (!(entry.mesh.material instanceof THREE.MeshStandardMaterial)) continue
      const selected = this.selectedObjectIds.has(objectId)
      entry.mesh.material.color.set(selected ? 0x5ed6d6 : 0x579a9c)
      entry.mesh.material.emissive.set(selected ? 0x123c3c : 0x000000)
      entry.mesh.material.emissiveIntensity = selected ? 1 : 0
    }
  }

  flashSelection() {
    if (!this.objectMeshes.has(this.selectedObjectId)) return
    this.selectionFlashObjectId = this.selectedObjectId
    this.selectionFlashUntil = performance.now() + 1400
  }

  private updateSelectionFlash() {
    if (this.selectionFlashUntil <= 0) return
    const selection = this.objectMeshes.get(this.selectionFlashObjectId)?.mesh
    if (!selection || !(selection.material instanceof THREE.MeshStandardMaterial)) {
      this.selectionFlashUntil = 0
      this.selectionFlashObjectId = ''
      return
    }
    const material = selection.material
    const remaining = this.selectionFlashUntil - performance.now()
    if (remaining <= 0) {
      this.selectionFlashUntil = 0
      this.selectionFlashObjectId = ''
      this.applySelectionMaterials()
      return
    }
    const phase = (1400 - remaining) / 1400 * Math.PI * 6
    material.emissive.set(0xffb347)
    material.emissiveIntensity = 0.35 + (0.65 * (0.5 + 0.5 * Math.sin(phase)))
  }

  setView(orientation: 'axonometric' | 'front' | 'rear' | 'left' | 'right' | 'top' | 'bottom') {
    if (!this.camera) return
    const directions: Record<typeof orientation, THREE.Vector3> = {
      axonometric: new THREE.Vector3(1, 1, 1),
      front: new THREE.Vector3(0, 0, 1),
      rear: new THREE.Vector3(0, 0, -1),
      left: new THREE.Vector3(-1, 0, 0),
      right: new THREE.Vector3(1, 0, 0),
      top: new THREE.Vector3(0, 1, 0),
      bottom: new THREE.Vector3(0, -1, 0),
    }
    const distance = Math.max(this.camera.position.distanceTo(this.viewTarget), 0.1)
    const direction = directions[orientation].normalize()
    this.camera.up.set(0, orientation === 'top' || orientation === 'bottom' ? 0 : 1, orientation === 'top' ? -1 : orientation === 'bottom' ? 1 : 0)
    this.camera.position.copy(this.viewTarget).addScaledVector(direction, distance)
    this.camera.lookAt(this.viewTarget)
    this.camera.updateProjectionMatrix()
  }

  fitAll() {
    if (!this.camera || this.objectMeshes.size === 0) return
    const bounds = new THREE.Box3()
    for (const { mesh } of this.objectMeshes.values()) bounds.expandByObject(mesh)
    if (bounds.isEmpty()) return
    const center = bounds.getCenter(new THREE.Vector3())
    const size = bounds.getSize(new THREE.Vector3())
    const radius = Math.max(size.x, size.y, size.z, 0.1) * 0.5
    const distance = radius / Math.tan(THREE.MathUtils.degToRad(this.camera.fov * 0.5)) * 1.35
    const direction = this.camera.position.clone().sub(this.viewTarget).normalize()
    if (direction.lengthSq() === 0) direction.set(1, 1, 1).normalize()
    this.viewTarget.copy(center)
    this.camera.position.copy(center).addScaledVector(direction, distance)
    this.camera.near = Math.max(distance / 1000, 0.01)
    this.camera.far = Math.max(distance * 100, 100)
    this.camera.lookAt(center)
    this.camera.updateProjectionMatrix()
  }

  zoomBy(factor: number) {
    if (!this.camera || !Number.isFinite(factor) || factor <= 0) return
    const offset = this.camera.position.clone().sub(this.viewTarget)
    const distance = THREE.MathUtils.clamp(offset.length() * factor, 0.1, 500)
    this.camera.position.copy(this.viewTarget).addScaledVector(offset.normalize(), distance)
    this.camera.lookAt(this.viewTarget)
    this.camera.updateProjectionMatrix()
  }

  dispose() {
    cancelAnimationFrame(this.frame)
    this.renderer?.domElement.removeEventListener('wheel', this.handleWheel)
    this.renderer?.domElement.removeEventListener('pointerdown', this.handlePointerDown)
    this.renderer?.domElement.removeEventListener('pointermove', this.handlePointerMove)
    this.renderer?.domElement.removeEventListener('pointerup', this.handlePointerUp)
    this.renderer?.domElement.removeEventListener('pointercancel', this.handlePointerUp)
    this.renderer?.domElement.removeEventListener('pointerleave', this.handlePointerLeave)
    this.renderer?.domElement.removeEventListener('click', this.handleClick)
    this.renderer?.domElement.removeEventListener('contextmenu', this.handleContextMenu)
    this.clearSubshapeHighlight()
    this.clearSelectionBox()
    this.clearObjectMeshes()
    this.clearToolpath()
    if (this.toolpath && this.scene) this.scene.remove(this.toolpath)
    this.renderer?.dispose()
    this.renderer?.domElement.remove()
    this.host = null
    this.renderer = null
    this.scene = null
    this.camera = null
    this.toolpath = null
    this.toolpathSource = []
    this.interactionHandlers = {}
    this.hoveredSubshapeId = ''
    this.selectedObjectId = ''
    this.selectedObjectIds.clear()
    this.selectionFlashObjectId = ''
    this.selectionFlashUntil = 0
    this.viewTarget.set(0, 0.8, 0)
    this.drag = null
    this.boxDrag = null
    this.suppressNextClick = false
  }

  getBackend() { return 'webgl2' as const }
}
