import { getTileSizePx, type CameraState } from '../camera/camera'
import { worldTileToLonLat } from '../../utils/projection'
import { type MapScene } from './mapRenderer'
import { CourseLayer, type ProjectedCourseLayers, type ProjectedCourseLeg } from '../layer/courseLayer'
import { TrackLayer } from '../layer/trackLayer'
import { GpsLayer } from '../layer/gpsLayer'
import { type GpsMarkerStyleConfig } from '../../game/presentation/gpsMarkerStyleConfig'
import {
  type ControlPointStyleEntry,
  type CourseLegStyleEntry,
} from '../../game/presentation/courseStyleConfig'
import { hexToRgbaColor, resolveControlStyle, resolveLegStyle, type RgbaColor } from './courseStyleResolver'

const ACTIVE_CONTROL_COLOR: [number, number, number, number] = [0.22, 1, 0.95, 1]
const READY_CONTROL_COLOR: [number, number, number, number] = [0.38, 1, 0.92, 1]
const FOCUSED_CONTROL_COLOR: [number, number, number, number] = [0.98, 0.96, 0.98, 1]
const MULTI_ACTIVE_PULSE_COLOR: [number, number, number, number] = [0.18, 1, 0.96, 0.86]
const FOCUSED_PULSE_COLOR: [number, number, number, number] = [1, 0.36, 0.84, 0.88]
const READY_PULSE_COLOR: [number, number, number, number] = [0.44, 1, 0.92, 0.98]
const COMPLETED_SETTLE_COLOR: [number, number, number, number] = [0.86, 0.9, 0.94, 0.24]
const SKIPPED_SETTLE_COLOR: [number, number, number, number] = [0.72, 0.76, 0.82, 0.18]
const SKIPPED_SLASH_COLOR: [number, number, number, number] = [0.78, 0.82, 0.88, 0.9]
const ACTIVE_LEG_COLOR: [number, number, number, number] = [0.18, 1, 0.94, 0.5]
const EARTH_CIRCUMFERENCE_METERS = 40075016.686
const CONTROL_RING_WIDTH_RATIO = 0.2
const FINISH_INNER_RADIUS_RATIO = 0.6
const FINISH_RING_WIDTH_RATIO = 0.2
const START_RING_WIDTH_RATIO = 0.2
const LEG_WIDTH_RATIO = 0.2
const LEG_TRIM_TO_RING_CENTER_RATIO = 1 - CONTROL_RING_WIDTH_RATIO / 2
const ACTIVE_CONTROL_PULSE_SPEED = 0.18
const ACTIVE_CONTROL_PULSE_MIN_SCALE = 1.12
const ACTIVE_CONTROL_PULSE_MAX_SCALE = 1.46
const ACTIVE_CONTROL_PULSE_WIDTH_RATIO = 0.12
const GUIDE_FLOW_COUNT = 5
const GUIDE_FLOW_SPEED = 0.02
const GUIDE_FLOW_TRAIL = 0.16
const GUIDE_FLOW_MIN_WIDTH_RATIO = 0.12
const GUIDE_FLOW_MAX_WIDTH_RATIO = 0.22
const GUIDE_FLOW_HEAD_RADIUS_RATIO = 0.18
const LEG_ARROW_HEAD_LENGTH_RATIO = 0.34
const LEG_ARROW_HEAD_WIDTH_RATIO = 0.24

function getGpsMarkerMetrics(size: GpsMarkerStyleConfig['size']) {
  if (size === 'small') {
    return {
      coreRadiusPx: 7,
      ringRadiusPx: 8.35,
      pulseRadiusPx: 14,
      indicatorOffsetPx: 1.1,
      indicatorSizePx: 7,
      ringWidthPx: 2.5,
    }
  }
  if (size === 'large') {
    return {
      coreRadiusPx: 11,
      ringRadiusPx: 12.95,
      pulseRadiusPx: 22,
      indicatorOffsetPx: 1.45,
      indicatorSizePx: 10,
      ringWidthPx: 3.5,
    }
  }
  return {
    coreRadiusPx: 9,
    ringRadiusPx: 10.65,
    pulseRadiusPx: 18,
    indicatorOffsetPx: 1.25,
    indicatorSizePx: 8.5,
    ringWidthPx: 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 {
    coreRadiusPx: metrics.coreRadiusPx * safeScale,
    ringRadiusPx: metrics.ringRadiusPx * safeScale,
    pulseRadiusPx: metrics.pulseRadiusPx * safeScale,
    indicatorOffsetPx: metrics.indicatorOffsetPx * safeScale,
    indicatorSizePx: metrics.indicatorSizePx * safeScale,
    ringWidthPx: Math.max(2, metrics.ringWidthPx * (0.96 + (safeScale - 1) * 0.35)),
  }
}

function getGpsPulsePhase(
  pulseFrame: number,
  motionState: 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 getAnimatedGpsPulseRadius(
  pulseFrame: number,
  metrics: ReturnType<typeof getGpsMarkerMetrics>,
  motionState: GpsMarkerStyleConfig['motionState'],
  pulseStrength: number,
  motionIntensity: number,
): number {
  const phase = getGpsPulsePhase(pulseFrame, motionState)
  const baseRadius = motionState === 'idle'
    ? metrics.pulseRadiusPx * 0.82
    : motionState === 'moving'
      ? metrics.pulseRadiusPx * 0.94
      : motionState === 'fast-moving'
        ? metrics.pulseRadiusPx * 1.04
        : metrics.pulseRadiusPx
  const amplitude = motionState === 'idle'
    ? metrics.pulseRadiusPx * 0.12
    : motionState === 'moving'
      ? metrics.pulseRadiusPx * 0.18
      : motionState === 'fast-moving'
        ? metrics.pulseRadiusPx * 0.24
        : metrics.pulseRadiusPx * 0.2
  return baseRadius + amplitude * phase * (0.8 + pulseStrength * 0.18 + motionIntensity * 0.1)
}

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 createShader(gl: any, type: number, source: string): any {
  const shader = gl.createShader(type)
  if (!shader) {
    throw new Error('WebGL shader 创建失败')
  }

  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
    gl.deleteShader(shader)
    throw new Error(message)
  }

  return shader
}

function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
  const program = gl.createProgram()
  if (!program) {
    throw new Error('WebGL program 创建失败')
  }

  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  gl.linkProgram(program)
  gl.deleteShader(vertexShader)
  gl.deleteShader(fragmentShader)

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const message = gl.getProgramInfoLog(program) || 'unknown program error'
    gl.deleteProgram(program)
    throw new Error(message)
  }

  return program
}

export class WebGLVectorRenderer {
  canvas: any
  gl: any
  dpr: number
  courseLayer: CourseLayer
  trackLayer: TrackLayer
  gpsLayer: GpsLayer
  program: any
  positionBuffer: any
  colorBuffer: any
  positionLocation: number
  colorLocation: number

  constructor(courseLayer: CourseLayer, trackLayer: TrackLayer, gpsLayer: GpsLayer) {
    this.canvas = null
    this.gl = null
    this.dpr = 1
    this.courseLayer = courseLayer
    this.trackLayer = trackLayer
    this.gpsLayer = gpsLayer
    this.program = null
    this.positionBuffer = null
    this.colorBuffer = null
    this.positionLocation = -1
    this.colorLocation = -1
  }

  attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
    this.canvas = canvasNode
    this.dpr = dpr || 1
    canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
    canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
    this.attachContext(canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl'), canvasNode)
  }

  attachContext(gl: any, canvasNode: any): void {
    if (!gl) {
      throw new Error('当前环境不支持 WebGL Vector Layer')
    }

    this.canvas = canvasNode
    this.gl = gl
    this.program = createProgram(
      gl,
      'attribute vec2 a_position; attribute vec4 a_color; varying vec4 v_color; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_color = a_color; }',
      'precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }',
    )
    this.positionBuffer = gl.createBuffer()
    this.colorBuffer = gl.createBuffer()
    this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
    this.colorLocation = gl.getAttribLocation(this.program, 'a_color')

    gl.viewport(0, 0, canvasNode.width, canvasNode.height)
    gl.enable(gl.BLEND)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
  }

  destroy(): void {
    if (this.gl) {
      if (this.program) {
        this.gl.deleteProgram(this.program)
      }
      if (this.positionBuffer) {
        this.gl.deleteBuffer(this.positionBuffer)
      }
      if (this.colorBuffer) {
        this.gl.deleteBuffer(this.colorBuffer)
      }
    }

    this.program = null
    this.positionBuffer = null
    this.colorBuffer = null
    this.gl = null
    this.canvas = null
  }

  render(scene: MapScene, pulseFrame: number): void {
    if (!this.gl || !this.program || !this.positionBuffer || !this.colorBuffer || !this.canvas) {
      return
    }

    const gl = this.gl
    const course = this.courseLayer.projectCourse(scene)
    const trackPoints = this.trackLayer.projectPoints(scene)
    const gpsPoint = this.gpsLayer.projectPoint(scene)
    const positions: number[] = []
    const colors: number[] = []

    if (course) {
      this.pushCourse(positions, colors, course, scene, pulseFrame)
    }

    this.pushTrack(positions, colors, trackPoints, scene)

    if (gpsPoint && scene.gpsMarkerStyleConfig.visible) {
      this.pushGpsMarker(positions, colors, gpsPoint.x, gpsPoint.y, scene, pulseFrame)
    }

    if (!positions.length) {
      return
    }

    gl.viewport(0, 0, this.canvas.width, this.canvas.height)
    gl.useProgram(this.program)

    gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
    gl.enableVertexAttribArray(this.positionLocation)
    gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)

    gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STREAM_DRAW)
    gl.enableVertexAttribArray(this.colorLocation)
    gl.vertexAttribPointer(this.colorLocation, 4, gl.FLOAT, false, 0, 0)

    gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
  }

  isLite(scene: MapScene): boolean {
    return scene.animationLevel === 'lite'
  }

  getRingSegments(scene: MapScene): number {
    return this.isLite(scene) ? 24 : 36
  }

  getCircleSegments(scene: MapScene): number {
    return this.isLite(scene) ? 14 : 20
  }

  getPixelsPerMeter(scene: MapScene): number {
    const camera: CameraState = {
      centerWorldX: scene.exactCenterWorldX,
      centerWorldY: scene.exactCenterWorldY,
      viewportWidth: scene.viewportWidth,
      viewportHeight: scene.viewportHeight,
      visibleColumns: scene.visibleColumns,
    }
    const tileSizePx = getTileSizePx(camera)
    const centerLonLat = worldTileToLonLat({ x: scene.exactCenterWorldX, y: scene.exactCenterWorldY }, scene.zoom)
    const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
    if (!tileSizePx || !metersPerTile) {
      return 0
    }
    return tileSizePx / metersPerTile
  }

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

  getControlRadiusMeters(scene: MapScene): number {
    return scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
  }

  pushCourse(
    positions: number[],
    colors: number[],
    course: ProjectedCourseLayers,
    scene: MapScene,
    pulseFrame: number,
  ): void {
    const controlRadiusMeters = this.getControlRadiusMeters(scene)

    if (scene.revealFullCourse && scene.showCourseLegs) {
      for (let index = 0; index < course.legs.length; index += 1) {
        const leg = course.legs[index]
        this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, leg.index, scene)
        if (scene.guidanceLegAnimationEnabled && scene.activeLegIndices.includes(index)) {
          this.pushCourseLegHighlight(positions, colors, leg, controlRadiusMeters, scene)
        }
      }

      const guideLeg = this.getGuideLeg(course, scene)
      if (guideLeg) {
        this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
      }
    }

    for (const start of course.starts) {
      if (scene.activeStart) {
        this.pushActiveStartPulse(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene, pulseFrame)
      }
      if (scene.completedStart) {
        this.pushRing(
          positions,
          colors,
          start.point.x,
          start.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.16),
          this.getMetric(scene, controlRadiusMeters * 1.02),
          COMPLETED_SETTLE_COLOR,
          scene,
        )
      }
      const startStyle = resolveControlStyle(scene, 'start', null, start.index)
      this.pushStartMarker(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, startStyle.entry, startStyle.color, scene)
    }
    if (!scene.revealFullCourse) {
      return
    }

    for (const control of course.controls) {
      const controlStyle = resolveControlStyle(scene, 'control', control.sequence)
      if (scene.activeControlSequences.includes(control.sequence)) {
        if (scene.controlVisualMode === 'single-target') {
          this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
        } else {
          this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame, MULTI_ACTIVE_PULSE_COLOR)
          if (!this.isLite(scene)) {
            this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.2, scene, pulseFrame + 9, [0.9, 1, 1, 0.52])
          }
        }
      }

      if (scene.readyControlSequences.includes(control.sequence)) {
        this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.04, scene, pulseFrame, READY_PULSE_COLOR)
        if (!this.isLite(scene)) {
          this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.22, scene, pulseFrame + 11, [0.92, 1, 1, 0.42])
        }
        this.pushRing(
          positions,
          colors,
          control.point.x,
          control.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.16),
          this.getMetric(scene, controlRadiusMeters * 1.02),
          READY_CONTROL_COLOR,
          scene,
        )
      }

      this.pushControlShape(
        positions,
        colors,
        control.point.x,
        control.point.y,
        controlRadiusMeters,
        controlStyle.entry,
        controlStyle.color,
        scene,
      )

      if (scene.focusedControlSequences.includes(control.sequence)) {
        this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.02, scene, pulseFrame, FOCUSED_PULSE_COLOR)
        if (!this.isLite(scene)) {
          this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.32, scene, pulseFrame + 15, [1, 0.86, 0.94, 0.5])
        }
        this.pushRing(
          positions,
          colors,
          control.point.x,
          control.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.24),
          this.getMetric(scene, controlRadiusMeters * 1.06),
          FOCUSED_CONTROL_COLOR,
          scene,
        )
      }

      if (scene.completedControlSequences.includes(control.sequence)) {
        this.pushRing(
          positions,
          colors,
          control.point.x,
          control.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.14),
          this.getMetric(scene, controlRadiusMeters * 1.02),
          COMPLETED_SETTLE_COLOR,
          scene,
        )
      }

      if (this.isSkippedControl(scene, control.sequence)) {
        this.pushRing(
          positions,
          colors,
          control.point.x,
          control.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.1),
          this.getMetric(scene, controlRadiusMeters * 1.01),
          SKIPPED_SETTLE_COLOR,
          scene,
        )
        this.pushSkippedControlSlash(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene)
      }
    }

    for (const finish of course.finishes) {
      if (scene.activeFinish) {
        this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters, scene, pulseFrame)
      }
      if (scene.focusedFinish) {
        this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters * 1.04, scene, pulseFrame, FOCUSED_PULSE_COLOR)
        if (!this.isLite(scene)) {
          this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters * 1.34, scene, pulseFrame + 12, [1, 0.86, 0.94, 0.46])
        }
      }

      const finishStyle = resolveControlStyle(scene, 'finish', null, finish.index)
      if (scene.completedFinish) {
        this.pushRing(
          positions,
          colors,
          finish.point.x,
          finish.point.y,
          this.getMetric(scene, controlRadiusMeters * 1.18),
          this.getMetric(scene, controlRadiusMeters * 1.02),
          COMPLETED_SETTLE_COLOR,
          scene,
        )
      }
      this.pushFinishMarker(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters, finishStyle.entry, finishStyle.color, scene)
    }
  }

  pushTrack(
    positions: number[],
    colors: number[],
    trackPoints: Array<{ x: number; y: number }>,
    scene: MapScene,
  ): void {
    if (scene.trackMode === 'none' || trackPoints.length < 2) {
      return
    }

    const bodyColor = hexToRgbaColor(scene.trackStyleConfig.colorHex)
    const headColor = hexToRgbaColor(scene.trackStyleConfig.headColorHex)
    const baseWidth = scene.trackStyleConfig.widthPx
    const headWidth = Math.max(baseWidth, scene.trackStyleConfig.headWidthPx)
    const glowStrength = scene.trackStyleConfig.glowStrength
    const displayPoints = this.smoothTrackPoints(trackPoints)

    if (scene.trackMode === 'full') {
      for (let index = 1; index < displayPoints.length; index += 1) {
        const from = displayPoints[index - 1]
        const to = displayPoints[index]
        if (glowStrength > 0) {
          this.pushSegment(positions, colors, from, to, baseWidth * (1.45 + glowStrength * 0.75), this.applyAlpha(bodyColor, 0.05 + glowStrength * 0.08), scene)
        }
        this.pushSegment(positions, colors, from, to, baseWidth, this.applyAlpha(bodyColor, 0.88), scene)
      }
      return
    }

    for (let index = 1; index < displayPoints.length; index += 1) {
      const from = displayPoints[index - 1]
      const to = displayPoints[index]
      const progress = index / Math.max(1, displayPoints.length - 1)
      const segmentWidth = baseWidth + (headWidth - baseWidth) * progress
      const segmentColor = this.mixTrackColor(bodyColor, headColor, progress, 0.12 + progress * 0.88)

      if (glowStrength > 0) {
        this.pushSegment(
            positions,
            colors,
            from,
            to,
            segmentWidth * (1.35 + glowStrength * 0.55),
            this.applyAlpha(segmentColor, (0.03 + progress * 0.14) * (0.7 + glowStrength * 0.38)),
            scene,
          )
        }

      this.pushSegment(positions, colors, from, to, segmentWidth, segmentColor, scene)
    }

      const head = displayPoints[displayPoints.length - 1]
      if (glowStrength > 0) {
      this.pushCircle(positions, colors, head.x, head.y, headWidth * (1.04 + glowStrength * 0.28), this.applyAlpha(headColor, 0.1 + glowStrength * 0.12), scene)
      }
    this.pushCircle(positions, colors, head.x, head.y, Math.max(3.4, headWidth * 0.46), this.applyAlpha(headColor, 0.94), scene)
  }

  pushGpsMarker(
    positions: number[],
    colors: number[],
    x: number,
    y: number,
    scene: MapScene,
    pulseFrame: number,
    ): void {
      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 markerColor = hexToRgbaColor(scene.gpsMarkerStyleConfig.colorHex)
      const ringColor = hexToRgbaColor(scene.gpsMarkerStyleConfig.ringColorHex)
      const indicatorColor = hexToRgbaColor(scene.gpsMarkerStyleConfig.indicatorColorHex)

      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.coreRadiusPx * (0.85 + index * 0.64) * (0.9 + wakeStrength * 0.72)
          const center = rotatePoint(0, -offset, wakeHeadingRad)
          const radius = metrics.coreRadiusPx * 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))
          this.pushCircle(positions, colors, x + center.x, y + center.y, radius, [markerColor[0], markerColor[1], markerColor[2], alpha], scene)
        }
      }
      if (warningGlowStrength > 0.04) {
        const glowPhase = getGpsPulsePhase(pulseFrame, motionState)
        this.pushRing(
          positions,
          colors,
          x,
          y,
          metrics.ringRadiusPx * (1.18 + warningGlowStrength * 0.12 + glowPhase * 0.04),
          metrics.ringRadiusPx * (1.02 + warningGlowStrength * 0.08 + glowPhase * 0.02),
          [markerColor[0], markerColor[1], markerColor[2], 0.18 + warningGlowStrength * 0.18],
          scene,
        )
      }
      if (style === 'beacon' || (style === 'badge' && !hasBadgeLogo)) {
        const pulseRadius = getAnimatedGpsPulseRadius(pulseFrame, metrics, motionState, pulseStrength, motionIntensity)
        const pulseAlpha = style === 'badge'
          ? Math.min(0.2, 0.08 + pulseStrength * 0.06)
          : Math.min(0.26, 0.1 + pulseStrength * 0.08)
        this.pushCircle(positions, colors, x, y, pulseRadius, [1, 1, 1, pulseAlpha], scene)
      }

      if (style === 'dot') {
        this.pushRing(
          positions,
          colors,
          x,
          y,
          metrics.coreRadiusPx + metrics.ringWidthPx * 0.72,
          metrics.coreRadiusPx + 0.08,
          ringColor,
          scene,
        )
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx * 0.82, markerColor, scene)
      } else if (style === 'disc') {
        this.pushRing(
          positions,
          colors,
          x,
          y,
          metrics.ringRadiusPx * 1.05,
          Math.max(metrics.coreRadiusPx + 0.04, metrics.ringRadiusPx * 1.05 - metrics.ringWidthPx * 1.18),
          ringColor,
          scene,
        )
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx * 1.02, markerColor, scene)
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx * 0.22, [1, 1, 1, 0.96], scene)
      } else if (style === 'badge') {
        this.pushRing(
          positions,
          colors,
          x,
          y,
          metrics.ringRadiusPx * 1.06,
          Math.max(metrics.coreRadiusPx + 0.12, metrics.ringRadiusPx * 1.06 - metrics.ringWidthPx * 1.12),
          [1, 1, 1, 0.98],
          scene,
        )
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx * 0.98, markerColor, scene)
        if (!hasBadgeLogo) {
          this.pushCircle(positions, colors, x - metrics.coreRadiusPx * 0.16, y - metrics.coreRadiusPx * 0.22, metrics.coreRadiusPx * 0.18, [1, 1, 1, 0.16], scene)
        }
      } else {
        this.pushRing(
          positions,
          colors,
          x,
          y,
          metrics.ringRadiusPx,
          Math.max(metrics.coreRadiusPx + 0.15, metrics.ringRadiusPx - metrics.ringWidthPx),
          ringColor,
          scene,
        )
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx, markerColor, scene)
        this.pushCircle(positions, colors, x, y, metrics.coreRadiusPx * 0.18, [1, 1, 1, 0.22], scene)
      }

      if (scene.gpsHeadingDeg !== null && scene.gpsMarkerStyleConfig.showHeadingIndicator) {
        const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
        const alpha = scene.gpsHeadingAlpha
        const indicatorBaseDistance = metrics.ringRadiusPx + metrics.indicatorOffsetPx
        const indicatorSize = metrics.indicatorSizePx * indicatorScale
        const indicatorTipDistance = indicatorBaseDistance + indicatorSize * 0.94
        const tip = rotatePoint(0, -indicatorTipDistance, headingScreenRad)
        const left = rotatePoint(-indicatorSize * 0.58, -indicatorBaseDistance, headingScreenRad)
        const right = rotatePoint(indicatorSize * 0.58, -indicatorBaseDistance, headingScreenRad)
        this.pushTriangleScreen(
          positions,
          colors,
          x + tip.x,
        y + tip.y,
        x + left.x,
        y + left.y,
        x + right.x,
        y + right.y,
        [1, 1, 1, Math.max(0.42, alpha)],
        scene,
        )

        const innerTip = rotatePoint(0, -(indicatorBaseDistance + indicatorSize * 0.72), headingScreenRad)
        const innerLeft = rotatePoint(-indicatorSize * 0.4, -(indicatorBaseDistance + 0.15), headingScreenRad)
        const innerRight = rotatePoint(indicatorSize * 0.4, -(indicatorBaseDistance + 0.15), headingScreenRad)
        this.pushTriangleScreen(
          positions,
          colors,
          x + innerTip.x,
        y + innerTip.y,
        x + innerLeft.x,
        y + innerLeft.y,
        x + innerRight.x,
        y + innerRight.y,
        [indicatorColor[0], indicatorColor[1], indicatorColor[2], alpha],
        scene,
      )
    }
  }

  pushTriangleScreen(
    positions: number[],
    colors: number[],
    x1: number,
    y1: number,
    x2: number,
    y2: number,
    x3: number,
    y3: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const p1 = this.toClip(x1, y1, scene)
    const p2 = this.toClip(x2, y2, scene)
    const p3 = this.toClip(x3, y3, scene)
    positions.push(
      p1.x, p1.y,
      p2.x, p2.y,
      p3.x, p3.y,
    )
    for (let index = 0; index < 3; index += 1) {
      colors.push(color[0], color[1], color[2], color[3])
    }
  }

  smoothTrackPoints(points: Array<{ x: number; y: number }>): Array<{ x: number; y: number }> {
    if (points.length < 3) {
      return points
    }

    const smoothed: Array<{ x: number; y: number }> = [points[0]]
    for (let index = 1; index < points.length - 1; index += 1) {
      const prev = points[index - 1]
      const current = points[index]
      const next = points[index + 1]
      smoothed.push({
        x: prev.x * 0.18 + current.x * 0.64 + next.x * 0.18,
        y: prev.y * 0.18 + current.y * 0.64 + next.y * 0.18,
      })
    }
    smoothed.push(points[points.length - 1])
    return smoothed
  }

  mixTrackColor(from: RgbaColor, to: RgbaColor, progress: number, alpha: number): RgbaColor {
    return [
      from[0] + (to[0] - from[0]) * progress,
      from[1] + (to[1] - from[1]) * progress,
      from[2] + (to[2] - from[2]) * progress,
      alpha,
    ]
  }

  getGuideLeg(course: ProjectedCourseLayers, scene: MapScene): ProjectedCourseLeg | null {
    if (!scene.guidanceLegAnimationEnabled) {
      return null
    }

    const activeIndex = scene.activeLegIndices.length ? scene.activeLegIndices[0] : -1
    if (activeIndex >= 0 && activeIndex < course.legs.length) {
      return course.legs[activeIndex]
    }

    return null
  }

  isCompletedLeg(scene: MapScene, index: number): boolean {
    return scene.completedLegIndices.includes(index)
  }

  isSkippedControl(scene: MapScene, sequence: number): boolean {
    return scene.skippedControlSequences.includes(sequence)
  }

  pushCourseLeg(
    positions: number[],
    colors: number[],
    leg: ProjectedCourseLeg,
    controlRadiusMeters: number,
    index: number,
    scene: MapScene,
  ): void {
    const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
    if (!trimmed) {
      return
    }

    const legStyle = resolveLegStyle(scene, index)
    this.pushLegWithStyle(
      positions,
      colors,
      trimmed.from,
      trimmed.to,
      controlRadiusMeters,
        legStyle.entry,
        legStyle.color,
        scene,
      )
  }

  pushCourseLegHighlight(
    positions: number[],
    colors: number[],
    leg: ProjectedCourseLeg,
    controlRadiusMeters: number,
    scene: MapScene,
  ): void {
    const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
    if (!trimmed) {
      return
    }

    this.pushSegment(
      positions,
      colors,
      trimmed.from,
      trimmed.to,
      this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO * 1.5),
      ACTIVE_LEG_COLOR,
      scene,
    )
  }

  pushActiveControlPulse(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    controlRadiusMeters: number,
    scene: MapScene,
    pulseFrame: number,
    pulseColor?: RgbaColor,
  ): void {
    const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
    const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
    const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
    const baseColor = pulseColor || ACTIVE_CONTROL_COLOR
    const glowAlpha = Math.min(1, baseColor[3] * (0.46 + pulse * 0.5))
    const glowColor: RgbaColor = [baseColor[0], baseColor[1], baseColor[2], glowAlpha]

    this.pushRing(
      positions,
      colors,
      centerX,
      centerY,
      this.getMetric(scene, controlRadiusMeters * pulseScale),
      this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
      glowColor,
      scene,
    )
  }

  pushSkippedControlSlash(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    controlRadiusMeters: number,
    scene: MapScene,
  ): void {
    const slashRadius = this.getMetric(scene, controlRadiusMeters * 0.72)
    const slashWidth = this.getMetric(scene, controlRadiusMeters * 0.08)
    this.pushSegment(
      positions,
      colors,
      { x: centerX - slashRadius, y: centerY + slashRadius },
      { x: centerX + slashRadius, y: centerY - slashRadius },
      slashWidth,
      SKIPPED_SLASH_COLOR,
      scene,
    )
  }

  pushActiveStartPulse(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    headingDeg: number | null,
    controlRadiusMeters: number,
    scene: MapScene,
    pulseFrame: number,
  ): void {
    const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
    const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
    const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
    const glowAlpha = 0.24 + pulse * 0.34
    const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
    const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
    const ringCenterX = centerX + Math.cos(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)
    const ringCenterY = centerY + Math.sin(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)

    this.pushRing(
      positions,
      colors,
      ringCenterX,
      ringCenterY,
      this.getMetric(scene, controlRadiusMeters * pulseScale),
      this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
      glowColor,
      scene,
    )
  }

  pushGuidanceFlow(
    positions: number[],
    colors: number[],
    leg: ProjectedCourseLeg,
    controlRadiusMeters: number,
    scene: MapScene,
    pulseFrame: number,
  ): void {
    const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
    if (!trimmed) {
      return
    }

    const dx = trimmed.to.x - trimmed.from.x
    const dy = trimmed.to.y - trimmed.from.y
    const length = Math.sqrt(dx * dx + dy * dy)
    if (!length) {
      return
    }

    for (let index = 0; index < GUIDE_FLOW_COUNT; index += 1) {
      const progress = (pulseFrame * GUIDE_FLOW_SPEED + index / GUIDE_FLOW_COUNT) % 1
      const tailProgress = Math.max(0, progress - GUIDE_FLOW_TRAIL)
      const head = {
        x: trimmed.from.x + dx * progress,
        y: trimmed.from.y + dy * progress,
      }
      const tail = {
        x: trimmed.from.x + dx * tailProgress,
        y: trimmed.from.y + dy * tailProgress,
      }
      const eased = progress * progress
      const width = this.getMetric(
        scene,
        controlRadiusMeters * (GUIDE_FLOW_MIN_WIDTH_RATIO + (GUIDE_FLOW_MAX_WIDTH_RATIO - GUIDE_FLOW_MIN_WIDTH_RATIO) * eased),
      )
      const outerColor = this.getGuideFlowOuterColor(eased)
      const innerColor = this.getGuideFlowInnerColor(eased)
      const headRadius = this.getMetric(scene, controlRadiusMeters * GUIDE_FLOW_HEAD_RADIUS_RATIO * (0.72 + eased * 0.42))

      this.pushSegment(positions, colors, tail, head, width * 1.9, outerColor, scene)
      this.pushSegment(positions, colors, tail, head, width, innerColor, scene)
      this.pushCircle(positions, colors, head.x, head.y, headRadius * 1.35, outerColor, scene)
      this.pushCircle(positions, colors, head.x, head.y, headRadius, innerColor, scene)
    }
  }

  getTrimmedCourseLeg(
    leg: ProjectedCourseLeg,
    controlRadiusMeters: number,
    scene: MapScene,
  ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
    return this.trimSegment(
      leg.from,
      leg.to,
      this.getLegTrim(leg.fromKind, controlRadiusMeters, scene),
      this.getLegTrim(leg.toKind, controlRadiusMeters, scene),
    )
  }

  getGuideFlowOuterColor(progress: number): RgbaColor {
    return [0.28, 0.92, 1, 0.14 + progress * 0.22]
  }

  getGuideFlowInnerColor(progress: number): RgbaColor {
    return [0.94, 0.99, 1, 0.38 + progress * 0.42]
  }

  getLegTrim(kind: ProjectedCourseLeg['fromKind'], controlRadiusMeters: number, scene: MapScene): number {
    if (kind === 'start') {
      return this.getMetric(scene, controlRadiusMeters * (1 - START_RING_WIDTH_RATIO / 2))
    }

    if (kind === 'finish') {
      return this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO / 2))
    }

    return this.getMetric(scene, controlRadiusMeters * LEG_TRIM_TO_RING_CENTER_RATIO)
  }

  trimSegment(
    from: { x: number; y: number },
    to: { x: number; y: number },
    fromTrim: number,
    toTrim: number,
  ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
    const dx = to.x - from.x
    const dy = to.y - from.y
    const length = Math.sqrt(dx * dx + dy * dy)
    if (!length || length <= fromTrim + toTrim) {
      return null
    }

    const ux = dx / length
    const uy = dy / length
    return {
      from: {
        x: from.x + ux * fromTrim,
        y: from.y + uy * fromTrim,
      },
      to: {
        x: to.x - ux * toTrim,
        y: to.y - uy * toTrim,
      },
    }
  }

  pushStartTriangle(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    headingDeg: number | null,
    controlRadiusMeters: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const startRadius = this.getMetric(scene, controlRadiusMeters)
    const startRingWidth = this.getMetric(scene, controlRadiusMeters * START_RING_WIDTH_RATIO)
    const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
    const vertices = [0, 1, 2].map((index) => {
      const angle = headingRad + index * (Math.PI * 2 / 3)
      return {
        x: centerX + Math.cos(angle) * startRadius,
        y: centerY + Math.sin(angle) * startRadius,
      }
    })

    this.pushSegment(positions, colors, vertices[0], vertices[1], startRingWidth, color, scene)
    this.pushSegment(positions, colors, vertices[1], vertices[2], startRingWidth, color, scene)
    this.pushSegment(positions, colors, vertices[2], vertices[0], startRingWidth, color, scene)
  }

  pushStartMarker(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    headingDeg: number | null,
    controlRadiusMeters: number,
    entry: ControlPointStyleEntry,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const style = entry.style
    const radiusMeters = controlRadiusMeters * this.getPointSizeScale(entry)
    const accentRingScale = this.getAccentRingScale(entry, 1.22)
    const glowStrength = this.getPointGlowStrength(entry)
    if (glowStrength > 0) {
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * Math.max(1.1, accentRingScale)),
        this.applyAlpha(color, 0.06 + glowStrength * 0.12),
        scene,
      )
    }
    if (style === 'double-ring' || style === 'badge' || style === 'pulse-core') {
      this.pushRing(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * accentRingScale),
        this.getMetric(scene, radiusMeters * Math.max(1.02, accentRingScale - 0.14)),
        this.applyAlpha(color, 0.92),
        scene,
      )
    }

    if (style === 'badge' || style === 'pulse-core') {
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * 0.2),
        this.applyAlpha(color, 0.96),
        scene,
      )
    }

    this.pushStartTriangle(positions, colors, centerX, centerY, headingDeg, radiusMeters, color, scene)
  }

  pushFinishMarker(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    controlRadiusMeters: number,
    entry: ControlPointStyleEntry,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const style = entry.style
    const radiusMeters = controlRadiusMeters * this.getPointSizeScale(entry)
    const accentRingScale = this.getAccentRingScale(entry, 1.18)
    const glowStrength = this.getPointGlowStrength(entry)
    if (glowStrength > 0) {
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * Math.max(1.08, accentRingScale)),
        this.applyAlpha(color, 0.05 + glowStrength * 0.11),
        scene,
      )
    }
    if (style === 'double-ring' || style === 'badge' || style === 'pulse-core') {
      this.pushRing(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * accentRingScale),
        this.getMetric(scene, radiusMeters * Math.max(1.02, accentRingScale - 0.14)),
        this.applyAlpha(color, 0.92),
        scene,
      )
    }

    this.pushRing(
      positions,
      colors,
      centerX,
      centerY,
      this.getMetric(scene, radiusMeters),
      this.getMetric(scene, radiusMeters * (1 - FINISH_RING_WIDTH_RATIO)),
      color,
      scene,
    )
    this.pushRing(
      positions,
      colors,
      centerX,
      centerY,
      this.getMetric(scene, radiusMeters * FINISH_INNER_RADIUS_RATIO),
      this.getMetric(scene, radiusMeters * FINISH_INNER_RADIUS_RATIO * (1 - FINISH_RING_WIDTH_RATIO / FINISH_INNER_RADIUS_RATIO)),
      color,
      scene,
    )

    if (style === 'badge' || style === 'pulse-core') {
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * 0.16),
        this.applyAlpha(color, 0.94),
        scene,
      )
    }
  }

  pushControlShape(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    controlRadiusMeters: number,
    entry: ControlPointStyleEntry,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const style = entry.style
    const radiusMeters = controlRadiusMeters * this.getPointSizeScale(entry)
    const accentRingScale = this.getAccentRingScale(entry, 1.24)
    const glowStrength = this.getPointGlowStrength(entry)
    const outerRadius = this.getMetric(scene, radiusMeters)
    const innerRadius = this.getMetric(scene, radiusMeters * (1 - CONTROL_RING_WIDTH_RATIO))

    if (glowStrength > 0) {
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        this.getMetric(scene, radiusMeters * Math.max(1.1, accentRingScale)),
        this.applyAlpha(color, 0.05 + glowStrength * 0.1),
        scene,
      )
    }

    if (style === 'solid-dot') {
      this.pushCircle(positions, colors, centerX, centerY, this.getMetric(scene, radiusMeters * 0.56), this.applyAlpha(color, 0.92), scene)
      this.pushRing(positions, colors, centerX, centerY, outerRadius, innerRadius, color, scene)
      return
    }

    if (style === 'double-ring') {
      this.pushRing(positions, colors, centerX, centerY, this.getMetric(scene, radiusMeters * accentRingScale), this.getMetric(scene, radiusMeters * Math.max(1.04, accentRingScale - 0.16)), this.applyAlpha(color, 0.88), scene)
      this.pushRing(positions, colors, centerX, centerY, outerRadius, innerRadius, color, scene)
      return
    }

    if (style === 'badge') {
      const borderOuterRadius = this.getMetric(scene, radiusMeters)
      const borderInnerRadius = this.getMetric(scene, radiusMeters * 0.86)
      if (accentRingScale > 1.04 || glowStrength > 0) {
        this.pushRing(
          positions,
          colors,
          centerX,
          centerY,
          this.getMetric(scene, radiusMeters * Math.max(1.04, accentRingScale)),
          this.getMetric(scene, radiusMeters * Math.max(0.96, accentRingScale - 0.08)),
          this.applyAlpha(color, 0.2 + glowStrength * 0.14),
          scene,
        )
      }
      this.pushRing(
        positions,
        colors,
        centerX,
        centerY,
        borderOuterRadius,
        borderInnerRadius,
        [1, 1, 1, 0.98],
        scene,
      )
      this.pushCircle(
        positions,
        colors,
        centerX,
        centerY,
        borderInnerRadius,
        this.applyAlpha(color, 0.98),
        scene,
      )
      return
    }

    if (style === 'pulse-core') {
      this.pushRing(positions, colors, centerX, centerY, this.getMetric(scene, radiusMeters * Math.max(1.14, accentRingScale)), this.getMetric(scene, radiusMeters * Math.max(1.02, accentRingScale - 0.12)), this.applyAlpha(color, 0.76), scene)
      this.pushRing(positions, colors, centerX, centerY, outerRadius, innerRadius, color, scene)
      this.pushCircle(positions, colors, centerX, centerY, this.getMetric(scene, radiusMeters * 0.18), this.applyAlpha(color, 0.98), scene)
      return
    }

    this.pushRing(positions, colors, centerX, centerY, outerRadius, innerRadius, color, scene)
  }

  pushLegWithStyle(
    positions: number[],
    colors: number[],
    from: { x: number; y: number },
    to: { x: number; y: number },
    controlRadiusMeters: number,
    entry: CourseLegStyleEntry,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const style = entry.style
    const widthScale = Math.max(0.55, entry.widthScale || 1)
    const glowStrength = this.getLegGlowStrength(entry)
    const baseWidth = this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO * widthScale)

    if (glowStrength > 0) {
      this.pushSegment(
        positions,
        colors,
        from,
        to,
        baseWidth * (1.5 + glowStrength * 0.9),
        this.applyAlpha(color, 0.06 + glowStrength * 0.1),
        scene,
      )
    }

    if (style === 'dashed-leg') {
      this.pushDashedSegment(positions, colors, from, to, baseWidth * 0.92, color, scene)
      return
    }

    if (style === 'glow-leg') {
      this.pushSegment(positions, colors, from, to, baseWidth * 2.7, this.applyAlpha(color, 0.16), scene)
      this.pushSegment(positions, colors, from, to, baseWidth * 1.54, this.applyAlpha(color, 0.34), scene)
      this.pushSegment(positions, colors, from, to, baseWidth, color, scene)
      this.pushArrowHead(positions, colors, from, to, controlRadiusMeters, color, scene, 1.08)
      return
    }

    if (style === 'progress-leg') {
      this.pushSegment(positions, colors, from, to, baseWidth * 1.9, this.applyAlpha(color, 0.18), scene)
      this.pushSegment(positions, colors, from, to, baseWidth * 1.26, color, scene)
      this.pushSegment(positions, colors, from, to, baseWidth * 0.44, [1, 1, 1, 0.54], scene)
      this.pushArrowHead(positions, colors, from, to, controlRadiusMeters, this.applyAlpha(color, 0.94), scene, 0.92)
      return
    }

    this.pushSegment(positions, colors, from, to, baseWidth, color, scene)
  }

  getPointSizeScale(entry: ControlPointStyleEntry): number {
    return Math.max(0.72, entry.sizeScale || 1)
  }

  getAccentRingScale(entry: ControlPointStyleEntry, fallback: number): number {
    return Math.max(0.96, entry.accentRingScale || fallback)
  }

  getPointGlowStrength(entry: ControlPointStyleEntry): number {
    return Math.max(0, Math.min(1.2, entry.glowStrength || 0))
  }

  getLegGlowStrength(entry: CourseLegStyleEntry): number {
    return Math.max(0, Math.min(1.2, entry.glowStrength || 0))
  }

  pushDashedSegment(
    positions: number[],
    colors: number[],
    start: { x: number; y: number },
    end: { x: number; y: number },
    width: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const dx = end.x - start.x
    const dy = end.y - start.y
    const length = Math.sqrt(dx * dx + dy * dy)
    if (!length) {
      return
    }

    const dashLength = Math.max(width * 3.1, 12)
    const gapLength = Math.max(width * 1.7, 8)
    let offset = 0
    while (offset < length) {
      const dashEnd = Math.min(length, offset + dashLength)
      const fromRatio = offset / length
      const toRatio = dashEnd / length
      this.pushSegment(
        positions,
        colors,
        { x: start.x + dx * fromRatio, y: start.y + dy * fromRatio },
        { x: start.x + dx * toRatio, y: start.y + dy * toRatio },
        width,
        color,
        scene,
      )
      offset += dashLength + gapLength
    }
  }

  applyAlpha(color: RgbaColor, alpha: number): RgbaColor {
    return [color[0], color[1], color[2], alpha]
  }

  pushArrowHead(
    positions: number[],
    colors: number[],
    start: { x: number; y: number },
    end: { x: number; y: number },
    controlRadiusMeters: number,
    color: RgbaColor,
    scene: MapScene,
    scale: number,
  ): void {
    const dx = end.x - start.x
    const dy = end.y - start.y
    const length = Math.sqrt(dx * dx + dy * dy)
    if (!length) {
      return
    }

    const ux = dx / length
    const uy = dy / length
    const nx = -uy
    const ny = ux
    const headLength = this.getMetric(scene, controlRadiusMeters * LEG_ARROW_HEAD_LENGTH_RATIO * scale)
    const headWidth = this.getMetric(scene, controlRadiusMeters * LEG_ARROW_HEAD_WIDTH_RATIO * scale)
    const baseCenterX = end.x - ux * headLength
    const baseCenterY = end.y - uy * headLength

    const tip = this.toClip(end.x, end.y, scene)
    const left = this.toClip(baseCenterX + nx * headWidth * 0.5, baseCenterY + ny * headWidth * 0.5, scene)
    const right = this.toClip(baseCenterX - nx * headWidth * 0.5, baseCenterY - ny * headWidth * 0.5, scene)
    this.pushTriangle(positions, colors, tip, left, right, color)
  }

  pushRing(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    outerRadius: number,
    innerRadius: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const segments = this.getRingSegments(scene)
    for (let index = 0; index < segments; index += 1) {
      const startAngle = index / segments * Math.PI * 2
      const endAngle = (index + 1) / segments * Math.PI * 2
      const outerStart = this.toClip(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius, scene)
      const outerEnd = this.toClip(centerX + Math.cos(endAngle) * outerRadius, centerY + Math.sin(endAngle) * outerRadius, scene)
      const innerStart = this.toClip(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius, scene)
      const innerEnd = this.toClip(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius, scene)
      this.pushTriangle(positions, colors, outerStart, outerEnd, innerStart, color)
      this.pushTriangle(positions, colors, innerStart, outerEnd, innerEnd, color)
    }
  }

  pushSegment(
    positions: number[],
    colors: number[],
    start: { x: number; y: number },
    end: { x: number; y: number },
    width: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const deltaX = end.x - start.x
    const deltaY = end.y - start.y
    const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
    if (!length) {
      return
    }

    const normalX = -deltaY / length * (width / 2)
    const normalY = deltaX / length * (width / 2)
    const topLeft = this.toClip(start.x + normalX, start.y + normalY, scene)
    const topRight = this.toClip(end.x + normalX, end.y + normalY, scene)
    const bottomLeft = this.toClip(start.x - normalX, start.y - normalY, scene)
    const bottomRight = this.toClip(end.x - normalX, end.y - normalY, scene)

    this.pushTriangle(positions, colors, topLeft, topRight, bottomLeft, color)
    this.pushTriangle(positions, colors, bottomLeft, topRight, bottomRight, color)
  }

  pushCircle(
    positions: number[],
    colors: number[],
    centerX: number,
    centerY: number,
    radius: number,
    color: RgbaColor,
    scene: MapScene,
  ): void {
    const segments = this.getCircleSegments(scene)
    const center = this.toClip(centerX, centerY, scene)
    for (let index = 0; index < segments; index += 1) {
      const startAngle = index / segments * Math.PI * 2
      const endAngle = (index + 1) / segments * Math.PI * 2
      const start = this.toClip(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius, scene)
      const end = this.toClip(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius, scene)
      this.pushTriangle(positions, colors, center, start, end, color)
    }
  }

  pushTriangle(
    positions: number[],
    colors: number[],
    first: { x: number; y: number },
    second: { x: number; y: number },
    third: { x: number; y: number },
    color: RgbaColor,
  ): void {
    positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
    for (let index = 0; index < 3; index += 1) {
      colors.push(color[0], color[1], color[2], color[3])
    }
  }

  toClip(x: number, y: number, scene: MapScene): { x: number; y: number } {
    const previewScale = scene.previewScale || 1
    const originX = scene.previewOriginX || scene.viewportWidth / 2
    const originY = scene.previewOriginY || scene.viewportHeight / 2
    const scaledX = originX + (x - originX) * previewScale
    const scaledY = originY + (y - originY) * previewScale

    return {
      x: scaledX / scene.viewportWidth * 2 - 1,
      y: 1 - scaledY / scene.viewportHeight * 2,
    }
  }
}




