import { buildDefaultClips } from "../anim/procedural.js";
import { exportGlb, meshStatistics } from "../export/glb.js";
import { appendMesh, computeFlatNormals, createMesh, triangleCount, vertexCount } from "../mesh/MeshBuilder.js";
import { buildSkeleton } from "../rig/skeleton.js";
import { computeInverseBindMatrices, computeSkinWeights } from "../rig/skin.js";
import { buildBaseBody } from "./body.js";
import { normalizeBodyStyle } from "./bodyStyle.js";
import { normalizeHeadStyle } from "./headStyle.js";
import { applyCoverage, type CoverageBand } from "./coverage.js";
import { PART_CATALOGUE } from "./parts.js";
import { measurementsFor } from "./proportions.js";
import type { AssembledCharacter, CharacterRecipe, PartContext, PartSlot } from "./types.js";

const SLOT_ORDER: PartSlot[] = ["lower", "upper", "feet", "hair", "accessory"];

/**
 * Assemble a character from a modular recipe.
 *
 *   base body (male|female) + equipped parts → single skinned mesh → GLB
 */
export function assembleCharacter(recipe: CharacterRecipe): AssembledCharacter {
  const measurements = measurementsFor(recipe.body);
  const skeleton = buildSkeleton(measurements, { height: recipe.height });
  const bodyStyle = normalizeBodyStyle(recipe.bodyStyle);
  const headStyle = normalizeHeadStyle(recipe.headStyle);

  // Resolve the outfit first: the body is built knowing what will cover it, so
  // the skin underneath can be deleted rather than fought with padding.
  const ctx: PartContext = {
    skeleton,
    body: recipe.body,
    palette: recipe.palette,
    bodyStyle,
    headStyle,
  };
  const worn: { def: (typeof PART_CATALOGUE) extends ReadonlyMap<string, infer D> ? D : never; id: string }[] = [];
  const coverage: CoverageBand[] = [];

  for (const slot of SLOT_ORDER) {
    const partId = recipe.parts[slot];
    if (!partId || partId.endsWith(".none")) continue;

    const def = PART_CATALOGUE.get(partId);
    if (!def) throw new Error(`Unknown part "${partId}" on recipe "${recipe.id}"`);
    if (def.slot !== slot) throw new Error(`Part "${partId}" is slot ${def.slot}, expected ${slot}`);
    if (def.body && def.body !== recipe.body) {
      throw new Error(`Part "${partId}" is only valid on ${def.body} bodies`);
    }
    worn.push({ def, id: partId });
    if (def.coverage) coverage.push(...def.coverage);
  }

  const mesh = createMesh();
  const skin = buildBaseBody(skeleton, recipe.body, recipe.palette, bodyStyle, headStyle);
  const hidden = applyCoverage(skin, coverage, { recessDepth: skeleton.height * 0.009 });
  appendMesh(mesh, hidden.mesh);

  for (const { def, id } of worn) {
    appendMesh(mesh, def.build({
      ...ctx,
      ...(recipe.partColors?.[id] ? { color: recipe.partColors[id] } : {}),
    }));
  }

  computeFlatNormals(mesh);
  computeSkinWeights(mesh, skeleton);
  const inverseBindMatrices = computeInverseBindMatrices(skeleton);
  const clips = buildDefaultClips();
  const glb = exportGlb(mesh, skeleton, inverseBindMatrices, {
    name: recipe.name,
    clips,
    generator: "psx-adventure-engine character creator",
  });

  const meshStats = meshStatistics(mesh);

  return {
    recipe,
    skeleton,
    mesh,
    glb,
    stats: {
      vertices: meshStats.vertices,
      triangles: meshStats.triangles,
      bones: skeleton.joints.length,
      clips: clips.length,
      skinHidden: hidden.dropped,
      skinRecessed: hidden.recessed,
    },
  };
}

export function formatAssemblyReport(result: AssembledCharacter): string {
  const { recipe, stats } = result;
  const parts = Object.entries(recipe.parts)
    .filter(([, id]) => id && !id.endsWith(".none"))
    .map(([slot, id]) => `${slot}=${id}`)
    .join(", ");

  const style = normalizeBodyStyle(recipe.bodyStyle);
  const face = normalizeHeadStyle(recipe.headStyle);
  return [
    `  body       ${recipe.body} @ ${recipe.height.toFixed(2)}m`,
    `  physique   mass=${style.mass.toFixed(2)} muscle=${style.muscle.toFixed(2)} fat=${style.fat.toFixed(2)}`,
    `  face       length=${face.length.toFixed(2)} jaw=${face.jaw.toFixed(2)} brow=${face.brow.toFixed(2)}`,
    `  parts      ${parts || "(bare)"}`,
    `  mesh       ${stats.vertices} vertices, ${stats.triangles} triangles`,
    `  skin       ${stats.skinHidden} triangles hidden under clothing, ${stats.skinRecessed} recessed`,
    `  rig        ${stats.bones} bones, ${stats.clips} clips`,
  ].join("\n");
}

// Re-export for tests / diagnostics.
export { vertexCount, triangleCount };
