import { calibratedLonLatToWorldTile } from '../../utils/projection'
import { worldToScreen, type CameraState } from '../camera/camera'
import { type MapLayer, type LayerRenderContext } from './mapLayer'
import { type MapScene } from '../renderer/mapRenderer'
import { type ScreenPoint } from './trackLayer'

function getGpsMarkerMetrics(size: MapScene['gpsMarkerStyleConfig']['size']) {
  if (size === 'small') {
    return {
      coreRadius: 7,
      ringRadius: 8.35,
      pulseRadius: 14,
      indicatorOffset: 1.1,
      indicatorSize: 7,
      ringWidth: 2.5,
    }
  }
  if (size === 'large') {
    return {
      coreRadius: 11,
      ringRadius: 12.95,
      pulseRadius: 22,
      indicatorOffset: 1.45,
      indicatorSize: 10,
      ringWidth: 3.5,
    }
  }
  return {
    coreRadius: 9,
    ringRadius: 10.65,
    pulseRadius: 18,
    indicatorOffset: 1.25,
    indicatorSize: 8.5,
    ringWidth: 3,
  }
}

function scaleGpsMarkerMetrics(
  metrics: ReturnType<typeof getGpsMarkerMetrics>,
  effectScale: number,
): ReturnType<typeof getGpsMarkerMetrics> {
  const safeScale = Math.max(0.88, Math.min(1.28, effectScale || 1))
  return {
    coreRadius: metrics.coreRadius * safeScale,
    ringRadius: metrics.ringRadius * safeScale,
    pulseRadius: metrics.pulseRadius * safeScale,
    indicatorOffset: metrics.indicatorOffset * safeScale,
    indicatorSize: metrics.indicatorSize * safeScale,
    ringWidth: Math.max(2, metrics.ringWidth * (0.96 + (safeScale - 1) * 0.35)),
  }
}

function rotatePoint(x: number, y: number, angleRad: number): { x: number; y: number } {
  const cos = Math.cos(angleRad)
  const sin = Math.sin(angleRad)
  return {
    x: x * cos - y * sin,
    y: x * sin + y * cos,
  }
}

function hexToRgbTuple(hex: string): [number, number, number] {
  const safeHex = /^#[0-9a-fA-F]{6}$/.test(hex) ? hex : '#ffffff'
  return [
    parseInt(safeHex.slice(1, 3), 16),
    parseInt(safeHex.slice(3, 5), 16),
    parseInt(safeHex.slice(5, 7), 16),
  ]
}

function getGpsPulsePhase(
  pulseFrame: number,
  motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
): number {
  const divisor = motionState === 'idle'
    ? 11.5
    : motionState === 'moving'
      ? 6.2
      : motionState === 'fast-moving'
        ? 4.3
        : 4.8
  return 0.5 + 0.5 * Math.sin(pulseFrame / divisor)
}

function getAnimatedPulseRadius(
  pulseFrame: number,
  metrics: ReturnType<typeof getGpsMarkerMetrics>,
  motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
  pulseStrength: number,
  motionIntensity: number,
): number {
  const phase = getGpsPulsePhase(pulseFrame, motionState)
  const baseRadius = motionState === 'idle'
    ? metrics.pulseRadius * 0.82
    : motionState === 'moving'
      ? metrics.pulseRadius * 0.94
      : motionState === 'fast-moving'
        ? metrics.pulseRadius * 1.04
        : metrics.pulseRadius
  const amplitude = motionState === 'idle'
    ? metrics.pulseRadius * 0.12
    : motionState === 'moving'
      ? metrics.pulseRadius * 0.18
      : motionState === 'fast-moving'
        ? metrics.pulseRadius * 0.24
        : metrics.pulseRadius * 0.2
  return baseRadius + amplitude * phase * (0.8 + pulseStrength * 0.18 + motionIntensity * 0.1)
}

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

export class GpsLayer implements MapLayer {
  projectPoint(scene: MapScene): ScreenPoint | null {
    if (!scene.gpsPoint) {
      return null
    }

    const camera = buildVectorCamera(scene)
    const worldPoint = calibratedLonLatToWorldTile(scene.gpsPoint, scene.zoom, scene.gpsCalibration, scene.gpsCalibrationOrigin)
    return worldToScreen(camera, worldPoint, false)
  }

  getPulseRadius(pulseFrame: number): number {
    return 18 + 6 * (0.5 + 0.5 * Math.sin(pulseFrame / 6))
  }

  draw(context: LayerRenderContext): void {
    const { ctx, scene, pulseFrame } = context
    if (!scene.gpsMarkerStyleConfig.visible) {
      return
    }
    const gpsScreenPoint = this.projectPoint(scene)
    if (!gpsScreenPoint) {
      return
    }

    const metrics = scaleGpsMarkerMetrics(
      getGpsMarkerMetrics(scene.gpsMarkerStyleConfig.size),
      scene.gpsMarkerStyleConfig.effectScale || 1,
    )
    const style = scene.gpsMarkerStyleConfig.style
    const hasBadgeLogo = style === 'badge' && !!scene.gpsMarkerStyleConfig.logoUrl
    const pulseStrength = Math.max(0.45, Math.min(1.85, scene.gpsMarkerStyleConfig.pulseStrength || 1))
    const motionState = scene.gpsMarkerStyleConfig.motionState || 'idle'
    const motionIntensity = Math.max(0, Math.min(1.2, scene.gpsMarkerStyleConfig.motionIntensity || 0))
    const wakeStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.wakeStrength || 0))
    const warningGlowStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.warningGlowStrength || 0))
    const indicatorScale = Math.max(0.86, Math.min(1.28, scene.gpsMarkerStyleConfig.indicatorScale || 1))
    const pulse = style === 'dot'
      ? metrics.pulseRadius * 0.82
      : getAnimatedPulseRadius(pulseFrame, metrics, motionState, pulseStrength, motionIntensity)
    const [markerR, markerG, markerB] = hexToRgbTuple(scene.gpsMarkerStyleConfig.colorHex)

    ctx.save()
    if (wakeStrength > 0.05 && scene.gpsHeadingDeg !== null) {
      const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
      const wakeHeadingRad = headingScreenRad + Math.PI
      const wakeCount = motionState === 'fast-moving' ? 3 : 2
      for (let index = 0; index < wakeCount; index += 1) {
        const offset = metrics.coreRadius * (0.85 + index * 0.64) * (0.9 + wakeStrength * 0.72)
        const center = rotatePoint(0, -offset, wakeHeadingRad)
        const radius = metrics.coreRadius * Math.max(0.22, 0.58 - index * 0.12 + wakeStrength * 0.08)
        const alpha = Math.max(0.06, (0.14 + wakeStrength * 0.12) * (1 - index * 0.26))
        ctx.beginPath()
        ctx.fillStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${alpha})`
        ctx.arc(gpsScreenPoint.x + center.x, gpsScreenPoint.y + center.y, radius, 0, Math.PI * 2)
        ctx.fill()
      }
    }
    if (warningGlowStrength > 0.04) {
      const glowPhase = getGpsPulsePhase(pulseFrame, motionState)
      ctx.beginPath()
      ctx.strokeStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${0.18 + warningGlowStrength * 0.18})`
      ctx.lineWidth = Math.max(2, metrics.ringWidth * (1.04 + warningGlowStrength * 0.2))
      ctx.arc(
        gpsScreenPoint.x,
        gpsScreenPoint.y,
        metrics.ringRadius * (1.18 + warningGlowStrength * 0.12 + glowPhase * 0.04),
        0,
        Math.PI * 2,
      )
      ctx.stroke()
    }
    if (style === 'beacon' || (style === 'badge' && !hasBadgeLogo)) {
      ctx.beginPath()
      const pulseAlpha = style === 'badge'
        ? Math.min(0.2, 0.08 + pulseStrength * 0.06)
        : Math.min(0.26, 0.1 + pulseStrength * 0.08)
      ctx.fillStyle = `rgba(255, 255, 255, ${pulseAlpha})`
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, pulse, 0, Math.PI * 2)
      ctx.fill()
    }

    if (style === 'dot') {
      ctx.beginPath()
      ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.82, 0, Math.PI * 2)
      ctx.fill()
      ctx.beginPath()
      ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
      ctx.lineWidth = metrics.ringWidth
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius + metrics.ringWidth * 0.3, 0, Math.PI * 2)
      ctx.stroke()
    } else if (style === 'disc') {
      ctx.beginPath()
      ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
      ctx.lineWidth = metrics.ringWidth * 1.18
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.05, 0, Math.PI * 2)
      ctx.stroke()
      ctx.beginPath()
      ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 1.02, 0, Math.PI * 2)
      ctx.fill()
      ctx.beginPath()
      ctx.fillStyle = 'rgba(255, 255, 255, 0.96)'
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.22, 0, Math.PI * 2)
      ctx.fill()
    } else if (style === 'badge') {
      ctx.beginPath()
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.98)'
      ctx.lineWidth = metrics.ringWidth * 1.12
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.06, 0, Math.PI * 2)
      ctx.stroke()
      ctx.beginPath()
      ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.98, 0, Math.PI * 2)
      ctx.fill()
      if (!hasBadgeLogo) {
        ctx.beginPath()
        ctx.fillStyle = 'rgba(255, 255, 255, 0.16)'
        ctx.arc(gpsScreenPoint.x - metrics.coreRadius * 0.16, gpsScreenPoint.y - metrics.coreRadius * 0.22, metrics.coreRadius * 0.18, 0, Math.PI * 2)
        ctx.fill()
      }
    } else {
      ctx.beginPath()
      ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius, 0, Math.PI * 2)
      ctx.fill()
      ctx.beginPath()
      ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
      ctx.lineWidth = metrics.ringWidth
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius, 0, Math.PI * 2)
      ctx.stroke()
      ctx.beginPath()
      ctx.fillStyle = 'rgba(255, 255, 255, 0.22)'
      ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.18, 0, Math.PI * 2)
      ctx.fill()
    }

    if (scene.gpsHeadingDeg !== null && scene.gpsMarkerStyleConfig.showHeadingIndicator) {
      const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
      const indicatorBaseDistance = metrics.ringRadius + metrics.indicatorOffset
      const indicatorSize = metrics.indicatorSize * indicatorScale
      const indicatorTipDistance = indicatorBaseDistance + indicatorSize * 0.92
      const tip = rotatePoint(0, -indicatorTipDistance, headingScreenRad)
      const left = rotatePoint(-indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
      const right = rotatePoint(indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
      const alpha = scene.gpsHeadingAlpha

      ctx.beginPath()
      ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0.42, alpha)})`
      ctx.moveTo(gpsScreenPoint.x + tip.x, gpsScreenPoint.y + tip.y)
      ctx.lineTo(gpsScreenPoint.x + left.x, gpsScreenPoint.y + left.y)
      ctx.lineTo(gpsScreenPoint.x + right.x, gpsScreenPoint.y + right.y)
      ctx.closePath()
      ctx.fill()

      const innerTip = rotatePoint(0, -(indicatorBaseDistance + indicatorSize * 0.72), headingScreenRad)
      const innerLeft = rotatePoint(-indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
      const innerRight = rotatePoint(indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
      ctx.beginPath()
      ctx.fillStyle = `rgba(${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(1, 3), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(3, 5), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(5, 7), 16)}, ${alpha})`
      ctx.moveTo(gpsScreenPoint.x + innerTip.x, gpsScreenPoint.y + innerTip.y)
      ctx.lineTo(gpsScreenPoint.x + innerLeft.x, gpsScreenPoint.y + innerLeft.y)
      ctx.lineTo(gpsScreenPoint.x + innerRight.x, gpsScreenPoint.y + innerRight.y)
      ctx.closePath()
      ctx.fill()
    }
    ctx.restore()
  }
}
