/**
 * The office contract — what an office pack is allowed to say.
 *
 * This is the interiors half of `engine/types.ts`, and it obeys the same rule:
 * the engine renders what an `Office` describes and takes no position on what it
 * means. It does not know that a room is a room because people meet in it, that
 * `zone: "eng"` is a team, or that anybody is sitting anywhere. See
 * ARCHITECTURE.md §3.3 and CONTRACT.md §2.
 *
 * **Everything here is strictly JSON-serialisable.** No functions, no classes,
 * no getters, no `THREE` types, no `Date`. A pack hand-written as a `.ts` module
 * and a pack arriving as a `.json` body over HTTP have to be literally the same
 * thing — the moment one of them can carry a callback, the other stops being a
 * pack and starts being a second format nobody maintains.
 *
 * `Plan` (`src/interiors/plan.ts`) is the only thing that turns an `Office` into
 * geometry. Its output — wall runs, collision segments, resolved placements — is
 * a build product and is deliberately not authorable here.
 *
 * ### Coordinates and units
 *
 * Offices are authored in **metres, 1 unit = 1 m**, which is also how assets are
 * authored (CONTRACT.md §3). This is not the city's scale and cannot be: SF puts
 * one scene unit at ~94 m with 3.6x vertical exaggeration, which is why an
 * office gets its own `THREE.Scene`.
 *
 * The floor is the **XZ plane** with **+Y up**, three.js's convention. Plan view
 * throughout this file means looking down at that plane with +X to the right and
 * +Z down the page.
 */

import type { DeviceDeclaration } from "../devices/types.ts";
import type { BuildingGlyph, Pin, View } from "../engine/types.ts";

// ---- Geometry -------------------------------------------------------------

/**
 * A point on the floor plane, in metres.
 *
 * Named fields rather than a `[number, number]` tuple — unlike the city's
 * `LatLng`, where the pair has an obvious reading, `[4, 6]` in an office gives a
 * reader no way to tell whether the second number is depth or height. The field
 * is called `z` precisely so that the answer is on the page.
 */
export interface Point2 {
  x: number;
  z: number;
}

/**
 * A polygon, in plan.
 *
 * Do **not** repeat the first point at the end; the outline is implicitly
 * closed. Author them counter-clockwise in plan view. `Plan` re-winds anything
 * that arrives the other way round rather than rendering a black hole, so this
 * is a style rule and not a trap.
 */
export type Outline = Point2[];

/**
 * Yaw about the +Y axis, in radians. Zero faces **-Z**, and the angle increases
 * counter-clockwise seen from above.
 *
 * That is exactly three.js's `object.rotation.y`, and it is stated in those
 * terms on purpose. A plan-space "degrees clockwise from north" angle — which is
 * what `District.gridAngle` and `Aircraft.heading` use, because for a city it
 * reads better — would need a sign flip on the way into the scene, and a sign
 * flip that lives in one place is a sign flip that eventually gets applied
 * twice. Nothing converts this.
 */
export type Yaw = number;

// ---- Identifiers ----------------------------------------------------------

/**
 * A namespaced asset id, like `"tera:desk.workstation"`.
 *
 * **`src/assets/kit.ts` is the authority** on what ids exist and what they
 * build; this alias exists so that the office contract does not import the mesh
 * library. It is the same type by the same name in both places — one string, one
 * meaning — not two ideas that collided. Interiors is data; assets is code that
 * turns data into geometry, and data should not depend on it to be parsed,
 * validated or stored.
 *
 * The `tera:` namespace is the one this repo ships. A self-hoster registers
 * `acme:desk.standing` with `overrides: "tera:desk.workstation"` and reskins the
 * reference office without forking it. An id with no registration resolves to a
 * placeholder box rather than throwing, because an office pack with one typo in
 * it should still open.
 */
export type AssetId = string;

/**
 * A material id for a floor, wall or ceiling finish, like `"tera:carpet.loop"`.
 *
 * Same arrangement as `AssetId`, one level down: `src/assets/materials.ts` holds
 * the `MaterialRegistry` and the closed `SurfaceRole` union it is keyed on, and
 * this alias is the loose string an authored pack carries. The name differs from
 * `SurfaceRole` deliberately — a type name exported twice meaning two different
 * things is the exact failure CONTRACT.md §4 was written to stop.
 *
 * There is no per-instance tint here, unlike `Prop.colorKey`. One blue meeting
 * room is a *material* — register `acme:paint.blue` and point the wall at it —
 * whereas one red chair in a row of grey ones is genuinely an instance. Giving
 * surfaces a tint key as well would duplicate the override mechanism that
 * already exists and leave two ways to answer the same question.
 */
export type SurfaceId = string;

// ---- Audience -------------------------------------------------------------

/**
 * Who a piece of a pack is built for.
 *
 * An office has two audiences now. `office.lumbridgecorp.com` is a front door
 * anyone can walk up to, and the same building signed in is the one with the
 * people in it. Marking a room, a prop, a bank, a seat, a zone or a viewpoint
 * `"private"` says: this exists for the second audience and not the first, and
 * a public build must never construct it.
 *
 * Absent means `"public"`. Every pack written before this field existed keeps
 * working, and a pack that never thinks about it never has to.
 *
 * ### It is not built, rather than built and hidden
 *
 * `Plan` drops private items during resolution, so a public build has no
 * `PropPlacement`, no `ResolvedSeat` and no mesh for them at all. Building them
 * and setting `visible = false` would leave every one of them in
 * `scene.traverse`, in the devtools scene graph and in a `JSON.stringify` of the
 * plan — a data leak dressed as a privacy feature. See `PlanOptions.depth` in
 * `plan.ts`, which is where the drop happens.
 *
 * ### Walls have no audience, and cannot get one
 *
 * A wall is the difference between a floor plan and a floor, and it is what the
 * collision pass is made of. A building whose partitions come and go with who is
 * looking at it is two different buildings, and the walk-mode collider would be
 * describing whichever one you were not in. Mark what stands in the room. A
 * `Room` *can* be marked, but a private room takes its floor slab and its
 * ceiling with it and leaves a hole in the plan, so that is nearly always the
 * wrong field to reach for — mark the contents.
 *
 * ### This is a UI tier and it is not a security boundary
 *
 * **A pack is bundled into the static build, so everything in it is public by
 * construction**, whatever this field says. The file is in the JavaScript;
 * anyone who wants the private half can read it out of the bundle in ten
 * seconds. What the field buys is that an anonymous visitor is not *shown* the
 * parts of a building that are nobody's business. That is a product decision
 * worth making, and it is not the same act as withholding them.
 *
 * The thing that is genuinely private is `Presence` — who is in today and where
 * they sit — and it is private because it never appears in a pack at all. It
 * arrives from an API over authentication, and **the API is what refuses an
 * anonymous caller**. Nothing on this side of the wire can enforce that. A pack
 * that puts something actually secret behind `audience: "private"` has published
 * it, and the reason this paragraph is here is so that nobody discovers that
 * later.
 */
export type Audience = "public" | "private";

// ---- The office -----------------------------------------------------------

/**
 * One building's interior: the whole authored pack, and the thing a self-hoster
 * copies to make their own.
 *
 * An office contains no people. `Presence` is runtime data that arrives
 * separately and binds by seat id — see the note on that type, which is the
 * single most important paragraph in this file.
 */
export interface Office {
  id: string;
  name: string;

  /**
   * Ground floor first. An office with one storey declares one level; nothing
   * else in the format changes.
   */
  levels: Level[];

  /**
   * Named camera poses. `viewpoints[0]` is where you arrive, so put reception —
   * or whatever the pack wants a first impression to be — at the front.
   */
  viewpoints: Viewpoint[];

  /**
   * The ways up and down: where a walker may cross between two storeys.
   *
   * On the `Office` rather than on a `Floorplan` because a transition is the one
   * authored record that is a fact about *two* levels at once, and putting it on
   * either one would make the other one's copy a restatement — which is the
   * failure `DeviceAnchor` already argues about at length. It resolves late in
   * `Plan`, beside devices and the exterior arrival stall, for the same reason
   * they do: it is a cross-level address, and checking one against a half-built
   * plan reports a problem that is not there.
   *
   * Absent — and it is absent from most packs — means the storeys are places the
   * camera flies to and nothing more, which is what every pack in this repo was
   * until `src/interiors/walker.ts` learned to change levels.
   */
  transitions?: Transition[];

  /**
   * Where on the earth this building stands, if it stands anywhere.
   *
   * Optional, and its absence is a supported state rather than a gap: a pack
   * with no `site` renders exactly as every pack did before this field existed,
   * under the fixed interior rig. That matters because the format's promise is
   * that you can author a floor plan without an account, a key or a coordinate.
   *
   * What it buys when you do supply it is the sun. `interiors/types.ts` used to
   * say flatly that an office has no orientation on the earth, and that was
   * true and also the thing standing between an office and real daylight: you
   * cannot put the sun in the right place without knowing both where the
   * building is and which way it faces.
   */
  site?: OfficeSite;

  meta?: OfficeMeta;
}

/**
 * A building's address in the world, in the four numbers that change what you
 * see out of the window.
 *
 * Deliberately not a street address. Nothing here is geocoded, nothing is looked
 * up, and no network call can be made from any of it — see CONTRACT.md §8 for
 * why a coordinate's provenance is a licensing question in this repo. These are
 * numbers a pack author types, the same as every other number in a pack.
 */
export interface OfficeSite {
  /** Degrees north. */
  lat: number;
  /** Degrees east. */
  lng: number;
  /**
   * How far this pack's level-0 floor sits above **the ground outside**, in
   * metres. Not above sea level.
   *
   * The renderer's question is "how high up am I", not "how tall is the tower",
   * and this is the number that answers it: it is exactly where the horizon
   * goes. An office on the 48th floor of a downtown tower is a couple of hundred
   * metres here and a shed on reclaimed land is three, and that single
   * difference is most of what makes the two feel like different places.
   */
  elevation: number;
  /**
   * The compass bearing, in degrees clockwise from true north, that the pack's
   * **−Z direction** points along.
   *
   * `0` means the pack's "north" really is north, which is what the reference
   * pack's comments have always assumed while being careful to say it was only a
   * convenience. A building rotated to face the street sets this, and the sun
   * then comes through the windows it actually comes through.
   *
   * Degrees clockwise from north, like `District.gridAngle` and unlike `Yaw` —
   * a bearing reads better for a thing on a map, and radians-counter-clockwise
   * reads better for a thing in a scene graph. The conversion happens once, at
   * the point the two meet.
   */
  heading: number;
  /**
   * What to call the place, for a caption. `null` or absent where the building
   * has no name worth printing.
   */
  label?: string;
  /**
   * The public silhouette the city can draw before this office pack is loaded.
   *
   * Optional for the same reason `site` is optional: an office does not need a
   * world address to render, and a sited office does not need to claim that its
   * exterior is known. When present, `offices/sites.ts` hands this plain data to
   * the generic marker layer; the city still never imports an office pack.
   */
  exterior?: BuildingGlyph;
  /**
   * Where a vehicle stands on the ground outside the front door.
   *
   * Optional, and it lives on the *site* rather than on the `Office` for the
   * same reason `elevation` does: it is a fact about the building's
   * relationship to the ground outside it, and a pack with no site has no
   * outside for anything to stand in. A pack that never mentions a vehicle
   * renders exactly as it did before this field existed, which is the property
   * every addition to this file has to keep.
   */
  arrival?: ExteriorArrival;
}

/**
 * A marked place on the ground outside the building.
 *
 * The exterior layer needs one number a pack cannot get from anywhere else:
 * **where, in the plan's own coordinates, is the apron outside the front
 * door.** `lat`/`lng` says where the building is on the earth and nothing about
 * which corner of the lot you park on; `Plan.bounds` is the extent of what was
 * authored and its edge is a wall rather than a kerb. So the stall is authored,
 * like every other number in a pack.
 *
 * ### It is in the plan's frame, not the world's
 *
 * `position` is metres in exactly the same XZ frame the walls are in, and
 * `rotation` is a `Yaw` — zero faces −Z, as everything else here does. That is
 * what makes the anchor legible next to the wall it stands outside of: a stall
 * on the street side of a façade authored at `z = 0` has a negative `z`, and a
 * reader can see it is outside the building without converting anything.
 *
 * `levelId` names the storey whose floor the stall is measured from, which for
 * every shipped pack is the ground floor and for a building on a slope is not
 * necessarily so.
 *
 * ### One kind, spelled out
 *
 * `kind` is a closed union with a single member rather than a free string, so
 * that the second member — a loading bay, a bike rack, a helipad — arrives as a
 * decision somebody made rather than as a typo that happened to render.
 */
export interface ExteriorArrival {
  /** The storey whose floor this stall is measured from. */
  levelId: string;
  /** Metres, in the pack's own plan frame, outside the building's footprint. */
  position: Point2;
  /** Which way a vehicle parked here faces. See `Yaw`. */
  rotation: Yaw;
  kind: "vehicle-stall";
  /** What to call it, for a caption. Absent where it needs no name. */
  label?: string;
}

/**
 * Provenance for a pack, and nothing the renderer reads.
 *
 * Optional, but a pack meant to be shared should fill in `author` and `license`.
 * The art in this repo is Apache-2.0 with the artistic output additionally
 * dedicated under CC0-1.0 (CONTRACT.md §3.1); a pack built elsewhere is under
 * whatever its author says here, and saying nothing helps nobody.
 */
export interface OfficeMeta {
  description?: string;
  author?: string;
  /** SPDX identifier where there is one, e.g. `"CC0-1.0"`. */
  license?: string;
  version?: string;
  /** ISO-8601 date string. A string, not a `Date` — this has to survive JSON. */
  updated?: string;
}

/**
 * One storey.
 *
 * The three `wall*` fields are the storey's defaults, not a constraint: an
 * individual `Wall` overrides any of them. They live here because a floor of
 * forty walls that are all 3 m of painted plasterboard should say so once, and
 * the interesting wall — the 1.4 m partition around the desk bay — should be the
 * one that stands out in the source.
 */
export interface Level {
  id: string;
  name: string;

  /** Floor slab height above the office origin, in metres. Ground is `0`. */
  elevation: number;

  /** Storey height: the default top of a wall, measured from this floor. */
  wallHeight: number;
  /** Default wall thickness in metres. `Plan` uses 0.12 when this is absent. */
  wallThickness?: number;
  /** Default wall finish. */
  wallSurface?: SurfaceId;

  floorplan: Floorplan;
}

// ---- Transitions ----------------------------------------------------------

/**
 * What kind of way up this is.
 *
 * Two members, and the difference between them is presentation rather than
 * mechanism: a `stair` is drawn as a flight of treads climbing its own path and
 * a walker rises along it, a `lift` is a box that goes up. Both resolve
 * identically and both hand the walker the same two footprints. A third kind
 * would be a ladder or a ramp; neither is authored anywhere yet, and a union
 * member nothing draws is a promise the renderer has not made.
 */
export type TransitionKind = "stair" | "lift";

/**
 * A way between two storeys, authored once and read by both the walker and the
 * renderer.
 *
 * ### Why the format needs this at all
 *
 * `Plan` carries no vertical beyond `Level.elevation`, and the collider is a
 * flat list of 2-D segments. There is nothing in the build product that can tell
 * a staircase from a rug — a `Room` with a `STEEL` floor the shape of a flight
 * is exactly as walkable as the carpet next to it, and exactly as unable to take
 * anybody upstairs. So the way up is stated rather than inferred, which is the
 * same decision CONTRACT.md §2 already made for wall openings: one explicit
 * list, resolved once, read by the thing that draws it and the thing that walks
 * it, with no second list to keep in step.
 *
 * ### Both ends, in plan coordinates, like everything else
 *
 * Each end names a level and gives a **footprint**: the patch of that storey's
 * floor a walker has to be standing on for this to be the way up. Standing in it
 * *is* the input — there is no key to press — which is why the footprints are
 * the shape they are in `mateo-court`: the lower one is the bottom of the flight
 * and not the whole stair, and the upper one is the gap in the balustrade rather
 * than the whole walkway.
 *
 * The upper footprint doubles as a guard rail in the literal sense. A gap in a
 * balustrade is a hole a walker can otherwise stroll straight out of, five
 * metres above a paved yard; a footprint that covers the gap catches them before
 * the edge and sends them downstairs instead, which is what the gap is for.
 *
 * Coordinates are the pack's plan frame — the same one every level is authored
 * in, with each slab at zero — and `Plan` adds the level's elevation exactly
 * once, in one place, the way it does for everything else.
 */
export interface Transition {
  /** Unique in the office, across every kind of record `Plan` checks ids for. */
  id: string;
  kind: TransitionKind;
  /** Shown wherever a pack's own vocabulary is shown. "The Stair". */
  label?: string;
  /** The storey it starts on: the lower `elevation` of the two. */
  lower: TransitionEnd;
  /** The storey it arrives at. `Plan` drops the record if this is not higher. */
  upper: TransitionEnd;
  /**
   * The route it takes between the two landings, in plan coordinates, foot
   * first.
   *
   * Absent means one straight flight from `lower.landing` to `upper.landing`,
   * which is what a single run of stairs or a lift shaft is. A dog-leg is three
   * legs — a flight, a landing, a flight — and it has to be authored, because
   * the shape of the flight is a fact about the building that nothing in a
   * floorplan implies. It is also what the treads are built from: the drawn
   * stair and the climb a walker takes read the same list, so a stair that is
   * drawn somewhere the walker does not go is not expressible.
   */
  legs?: TransitionLeg[];
  /** The flight's width in metres. `Plan` uses 1.2 when this is absent. */
  width?: number;
  /** The finish of the treads. Falls back to the lower level's wall surface. */
  surface?: SurfaceId;
  audience?: Audience;
}

/** One end of a `Transition`: a storey, a patch of its floor, and where you stand. */
export interface TransitionEnd {
  levelId: string;
  /**
   * The patch of floor that is this end of the way up, in plan coordinates.
   *
   * Wound either way and closed implicitly, exactly like a `Room.outline`.
   * `Plan` drops the whole transition if this falls outside its level's bounds
   * or if a collision segment runs through it — half a staircase is a hole in a
   * collider, and dropping one end and keeping the other is how you get one.
   */
  footprint: Outline;
  /**
   * Where a walker is set down arriving at this end, in plan coordinates.
   *
   * Must be inside `footprint` and clear of walls at the default walker radius,
   * or the transition is dropped. It is authored rather than derived from the
   * footprint's centroid because the centroid of an L-shaped landing is not
   * necessarily on it, and because a pack author knows which way the last tread
   * faces and a centroid does not.
   */
  landing: Point2;
  /** Which way a walker faces on arrival. Defaults to along the last leg. */
  facing?: Yaw;
}

/**
 * One straight leg of a transition: a flight, or a landing between two flights.
 *
 * `rise` is this leg's share of the total climb, as a fraction — so a dog-leg's
 * two flights are `0.5` each and the half landing between them is `0`. It is a
 * share rather than a height because the height is already known exactly (it is
 * the difference between the two levels' elevations) and stating it twice is how
 * a pack ends up with a staircase that does not reach its own landing.
 *
 * `Plan` normalises the shares to sum to one and reports a pack whose shares are
 * all zero, which is a lift shaft authored as a stair.
 */
export interface TransitionLeg {
  /** Where this leg ends, in plan coordinates. */
  to: Point2;
  /** This leg's share of the total rise, 0..1. A flat landing is 0. */
  rise: number;
}

/**
 * Everything on one storey.
 *
 * `rooms` and `walls` are required because a level without them is not a level;
 * the rest are optional because a bare lobby genuinely has no desk banks, and a
 * pack arriving over HTTP will drop empty arrays. Consumers read the optional
 * ones as `?? []`.
 */
export interface Floorplan {
  rooms: Room[];
  walls: Wall[];
  props?: Prop[];
  deskBanks?: DeskBank[];
  seats?: Seat[];
  zones?: Zone[];
  /**
   * Smart hardware standing on the furniture — see `src/devices/types.ts`,
   * which owns the type and is imported here for it.
   *
   * A `DeviceDeclaration` is authored, public and inert: it says a microphone
   * exists, what it can be asked to do, and which prop is its hardware. What
   * that microphone is *hearing* is a `DeviceState`, which never appears in a
   * pack at all — it arrives over the API from a route that can refuse an
   * anonymous caller. That is the same line `Presence` draws one type down, for
   * the same reason, and it is why a pack can be published and a reading cannot.
   *
   * The list is on the floorplan rather than on the `Office` because a device is
   * anchored to a prop and props are per storey, so the two lists that have to
   * agree with each other sit next to each other.
   */
  devices?: readonly DeviceDeclaration[];
}

// ---- Rooms ----------------------------------------------------------------

/**
 * A floor slab with a name.
 *
 * **A room implies no walls.** This is the load-bearing half of CONTRACT.md §2:
 * rooms are surfaces, walls are a separate explicit list, and the two are not
 * derived from each other. Deriving walls from shared room edges sounds tidy
 * until it needs float-equality dedup to decide whether two rooms touch, and
 * then it is a source of gaps that only appear in one build out of ten.
 *
 * Rooms may overlap and may leave gaps. An open-plan floor is one big room with
 * a handful of walls standing on it.
 */
export interface Room {
  id: string;
  name: string;
  outline: Outline;
  floor: SurfaceId;
  /**
   * Omit for the level default. Explicit `null` means **no ceiling at all** —
   * an atrium, a double-height void, or a cutaway you want to look down into.
   */
  ceiling?: RoomCeiling | null;
  /**
   * See `Audience`. Absent means public. A private room takes its floor slab and
   * its ceiling with it and leaves a hole in the plan, which is almost never
   * what is wanted — mark the props in the room instead.
   */
  audience?: Audience;
}

/** A ceiling override for one room. Both fields fall back to the level. */
export interface RoomCeiling {
  /** Metres above this level's floor. */
  height?: number;
  surface?: SurfaceId;
}

// ---- Walls and openings ---------------------------------------------------

/**
 * A single straight wall segment, from `from` to `to`, centred on that line.
 *
 * Walls are an explicit list rather than something inferred from room edges, and
 * this is the decision the rest of the interiors code is built on. The pass that
 * splits a wall around its openings has to run anyway to produce the solid runs
 * you can see; running it once produces the **walk-mode collision segments for
 * free**, with the gaps in exactly the places you can walk through. Any other
 * arrangement keeps two lists in sync by hand.
 *
 * A wall belongs to no room. It stands where it is put.
 */
export interface Wall {
  id: string;
  from: Point2;
  to: Point2;
  /** Metres. Falls back to the level's `wallThickness`. */
  thickness?: number;
  /** Metres above this level's floor. Falls back to the level's `wallHeight`. */
  height?: number;
  surface?: SurfaceId;
  /** Doors, windows and arches punched out of this wall. Order is irrelevant. */
  openings?: Opening[];
}

export type OpeningKind = "door" | "window" | "arch";

/**
 * A hole in a wall, described as a 1-D interval along it.
 *
 * `start` is measured **from the wall's `from` end**, along the wall, in metres;
 * `width` runs on from there. That is the whole of the horizontal placement —
 * an opening has no position of its own and cannot drift off its wall, which is
 * the point of expressing it this way.
 *
 * Doors and windows are openings, never placeable assets. There is no
 * `tera:shell.door`: shipping both a door prop and a door-shaped hole would put
 * every opening in the scene twice, or — worse, because it is invisible until
 * somebody walks through a wall — leave the collider with no gap where the door
 * is. `Plan` hands each solid run to a parameterised `wallRun` part, and the
 * frame, leaf or glazing is drawn by the opening itself.
 *
 * Typical values, in metres: a door is `sill: 0, head: 2.1`; a window is
 * `sill: 0.9, head: 2.2`; an arch is `sill: 0, head: 2.4`. They are required
 * rather than defaulted by kind, because a data contract with hidden per-kind
 * defaults is one where the numbers you read are not the numbers you get.
 */
export interface Opening {
  kind: OpeningKind;
  /** Metres along the wall from the `from` end to the near edge of the hole. */
  start: number;
  /** Metres. Must be positive, and must fit inside the wall. */
  width: number;
  /** Bottom of the hole, metres above this level's floor. */
  sill: number;
  /** Top of the hole, metres above this level's floor. */
  head: number;
}

// ---- Props ----------------------------------------------------------------

/**
 * One instance of one asset, placed.
 *
 * `kind` is an `AssetId` because the prop registry and the asset registry are
 * the same registry (CONTRACT.md §3) — there is no separate table of things you
 * are allowed to put in a room.
 */
export interface Prop {
  id: string;
  kind: AssetId;
  /** Where it stands, on the floor plane. */
  position: Point2;
  /** See `Yaw`. */
  rotation: Yaw;
  /**
   * Metres above this level's floor. Omitted means standing on it, which is
   * true of nearly everything; a wall-mounted screen or a monitor on a desktop
   * says so here.
   */
  elevation?: number;
  /**
   * Uniform scale, or per-axis. Use sparingly — an asset that is wanted at
   * another size is usually better registered as its own id.
   */
  scale?: number | [number, number, number];
  /**
   * An opaque palette key, resolved by the caller's palette exactly as
   * `Pin.colorKey` is. The engine will not learn that `"focus"` means a quiet
   * booth or that red means anything at all.
   */
  colorKey?: string;
  /**
   * The id of a `Seat` this prop belongs to — the chair pulled up to `eng-04`.
   *
   * Purely an address. The prop is still positioned by its own `position`;
   * binding it to a seat is what lets an occupancy layer dim the empty chairs
   * without knowing which mesh is which.
   */
  seat?: string;
  /** See `Audience`. Absent means public. */
  audience?: Audience;
}

/**
 * A row or grid of identical desks, declared once.
 *
 * `Plan` expands one of these into props and seats. It exists for a plain
 * reason: the reference office is about 180 props, and 180 hand-written literals
 * is not a file anybody edits twice. A bank of twelve is six lines here.
 *
 * The grid is laid out in the bank's own frame — `columns` run along its local
 * +X, `rows` step along its local +Z — and then rotated by `rotation` about
 * `origin`, which is the centre of station (1, 1).
 *
 * ### Generated ids
 *
 * These are part of the contract, because a `Presence` binds to a seat id and a
 * pack author has to be able to predict what it will be without running the
 * expansion. Stations are numbered from 1, along each row and then down the
 * rows, and the number is zero-padded to two digits:
 *
 * - seat `${seatPrefix ?? id}-01`, `-02`, …
 * - desk prop `${id}-desk-01`, chair prop `${id}-chair-01`
 *
 * So a bank with `id: "eng"` and no `seatPrefix` gives you `eng-04` as the
 * fourth seat, which is the id the private occupancy API is expected to know.
 */
export interface DeskBank {
  id: string;
  /** The asset placed at every station. */
  desk: AssetId;
  /** Placed at every seat, if given. */
  chair?: AssetId;

  /** Centre of the first station, before rotation. */
  origin: Point2;
  /** Orientation of the whole bank. See `Yaw`. */
  rotation: Yaw;

  /** Stations across, along the bank's local +X. At least 1. */
  columns: number;
  /** Rows deep, along the bank's local +Z. At least 1. */
  rows: number;
  /** Centre-to-centre spacing between columns, in metres. */
  pitch: number;
  /** Centre-to-centre spacing between rows, in metres. Defaults to `pitch`. */
  rowPitch?: number;

  /**
   * When true, consecutive rows face each other rather than all facing the same
   * way — bench seating, where two rows share a run of desktop. This is the
   * difference between an office that looks laid out and one that looks like a
   * spreadsheet, which is why it is here rather than left to the author to fake
   * with two banks.
   */
  facingRows?: boolean;

  /** Metres from the desk centre to the seat, on the seated side. */
  seatOffset?: number;
  /** Pose for every seat in the bank. Defaults to `"sit"`. */
  pose?: SeatPose;
  /** Overrides the bank `id` as the seat-id prefix. */
  seatPrefix?: string;
  /**
   * See `Audience`. Absent means public, and it covers the whole expansion: a
   * private bank generates no desks, no chairs and no seats, so there is nothing
   * left for a presence to bind to.
   */
  audience?: Audience;
}

// ---- Seats and zones ------------------------------------------------------

export type SeatPose = "sit" | "stand";

/**
 * A place a person can be.
 *
 * **Seats are addresses.** A seat is not a chair and not a person; it is a
 * stable name for a spot on the floor, so that something outside this repo can
 * say "eng-04" and mean somewhere without ever being told a coordinate. Ids
 * should be stable across pack edits for the same reason street numbers are.
 */
export interface Seat {
  id: string;
  position: Point2;
  /** Which way an occupant looks. See `Yaw`. */
  facing: Yaw;
  pose: SeatPose;
  /**
   * See `Audience`. Absent means public. A private seat is not resolved at
   * public depth, so a `Presence` naming it is dropped the same way one naming a
   * seat that does not exist is — which is the answer you want, since at public
   * depth there is no presence layer to drop it into either.
   */
  audience?: Audience;
}

/**
 * A named region of floor.
 *
 * A zone has no behaviour and no effect on geometry. It is a label on an area —
 * a team's corner, a quiet zone, a phone-booth cluster — that a consuming app
 * can highlight, filter or count against. Whether membership of a zone *means*
 * anything is not the engine's business, which is why `colorKey` is opaque and
 * there is no `kind` field.
 */
export interface Zone {
  id: string;
  name: string;
  outline: Outline;
  /** Opaque palette key, resolved by the caller. */
  colorKey?: string;
  /**
   * See `Audience`. Absent means public. A zone is a label on an area and a
   * label is exactly the sort of thing that turns out to be organisational —
   * "Engineering" says who sits there — so this is the field a pack reaches for
   * most.
   */
  audience?: Audience;
}

// ---- Viewpoints -----------------------------------------------------------

/**
 * A named camera pose — the office analogue of a city `Chapter`.
 *
 * It shares `View` with the city rather than redeclaring it, because the half
 * that the interface cares about — what the legend prints, what `flyTo` is keyed
 * on — is identical, and only the pose differs: a chapter focuses on a latitude
 * and longitude, a viewpoint focuses on a point in metres on a particular level.
 */
export interface Viewpoint extends View {
  levelId: string;
  focus: {
    /** What the camera looks at, on the floor plane. */
    at: Point2;
    /** Metres from the target to the camera. */
    distance: number;
    /** Metres above this level's floor, for both target and camera height. */
    height: number;
    /** Camera azimuth about the target. See `Yaw`. */
    rotation: Yaw;
  };
  /**
   * See `Audience`. Absent means public.
   *
   * Use it sparingly and think first. A viewpoint is a promise printed in a
   * legend, and a visitor told there are five and shown three has been lied to;
   * a private viewpoint disappears from `views` entirely rather than leaving a
   * dead button, but the honest fix is usually to reframe the shot rather than
   * to withhold it. Mark one private only when the *pose itself* is the
   * disclosure — a camera two metres from the whiteboard in the board room.
   */
  audience?: Audience;
}

// ---- Presence -------------------------------------------------------------

/**
 * Somebody at a seat.
 *
 * **A `Presence` binds to a `seatId` and never to a coordinate, and it never
 * appears in an office pack.** This is the whole trick, and it is the marker
 * rule one level in.
 *
 * The pack knows where seat `eng-04` is. A private API knows who is sitting in
 * it. Neither knows the other, so occupancy can be private data behind
 * authentication — names, faces, who is in today — while the office geometry
 * stays public, open-source and copyable by anyone. If a presence carried an
 * `{x, z}`, then publishing the geometry and publishing the people would be the
 * same act, and one of them could never be published at all.
 *
 * It extends `Pin` for the same reason `Marker` does: a thing worth pointing at,
 * with a label and an opaque colour key, that a detail card can render without
 * caring whether it was placed by latitude or by seat id.
 */
export interface Presence extends Pin {
  seatId: string;
}
