/**
 * LANE-RENDER — mineral seams: the glinting patches that tell you where to mine.
 *
 * Data is `data/seams.json` (LANE-DATA). Its doc sets the division of labour:
 *   SIM    — an extractor extracts only while its footprint overlaps a seam rect.
 *   RENDER — `era` skins the ground; `resource` is the stratum's codex payload.
 * So this layer does two jobs at once: it skins each region with its era (§1 strata,
 * with a wide soft falloff so it reads as a whisper across the crust, not a decal), and
 * inside the rect it lays the mineral itself — obsidian darkening with the resource's
 * colour flickering through fracture veins (§2: "thin glowing rainbow circuit veins").
 *
 * The tell is deliberately quiet. There is NO label and no icon: a seam is a patch of
 * ground that glints. A player who looks discovers that extractors want to sit on them;
 * a player who doesn't just sees texture. `richness` drives how strongly it glints, so
 * the rich remote seams are the ones that reward wandering.
 */
import * as THREE from 'three';
import type { GameData } from '../contracts';
import seamsFile from '../../data/seams.json';

export interface SeamRect { x: number; y: number; w: number; h: number }
export interface SeamDef {
  id: string;
  era: string;
  resource: string;
  rect: SeamRect;
  richness: number;
}

/** §1 strata palettes — the era whisper laid over the crust around each seam. */
const ERA_TINT: Record<string, number> = {
  reel: 0x3a2a16, // celluloid shale, silver-nitrate brown
  broadcast: 0x14301c, // phosphor dunes
  disc: 0x241634, // rainbow-on-black gloss
  stream: 0x1e2430, // flat plastic crust
  fortress: 0x2c3038,
};

/** How far past the rect the era whisper bleeds, in tiles. */
const ERA_BLEED = 7;

const VERT = /* glsl */ `
  attribute vec3 aEra;
  attribute vec3 aOre;
  attribute vec2 aHalf;    // rect half-extent in tiles (for the inside/outside test)
  attribute float aRich;
  varying vec3 vEra;
  varying vec3 vOre;
  varying vec2 vHalf;
  varying float vRich;
  varying vec2 vLocal;     // tiles from the rect centre
  varying vec2 vWorld;
  void main() {
    vEra = aEra; vOre = aOre; vHalf = aHalf; vRich = aRich;
    vec4 wp = modelMatrix * instanceMatrix * vec4(position, 1.0);
    vWorld = wp.xz;
    // instanceMatrix scales a unit quad to (rect + bleed); recover tiles-from-centre.
    vLocal = position.xz * (aHalf + vec2(${ERA_BLEED}.0)) * 2.0;
    gl_Position = projectionMatrix * viewMatrix * wp;
  }
`;

const FRAG = /* glsl */ `
  precision highp float;
  uniform float uTime;
  varying vec3 vEra;
  varying vec3 vOre;
  varying vec2 vHalf;
  varying float vRich;
  varying vec2 vLocal;
  varying vec2 vWorld;

  float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
  float vnoise(vec2 p){
    vec2 i = floor(p), f = fract(p);
    f = f*f*(3.0-2.0*f);
    return mix(mix(hash(i), hash(i+vec2(1,0)), f.x),
               mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y);
  }

  void main(){
    vec2 d = abs(vLocal) - vHalf;          // >0 outside the rect, in tiles

    // --- era whisper: strongest at the rect, faded out over ERA_BLEED tiles ---
    float outside = max(max(d.x, d.y), 0.0);
    float era = 1.0 - smoothstep(0.0, ${ERA_BLEED}.0, outside);
    // fbm-break the edge so a rect never reads as a rectangle
    era *= 0.55 + 0.45 * vnoise(vWorld * 0.35);

    // --- the mineral itself: only inside the rect ---
    // Soft-edged so a seam never reads as a rectangle you could have drawn in CSS, but
    // the fade is SHORT: SIM extracts on any footprint/rect overlap, so a glint that died
    // two tiles inside the boundary would under-report where mining actually works.
    float body = (1.0 - smoothstep(-0.8, 0.6, max(d.x, d.y)))
               * (0.6 + 0.4 * vnoise(vWorld * 0.55));

    // GLINT, not veins. The first pass drew continuous sinusoidal lines and they read as
    // green worms crawling over the floor — organic, loud, competing with the belts.
    // Mineral glitter is sparse and it TWINKLES: quantise to a fleck grid, keep the top
    // few percent, and let each fleck sparkle on its own clock.
    vec2 g = vWorld * 7.0;
    vec2 cell = floor(g);
    float pick = hash(cell);
    // Shape a round fleck inside its cell instead of filling the cell — a full cell reads
    // as confetti at this zoom, a point reads as a mineral fleck.
    vec2 sub = fract(g) - 0.5 - (vec2(hash(cell + 3.7), hash(cell + 9.1)) - 0.5) * 0.5;
    float dot_ = 1.0 - smoothstep(0.10, 0.30, length(sub));
    float fleck = smoothstep(0.90 - vRich * 0.10, 1.0, pick) * dot_;
    float twinkle = 0.35 + 0.65 * pow(max(0.0, sin(uTime * 2.2 + pick * 43.0)), 3.0);
    float glint = fleck * twinkle * body;

    vec3 col = vEra * era * 0.75;                // stratum tint on the crust
    col = mix(col, vOre, min(1.0, glint * 1.6)); // ore colour only in the flecks
    float a = era * 0.22 + body * 0.09 + glint * (0.35 + vRich * 0.4);
    if (a < 0.004) discard;
    gl_FragColor = vec4(col, a);

    // sRGB out — a raw ShaderMaterial gets none of the output includes (see ground.ts).
    #include <colorspace_fragment>
  }
`;

export class SeamLayer {
  readonly mesh: THREE.InstancedMesh;
  private uniforms = { uTime: { value: 0 } };
  private dummy = new THREE.Object3D();

  constructor(seams: SeamDef[], data: GameData) {
    const geo = new THREE.PlaneGeometry(1, 1);
    geo.rotateX(-Math.PI / 2); // lie flat on the ground

    const n = Math.max(1, seams.length);
    const era = new Float32Array(n * 3);
    const ore = new Float32Array(n * 3);
    const half = new Float32Array(n * 2);
    const rich = new Float32Array(n);
    const c = new THREE.Color();

    const mat = new THREE.ShaderMaterial({
      uniforms: this.uniforms,
      vertexShader: VERT,
      fragmentShader: FRAG,
      transparent: true,
      depthWrite: false,
    });
    this.mesh = new THREE.InstancedMesh(geo, mat, n);
    this.mesh.name = 'seams';
    this.mesh.renderOrder = -1; // above the ground plane, below everything else
    this.mesh.frustumCulled = false;

    seams.forEach((s, i) => {
      const { x, y, w, h } = s.rect;
      // Quad covers the rect plus the era bleed on every side.
      this.dummy.position.set(x + w / 2, 0.012, y + h / 2);
      this.dummy.scale.set(w + ERA_BLEED * 2, 1, h + ERA_BLEED * 2);
      this.dummy.updateMatrix();
      this.mesh.setMatrixAt(i, this.dummy.matrix);

      c.setHex(ERA_TINT[s.era] ?? ERA_TINT.stream);
      era.set([c.r, c.g, c.b], i * 3);
      const item = data.items.find((it) => it.id === s.resource);
      c.set(item?.color ?? '#8899aa');
      ore.set([c.r, c.g, c.b], i * 3);
      half.set([w / 2, h / 2], i * 2);
      rich[i] = s.richness ?? 0.5;
    });
    this.mesh.count = seams.length;
    this.mesh.instanceMatrix.needsUpdate = true;
    geo.setAttribute('aEra', new THREE.InstancedBufferAttribute(era, 3));
    geo.setAttribute('aOre', new THREE.InstancedBufferAttribute(ore, 3));
    geo.setAttribute('aHalf', new THREE.InstancedBufferAttribute(half, 2));
    geo.setAttribute('aRich', new THREE.InstancedBufferAttribute(rich, 1));
  }

  frame(timeSec: number): void {
    this.uniforms.uTime.value = timeSec;
  }
}

/** The seams DATA authored. Statically imported so it works in a production build too. */
export function loadSeams(): SeamDef[] {
  return ((seamsFile as { seams?: SeamDef[] }).seams ?? []) as SeamDef[];
}

/** The SE corner DATA keeps seam-free so the relic always has somewhere to land. */
export function relicReserve(): SeamRect | null {
  return (seamsFile as { relicReserve?: SeamRect }).relicReserve ?? null;
}
