/**
 * Pure paint math — no THREE / Rapier / DOM imports, so it is unit-testable in a
 * bare node runtime (see coverage-math.test.ts). Everything that touches only
 * numbers lives here: the exact pixel-bucket coverage count and the
 * coverage -> BlobModifiers buff mapping (GDD §6 MVP subset).
 */
import type { BlobModifiers, CoverageReport, PaintColor } from '../contracts'

/**
 * Canonical colour ordering. Index 0 in the mask is the unpainted base; palette
 * colours occupy indices 1..7 in this order (matches the PALETTE key order in
 * contracts.ts). Keep this list and the mask encoding in lockstep.
 */
export const COLOR_ORDER: readonly PaintColor[] = [
  'red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink',
]

/** Mask palette index for a colour (1..7). Base/unpainted is 0. */
export function colorIndex(color: PaintColor): number {
  return COLOR_ORDER.indexOf(color) + 1
}

/**
 * Exact integer bucketing of a quantized mask. `mask[i]` is a palette index
 * (0 = base, 1..n = colour). Returns counts of length n+1 where counts[0] is the
 * unpainted pixel count. This is the whole point of quantizing on stamp: coverage
 * is an exact histogram, never a fuzzy nearest-colour match.
 */
export function countBuckets(mask: Uint8Array, numColors: number): number[] {
  const counts = new Array<number>(numColors + 1).fill(0)
  for (let i = 0; i < mask.length; i++) counts[mask[i]]++
  return counts
}

/**
 * Turn raw bucket counts into a CoverageReport (fractions of TOTAL surface;
 * unpainted counts in the denominator, per contracts).
 */
export function coverageReportFromCounts(
  counts: number[],
  totalPixels: number,
): CoverageReport {
  const byColor = {} as Record<PaintColor, number>
  for (let i = 0; i < COLOR_ORDER.length; i++) {
    byColor[COLOR_ORDER[i]] = totalPixels > 0 ? counts[i + 1] / totalPixels : 0
  }
  const painted = totalPixels > 0 ? (totalPixels - counts[0]) / totalPixels : 0
  return { total: painted, byColor }
}

// ---- Buff thresholds (GDD §5.1 / §6 MVP subset) ---------------------------
export const ACTIVATE = 0.20 // buff activates at ≥20% of that colour
export const SUPER = 0.70 // ≥70% one colour = super state
/** Strength floor at the activation threshold so the buff visibly "kicks in". */
const ACTIVATE_FLOOR = 0.15

// ---- Scale buffs (GDD §5.3 / §6): purple MEGA + pink MINI (Lane G) ---------
// Both drive `modifiers.size` (the controller scales collider + visuals) and
// bias `massMul` on top of the paint=weight base. They oppose each other: a blob
// carrying both purple and pink nets out toward normal size (they "fight").
/** Uniform scale added at full purple strength: 1.0 → 1.45 (grow, blocks/plates). */
export const MEGA_SIZE = 0.45
/** Uniform scale removed at full pink strength: 1.0 → 0.62 (shrink, tunnels). */
export const MINI_SIZE = 0.38
/** Extra massMul at full purple strength — MEGA is heavy beyond its paint total. */
export const MEGA_MASS = 1.5
/** massMul removed at full pink strength — MINI is lighter than clean at high %. */
export const MINI_MASS = 0.9
/** Lower bound so a fully-MINI blob stays positively massed (flingable, not zero). */
export const MASS_FLOOR = 0.35

const clamp01 = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)

/**
 * Buff strength 0..1 for a single colour's coverage fraction.
 * 0 below the 20% threshold; ramps ACTIVATE_FLOOR..1 across 20%→70%; pinned at 1
 * once super. This is the "strength scales linearly between thresholds" curve.
 */
export function buffStrength(coverage: number): number {
  if (coverage < ACTIVATE) return 0
  if (coverage >= SUPER) return 1
  const t = (coverage - ACTIVATE) / (SUPER - ACTIVATE)
  return ACTIVATE_FLOOR + (1 - ACTIVATE_FLOOR) * t
}

/** Is any single colour at/above the super threshold? */
export function superColor(cov: CoverageReport): PaintColor | null {
  let best: PaintColor | null = null
  let bestV = SUPER
  for (const c of COLOR_ORDER) {
    if (cov.byColor[c] >= bestV) {
      bestV = cov.byColor[c]
      best = c
    }
  }
  return best
}

/** Dominant (highest-coverage) colour, or null if nothing is painted. */
export function dominantColor(cov: CoverageReport): PaintColor | null {
  let best: PaintColor | null = null
  let bestV = 0
  for (const c of COLOR_ORDER) {
    if (cov.byColor[c] > bestV) {
      bestV = cov.byColor[c]
      best = c
    }
  }
  return best
}

/**
 * The core mapping: coverage -> modifiers (GDD §6).
 *   RED    -> speedMul up to 1.6 (BURN)
 *   GREEN  -> grip up to 1.0     (GRIP)
 *   BLUE   -> waterproof         (SLICK)
 *   PURPLE -> size up to 1.45 + extra mass (MEGA — grow big & heavy, §5.3)
 *   PINK   -> size down to 0.62 + less mass (MINI — shrink small & light)
 *   total coverage -> massMul base 1.0..1.8 (paint = weight, §5.2)
 *   any colour ≥70% -> super: that buff pinned at max + glow
 * Pure and deterministic; the visual side-effects (ember trail, emissive pulse)
 * live in BuffSystem and read `glow` / dominant colour off this result + cov.
 */
export function computeModifiers(cov: CoverageReport): BlobModifiers {
  const red = cov.byColor.red
  const green = cov.byColor.green
  const blue = cov.byColor.blue
  const purpleS = buffStrength(cov.byColor.purple)
  const pinkS = buffStrength(cov.byColor.pink)

  const speedMul = 1 + 0.6 * buffStrength(red)
  const grip = buffStrength(green) // 0..1
  const waterproof = blue >= ACTIVATE

  // ---- scale (MEGA vs MINI) ----
  // net grow/shrink strength; purple and pink oppose so a mixed blob nets out.
  const net = purpleS - pinkS
  const size = net >= 0 ? 1 + MEGA_SIZE * net : 1 + MINI_SIZE * net

  // ---- mass (paint=weight base, biased by MEGA heavier / MINI lighter) ----
  const massMul = Math.max(
    MASS_FLOOR,
    1 + 0.8 * clamp01(cov.total) + MEGA_MASS * purpleS - MINI_MASS * pinkS,
  )

  const glow = superColor(cov) ? 1 : 0

  return {
    speedMul,
    jumpMul: 1,
    massMul,
    grip,
    size,
    waterproof,
    glow,
  }
}
