import {
  createMesh,
  padTags,
  vertexCount,
  type LoftResult,
  type MeshData,
} from "../mesh/MeshBuilder.js";

/**
 * Hiding skin under clothing.
 *
 * Ported from Ludus `equipment/hideMasks.js` + `recessField.js`, which in turn
 * came from Dreamfall's `bodyHideUnderOutfit`. The idea: don't try to make
 * every garment clear the body by padding — padding fails wherever the two
 * lofts sample their rings at different heights, and it costs triangles nobody
 * ever sees. Instead the garment *claims* the skin it covers, and that skin is
 * deleted. The outer mesh then owns the form outright.
 *
 * Three classes, in Ludus's precedence order:
 *
 *   DROP    triangle fully enclosed by a garment — removed from the body
 *   RECESS  triangle covered but within a rim of an opening (collar, cuff,
 *           hem) — kept, but pushed inward so it reads as skin *inside* the
 *           opening rather than z-fighting the cloth
 *   KEEP    nothing covers it
 *
 * Limbs drop rather than recess: a recess deep enough to read on a torso
 * punches straight through the far side of a forearm.
 *
 * Coverage is a claim that nothing shows through. Only closed shells belong in
 * a coverage list — an open or banded piece that claims skin leaves you looking
 * at the inside of the character.
 */

export const PART = {
  NONE: 0,
  HEAD: 1,
  NECK: 2,
  TORSO: 3,
  ARM_L: 4,
  ARM_R: 5,
  LEG_L: 6,
  LEG_R: 7,
  FOOT_L: 8,
  FOOT_R: 9,
} as const;

export type BodyPart = (typeof PART)[keyof typeof PART];

/** Limbs drop outright; only the torso and head recess. */
const LIMB_PARTS = new Set<number>([
  PART.ARM_L,
  PART.ARM_R,
  PART.LEG_L,
  PART.LEG_R,
  PART.FOOT_L,
  PART.FOOT_R,
]);

/** A band of one body region's loft, `tMin`..`tMax` in loft parameter. */
export interface CoverageBand {
  part: BodyPart;
  tMin: number;
  tMax: number;
}

export const CLASS = { KEEP: 0, RECESS: 1, DROP: 2 } as const;

/** Tag a loft's vertices: `t` runs 0 at the first ring to 1 at the last. */
export function tagLoft(mesh: MeshData, result: LoftResult, part: BodyPart): void {
  const { firstVertex, rings, sides } = result;
  if (rings < 1 || sides < 1) return;
  padTags(mesh, firstVertex);
  const span = Math.max(1, rings - 1);
  for (let ring = 0; ring < rings; ring++) {
    const t = ring / span;
    for (let s = 0; s < sides; s++) {
      const v = firstVertex + ring * sides + s;
      mesh.parts[v] = part;
      mesh.ts[v] = t;
    }
  }
}

/** Tag everything added since `from` with one region and a fixed parameter. */
export function tagRange(mesh: MeshData, from: number, part: BodyPart, t: number): void {
  padTags(mesh, from);
  for (let v = from; v < vertexCount(mesh); v++) {
    mesh.parts[v] = part;
    mesh.ts[v] = t;
  }
}

export interface CoverageOptions {
  /**
   * How close to the edge of coverage a vertex must be to recess instead of
   * drop, in loft parameter. Roughly one ring on the torso.
   */
  rimBand?: number;
  /** How far inward recessed skin is pushed, in metres. */
  recessDepth?: number;
  /** Graph-distance rings over which the recess ramps up from the seam. */
  recessRings?: number;
}

export interface CoverageResult {
  mesh: MeshData;
  kept: number;
  recessed: number;
  dropped: number;
}

/**
 * Remove the skin a garment set covers, and recess what sits inside its
 * openings. Returns a fresh mesh; the input is untouched.
 */
export function applyCoverage(
  body: MeshData,
  bands: CoverageBand[],
  options: CoverageOptions = {},
): CoverageResult {
  const triangles = body.indices.length / 3;
  if (bands.length === 0) {
    return { mesh: body, kept: triangles, recessed: 0, dropped: 0 };
  }

  const rimBand = options.rimBand ?? 0.09;
  const recessDepth = options.recessDepth ?? 0.014;
  const recessRings = options.recessRings ?? 2;

  const n = vertexCount(body);
  padTags(body, n);

  const covers = (part: number, t: number): boolean => {
    for (const b of bands) if (part === b.part && t >= b.tMin && t <= b.tMax) return true;
    return false;
  };

  const covered = new Uint8Array(n);
  const nearRim = new Uint8Array(n);
  const isLimb = new Uint8Array(n);
  for (let i = 0; i < n; i++) {
    const part = body.parts[i]!;
    const t = body.ts[i]!;
    isLimb[i] = LIMB_PARTS.has(part) ? 1 : 0;
    if (part === PART.NONE || !covers(part, t)) continue;
    covered[i] = 1;
    // Just inside an opening: coverage would lapse a rim's width away.
    if (!covers(part, t - rimBand) || !covers(part, t + rimBand)) nearRim[i] = 1;
  }

  const classes = new Uint8Array(triangles);
  for (let t = 0; t < triangles; t++) {
    const a = body.indices[t * 3]!;
    const b = body.indices[t * 3 + 1]!;
    const c = body.indices[t * 3 + 2]!;
    const cov = covered[a]! + covered[b]! + covered[c]!;
    if (cov === 0) {
      classes[t] = CLASS.KEEP;
      continue;
    }
    const rim = nearRim[a]! || nearRim[b]! || nearRim[c]!;
    const limb = isLimb[a]! || isLimb[b]! || isLimb[c]!;
    if (cov === 3 && !rim) classes[t] = CLASS.DROP;
    else if (limb && !rim) classes[t] = CLASS.DROP;
    else classes[t] = CLASS.RECESS;
  }

  // Vertices touched by kept geometry pin the recess to depth 0, so the
  // recessed sheet stays welded to the visible body instead of floating inside
  // the collar as a detached shell.
  const onKept = new Uint8Array(n);
  const inRecess = new Uint8Array(n);
  for (let t = 0; t < triangles; t++) {
    const target = classes[t] === CLASS.KEEP ? onKept : classes[t] === CLASS.RECESS ? inRecess : null;
    if (!target) continue;
    target[body.indices[t * 3]!] = 1;
    target[body.indices[t * 3 + 1]!] = 1;
    target[body.indices[t * 3 + 2]!] = 1;
  }

  const depth = recessField(body, inRecess, onKept, recessDepth, recessRings);

  const out = createMesh();
  const remap = new Int32Array(n).fill(-1);
  const emit = (v: number): number => {
    const existing = remap[v]!;
    if (existing >= 0) return existing;
    const index = vertexCount(out);
    const d = depth[v]!;
    out.positions.push(
      body.positions[v * 3]! - body.normals[v * 3]! * d,
      body.positions[v * 3 + 1]! - body.normals[v * 3 + 1]! * d,
      body.positions[v * 3 + 2]! - body.normals[v * 3 + 2]! * d,
    );
    out.normals.push(body.normals[v * 3]!, body.normals[v * 3 + 1]!, body.normals[v * 3 + 2]!);
    out.colors.push(
      body.colors[v * 4]!,
      body.colors[v * 4 + 1]!,
      body.colors[v * 4 + 2]!,
      body.colors[v * 4 + 3]!,
    );
    if (body.uvs.length > 0) out.uvs.push(body.uvs[v * 2] ?? 0, body.uvs[v * 2 + 1] ?? 0);
    out.parts.push(body.parts[v]!);
    out.ts.push(body.ts[v]!);
    remap[v] = index;
    return index;
  };

  let kept = 0;
  let recessed = 0;
  for (let t = 0; t < triangles; t++) {
    if (classes[t] === CLASS.DROP) continue;
    if (classes[t] === CLASS.KEEP) kept++;
    else recessed++;
    out.indices.push(
      emit(body.indices[t * 3]!),
      emit(body.indices[t * 3 + 1]!),
      emit(body.indices[t * 3 + 2]!),
    );
  }

  return { mesh: out, kept, recessed, dropped: triangles - kept - recessed };
}

/**
 * Per-vertex recess depth: 0 on any vertex shared with kept surface, ramping to
 * full depth over `rings` steps of graph distance. A constant offset instead of
 * a ramp gives either a hard step at the collar or a detached sheet.
 *
 * Graph distance over mesh edges, not geodesic — at this triangle count the
 * difference is invisible.
 */
function recessField(
  mesh: MeshData,
  inRecess: Uint8Array,
  boundary: Uint8Array,
  depth: number,
  rings: number,
): Float32Array {
  const n = vertexCount(mesh);
  const field = new Float32Array(n);
  if (depth <= 0) return field;

  const neighbours: number[][] = Array.from({ length: n }, () => []);
  for (let t = 0; t < mesh.indices.length; t += 3) {
    const a = mesh.indices[t]!;
    const b = mesh.indices[t + 1]!;
    const c = mesh.indices[t + 2]!;
    neighbours[a]!.push(b, c);
    neighbours[b]!.push(a, c);
    neighbours[c]!.push(a, b);
  }

  const dist = new Int32Array(n).fill(-1);
  const queue: number[] = [];
  for (let i = 0; i < n; i++) {
    if (boundary[i]) {
      dist[i] = 0;
      queue.push(i);
    }
  }
  for (let head = 0; head < queue.length; head++) {
    const v = queue[head]!;
    const d = dist[v]!;
    if (d >= rings) continue;
    for (const nb of neighbours[v]!) {
      if (dist[nb] !== -1) continue;
      dist[nb] = d + 1;
      queue.push(nb);
    }
  }

  for (let i = 0; i < n; i++) {
    if (!inRecess[i]) continue;
    // Unreached vertices sit deep inside the covered area: full depth.
    const d = dist[i]!;
    const u = d === -1 ? 1 : Math.min(1, d / rings);
    field[i] = depth * (u * u * (3 - 2 * u));
  }
  return field;
}
