import {
  type AnyNodeId,
  emitter,
  sceneRegistry,
  useInteractive,
  useScene,
  type WindowNode,
} from '@pascal-app/core'
import { useFrame } from '@react-three/fiber'
import type { Object3D } from 'three'
import {
  AWNING_WINDOW_SASH_NAME,
  CASEMENT_WINDOW_SASH_NAME,
  DOUBLE_HUNG_BOTTOM_SASH_NAME,
  DOUBLE_HUNG_TOP_SASH_NAME,
  FRENCH_CASEMENT_LEFT_SASH_NAME,
  FRENCH_CASEMENT_RIGHT_SASH_NAME,
  HOPPER_WINDOW_SASH_NAME,
  LOUVERED_WINDOW_SLATS_NAME,
  SINGLE_HUNG_ACTIVE_SASH_NAME,
  SLIDING_WINDOW_ACTIVE_PANEL_NAME,
} from './window-system'

const easeWindowAnimation = (value: number) => value * value * (3 - 2 * value)

function markWindowDirty(windowId: AnyNodeId) {
  const scene = useScene.getState()
  const node = scene.nodes[windowId]
  scene.dirtyNodes.add(windowId)
}

/**
 * Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
 * 1 = open) by mutating the named child groups under `mesh`. Returns true when
 * the window type has a direct pose path and the named parts were found.
 *
 * This is the single source of truth for window kinematics: the live animation
 * system poses the registered scene mesh, and the GLB exporter poses an export
 * clone to sample the open/close keyframes for a baked animation clip.
 */
export function poseWindowMovingParts(
  node: WindowNode,
  mesh: Object3D | undefined,
  value: number,
): boolean {
  if (node.windowType === 'sliding') {
    const activePanel = mesh?.getObjectByName(SLIDING_WINDOW_ACTIVE_PANEL_NAME)
    if (!activePanel) return false

    const innerW = node.width - 2 * node.frameThickness
    const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerW * 0.12)
    const travel = Math.max(innerW / 2 - panelOverlap, 0) * value
    activePanel.position.x = -innerW / 4 - panelOverlap / 4 + travel
    return true
  }

  if (node.windowType === 'single-hung') {
    const activeSash = mesh?.getObjectByName(SINGLE_HUNG_ACTIVE_SASH_NAME)
    if (!activeSash) return false

    const innerH = node.height - 2 * node.frameThickness
    const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12)
    const travel = Math.max(innerH / 2 - panelOverlap, 0) * value
    activeSash.position.y = -innerH / 4 - panelOverlap / 4 + travel
    return true
  }

  if (node.windowType === 'double-hung') {
    const topSash = mesh?.getObjectByName(DOUBLE_HUNG_TOP_SASH_NAME)
    const bottomSash = mesh?.getObjectByName(DOUBLE_HUNG_BOTTOM_SASH_NAME)
    if (!(topSash && bottomSash)) return false

    const innerH = node.height - 2 * node.frameThickness
    const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12)
    const travel = Math.max(innerH / 2 - panelOverlap, 0) * value
    topSash.position.y = innerH / 4 + panelOverlap / 4 - travel
    bottomSash.position.y = -innerH / 4 - panelOverlap / 4 + travel
    return true
  }

  if (node.windowType === 'louvered') {
    const slats = mesh?.getObjectByName(LOUVERED_WINDOW_SLATS_NAME)
    if (!slats) return false

    const slatAngle = -value * (Math.PI / 3)
    for (const slat of slats.children) {
      slat.rotation.x = slatAngle
    }
    return true
  }

  if (node.windowType === 'casement') {
    if ((node.casementStyle ?? 'single') === 'french') {
      const leftSash = mesh?.getObjectByName(FRENCH_CASEMENT_LEFT_SASH_NAME)
      const rightSash = mesh?.getObjectByName(FRENCH_CASEMENT_RIGHT_SASH_NAME)
      if (!(leftSash && rightSash)) return false

      leftSash.rotation.y = -value * (Math.PI / 2)
      rightSash.rotation.y = value * (Math.PI / 2)
      return true
    }

    const sash = mesh?.getObjectByName(CASEMENT_WINDOW_SASH_NAME)
    if (!sash) return false

    const hingeSign = (node.hingesSide ?? 'left') === 'left' ? -1 : 1
    sash.rotation.y = hingeSign * value * (Math.PI / 2)
    return true
  }

  if (node.windowType === 'awning') {
    const sash = mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME)
    if (!sash) return false

    sash.rotation.x = -value * (Math.PI / 3)
    return true
  }

  if (node.windowType === 'hopper') {
    const sash =
      mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME) ??
      mesh?.getObjectByName(HOPPER_WINDOW_SASH_NAME)
    if (!sash) return false

    sash.rotation.x = -value * (Math.PI / 3)
    return true
  }

  return false
}

function applyDirectWindowAnimation(windowId: AnyNodeId, value: number) {
  const node = useScene.getState().nodes[windowId]
  if (node?.type !== 'window') return false
  return poseWindowMovingParts(node, sceneRegistry.nodes.get(windowId), value)
}

export const WindowAnimationSystem = () => {
  useFrame(({ clock }) => {
    const interactive = useInteractive.getState()
    const entries = Object.entries(interactive.windowAnimations)
    if (entries.length === 0) return

    const now = clock.getElapsedTime() * 1000

    for (const [windowId, animation] of entries) {
      const typedWindowId = windowId as AnyNodeId
      const scene = useScene.getState()
      const node = scene.nodes[typedWindowId]
      if (node?.type !== 'window') {
        interactive.cancelWindowAnimation(typedWindowId)
        interactive.removeWindowOpenState(typedWindowId)
        continue
      }

      const startedAt = animation.startedAt ?? now
      if (animation.startedAt === null) {
        interactive.startWindowAnimation(typedWindowId, { ...animation, startedAt })
      }

      const progress = Math.min(1, (now - startedAt) / animation.durationMs)
      const value = animation.from + (animation.to - animation.from) * easeWindowAnimation(progress)
      interactive.setWindowOpenState(typedWindowId, { [animation.field]: value })
      const appliedDirectly = applyDirectWindowAnimation(typedWindowId, value)
      if (!appliedDirectly) markWindowDirty(typedWindowId)

      if (progress < 1) continue

      interactive.cancelWindowAnimation(typedWindowId)
      if (animation.persist) {
        scene.updateNode(typedWindowId, { [animation.field]: animation.to })
        interactive.removeWindowOpenState(typedWindowId)
        markWindowDirty(typedWindowId)
      } else {
        interactive.setWindowOpenState(typedWindowId, { [animation.field]: animation.to })
      }
      emitter.emit('window:animation-completed', {
        windowId: typedWindowId as WindowNode['id'],
        field: animation.field,
      })
    }
  }, 2)

  return null
}
