/**
 * Storage: an open shelf unit and a bank of lockers.
 *
 * Both are used from +Z and have a solid back at −Z, so a pack stands one
 * against a wall by giving it the rotation that turns its back to the wall —
 * the same rotation it would give a person standing in front of it.
 */

import { defineAsset } from "../kit.ts";
import { MeshBin } from "../parts.ts";
import { clamp, jitter, tintable } from "./common.ts";

type ShelfParams = {
  width: number;
  depth: number;
  height: number;
  /** Open bays, not boards. Four bays is five boards. */
  shelves: number;
  /** Fill the bays with books. Seeded from `ctx.rand`, so it is stable. */
  books: boolean;
};

const BOARD = 0.02;

/** An open shelf unit. The boards are the tintable part. */
export const storageShelf = defineAsset<ShelfParams>({
  id: "tera:storage.shelf",
  label: "Shelf unit",
  defaults: { width: 0.9, depth: 0.35, height: 1.6, shelves: 4, books: true },

  footprint(p) {
    return { width: p.width, depth: p.depth, height: p.height, clearance: 0.6 };
  },

  build(p, ctx) {
    const P = ctx.parts;
    const bin = new MeshBin();
    const board = tintable(ctx, "shelf");
    const bays = Math.max(1, Math.round(p.shelves));
    const bayH = (p.height - (bays + 1) * BOARD) / bays;
    const inner = p.width - 2 * BOARD;

    // Every part in the `shelf` material is a `roundedBoxOf`, uprights included,
    // because the primitive class has to be uniform across a material and a
    // chamfered board beside a sharp upright would look like a mistake anyway.
    // A 2.5 mm radius: shelf boards are thin and anything larger reads as a
    // moulded plastic unit rather than as a board.
    for (const sx of [-1, 1]) {
      bin.add(P.roundedBoxOf(BOARD, p.height, p.depth, 0.0025), board, {
        x: (sx * (p.width - BOARD)) / 2,
      });
    }
    bin.add(P.box(), ctx.materials.get("cabinet"), {
      z: -(p.depth / 2 - 0.006),
      size: [inner, p.height, 0.012],
    });

    for (let i = 0; i <= bays; i++) {
      bin.add(P.roundedBoxOf(inner, BOARD, p.depth, 0.0025), board, {
        y: i * (bayH + BOARD),
      });
    }

    if (p.books) {
      // Books are three materials and nothing else, so a full wall of shelving
      // is three more merged meshes rather than three hundred.
      const spines = [
        ctx.materials.get("paper"),
        ctx.materials.get("accent"),
        ctx.materials.get("cabinet"),
      ];
      for (let i = 0; i < bays; i++) {
        const shelfY = i * (bayH + BOARD) + BOARD;
        let x = -inner / 2 + 0.015;
        while (x < inner / 2 - 0.06) {
          if (ctx.rand() < 0.14) {
            x += 0.04 + ctx.rand() * 0.08;
            continue;
          }
          const w = 0.018 + ctx.rand() * 0.038;
          const h = bayH * (0.62 + ctx.rand() * 0.26);
          const material = spines[Math.floor(ctx.rand() * spines.length)] ?? spines[0];
          if (!material) break;
          bin.add(P.box(), material, {
            x: x + w / 2,
            y: shelfY,
            z: 0.02 + jitter(ctx.rand, 0.015),
            size: [w, h, clamp(p.depth * 0.6, 0.12, 0.26)],
            roll: jitter(ctx.rand, 0.03),
          });
          x += w + 0.004;
        }
      }
    }

    return bin.build("storage.shelf");
  },
});

type LockerParams = {
  width: number;
  depth: number;
  height: number;
  columns: number;
  /** Doors stacked per column. Two is the usual personal-locker bank. */
  tiers: number;
};

/** A bank of lockers. The doors are the tintable part. */
export const storageLocker = defineAsset<LockerParams>({
  id: "tera:storage.locker",
  label: "Locker bank",
  defaults: { width: 1.2, depth: 0.5, height: 1.8, columns: 3, tiers: 2 },

  footprint(p) {
    // A door has to swing, and a locker with a metre of nothing in front of it
    // is the difference between a corridor and a corridor you can use.
    //
    // The stated depth includes the doors and their pulls, which stand 24 mm
    // proud of the carcass at +Z. It did not, and a bank of lockers pushed
    // flush to a wall by its own footprint put its handles through the plaster.
    return { width: p.width, depth: p.depth + 0.05, height: p.height, clearance: 0.9 };
  },

  build(p, ctx) {
    const P = ctx.parts;
    const bin = new MeshBin();
    const carcass = ctx.materials.get("cabinet");
    const door = tintable(ctx, "cabinet");
    const trim = ctx.materials.get("metalTrim");

    const plinth = 0.08;
    bin.add(P.box(), trim, { y: 0, size: [p.width - 0.04, plinth, p.depth - 0.04] });
    bin.add(P.box(), carcass, {
      y: plinth,
      size: [p.width, p.height - plinth, p.depth],
    });

    const columns = Math.max(1, Math.round(p.columns));
    const tiers = Math.max(1, Math.round(p.tiers));
    const cellW = p.width / columns;
    const cellH = (p.height - plinth) / tiers;

    for (let c = 0; c < columns; c++) {
      const x = -p.width / 2 + cellW * (c + 0.5);
      for (let t = 0; t < tiers; t++) {
        const y = plinth + cellH * t + 0.008;
        bin.add(P.box(), door, {
          x,
          y,
          z: p.depth / 2,
          size: [cellW - 0.016, cellH - 0.016, 0.022],
        });
        // A vertical pull on the leading edge, plus the vent slot that is the
        // one detail that makes a painted box read as a locker.
        bin.add(P.box(), trim, {
          x: x + cellW / 2 - 0.05,
          y: y + cellH * 0.32,
          z: p.depth / 2 + 0.016,
          size: [0.016, cellH * 0.3, 0.016],
        });
        for (let s = 0; s < 3; s++) {
          bin.add(P.box(), trim, {
            x,
            y: y + cellH - 0.09 + s * 0.022,
            z: p.depth / 2 + 0.012,
            size: [cellW * 0.4, 0.008, 0.006],
          });
        }
      }
    }

    return bin.build("storage.locker");
  },
});
