import { LGraphNode } from '@comfyorg/litegraph'
import * as THREE from 'three'

import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'

import { CameraManager } from './CameraManager'
import { ControlsManager } from './ControlsManager'
import { EventManager } from './EventManager'
import { LightingManager } from './LightingManager'
import { LoaderManager } from './LoaderManager'
import { ModelExporter } from './ModelExporter'
import { ModelManager } from './ModelManager'
import { NodeStorage } from './NodeStorage'
import { PreviewManager } from './PreviewManager'
import { RecordingManager } from './RecordingManager'
import { SceneManager } from './SceneManager'
import { ViewHelperManager } from './ViewHelperManager'
import {
  CameraState,
  CaptureResult,
  Load3DOptions,
  MaterialMode,
  UpDirection
} from './interfaces'

class Load3d {
  renderer: THREE.WebGLRenderer
  protected clock: THREE.Clock
  protected animationFrameId: number | null = null
  node: LGraphNode

  protected eventManager: EventManager
  protected nodeStorage: NodeStorage
  protected sceneManager: SceneManager
  protected cameraManager: CameraManager
  protected controlsManager: ControlsManager
  protected lightingManager: LightingManager
  protected viewHelperManager: ViewHelperManager
  protected previewManager: PreviewManager
  protected loaderManager: LoaderManager
  protected modelManager: ModelManager
  protected recordingManager: RecordingManager

  STATUS_MOUSE_ON_NODE: boolean
  STATUS_MOUSE_ON_SCENE: boolean
  INITIAL_RENDER_DONE: boolean = false

  constructor(
    container: Element | HTMLElement,
    options: Load3DOptions = {
      node: {} as LGraphNode,
      inputSpec: {} as CustomInputSpec
    }
  ) {
    this.node = options.node || ({} as LGraphNode)
    this.clock = new THREE.Clock()

    this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
    this.renderer.setSize(300, 300)
    this.renderer.setClearColor(0x282828)
    this.renderer.autoClear = false
    this.renderer.outputColorSpace = THREE.SRGBColorSpace
    container.appendChild(this.renderer.domElement)

    this.eventManager = new EventManager()
    this.nodeStorage = new NodeStorage(this.node)

    this.sceneManager = new SceneManager(
      this.renderer,
      this.getActiveCamera.bind(this),
      this.getControls.bind(this),
      this.eventManager
    )

    this.cameraManager = new CameraManager(
      this.renderer,
      this.eventManager,
      this.nodeStorage
    )

    this.controlsManager = new ControlsManager(
      this.renderer,
      this.cameraManager.activeCamera,
      this.eventManager,
      this.nodeStorage
    )

    this.cameraManager.setControls(this.controlsManager.controls)

    this.lightingManager = new LightingManager(
      this.sceneManager.scene,
      this.eventManager
    )

    this.viewHelperManager = new ViewHelperManager(
      this.renderer,
      this.getActiveCamera.bind(this),
      this.getControls.bind(this),
      this.nodeStorage
    )

    this.previewManager = new PreviewManager(
      this.sceneManager.scene,
      this.getActiveCamera.bind(this),
      this.getControls.bind(this),
      () => this.renderer,
      this.eventManager,
      this.sceneManager.backgroundScene,
      this.sceneManager.backgroundCamera
    )

    this.modelManager = new ModelManager(
      this.sceneManager.scene,
      this.renderer,
      this.eventManager,
      this.getActiveCamera.bind(this),
      this.setupCamera.bind(this),
      options
    )

    this.loaderManager = new LoaderManager(
      this.modelManager,
      this.eventManager,
      options
    )

    this.recordingManager = new RecordingManager(
      this.sceneManager.scene,
      this.renderer,
      this.eventManager
    )
    this.sceneManager.init()
    this.cameraManager.init()
    this.controlsManager.init()
    this.lightingManager.init()
    this.loaderManager.init()
    this.loaderManager.init()

    this.viewHelperManager.createViewHelper(container)
    this.viewHelperManager.init()

    if (options && !options.inputSpec?.isPreview) {
      this.previewManager.createCapturePreview(container)
      this.previewManager.init()
    }

    this.STATUS_MOUSE_ON_NODE = false
    this.STATUS_MOUSE_ON_SCENE = false

    this.handleResize()
    this.startAnimation()

    setTimeout(() => {
      this.forceRender()
    }, 100)
  }

  forceRender(): void {
    const delta = this.clock.getDelta()
    this.viewHelperManager.update(delta)
    this.controlsManager.update()

    this.renderMainScene()

    if (this.previewManager.showPreview) {
      this.previewManager.renderPreview()
    }

    this.resetViewport()

    if (this.viewHelperManager.viewHelper.render) {
      this.viewHelperManager.viewHelper.render(this.renderer)
    }

    this.INITIAL_RENDER_DONE = true
  }

  renderMainScene(): void {
    const width = this.renderer.domElement.clientWidth
    const height = this.renderer.domElement.clientHeight

    this.renderer.setViewport(0, 0, width, height)
    this.renderer.setScissor(0, 0, width, height)
    this.renderer.setScissorTest(true)

    this.sceneManager.renderBackground()
    this.renderer.render(
      this.sceneManager.scene,
      this.cameraManager.activeCamera
    )
  }

  resetViewport(): void {
    const width = this.renderer.domElement.clientWidth
    const height = this.renderer.domElement.clientHeight

    this.renderer.setViewport(0, 0, width, height)
    this.renderer.setScissor(0, 0, width, height)
    this.renderer.setScissorTest(false)
  }

  private getActiveCamera(): THREE.Camera {
    return this.cameraManager.activeCamera
  }

  private getControls() {
    return this.controlsManager.controls
  }

  private setupCamera(size: THREE.Vector3): void {
    this.cameraManager.setupForModel(size)
  }

  private startAnimation(): void {
    const animate = () => {
      this.animationFrameId = requestAnimationFrame(animate)

      if (!this.isActive()) {
        return
      }

      const delta = this.clock.getDelta()
      this.viewHelperManager.update(delta)
      this.controlsManager.update()

      this.renderMainScene()

      if (this.previewManager.showPreview) {
        this.previewManager.renderPreview()
      }

      this.resetViewport()

      if (this.viewHelperManager.viewHelper.render) {
        this.viewHelperManager.viewHelper.render(this.renderer)
      }
    }

    animate()
  }

  updateStatusMouseOnNode(onNode: boolean): void {
    this.STATUS_MOUSE_ON_NODE = onNode
  }

  updateStatusMouseOnScene(onScene: boolean): void {
    this.STATUS_MOUSE_ON_SCENE = onScene
  }

  isActive(): boolean {
    return (
      this.STATUS_MOUSE_ON_NODE ||
      this.STATUS_MOUSE_ON_SCENE ||
      this.isRecording() ||
      !this.INITIAL_RENDER_DONE
    )
  }

  async exportModel(format: string): Promise<void> {
    if (!this.modelManager.currentModel) {
      throw new Error('No model to export')
    }

    const exportMessage = `Exporting as ${format.toUpperCase()}...`
    this.eventManager.emitEvent('exportLoadingStart', exportMessage)

    try {
      const model = this.modelManager.currentModel.clone()

      const originalFileName = this.modelManager.originalFileName || 'model'
      const filename = `${originalFileName}.${format}`

      const originalURL = this.modelManager.originalURL

      await new Promise((resolve) => setTimeout(resolve, 10))

      switch (format) {
        case 'glb':
          await ModelExporter.exportGLB(model, filename, originalURL)
          break
        case 'obj':
          await ModelExporter.exportOBJ(model, filename, originalURL)
          break
        case 'stl':
          await ModelExporter.exportSTL(model, filename), originalURL
          break
        default:
          throw new Error(`Unsupported export format: ${format}`)
      }

      await new Promise((resolve) => setTimeout(resolve, 10))
    } catch (error) {
      console.error(`Error exporting model as ${format}:`, error)
      throw error
    } finally {
      this.eventManager.emitEvent('exportLoadingEnd', null)
    }
  }

  async applyTexture(texturePath: string): Promise<void> {
    if (!this.modelManager.currentModel) {
      throw new Error('No model to apply texture to')
    }

    this.eventManager.emitEvent('textureLoadingStart', null)

    try {
      await this.modelManager.applyTexture(texturePath)
    } catch (error) {
      console.error('Error applying texture:', error)
      throw error
    } finally {
      this.eventManager.emitEvent('textureLoadingEnd', null)
    }
  }

  setBackgroundColor(color: string): void {
    this.sceneManager.setBackgroundColor(color)

    this.previewManager.setPreviewBackgroundColor(color)

    this.forceRender()
  }

  async setBackgroundImage(uploadPath: string): Promise<void> {
    await this.sceneManager.setBackgroundImage(uploadPath)

    this.previewManager.updateBackgroundTexture(
      this.sceneManager.backgroundTexture
    )

    this.forceRender()
  }

  removeBackgroundImage(): void {
    this.sceneManager.removeBackgroundImage()

    this.previewManager.setPreviewBackgroundColor(
      this.sceneManager.currentBackgroundColor
    )

    this.forceRender()
  }

  toggleGrid(showGrid: boolean): void {
    this.sceneManager.toggleGrid(showGrid)
    this.forceRender()
  }

  toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
    this.cameraManager.toggleCamera(cameraType)

    this.controlsManager.updateCamera(this.cameraManager.activeCamera)
    this.viewHelperManager.recreateViewHelper()

    this.handleResize()
    this.forceRender()
  }

  getCurrentCameraType(): 'perspective' | 'orthographic' {
    return this.cameraManager.getCurrentCameraType()
  }

  setCameraState(state: CameraState): void {
    this.cameraManager.setCameraState(state)

    this.forceRender()
  }

  getCameraState(): CameraState {
    return this.cameraManager.getCameraState()
  }

  setFOV(fov: number): void {
    this.cameraManager.setFOV(fov)
    this.forceRender()
  }

  setEdgeThreshold(threshold: number): void {
    this.modelManager.setEdgeThreshold(threshold)
    this.forceRender()
  }

  setMaterialMode(mode: MaterialMode): void {
    this.modelManager.setMaterialMode(mode)
    this.forceRender()
  }

  async loadModel(url: string, originalFileName?: string): Promise<void> {
    this.cameraManager.reset()
    this.controlsManager.reset()
    this.modelManager.reset()

    await this.loaderManager.loadModel(url, originalFileName)

    this.handleResize()
    this.forceRender()
  }

  clearModel(): void {
    this.modelManager.clearModel()
    this.forceRender()
  }

  setUpDirection(direction: UpDirection): void {
    this.modelManager.setUpDirection(direction)
    this.forceRender()
  }

  setLightIntensity(intensity: number): void {
    this.lightingManager.setLightIntensity(intensity)
    this.forceRender()
  }

  togglePreview(showPreview: boolean): void {
    this.previewManager.togglePreview(showPreview)
    this.forceRender()
  }

  setTargetSize(width: number, height: number): void {
    this.previewManager.setTargetSize(width, height)
    this.forceRender()
  }

  addEventListener(event: string, callback: (data?: any) => void): void {
    this.eventManager.addEventListener(event, callback)
  }

  removeEventListener(event: string, callback: (data?: any) => void): void {
    this.eventManager.removeEventListener(event, callback)
  }

  refreshViewport(): void {
    this.handleResize()
    this.forceRender()
  }

  handleResize(): void {
    const parentElement = this.renderer?.domElement?.parentElement

    if (!parentElement) {
      console.warn('Parent element not found')
      return
    }

    const width = parentElement.clientWidth
    const height = parentElement.clientHeight

    this.cameraManager.handleResize(width, height)
    this.sceneManager.handleResize(width, height)

    this.renderer.setSize(width, height)

    this.previewManager.handleResize()
    this.forceRender()
  }

  captureScene(width: number, height: number): Promise<CaptureResult> {
    return this.sceneManager.captureScene(width, height)
  }

  loadNodeProperty(name: string, defaultValue: any) {
    return this.nodeStorage.loadNodeProperty(name, defaultValue)
  }

  public async startRecording(): Promise<void> {
    this.viewHelperManager.visibleViewHelper(false)

    return this.recordingManager.startRecording()
  }

  public stopRecording(): void {
    this.viewHelperManager.visibleViewHelper(true)

    this.recordingManager.stopRecording()

    this.eventManager.emitEvent('recordingStatusChange', false)
  }

  public isRecording(): boolean {
    return this.recordingManager.getIsRecording()
  }

  public getRecordingDuration(): number {
    return this.recordingManager.getRecordingDuration()
  }

  public getRecordingData(): string | null {
    return this.recordingManager.getRecordingData()
  }

  public exportRecording(filename?: string): void {
    this.recordingManager.exportRecording(filename)
  }

  public clearRecording(): void {
    this.recordingManager.clearRecording()
  }

  public remove(): void {
    this.renderer.forceContextLoss()
    const canvas = this.renderer.domElement
    const event = new Event('webglcontextlost', {
      bubbles: true,
      cancelable: true
    })
    canvas.dispatchEvent(event)

    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId)
    }

    this.sceneManager.dispose()
    this.cameraManager.dispose()
    this.controlsManager.dispose()
    this.lightingManager.dispose()
    this.viewHelperManager.dispose()
    this.previewManager.dispose()
    this.loaderManager.dispose()
    this.modelManager.dispose()
    this.recordingManager.dispose()

    this.renderer.dispose()
    this.renderer.domElement.remove()
  }
}

export default Load3d
