import type { Stroke, RecognitionCandidate } from './types'

// $1 Unistroke Recognizer adapted for letter recognition.
// Resamples strokes to N points, rotates to canonical angle,
// scales to a unit square, then compares against templates
// using path distance. Handles variation in size, speed, and angle.

const N = 64 // resample point count
const SQUARE_SIZE = 250

interface Point { x: number; y: number }

function resample(points: Point[], n: number): Point[] {
  const totalLen = pathLength(points)
  const interval = totalLen / (n - 1)
  const out: Point[] = [points[0]]
  let D = 0

  for (let i = 1; i < points.length; i++) {
    const d = dist(points[i - 1], points[i])
    if (D + d >= interval) {
      const t = (interval - D) / d
      const q: Point = {
        x: points[i - 1].x + t * (points[i].x - points[i - 1].x),
        y: points[i - 1].y + t * (points[i].y - points[i - 1].y),
      }
      out.push(q)
      points.splice(i, 0, q)
      D = 0
    } else {
      D += d
    }
  }

  while (out.length < n) out.push(points[points.length - 1])
  return out.slice(0, n)
}

function rotateToZero(points: Point[]): Point[] {
  const c = centroid(points)
  const angle = Math.atan2(c.y - points[0].y, c.x - points[0].x)
  return rotateBy(points, -angle)
}

function scaleToSquare(points: Point[]): Point[] {
  const bb = boundingBox(points)
  const w = bb.maxX - bb.minX
  const h = bb.maxY - bb.minY
  if (w === 0 || h === 0) return points
  return points.map(p => ({
    x: p.x * (SQUARE_SIZE / w),
    y: p.y * (SQUARE_SIZE / h),
  }))
}

function translateToOrigin(points: Point[]): Point[] {
  const c = centroid(points)
  return points.map(p => ({ x: p.x - c.x, y: p.y - c.y }))
}

function pathDistance(a: Point[], b: Point[]): number {
  let d = 0
  for (let i = 0; i < Math.min(a.length, b.length); i++) {
    d += dist(a[i], b[i])
  }
  return d / Math.min(a.length, b.length)
}

function dist(a: Point, b: Point): number {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
}

function pathLength(points: Point[]): number {
  let d = 0
  for (let i = 1; i < points.length; i++) d += dist(points[i - 1], points[i])
  return d
}

function centroid(points: Point[]): Point {
  let x = 0, y = 0
  for (const p of points) { x += p.x; y += p.y }
  return { x: x / points.length, y: y / points.length }
}

function boundingBox(points: Point[]) {
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
  for (const p of points) {
    if (p.x < minX) minX = p.x
    if (p.y < minY) minY = p.y
    if (p.x > maxX) maxX = p.x
    if (p.y > maxY) maxY = p.y
  }
  return { minX, minY, maxX, maxY }
}

function rotateBy(points: Point[], angle: number): Point[] {
  const c = centroid(points)
  const cos = Math.cos(angle)
  const sin = Math.sin(angle)
  return points.map(p => ({
    x: (p.x - c.x) * cos - (p.y - c.y) * sin + c.x,
    y: (p.x - c.x) * sin + (p.y - c.y) * cos + c.y,
  }))
}

function normalize(points: Point[]): Point[] {
  let p = resample(points, N)
  p = rotateToZero(p)
  p = scaleToSquare(p)
  p = translateToOrigin(p)
  return p
}

// generate a template from a set of points defined as relative coordinates
function makeTemplate(coords: [number, number][]): Point[] {
  const points = coords.map(([x, y]) => ({ x, y }))
  return normalize(points)
}

// letter templates defined as coordinate paths
// drawn at roughly 100x100 scale
const LETTER_TEMPLATES: { letter: string; points: Point[] }[] = []

function addTemplate(letter: string, coords: [number, number][]) {
  LETTER_TEMPLATES.push({ letter, points: makeTemplate(coords) })
}

// uppercase printing forms
addTemplate('A', [[0,100],[50,0],[100,100],[75,50],[25,50]])
addTemplate('A', [[50,0],[0,100],[0,100],[25,50],[75,50],[100,100]])
addTemplate('B', [[0,100],[0,0],[80,0],[80,40],[0,50],[80,50],[80,100],[0,100]])
addTemplate('C', [[100,10],[50,0],[0,30],[0,70],[50,100],[100,90]])
addTemplate('D', [[0,100],[0,0],[70,0],[100,30],[100,70],[70,100],[0,100]])
addTemplate('E', [[100,0],[0,0],[0,50],[70,50],[0,50],[0,100],[100,100]])
addTemplate('F', [[100,0],[0,0],[0,50],[70,50],[0,50],[0,100]])
addTemplate('G', [[100,10],[50,0],[0,30],[0,70],[50,100],[100,70],[100,50],[50,50]])
addTemplate('H', [[0,0],[0,100],[0,50],[100,50],[100,0],[100,100]])
addTemplate('I', [[50,0],[50,100]])
addTemplate('J', [[80,0],[80,80],[50,100],[20,80]])
addTemplate('K', [[0,0],[0,100],[0,50],[100,0],[0,50],[100,100]])
addTemplate('L', [[0,0],[0,100],[100,100]])
addTemplate('M', [[0,100],[0,0],[50,40],[100,0],[100,100]])
addTemplate('N', [[0,100],[0,0],[100,100],[100,0]])
addTemplate('O', [[50,0],[0,30],[0,70],[50,100],[100,70],[100,30],[50,0]])
addTemplate('P', [[0,100],[0,0],[80,0],[80,50],[0,50]])
addTemplate('Q', [[50,0],[0,30],[0,70],[50,100],[100,70],[100,30],[50,0],[80,80],[100,100]])
addTemplate('R', [[0,100],[0,0],[80,0],[80,50],[0,50],[100,100]])
addTemplate('S', [[100,10],[60,0],[20,10],[0,30],[20,45],[80,55],[100,70],[80,90],[40,100],[0,90]])
addTemplate('T', [[0,0],[100,0],[50,0],[50,100]])
addTemplate('U', [[0,0],[0,80],[30,100],[70,100],[100,80],[100,0]])
addTemplate('V', [[0,0],[50,100],[100,0]])
addTemplate('W', [[0,0],[25,100],[50,40],[75,100],[100,0]])
addTemplate('X', [[0,0],[100,100],[50,50],[100,0],[0,100]])
addTemplate('Y', [[0,0],[50,50],[100,0],[50,50],[50,100]])
addTemplate('Z', [[0,0],[100,0],[0,100],[100,100]])

// lowercase variants that differ from uppercase
addTemplate('A', [[80,40],[60,20],[30,20],[10,40],[10,70],[30,90],[60,90],[80,70],[80,40],[80,90]])  // circle+stick a
addTemplate('B', [[10,0],[10,100],[10,60],[40,50],[70,60],[70,80],[40,100],[10,100]])  // round b
addTemplate('D', [[80,0],[80,100],[80,60],[50,50],[20,60],[20,80],[50,100],[80,100]])  // round d
addTemplate('E', [[70,50],[20,50],[10,40],[20,20],[50,15],[80,25],[80,50],[60,70],[30,80],[10,65]])  // curly e
addTemplate('G', [[80,40],[60,20],[30,20],[10,40],[10,70],[30,90],[60,90],[80,70],[80,100]])  // g with tail
addTemplate('N', [[10,100],[10,40],[10,30],[40,30],[70,40],[70,100]])  // lowercase n hump
addTemplate('R', [[10,100],[10,40],[30,30],[60,35]])  // lowercase r

// single-stroke S variants (people draw S many ways)
addTemplate('S', [[80,15],[50,0],[15,15],[10,35],[40,50],[70,55],[90,70],[85,90],[50,100],[15,85]])
addTemplate('S', [[70,0],[30,0],[10,20],[10,40],[50,50],[90,60],[90,80],[70,100],[30,100]])

export function recognizeStroke(strokes: Stroke[]): RecognitionCandidate[] {
  if (strokes.length === 0 || strokes.every(s => s.length < 3)) return []

  // combine all strokes into one path (for multi-stroke letters)
  const combined: Point[] = []
  for (const stroke of strokes) {
    for (const pt of stroke) {
      combined.push({ x: pt.x, y: pt.y })
    }
  }

  if (combined.length < 3) return []

  const input = normalize(combined)

  const scores: { letter: string; distance: number }[] = []
  for (const tmpl of LETTER_TEMPLATES) {
    const d = pathDistance(input, tmpl.points)
    scores.push({ letter: tmpl.letter, distance: d })
  }

  // keep best score per letter
  const best = new Map<string, number>()
  for (const s of scores) {
    const prev = best.get(s.letter)
    if (prev === undefined || s.distance < prev) {
      best.set(s.letter, s.distance)
    }
  }

  // convert distance to confidence (lower distance = higher confidence)
  // typical distances range from 0 (perfect) to ~150 (bad match)
  const maxDist = 120
  const candidates = Array.from(best.entries())
    .map(([letter, distance]) => ({
      letter,
      confidence: Math.max(0, 1 - distance / maxDist),
    }))
    .filter(c => c.confidence > 0.05)
    .sort((a, b) => b.confidence - a.confidence)
    .slice(0, 5)

  return candidates
}
