/**
 * Plants. A small one for a desk or a sill, and a tall one for a corner.
 *
 * ### The shard problem, and what fixed it
 *
 * Every leaf in here used to be a bare `P.panel()` — an untextured rectangle in
 * an opaque green material. That is fine in a thumbnail and catastrophic at eye
 * height, and the live LA studio proved it: walk into the courtyard and the
 * corner planting fills a third of the frame with flat green shards. It was the
 * worst-looking asset in the product and no amount of lighting work was ever
 * going to fix it, because the problem is the *silhouette* — a rectangle has the
 * wrong outline no matter how it is shaded.
 *
 * The fix is a coverage map. `foliage` now carries the `leafAlpha` cutout with
 * `alphaTest 0.5` (`materials.ts`), so the leaf-shaped part of each quad is
 * drawn and the rest is discarded. Three things follow from that, and all three
 * are why this is a cutout rather than a blend:
 *
 *  - the leaf still writes depth and still sorts like solid geometry, so a plant
 *    in front of a window does not have to be drawn in a particular order;
 *  - the shadow it casts is leaf-shaped, because three's depth material copies
 *    `alphaMap` and `alphaTest` across;
 *  - it survives `low` quality, because `leafAlpha` carries a resolution floor.
 *    "No maps" at low quality is a statement about *shading* cost; a cutout is
 *    one fetch and a discard, and the alternative at low quality is not a
 *    cheaper plant, it is the shard again.
 *
 * ### What the geometry still has to do
 *
 * A cutout only works if the quad's UVs run along the leaf. `leafAlpha` is drawn
 * tip-at-top with the stem at the bottom, so **+V must run from the base of the
 * quad to its tip**, which is exactly what `P.panel()` gives (0..1 over a
 * standing rectangle, base on the floor) — the panel was never the problem, the
 * missing map was.
 *
 * Beyond that, two changes make a card read as a frond rather than as a
 * postcard with a leaf printed on it. Blades are **curved**, by splitting each
 * one into two or three cards that each pick up a little more pitch, so the leaf
 * arches instead of standing dead straight; and each blade gets a small **roll**
 * so it is not edge-on flat to its own stem. Both are geometry the alpha map
 * cannot supply, and both cost one extra quad per leaf at most.
 *
 * Leaves stay cards rather than modelled solids: a modelled leaf is a hundred
 * triangles and forty of them is a plant nobody can afford in a room that also
 * has furniture in it.
 *
 * Neither asset takes a `colorKey`. A plant is the colour a plant is.
 */

import { defineAsset, type AssetContext } from "../kit.ts";
import type { SurfaceMaterial } from "../materials.ts";
import { MeshBin } from "../parts.ts";
import { clamp, jitter } from "./common.ts";

/** ~137.5°, the angle a real plant puts between successive leaves. */
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));

/**
 * One arching blade, as `segments` cards laid end to end.
 *
 * Exported because `studio.ts` plants a courtyard trough out of the same
 * blades: two files drawing foliage two different ways is how a library starts
 * looking like two libraries.
 *
 * Each card starts where the last one ended and carries a little more pitch, so
 * the blade curves over instead of leaving in a straight line. The arithmetic is
 * the only fiddly part: a card placed with `pitch` has its base at the given
 * point and its tip `length` away along the pitched direction, and pitch here
 * runs in the *yawed* frame — so `advance` steps along the plant's local −Z
 * (outward, in the blade's own yaw) and up by whatever the pitch leaves.
 *
 * `pitch` is measured from vertical, matching the rest of the file: 0 stands
 * straight up and π/2 lies flat.
 */
export function leafBlade(
  bin: MeshBin,
  ctx: AssetContext,
  material: SurfaceMaterial,
  b: {
    x?: number;
    y: number;
    z?: number;
    /** Total length along the blade. */
    length: number;
    /** Width of the widest card. Tapers toward the tip. */
    width: number;
    yaw: number;
    /** Pitch at the base, radians from vertical. */
    pitch: number;
    /** How much further the blade has arched over by its tip. */
    droop: number;
    roll?: number;
    segments: number;
  },
): void {
  const segments = Math.max(1, Math.round(b.segments));
  const step = b.length / segments;
  const sin = Math.sin(b.yaw);
  const cos = Math.cos(b.yaw);
  // Base of the current card, in the plant's own frame.
  let x = b.x ?? 0;
  let y = b.y;
  let z = b.z ?? 0;

  for (let i = 0; i < segments; i++) {
    const t = i / segments;
    const pitch = b.pitch + b.droop * t * t;
    // Cards narrow toward the tip; the last one is roughly half the first.
    const width = b.width * (1 - 0.42 * t);
    bin.add(ctx.parts.panel(), material, {
      x,
      y,
      z,
      size: [width, step, 1],
      yaw: b.yaw,
      pitch,
      roll: b.roll ?? 0,
    });
    // Where this card's tip is: `step` along the pitched direction, resolved
    // back into the plant's frame through the blade's yaw.
    const rise = Math.cos(pitch) * step;
    const reach = Math.sin(pitch) * step;
    y += rise;
    x -= sin * reach;
    z -= cos * reach;
  }
}

type PottedParams = {
  /** Overall height including the pot. */
  height: number;
  potDiameter: number;
  leaves: number;
};

export const plantPotted = defineAsset<PottedParams>({
  id: "tera:plant.potted",
  label: "Potted plant",
  defaults: { height: 0.6, potDiameter: 0.28, leaves: 16 },

  footprint(p) {
    // A plant is wider than its pot. The spread is what a passer-by brushes,
    // so it is the spread that layout should be told about.
    const spread = Math.max(p.potDiameter, (p.height - p.potDiameter * 0.6) * 0.9);
    return { width: spread, depth: spread, height: p.height };
  },

  build(p, ctx) {
    const P = ctx.parts;
    const bin = new MeshBin();
    const pot = ctx.materials.get("planter");
    const leaf = ctx.materials.get("foliage");

    const potH = clamp(p.height * 0.42, 0.1, 0.36);
    bin.add(P.cylinder(14), pot, { size: [p.potDiameter * 0.72, potH, p.potDiameter * 0.72] });
    bin.add(P.cylinder(14), pot, {
      y: potH - 0.03,
      size: [p.potDiameter, 0.03, p.potDiameter],
    });
    // The soil. A disc rather than nothing: without it you see straight down
    // into an open cylinder from the dollhouse camera.
    bin.add(P.disc(14), pot, {
      y: potH - 0.012,
      size: [p.potDiameter * 0.9, 1, p.potDiameter * 0.9],
    });

    const count = Math.max(3, Math.round(p.leaves));
    const reach = p.height - potH;
    for (let i = 0; i < count; i++) {
      const t = i / count;
      const length = reach * (0.55 + 0.45 * (1 - t)) * (0.85 + ctx.rand() * 0.3);
      leafBlade(bin, ctx, leaf, {
        y: potH - 0.02,
        length,
        width: length * 0.42,
        yaw: i * GOLDEN_ANGLE + jitter(ctx.rand, 0.2),
        // Outer leaves lean further out; the middle ones stand up.
        pitch: 0.22 + t * 0.7 + jitter(ctx.rand, 0.12),
        droop: 0.42 + t * 0.3,
        roll: jitter(ctx.rand, 0.2),
        segments: 2,
      });
    }

    return bin.build("plant.potted");
  },
});

type TallParams = {
  height: number;
  potDiameter: number;
  /** Whorls of leaves up the trunk. Three is a dracaena, one is a palm. */
  tiers: number;
};

/** Pitch of the lowest whorl and of the highest. The bottom droops, the top stands. */
const TALL_DROOP = 1.2;
const TALL_CROWN = 0.5;
/** Extra arch a blade picks up between its base and its tip. */
const TALL_ARCH = 0.5;

/**
 * The one piece of arithmetic `footprint` and `build` have to agree on.
 *
 * `footprint` may not build geometry, so it cannot measure the plant; if it
 * guesses instead, the number layout uses and the shape in the room drift
 * apart. Written the first time, they had — the stated height was a fifth
 * taller than the plant, because a leaf at 60° from vertical contributes
 * `cos 60°` of its length and not all of it.
 *
 * The blades now arch as well as lean, so the effective angle used here is the
 * *mid-blade* one — base pitch plus a third of the arch — which is the honest
 * average of a curve that starts at one angle and finishes at another.
 */
function tallCanopy(p: TallParams): {
  potHeight: number;
  trunk: number;
  leaf: number;
  spread: number;
} {
  const potHeight = clamp(p.height * 0.26, 0.24, 0.55);
  const trunk = (p.height - potHeight) * 0.55;
  const rise = p.height - potHeight - trunk;
  const crownAngle = TALL_CROWN + TALL_ARCH / 3;
  const droopAngle = TALL_DROOP + TALL_ARCH / 3;
  // The top whorl starts a third of the way up the canopy and reaches the rest
  // of the way with the vertical component of one leaf.
  const leaf = (rise * 0.66) / Math.cos(crownAngle);
  return {
    potHeight,
    trunk,
    leaf,
    spread: Math.max(p.potDiameter, 2 * leaf * Math.sin(droopAngle)),
  };
}

export const plantTall = defineAsset<TallParams>({
  id: "tera:plant.tall",
  label: "Tall plant",
  defaults: { height: 1.8, potDiameter: 0.44, tiers: 3 },

  footprint(p) {
    const { spread } = tallCanopy(p);
    return { width: spread, depth: spread, height: p.height };
  },

  build(p, ctx) {
    const P = ctx.parts;
    const bin = new MeshBin();
    const pot = ctx.materials.get("planter");
    const leaf = ctx.materials.get("foliage");

    const { potHeight: potH, trunk: trunkH, leaf: leafLen } = tallCanopy(p);
    bin.add(P.cylinder(16), pot, { size: [p.potDiameter * 0.78, potH, p.potDiameter * 0.78] });
    bin.add(P.cylinder(16), pot, {
      y: potH - 0.04,
      size: [p.potDiameter, 0.04, p.potDiameter],
    });
    bin.add(P.disc(16), pot, {
      y: potH - 0.015,
      size: [p.potDiameter * 0.9, 1, p.potDiameter * 0.9],
    });

    // Two trunk segments with a slight kink. A single straight rod reads as a
    // pole with leaves stapled to it.
    const lean = jitter(ctx.rand, 0.05);
    bin.add(P.rod(), pot, { y: potH - 0.05, size: [0.06, trunkH * 0.6, 0.06], roll: lean });
    bin.add(P.rod(), pot, {
      x: -Math.sin(lean) * trunkH * 0.6,
      y: potH - 0.05 + trunkH * 0.6,
      size: [0.05, trunkH * 0.5, 0.05],
      roll: -lean * 0.6,
    });

    // Whorls from the droop at the bottom to the crown at the top, stacked over
    // the last third of the canopy. The lowest blades arch over and hang, which
    // is the silhouette that makes this read as a plant rather than a sheaf.
    const tiers = Math.max(1, Math.round(p.tiers));
    const rise = p.height - potH - trunkH;
    let n = 0;
    for (let tier = 0; tier < tiers; tier++) {
      const t = tiers === 1 ? 1 : tier / (tiers - 1);
      const y = potH + trunkH + rise * 0.34 * t;
      const blades = 7 - tier;
      for (let i = 0; i < blades; i++) {
        leafBlade(bin, ctx, leaf, {
          y,
          length: leafLen * (0.85 + ctx.rand() * 0.3),
          width: leafLen * 0.3,
          yaw: n++ * GOLDEN_ANGLE + jitter(ctx.rand, 0.25),
          pitch: TALL_DROOP + (TALL_CROWN - TALL_DROOP) * t + jitter(ctx.rand, 0.15),
          droop: TALL_ARCH,
          roll: jitter(ctx.rand, 0.22),
          // Three cards on the long blades of a corner plant: this is the one
          // the camera gets closest to, and it is the one that was broken.
          segments: 3,
        });
      }
    }

    return bin.build("plant.tall");
  },
});
