import { CourseLayer } from '../layer/courseLayer'
import { TrackLayer } from '../layer/trackLayer'
import { GpsLayer } from '../layer/gpsLayer'
import { TileLayer } from '../layer/tileLayer'
import { TileStore, type TileStoreCallbacks } from '../tile/tileStore'
import { type MapRenderer, type MapRendererStats, type MapScene } from './mapRenderer'
import { WebGLTileRenderer } from './webglTileRenderer'
import { WebGLVectorRenderer } from './webglVectorRenderer'
import { CourseLabelRenderer } from './courseLabelRenderer'
import { type MockSimulatorDebugLogLevel } from '../debug/mockSimulatorDebugLogger'

const RENDER_FRAME_MS = 16
const ANIMATION_FRAME_MS = 33

export class WebGLMapRenderer implements MapRenderer {
  tileStore: TileStore
  osmTileStore: TileStore
  tileLayer: TileLayer
  osmTileLayer: TileLayer
  courseLayer: CourseLayer
  trackLayer: TrackLayer
  gpsLayer: GpsLayer
  tileRenderer: WebGLTileRenderer
  vectorRenderer: WebGLVectorRenderer
  labelRenderer: CourseLabelRenderer
  scene: MapScene | null
  renderTimer: number
  animationTimer: number
  destroyed: boolean
  animationPaused: boolean
  pulseFrame: number
  lastStats: MapRendererStats
  lastGpsLogoDebugInfo: { status: string; url: string; resolvedSrc: string }
  onStats?: (stats: MapRendererStats) => void
  onTileError?: (message: string) => void
  onGpsLogoDebug?: (info: { status: string; url: string; resolvedSrc: string }) => void
  onDebugLog?: (
    scope: string,
    level: MockSimulatorDebugLogLevel,
    message: string,
    payload?: Record<string, unknown>,
  ) => void

  constructor(
    onStats?: (stats: MapRendererStats) => void,
    onTileError?: (message: string) => void,
    onGpsLogoDebug?: (info: { status: string; url: string; resolvedSrc: string }) => void,
    onDebugLog?: (
      scope: string,
      level: MockSimulatorDebugLogLevel,
      message: string,
      payload?: Record<string, unknown>,
    ) => void,
  ) {
    this.onStats = onStats
    this.onTileError = onTileError
    this.onGpsLogoDebug = onGpsLogoDebug
    this.onDebugLog = onDebugLog
    this.tileStore = new TileStore({
      onTileReady: () => {
        this.scheduleRender()
      },
      onTileError: (message) => {
        if (this.onTileError) {
          this.onTileError(message)
        }
        this.scheduleRender()
      },
    } satisfies TileStoreCallbacks)
    this.osmTileStore = new TileStore({
      onTileReady: () => {
        this.scheduleRender()
      },
      onTileError: () => {
        this.scheduleRender()
      },
    } satisfies TileStoreCallbacks)
    this.tileLayer = new TileLayer()
    this.osmTileLayer = new TileLayer()
    this.courseLayer = new CourseLayer()
    this.trackLayer = new TrackLayer()
    this.gpsLayer = new GpsLayer()
    this.tileRenderer = new WebGLTileRenderer(this.tileLayer, this.tileStore, this.osmTileLayer, this.osmTileStore)
    this.vectorRenderer = new WebGLVectorRenderer(this.courseLayer, this.trackLayer, this.gpsLayer)
    this.labelRenderer = new CourseLabelRenderer(this.courseLayer, onDebugLog)
    this.scene = null
    this.renderTimer = 0
    this.animationTimer = 0
    this.destroyed = false
    this.animationPaused = false
    this.pulseFrame = 0
    this.lastStats = {
      visibleTileCount: 0,
      readyTileCount: 0,
      memoryTileCount: 0,
      diskTileCount: 0,
      memoryHitCount: 0,
      diskHitCount: 0,
      networkFetchCount: 0,
    }
    this.lastGpsLogoDebugInfo = {
      status: 'idle',
      url: '',
      resolvedSrc: '',
    }
  }

  attachCanvas(canvasNode: any, width: number, height: number, dpr: number, labelCanvasNode?: any): void {
    this.tileRenderer.attachCanvas(canvasNode, width, height, dpr)
    this.vectorRenderer.attachContext(this.tileRenderer.gl, canvasNode)
    if (labelCanvasNode) {
      this.labelRenderer.attachCanvas(labelCanvasNode, width, height, dpr)
    }
    this.startAnimation()
    this.scheduleRender()
  }

  updateScene(scene: MapScene): void {
    this.scene = scene
    this.scheduleRender()
  }

  setAnimationPaused(paused: boolean): void {
    this.animationPaused = paused
    if (!paused) {
      this.scheduleRender()
    }
  }

  destroy(): void {
    this.destroyed = true
    if (this.renderTimer) {
      clearTimeout(this.renderTimer)
      this.renderTimer = 0
    }
    if (this.animationTimer) {
      clearTimeout(this.animationTimer)
      this.animationTimer = 0
    }
    this.labelRenderer.destroy()
    this.vectorRenderer.destroy()
    this.tileRenderer.destroy()
    this.tileStore.destroy()
    this.osmTileStore.destroy()
    this.scene = null
  }

  startAnimation(): void {
    if (this.animationTimer) {
      return
    }

    const tick = () => {
      if (this.destroyed) {
        this.animationTimer = 0
        return
      }

      if (!this.animationPaused) {
        this.pulseFrame = (this.pulseFrame + 1) % 360
        this.scheduleRender()
      }

      this.animationTimer = setTimeout(tick, this.getAnimationFrameMs()) as unknown as number
    }

    tick()
  }

  getAnimationFrameMs(): number {
    return this.scene && this.scene.animationLevel === 'lite' ? 48 : ANIMATION_FRAME_MS
  }

  scheduleRender(): void {
    if (this.renderTimer || !this.scene || this.destroyed) {
      return
    }

    this.renderTimer = setTimeout(() => {
      this.renderTimer = 0
      this.renderFrame()
    }, RENDER_FRAME_MS) as unknown as number
  }

  renderFrame(): void {
    if (!this.scene) {
      return
    }

    this.tileRenderer.render(this.scene)
    this.vectorRenderer.render(this.scene, this.pulseFrame)
    this.labelRenderer.render(this.scene)
    this.emitGpsLogoDebug(this.labelRenderer.getGpsLogoDebugInfo())
    this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
  }

  getGpsLogoDebugInfo(): { status: string; url: string; resolvedSrc: string } {
    return this.labelRenderer.getGpsLogoDebugInfo()
  }

  emitStats(stats: MapRendererStats): void {
    if (
      stats.visibleTileCount === this.lastStats.visibleTileCount
      && stats.readyTileCount === this.lastStats.readyTileCount
      && stats.memoryTileCount === this.lastStats.memoryTileCount
      && stats.diskTileCount === this.lastStats.diskTileCount
      && stats.memoryHitCount === this.lastStats.memoryHitCount
      && stats.diskHitCount === this.lastStats.diskHitCount
      && stats.networkFetchCount === this.lastStats.networkFetchCount
    ) {
      return
    }

    this.lastStats = stats
    if (this.onStats) {
      this.onStats(stats)
    }
  }

  emitGpsLogoDebug(info: { status: string; url: string; resolvedSrc: string }): void {
    if (
      info.status === this.lastGpsLogoDebugInfo.status
      && info.url === this.lastGpsLogoDebugInfo.url
      && info.resolvedSrc === this.lastGpsLogoDebugInfo.resolvedSrc
    ) {
      return
    }

    this.lastGpsLogoDebugInfo = info
    if (this.onGpsLogoDebug) {
      this.onGpsLogoDebug(info)
    }
  }
}
