/**
 * Plants. A small one for a desk or a sill, and a tall one for a corner.
 *
 * Leaves are single quads in a double-sided `foliage` material rather than
 * modelled solids: forty cards is forty quads, a modelled leaf is a hundred
 * triangles each, and at the distance an office plant is ever seen the two look
 * the same. They are laid out on the golden angle, which is what stops a ring of
 * cards from reading as a ring, plus a little jitter from `ctx.rand` — seeded
 * per prop, so the plant on the third desk is the same plant on every reload.
 *
 * Neither takes a `colorKey`. A plant is the colour a plant is.
 */

import { defineAsset } from "../kit.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));

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],
    });
    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);
      bin.add(P.panel(), leaf, {
        y: potH - 0.02,
        size: [length * 0.34, length, 1],
        yaw: i * GOLDEN_ANGLE + jitter(ctx.rand, 0.2),
        // Outer leaves lean further out; the middle ones stand up. Pitch runs
        // in the yawed frame, so this is a lean along whichever way it faces.
        pitch: 0.25 + t * 0.8 + jitter(ctx.rand, 0.12),
      });
    }

    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.35;
const TALL_CROWN = 0.6;

/**
 * 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.
 */
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;
  // 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(TALL_CROWN);
  return {
    potHeight,
    trunk,
    leaf,
    spread: Math.max(p.potDiameter, 2 * leaf * Math.sin(TALL_DROOP)),
  };
}

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++) {
        bin.add(P.panel(), leaf, {
          y,
          size: [leafLen * 0.26, leafLen * (0.85 + ctx.rand() * 0.3), 1],
          yaw: n++ * GOLDEN_ANGLE + jitter(ctx.rand, 0.25),
          pitch: TALL_DROOP + (TALL_CROWN - TALL_DROOP) * t + jitter(ctx.rand, 0.15),
        });
      }
    }

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