/**
 * Chart utility functions
 * @countermeasure/web-components/charts/utils
 */

/** Create a linear scale mapping from domain to range */
export function scaleLinear(
  domain: [number, number],
  range: [number, number]
): (value: number) => number {
  const [d0, d1] = domain
  const [r0, r1] = range
  const domainSpan = d1 - d0
  const rangeSpan = r1 - r0

  return (value: number): number => {
    if (domainSpan === 0) return r0
    return r0 + ((value - d0) / domainSpan) * rangeSpan
  }
}

/** Band scale result with callable signature */
export interface ScaleBand {
  (value: string): number
  bandwidth(): number
  domain(): string[]
}

/** Create a band (categorical) scale mapping discrete values to a continuous range */
export function scaleBand(domain: string[], range: [number, number], padding = 0): ScaleBand {
  const [r0, r1] = range
  const n = domain.length
  const totalRange = r1 - r0
  const paddingTotal = n > 0 ? padding * (n + 1) : 0
  const step = n > 0 ? (totalRange - paddingTotal) / n : 0
  const bandWidth = Math.max(0, step)

  const lookup = new Map<string, number>()
  for (let i = 0; i < domain.length; i++) {
    const key = domain[i]
    if (key !== undefined) {
      lookup.set(key, r0 + padding + i * (bandWidth + padding))
    }
  }

  const scale = ((value: string): number => {
    return lookup.get(value) ?? r0
  }) as ScaleBand

  scale.bandwidth = (): number => bandWidth
  scale.domain = (): string[] => [...domain]

  return scale
}

/** Return a palette of 12 distinct chart colors */
export function defaultColors(): string[] {
  return [
    '#3b82f6', // blue
    '#ef4444', // red
    '#22c55e', // green
    '#f59e0b', // amber
    '#8b5cf6', // violet
    '#06b6d4', // cyan
    '#ec4899', // pink
    '#f97316', // orange
    '#14b8a6', // teal
    '#a855f7', // purple
    '#84cc16', // lime
    '#6366f1', // indigo
  ]
}

/** Convert a hex color string to an rgba string */
export function hexToRgba(hex: string, alpha: number): string {
  const cleaned = hex.replace('#', '')
  const r = parseInt(cleaned.substring(0, 2), 16)
  const g = parseInt(cleaned.substring(2, 4), 16)
  const b = parseInt(cleaned.substring(4, 6), 16)
  return `rgba(${String(r)}, ${String(g)}, ${String(b)}, ${String(alpha)})`
}

/** Clamp a value to a min/max range */
export function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max)
}

/** Format a number with K/M/B suffixes for compact display */
export function formatNumber(value: number): string {
  const abs = Math.abs(value)
  if (abs >= 1_000_000_000) {
    return `${(value / 1_000_000_000).toFixed(1)}B`
  }
  if (abs >= 1_000_000) {
    return `${(value / 1_000_000).toFixed(1)}M`
  }
  if (abs >= 1_000) {
    return `${(value / 1_000).toFixed(1)}K`
  }
  return String(Number(value.toFixed(2)))
}

/** Compute nice axis bounds with evenly spaced ticks */
export function niceScale(
  min: number,
  max: number,
  tickCount: number
): { min: number; max: number; step: number } {
  if (min === max) {
    return { min: min - 1, max: max + 1, step: 1 }
  }

  const range = max - min
  const roughStep = range / Math.max(1, tickCount - 1)

  const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)))
  const normalized = roughStep / magnitude

  let niceStep: number
  if (normalized <= 1) {
    niceStep = 1 * magnitude
  } else if (normalized <= 2) {
    niceStep = 2 * magnitude
  } else if (normalized <= 5) {
    niceStep = 5 * magnitude
  } else {
    niceStep = 10 * magnitude
  }

  const niceMin = Math.floor(min / niceStep) * niceStep
  const niceMax = Math.ceil(max / niceStep) * niceStep

  return { min: niceMin, max: niceMax, step: niceStep }
}

/** Parse a hex color string into [r, g, b] components */
function parseHex(hex: string): [number, number, number] {
  const cleaned = hex.replace('#', '')
  return [
    parseInt(cleaned.substring(0, 2), 16),
    parseInt(cleaned.substring(2, 4), 16),
    parseInt(cleaned.substring(4, 6), 16),
  ]
}

/** Linearly interpolate between two hex colors */
export function lerpColor(color1: string, color2: string, t: number): string {
  const [r1, g1, b1] = parseHex(color1)
  const [r2, g2, b2] = parseHex(color2)

  const r = Math.round(r1 + (r2 - r1) * t)
  const g = Math.round(g1 + (g2 - g1) * t)
  const b = Math.round(b1 + (b2 - b1) * t)

  const toHex = (v: number): string => v.toString(16).padStart(2, '0')
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}

/** Convert polar coordinates to cartesian */
export function polarToCartesian(
  cx: number,
  cy: number,
  radius: number,
  angleDeg: number
): { x: number; y: number } {
  const angleRad = ((angleDeg - 90) * Math.PI) / 180
  return {
    x: cx + radius * Math.cos(angleRad),
    y: cy + radius * Math.sin(angleRad),
  }
}

/**
 * Draw a rounded rectangle path on a canvas context.
 * Does not fill or stroke - caller should do that after calling this.
 */
export function roundRect(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  w: number,
  h: number,
  r: number
): void {
  const radius = Math.min(r, w / 2, h / 2)
  ctx.beginPath()
  ctx.moveTo(x + radius, y)
  ctx.lineTo(x + w - radius, y)
  ctx.quadraticCurveTo(x + w, y, x + w, y + radius)
  ctx.lineTo(x + w, y + h - radius)
  ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h)
  ctx.lineTo(x + radius, y + h)
  ctx.quadraticCurveTo(x, y + h, x, y + h - radius)
  ctx.lineTo(x, y + radius)
  ctx.quadraticCurveTo(x, y, x + radius, y)
  ctx.closePath()
}

/** Resolve a color from a palette by index, with a default fallback */
export function resolveColor(index: number, colors?: string[], fallback?: string): string {
  if (colors !== undefined && colors.length > 0) {
    return colors[index % colors.length] ?? fallback ?? '#3b82f6'
  }
  const palette = defaultColors()
  return palette[index % palette.length] ?? fallback ?? '#3b82f6'
}

/** Draw a filled rounded bar on a canvas context */
export function drawFilledBar(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  w: number,
  h: number,
  radius: number,
  color: string,
  hovered = false
): void {
  ctx.beginPath()
  const r = Math.min(radius, w / 2, h / 2)
  if (r > 0) {
    ctx.moveTo(x + r, y)
    ctx.lineTo(x + w - r, y)
    ctx.arcTo(x + w, y, x + w, y + r, r)
    ctx.lineTo(x + w, y + h - r)
    ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
    ctx.lineTo(x + r, y + h)
    ctx.arcTo(x, y + h, x, y + h - r, r)
    ctx.lineTo(x, y + r)
    ctx.arcTo(x, y, x + r, y, r)
  } else {
    ctx.rect(x, y, w, h)
  }
  ctx.closePath()
  ctx.fillStyle = hovered ? hexToRgba(color, 0.75) : color
  ctx.fill()
}

/** Test whether a point (mx, my) is inside a rectangle */
export function hitTestRect(
  mx: number,
  my: number,
  rect: { x: number; y: number; width: number; height: number }
): boolean {
  return mx >= rect.x && mx <= rect.x + rect.width && my >= rect.y && my <= rect.y + rect.height
}

/** Find the closest point to (mx, my) within a threshold distance */
export function findClosestPoint<T extends { px: number; py: number }>(
  mx: number,
  my: number,
  points: T[],
  threshold: number
): { point: T; dist: number } | null {
  let closest: T | null = null
  let closestDist = Infinity

  for (const pt of points) {
    const dist = Math.sqrt((mx - pt.px) ** 2 + (my - pt.py) ** 2)
    if (dist < closestDist) {
      closestDist = dist
      closest = pt
    }
  }

  if (closest !== null && closestDist < threshold) {
    return { point: closest, dist: closestDist }
  }
  return null
}

/** Point with pixel coordinates for smooth path drawing */
export interface PathPoint {
  px: number
  py: number
}

/**
 * Draw a smooth Catmull-Rom spline path through the given points.
 * Assumes ctx.moveTo has already been called to the first point.
 * Uses bezier curves for smooth interpolation.
 *
 * @param ctx Canvas 2D rendering context
 * @param points Array of points with px/py pixel coordinates
 * @param tension Controls curve tightness (default 0.5)
 */
export function drawSmoothPath(
  ctx: CanvasRenderingContext2D,
  points: PathPoint[],
  tension = 0.5
): void {
  for (let i = 0; i < points.length - 1; i++) {
    const p0 = points[Math.max(0, i - 1)]
    const p1 = points[i]
    const p2 = points[i + 1]
    const p3 = points[Math.min(points.length - 1, i + 2)]

    if (p0 === undefined || p1 === undefined || p2 === undefined || p3 === undefined) {
      continue
    }

    const cp1x = p1.px + ((p2.px - p0.px) * tension) / 3
    const cp1y = p1.py + ((p2.py - p0.py) * tension) / 3
    const cp2x = p2.px - ((p3.px - p1.px) * tension) / 3
    const cp2y = p2.py - ((p3.py - p1.py) * tension) / 3

    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.px, p2.py)
  }
}
