import { type LonLatPoint } from './projection'

export type OrienteeringCourseNodeKind = 'start' | 'control' | 'finish'

export interface OrienteeringCourseStart {
  label: string
  point: LonLatPoint
  headingDeg: number | null
}

export interface OrienteeringCourseControl {
  label: string
  point: LonLatPoint
  sequence: number
}

export interface OrienteeringCourseFinish {
  label: string
  point: LonLatPoint
}

export interface OrienteeringCourseLeg {
  fromKind: OrienteeringCourseNodeKind
  toKind: OrienteeringCourseNodeKind
  fromPoint: LonLatPoint
  toPoint: LonLatPoint
}

export interface OrienteeringCourseLayers {
  starts: OrienteeringCourseStart[]
  controls: OrienteeringCourseControl[]
  finishes: OrienteeringCourseFinish[]
  legs: OrienteeringCourseLeg[]
}

export interface OrienteeringCourseData {
  title: string
  layers: OrienteeringCourseLayers
}

interface ParsedPlacemarkPoint {
  label: string
  point: LonLatPoint
  explicitKind: OrienteeringCourseNodeKind | null
}

interface OrderedCourseNode {
  label: string
  point: LonLatPoint
  kind: OrienteeringCourseNodeKind
}

function decodeXmlEntities(text: string): string {
  return text
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&')
}

function stripXml(text: string): string {
  return decodeXmlEntities(text.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim()
}

function extractTagText(block: string, tagName: string): string {
  const match = block.match(new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i'))
  return match ? stripXml(match[1]) : ''
}

function parseCoordinateTuple(rawValue: string): LonLatPoint | null {
  const parts = rawValue.trim().split(',')
  if (parts.length < 2) {
    return null
  }

  const lon = Number(parts[0])
  const lat = Number(parts[1])
  if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
    return null
  }

  return { lon, lat }
}

function extractPointCoordinates(block: string): LonLatPoint | null {
  const pointMatch = block.match(/<Point\b[\s\S]*?<coordinates>([\s\S]*?)<\/coordinates>[\s\S]*?<\/Point>/i)
  if (!pointMatch) {
    return null
  }

  const coordinateMatch = pointMatch[1].trim().match(/-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?(?:,-?\d+(?:\.\d+)?)?/) 
  return coordinateMatch ? parseCoordinateTuple(coordinateMatch[0]) : null
}

function normalizeCourseLabel(label: string): string {
  return label.trim().replace(/\s+/g, ' ')
}

function inferExplicitKind(label: string, placemarkBlock: string): OrienteeringCourseNodeKind | null {
  const normalized = label.toUpperCase().replace(/[^A-Z0-9]/g, '')
  const styleHint = placemarkBlock.toUpperCase()

  if (
    normalized === 'S'
    || normalized.startsWith('START')
    || /^S\d+$/.test(normalized)
    || styleHint.includes('START')
    || styleHint.includes('TRIANGLE')
  ) {
    return 'start'
  }

  if (
    normalized === 'F'
    || normalized === 'M'
    || normalized.startsWith('FINISH')
    || normalized.startsWith('GOAL')
    || /^F\d+$/.test(normalized)
    || styleHint.includes('FINISH')
    || styleHint.includes('GOAL')
  ) {
    return 'finish'
  }

  return null
}

function extractPlacemarkPoints(kmlText: string): ParsedPlacemarkPoint[] {
  const placemarkBlocks = kmlText.match(/<Placemark\b[\s\S]*?<\/Placemark>/gi) || []
  const points: ParsedPlacemarkPoint[] = []

  for (const placemarkBlock of placemarkBlocks) {
    const point = extractPointCoordinates(placemarkBlock)
    if (!point) {
      continue
    }

    const label = normalizeCourseLabel(extractTagText(placemarkBlock, 'name'))
    points.push({
      label,
      point,
      explicitKind: inferExplicitKind(label, placemarkBlock),
    })
  }

  return points
}

function classifyOrderedNodes(points: ParsedPlacemarkPoint[]): OrderedCourseNode[] {
  if (!points.length) {
    return []
  }

  const startIndex = points.findIndex((point) => point.explicitKind === 'start')
  let finishIndex = -1
  for (let index = points.length - 1; index >= 0; index -= 1) {
    if (points[index].explicitKind === 'finish') {
      finishIndex = index
      break
    }
  }

  return points.map((point, index) => {
    let kind = point.explicitKind
    if (!kind) {
      if (startIndex === -1 && index === 0) {
        kind = 'start'
      } else if (finishIndex === -1 && points.length > 1 && index === points.length - 1) {
        kind = 'finish'
      } else {
        kind = 'control'
      }
    }

    return {
      label: point.label,
      point: point.point,
      kind,
    }
  })
}

function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number {
  const fromLatRad = from.lat * Math.PI / 180
  const toLatRad = to.lat * Math.PI / 180
  const deltaLonRad = (to.lon - from.lon) * Math.PI / 180
  const y = Math.sin(deltaLonRad) * Math.cos(toLatRad)
  const x = Math.cos(fromLatRad) * Math.sin(toLatRad)
    - Math.sin(fromLatRad) * Math.cos(toLatRad) * Math.cos(deltaLonRad)
  const bearingDeg = Math.atan2(y, x) * 180 / Math.PI
  return (bearingDeg + 360) % 360
}

function buildCourseLayers(nodes: OrderedCourseNode[]): OrienteeringCourseLayers {
  const starts: OrienteeringCourseStart[] = []
  const controls: OrienteeringCourseControl[] = []
  const finishes: OrienteeringCourseFinish[] = []
  const legs: OrienteeringCourseLeg[] = []
  let controlSequence = 1

  nodes.forEach((node, index) => {
    const nextNode = index < nodes.length - 1 ? nodes[index + 1] : null
    const label = node.label || (
      node.kind === 'start'
        ? 'Start'
        : node.kind === 'finish'
          ? 'Finish'
          : String(controlSequence)
    )

    if (node.kind === 'start') {
      starts.push({
        label,
        point: node.point,
        headingDeg: nextNode ? getInitialBearingDeg(node.point, nextNode.point) : null,
      })
      return
    }

    if (node.kind === 'finish') {
      finishes.push({
        label,
        point: node.point,
      })
      return
    }

    controls.push({
      label,
      point: node.point,
      sequence: controlSequence,
    })
    controlSequence += 1
  })

  for (let index = 1; index < nodes.length; index += 1) {
    legs.push({
      fromKind: nodes[index - 1].kind,
      toKind: nodes[index].kind,
      fromPoint: nodes[index - 1].point,
      toPoint: nodes[index].point,
    })
  }

  return {
    starts,
    controls,
    finishes,
    legs,
  }
}

export function parseOrienteeringCourseKml(kmlText: string): OrienteeringCourseData {
  const points = extractPlacemarkPoints(kmlText)
  if (!points.length) {
    throw new Error('KML 中没有可用的 Point 控制点')
  }

  const documentTitle = extractTagText(kmlText, 'name')
  const nodes = classifyOrderedNodes(points)

  return {
    title: documentTitle || 'Orienteering Course',
    layers: buildCourseLayers(nodes),
  }
}
