import { calibratedLonLatToWorldTile } from '../../utils/projection'
import { worldToScreen, type CameraState } from '../camera/camera'
import { type MapScene } from './mapRenderer'
import { CourseLayer } from '../layer/courseLayer'
import { resolveControlStyle } from './courseStyleResolver'
import { type MockSimulatorDebugLogLevel } from '../debug/mockSimulatorDebugLogger'

const EARTH_CIRCUMFERENCE_METERS = 40075016.686
const LABEL_FONT_SIZE_RATIO = 1.08
const LABEL_OFFSET_X_RATIO = 1.18
const LABEL_OFFSET_Y_RATIO = -0.68
const SCORE_LABEL_FONT_SIZE_RATIO = 0.7
const SCORE_LABEL_OFFSET_Y_RATIO = 0.06
const ACTIVE_LABEL_COLOR = 'rgba(255, 219, 54, 0.98)'
const READY_LABEL_COLOR = 'rgba(98, 255, 214, 0.98)'
const MULTI_ACTIVE_LABEL_COLOR = 'rgba(255, 202, 72, 0.96)'
const FOCUSED_LABEL_COLOR = 'rgba(255, 252, 255, 0.98)'

function rgbaToCss(color: [number, number, number, number], alphaOverride?: number): string {
  const alpha = alphaOverride !== undefined ? alphaOverride : color[3]
  return `rgba(${Math.round(color[0] * 255)}, ${Math.round(color[1] * 255)}, ${Math.round(color[2] * 255)}, ${alpha.toFixed(3)})`
}

function normalizeHexColor(rawValue: string | undefined): string | null {
  if (typeof rawValue !== 'string') {
    return null
  }
  const trimmed = rawValue.trim()
  return /^#[0-9a-fA-F]{6}$/.test(trimmed) || /^#[0-9a-fA-F]{8}$/.test(trimmed) ? trimmed : null
}

export class CourseLabelRenderer {
  courseLayer: CourseLayer
  canvas: any
  ctx: any
  dpr: number
  width: number
  height: number
  gpsLogoUrl: string
  gpsLogoResolvedSrc: string
  gpsLogoImage: any
  gpsLogoStatus: 'idle' | 'loading' | 'ready' | 'error'
  onDebugLog?: (
    scope: string,
    level: MockSimulatorDebugLogLevel,
    message: string,
    payload?: Record<string, unknown>,
  ) => void

  constructor(
    courseLayer: CourseLayer,
    onDebugLog?: (
      scope: string,
      level: MockSimulatorDebugLogLevel,
      message: string,
      payload?: Record<string, unknown>,
    ) => void,
  ) {
    this.courseLayer = courseLayer
    this.onDebugLog = onDebugLog
    this.canvas = null
    this.ctx = null
    this.dpr = 1
    this.width = 0
    this.height = 0
    this.gpsLogoUrl = ''
    this.gpsLogoResolvedSrc = ''
    this.gpsLogoImage = null
    this.gpsLogoStatus = 'idle'
  }

  emitDebugLog(
    level: MockSimulatorDebugLogLevel,
    message: string,
    payload?: Record<string, unknown>,
  ): void {
    if (this.onDebugLog) {
      this.onDebugLog('gps-logo', level, message, payload)
    }
  }

  attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
    this.canvas = canvasNode
    this.ctx = canvasNode.getContext('2d')
    this.dpr = dpr || 1
    this.width = width
    this.height = height
    canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
    canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  }

  destroy(): void {
    this.ctx = null
    this.canvas = null
    this.width = 0
    this.height = 0
    this.gpsLogoUrl = ''
    this.gpsLogoResolvedSrc = ''
    this.gpsLogoImage = null
    this.gpsLogoStatus = 'idle'
  }

  getGpsLogoDebugInfo(): { status: string; url: string; resolvedSrc: string } {
    return {
      status: this.gpsLogoStatus,
      url: this.gpsLogoUrl,
      resolvedSrc: this.gpsLogoResolvedSrc,
    }
  }

  render(scene: MapScene): void {
    if (!this.ctx || !this.canvas) {
      return
    }

    const course = this.courseLayer.projectCourse(scene)
    const ctx = this.ctx
    this.clearCanvas(ctx)

    this.ensureGpsLogo(scene)
    this.applyPreviewTransform(ctx, scene)
    ctx.save()
    if (course && course.controls.length && scene.revealFullCourse) {
      const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
      const scoreOffsetY = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_OFFSET_Y_RATIO)
      const offsetX = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_X_RATIO)
      const offsetY = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_Y_RATIO)

      if (scene.gameMode === 'score-o' || scene.controlVisualMode === 'multi-target') {
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'

        for (const control of course.controls) {
          const resolvedStyle = resolveControlStyle(scene, 'control', control.sequence)
          const labelScale = Math.max(0.72, resolvedStyle.entry.labelScale || 1)
          const scoreFontSizePx = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_FONT_SIZE_RATIO * labelScale)
          ctx.save()
          ctx.font = `900 ${scoreFontSizePx}px sans-serif`
          ctx.fillStyle = this.getScoreLabelColor(scene, control.sequence)
          ctx.translate(control.point.x, control.point.y)
          ctx.rotate(scene.rotationRad)
          ctx.fillText(this.getControlLabelText(scene, control.sequence), 0, scoreOffsetY)
          ctx.restore()
        }
      } else {
        ctx.textAlign = 'left'
        ctx.textBaseline = 'middle'

        for (const control of course.controls) {
          const resolvedStyle = resolveControlStyle(scene, 'control', control.sequence)
          const labelScale = Math.max(0.72, resolvedStyle.entry.labelScale || 1)
          const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_RATIO * labelScale)
          ctx.save()
          ctx.font = `700 ${fontSizePx}px sans-serif`
          ctx.fillStyle = this.getLabelColor(scene, control.sequence)
          ctx.translate(control.point.x, control.point.y)
          ctx.rotate(scene.rotationRad)
          ctx.fillText(String(control.sequence), offsetX, offsetY)
          ctx.restore()
        }
      }
    }

    this.renderGpsLogo(scene)
    ctx.restore()
  }

  buildVectorCamera(scene: MapScene): CameraState {
    return {
      centerWorldX: scene.exactCenterWorldX,
      centerWorldY: scene.exactCenterWorldY,
      viewportWidth: scene.viewportWidth,
      viewportHeight: scene.viewportHeight,
      visibleColumns: scene.visibleColumns,
      rotationRad: scene.rotationRad,
    }
  }

  ensureGpsLogo(scene: MapScene): void {
    const nextUrl = typeof scene.gpsMarkerStyleConfig.logoUrl === 'string'
      ? scene.gpsMarkerStyleConfig.logoUrl.trim()
      : ''
    if (!nextUrl) {
      if (this.gpsLogoUrl || this.gpsLogoStatus !== 'idle') {
        this.emitDebugLog('info', 'logo not configured')
      }
      this.gpsLogoUrl = ''
      this.gpsLogoResolvedSrc = ''
      this.gpsLogoImage = null
      this.gpsLogoStatus = 'idle'
      return
    }
    if (this.gpsLogoUrl === nextUrl && (this.gpsLogoStatus === 'loading' || this.gpsLogoStatus === 'ready')) {
      return
    }
    if (!this.canvas || typeof this.canvas.createImage !== 'function') {
      this.emitDebugLog('warn', 'canvas createImage unavailable')
      return
    }
    const image = this.canvas.createImage()
    this.gpsLogoUrl = nextUrl
    this.gpsLogoResolvedSrc = ''
    this.gpsLogoImage = image
    this.gpsLogoStatus = 'loading'
    this.emitDebugLog('info', 'start loading logo', {
      src: nextUrl,
      style: scene.gpsMarkerStyleConfig.style,
      logoMode: scene.gpsMarkerStyleConfig.logoMode,
    })
    const attachImageHandlers = () => {
      image.onload = () => {
        if (this.gpsLogoUrl !== nextUrl) {
          return
        }
        this.gpsLogoStatus = 'ready'
        this.emitDebugLog('info', 'logo image ready', {
          src: nextUrl,
          resolvedSrc: this.gpsLogoResolvedSrc,
        })
      }
      image.onerror = () => {
        if (this.gpsLogoUrl !== nextUrl) {
          return
        }
        this.gpsLogoStatus = 'error'
        this.gpsLogoImage = null
        this.emitDebugLog('error', 'logo image error', {
          src: nextUrl,
          resolvedSrc: this.gpsLogoResolvedSrc,
        })
      }
    }

    const assignImageSource = (src: string) => {
      if (this.gpsLogoUrl !== nextUrl) {
        return
      }
      this.gpsLogoResolvedSrc = src
      this.emitDebugLog('info', 'assign image source', {
        src: nextUrl,
        resolvedSrc: src,
      })
      attachImageHandlers()
      image.src = src
    }

    if (/^https?:\/\//i.test(nextUrl) && typeof wx !== 'undefined' && typeof wx.getImageInfo === 'function') {
      wx.getImageInfo({
        src: nextUrl,
        success: (result) => {
          if (this.gpsLogoUrl !== nextUrl || !result.path) {
            return
          }
          this.emitDebugLog('info', 'wx.getImageInfo success', {
            src: nextUrl,
            path: result.path,
          })
          assignImageSource(result.path)
        },
        fail: (error) => {
          if (this.gpsLogoUrl !== nextUrl) {
            return
          }
          this.emitDebugLog('warn', 'wx.getImageInfo failed, fallback to remote url', {
            src: nextUrl,
            error: error && typeof error === 'object' && 'errMsg' in error ? (error as { errMsg?: string }).errMsg || '' : '',
          })
          assignImageSource(nextUrl)
        },
      })
      return
    }

    assignImageSource(nextUrl)
  }

  getGpsLogoBadgeRadius(scene: MapScene): number {
      const base = scene.gpsMarkerStyleConfig.size === 'small'
        ? 4.1
        : scene.gpsMarkerStyleConfig.size === 'large'
          ? 6
          : 5
      const effectScale = Math.max(0.88, Math.min(1.28, scene.gpsMarkerStyleConfig.effectScale || 1))
      const logoScale = Math.max(0.86, Math.min(1.16, scene.gpsMarkerStyleConfig.logoScale || 1))
      return base * effectScale * logoScale
    }

  renderGpsLogo(scene: MapScene): void {
    if (
      !scene.gpsPoint
      || !scene.gpsMarkerStyleConfig.visible
      || scene.gpsMarkerStyleConfig.style !== 'badge'
      || scene.gpsMarkerStyleConfig.logoMode !== 'center-badge'
      || !scene.gpsMarkerStyleConfig.logoUrl
      || this.gpsLogoStatus !== 'ready'
      || !this.gpsLogoImage
    ) {
      return
    }
    const screenPoint = worldToScreen(
      this.buildVectorCamera(scene),
      calibratedLonLatToWorldTile(scene.gpsPoint, scene.zoom, scene.gpsCalibration, scene.gpsCalibrationOrigin),
      false,
    )
    const radius = this.getGpsLogoBadgeRadius(scene)
    const diameter = radius * 2
    const ctx = this.ctx
    ctx.save()
    ctx.beginPath()
    ctx.fillStyle = 'rgba(255, 255, 255, 0.96)'
    ctx.arc(screenPoint.x, screenPoint.y, radius + 1.15, 0, Math.PI * 2)
    ctx.fill()
    ctx.beginPath()
    ctx.strokeStyle = 'rgba(12, 36, 42, 0.18)'
    ctx.lineWidth = Math.max(1.1, radius * 0.18)
    ctx.arc(screenPoint.x, screenPoint.y, radius + 0.3, 0, Math.PI * 2)
    ctx.stroke()
    ctx.beginPath()
    ctx.arc(screenPoint.x, screenPoint.y, radius, 0, Math.PI * 2)
    ctx.clip()
    ctx.drawImage(this.gpsLogoImage, screenPoint.x - radius, screenPoint.y - radius, diameter, diameter)
    ctx.restore()
  }

  getLabelColor(scene: MapScene, sequence: number): string {
    const resolvedStyle = resolveControlStyle(scene, 'control', sequence)
    const customLabelColor = normalizeHexColor(resolvedStyle.entry.labelColorHex)
    if (customLabelColor) {
      return customLabelColor
    }
    if (scene.focusedControlSequences.includes(sequence)) {
      return FOCUSED_LABEL_COLOR
    }

    if (scene.readyControlSequences.includes(sequence)) {
      return READY_LABEL_COLOR
    }

    if (scene.activeControlSequences.includes(sequence)) {
      return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
    }

    if (scene.completedControlSequences.includes(sequence)) {
      return resolvedStyle.entry.style === 'badge'
        ? 'rgba(255, 255, 255, 0.96)'
        : rgbaToCss(resolvedStyle.color, 0.94)
    }

    if (scene.skippedControlSequences.includes(sequence)) {
      return resolvedStyle.entry.style === 'badge'
        ? 'rgba(255, 255, 255, 0.9)'
        : rgbaToCss(resolvedStyle.color, 0.88)
    }

    return resolvedStyle.entry.style === 'badge'
      ? 'rgba(255, 255, 255, 0.98)'
      : rgbaToCss(resolvedStyle.color, 0.98)
  }

  getScoreLabelColor(scene: MapScene, sequence: number): string {
    const resolvedStyle = resolveControlStyle(scene, 'control', sequence)
    const customLabelColor = normalizeHexColor(resolvedStyle.entry.labelColorHex)
    if (customLabelColor) {
      return customLabelColor
    }
    if (scene.focusedControlSequences.includes(sequence)) {
      return FOCUSED_LABEL_COLOR
    }

    if (scene.readyControlSequences.includes(sequence)) {
      return READY_LABEL_COLOR
    }

    if (scene.completedControlSequences.includes(sequence)) {
      return resolvedStyle.entry.style === 'badge'
        ? 'rgba(255, 255, 255, 0.96)'
        : rgbaToCss(resolvedStyle.color, 0.94)
    }

    if (scene.skippedControlSequences.includes(sequence)) {
      return resolvedStyle.entry.style === 'badge'
        ? 'rgba(255, 255, 255, 0.92)'
        : rgbaToCss(resolvedStyle.color, 0.9)
    }

    return resolvedStyle.entry.style === 'badge'
      ? 'rgba(255, 255, 255, 0.98)'
      : rgbaToCss(resolvedStyle.color, 0.98)
  }

  getControlLabelText(scene: MapScene, sequence: number): string {
    if (scene.gameMode === 'score-o') {
      const score = scene.controlScoresBySequence[sequence]
      if (typeof score === 'number' && Number.isFinite(score)) {
        return String(score)
      }
    }
    return String(sequence)
  }

  clearCanvas(ctx: any): void {
    ctx.setTransform(1, 0, 0, 1, 0, 0)
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }

  applyPreviewTransform(ctx: any, scene: MapScene): void {
    const previewScale = scene.previewScale || 1
    const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
    const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
    const translateX = (previewOriginX - previewOriginX * previewScale) * this.dpr
    const translateY = (previewOriginY - previewOriginY * previewScale) * this.dpr

    ctx.setTransform(
      this.dpr * previewScale,
      0,
      0,
      this.dpr * previewScale,
      translateX,
      translateY,
    )
  }

  getMetric(scene: MapScene, meters: number): number {
    return meters * this.getPixelsPerMeter(scene)
  }

  getPixelsPerMeter(scene: MapScene): number {
    const tileSizePx = scene.viewportWidth / scene.visibleColumns
    const centerLat = this.worldTileYToLat(scene.exactCenterWorldY, scene.zoom)
    const metersPerTile = Math.cos(centerLat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
    if (!tileSizePx || !metersPerTile) {
      return 0
    }
    return tileSizePx / metersPerTile
  }

  worldTileYToLat(worldY: number, zoom: number): number {
    const scale = Math.pow(2, zoom)
    const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * worldY / scale)))
    return latRad * 180 / Math.PI
  }
}

