import * as THREE from "three";
import gsap from "gsap";
import { makeMaterials } from "./textures.js";
import { buildGable, buildHip, buildStorm, buildEstate, buildMustang } from "./houses.js";

/*
 * Hidden-line blueprint renderer.
 * Faces render as opaque "paper" (near the page canvas colour) so nearer
 * geometry occludes the linework behind it, exactly like a technical drawing.
 * Edges are extracted per mesh and drawn in bone ink at three weights.
 * A pair of opposing clipping planes splits every ink line into "inked"
 * (behind the scan) and "pencil ghost" (ahead of it), so the LiDAR pass
 * literally draws the house, and chip swaps replay the same plotter wipe.
 */

const PAPER = 0x11141a;
const INK = 0xdfe4ea;
const AMBER = 0xf2a83b;

export function createScene(mount, { reduced = false } = {}) {
  const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
  renderer.localClippingEnabled = true;
  renderer.domElement.style.cssText = "width:100%;height:100%;display:block";
  mount.prepend(renderer.domElement);

  const scene = new THREE.Scene();
  let W = 880, H = 344;
  const camera = new THREE.PerspectiveCamera(33, W / H, 1, 500);

  const M = makeMaterials();

  /* ---- ink pipeline ------------------------------------------------------ */
  const clipInk = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 1e6);   /* keeps x < c */
  const clipGhost = new THREE.Plane(new THREE.Vector3(1, 0, 0), -1e6); /* keeps x > c */
  const lineMat = (opacity, color = INK, clipped = true) =>
    new THREE.LineBasicMaterial({
      color, transparent: true, opacity,
      clippingPlanes: clipped ? [clipInk] : null,
    });
  const inks = { primary: lineMat(0.95), roofBase: null, detail: lineMat(0.52), site: lineMat(0.26) };
  const ghost = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.16, clippingPlanes: [clipGhost] });
  const inkRed = new THREE.LineBasicMaterial({ color: 0xff6644, transparent: true, opacity: 0.9, clippingPlanes: [clipInk] });
  const paper = new THREE.MeshBasicMaterial({ color: PAPER, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
  /* dusk: window glass carries a faint interior warmth without leaving the ink language */
  const glassGlow = new THREE.MeshBasicMaterial({ color: 0x30260e, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
  function setScanX(x) { clipInk.constant = x; clipGhost.constant = -x; }
  setScanX(reduced ? 1e6 : -78);

  /* ---- houses, converted to hidden-line ---------------------------------- */
  const houses = { gable: buildGable(M), hip: buildHip(M), storm: buildStorm(M), estate: buildEstate(M), mustang: buildMustang(M) };
  let styleKey = "gable";

  function classify(m, house, inherited) {
    if (inherited) return inherited;
    const mat = m.material;
    if (house.hl.roofMats.includes(mat)) return "roof";
    if (mat === M.tarp) return "tarp";
    if (mat === M.trunk || mat === M.foliage || mat === M.foliage2) return "sitewire";
    if (mat === M.asphalt) return "siteflat";
    if (mat === M.glass || mat === M.trim || mat === M.doorRed || mat === M.doorBlue || mat === M.garage) return "detail";
    const h = m.geometry && m.geometry.parameters ? m.geometry.parameters.height : undefined;
    if (mat === M.concrete) return h !== undefined && h <= 0.35 ? "siteflat" : "primary";
    if (mat === M.porchRoof) return h !== undefined && h <= 0.25 ? "site" : "primary";
    return "primary";
  }

  function convert(house) {
    house.roofLineMats = [];
    const jobs = [];
    (function walk(o, inherited) {
      const tag = o.userData && o.userData.ink ? o.userData.ink : inherited;
      if (tag === "skip") return;
      if (o.isMesh) jobs.push([o, tag]);
      const kids = o.children.slice();
      for (const c of kids) walk(c, tag);
    })(house.group, null);
    for (const [m, tag] of jobs) {
      if (m.isInstancedMesh) {
        const im = new THREE.MeshBasicMaterial({ color: 0xff5030 });
        m.material = im;
        if (house.hl.damage) { house.hl.damage.mat = im; }
        continue;
      }
      const level = classify(m, house, tag);
      const eg = new THREE.EdgesGeometry(m.geometry, 30);
      let mat;
      if (level === "roof") {
        mat = lineMat(0.95);
        house.roofLineMats.push(mat);
      } else if (level === "tarp") {
        mat = inkRed;
      } else {
        mat = inks[level === "siteflat" || level === "sitewire" ? "site" : level] || inks.primary;
      }
      const ln = new THREE.LineSegments(eg, mat);
      ln.position.copy(m.position);
      ln.rotation.copy(m.rotation);
      ln.scale.copy(m.scale);
      m.parent.add(ln);
      if (!reduced) {
        const gl = new THREE.LineSegments(eg, ghost);
        gl.position.copy(m.position);
        gl.rotation.copy(m.rotation);
        gl.scale.copy(m.scale);
        m.parent.add(gl);
      }
      if (level === "siteflat" || level === "sitewire" || level === "tarp") m.visible = false;
      else if (m.material === M.glass) m.material = glassGlow;
      else m.material = paper;
    }
  }

  for (const k of Object.keys(houses)) {
    const h = houses[k];
    h.hl.ridge.userData.ink = "skip";
    h.hl.eave.userData.ink = "skip";
    h.group.add(h.hl.ridge, h.hl.eave);
    convert(h);
    h.group.visible = k === styleKey;
    scene.add(h.group);
  }

  /* ---- site plan linework (scene level, unclipped) ------------------------ */
  const sitePlan = new THREE.Group();
  {
    const gridMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.06 });
    const pts = [];
    const R = 50;
    for (let g = -R; g <= R; g += 10) {
      const m = Math.sqrt(Math.max(0, R * R - g * g));
      pts.push(g, 0, -m, g, 0, m, -m, 0, g, m, 0, g);
    }
    const gg = new THREE.BufferGeometry();
    gg.setAttribute("position", new THREE.BufferAttribute(new Float32Array(pts), 3));
    sitePlan.add(new THREE.LineSegments(gg, gridMat));

    /* dashed property boundary */
    const bMat = new THREE.LineDashedMaterial({ color: INK, transparent: true, opacity: 0.3, dashSize: 2.2, gapSize: 2 });
    const bx = 52, bz = 41;
    const bGeo = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(-bx, 0, -bz), new THREE.Vector3(bx, 0, -bz),
      new THREE.Vector3(bx, 0, bz), new THREE.Vector3(-bx, 0, bz),
    ]);
    const bound = new THREE.LineLoop(bGeo, bMat);
    bound.computeLineDistances();
    sitePlan.add(bound);

    /* survey crosses at the corners */
    const cMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.4 });
    const cp = [];
    [[-bx, -bz], [bx, -bz], [bx, bz], [-bx, bz]].forEach(([x, z]) => {
      cp.push(x - 1.6, 0, z, x + 1.6, 0, z, x, 0, z - 1.6, x, 0, z + 1.6);
    });
    const cGeo = new THREE.BufferGeometry();
    cGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(cp), 3));
    sitePlan.add(new THREE.LineSegments(cGeo, cMat));

    /* street edges + centre dashes */
    const sMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.2 });
    const sGeo = new THREE.BufferGeometry();
    sGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array([
      -62, 0, 44, 62, 0, 44, -62, 0, 57, 62, 0, 57,
    ]), 3));
    sitePlan.add(new THREE.LineSegments(sGeo, sMat));
    const dashes = [];
    for (let x = -58; x < 58; x += 9) dashes.push(x, 0, 50.5, x + 4, 0, 50.5);
    const dGeo = new THREE.BufferGeometry();
    dGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(dashes), 3));
    sitePlan.add(new THREE.LineSegments(dGeo, sMat));
    sitePlan.position.y = 0.01;
  }
  scene.add(sitePlan);

  /* soft contact blob keeps the mass grounded */
  const contact = new THREE.Mesh(new THREE.CircleGeometry(40, 48), M.contact);
  contact.rotation.x = -Math.PI / 2;
  contact.position.y = 0.005;
  contact.material.opacity = 0.32;
  contact.scale.set(1.25, 0.85, 1);
  scene.add(contact);

  /* ---- scan sheet visuals -------------------------------------------------- */
  const sheet = new THREE.Mesh(new THREE.PlaneGeometry(140, 44), M.scan);
  sheet.rotation.y = Math.PI / 2;
  sheet.position.y = 18;
  sheet.visible = false;
  sheet.renderOrder = 20;
  scene.add(sheet);
  const scanEdge = new THREE.Mesh(
    new THREE.BoxGeometry(0.22, 50, 128),
    new THREE.MeshBasicMaterial({ color: 0xffcf55, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false })
  );
  scanEdge.position.y = 22;
  scanEdge.visible = false;
  scanEdge.renderOrder = 21;
  scene.add(scanEdge);

  /* ---- camera orbit -------------------------------------------------------- */
  const target = new THREE.Vector3(3, 9.5, 0);
  let az = -0.62, el = 0.27, radius = 96;
  let vel = 0.00115, dragVel = 0, dragging = false;
  function placeCamera() {
    camera.position.set(
      target.x + radius * Math.cos(el) * Math.sin(az),
      target.y + radius * Math.sin(el),
      target.z + radius * Math.cos(el) * Math.cos(az)
    );
    camera.lookAt(target);
  }
  placeCamera();

  const cvs = renderer.domElement;
  cvs.style.cursor = "grab";
  let lastX = 0, lastT = 0;
  cvs.addEventListener("pointerdown", (e) => {
    dragging = true;
    lastX = e.clientX;
    lastT = performance.now();
    cvs.setPointerCapture(e.pointerId);
    cvs.style.cursor = "grabbing";
    onDrag();
  });
  cvs.addEventListener("pointermove", (e) => {
    if (!dragging) return;
    const now = performance.now();
    const dx = e.clientX - lastX;
    az -= dx * 0.008;
    dragVel = (dx / Math.max(now - lastT, 1)) * 0.11;
    lastX = e.clientX;
    lastT = now;
  });
  const up = () => {
    if (!dragging) return;
    dragging = false;
    cvs.style.cursor = "grab";
    vel = Math.max(-0.2, Math.min(0.2, -dragVel));
    dragVel = 0;
  };
  cvs.addEventListener("pointerup", up);
  cvs.addEventListener("pointercancel", up);
  let onDrag = () => {};

  /* ---- scan scrub + plotter wipe ------------------------------------------- */
  let stageT = 0;
  const swapWipe = { active: false, x: -80 };
  function scanUpdate() {
    const t = stageT;
    const sa = Math.min(1, Math.max(0, (t - 0.03) / 0.05)) * (1 - Math.min(1, Math.max(0, (t - 0.76) / 0.15)));
    const sp = Math.min(1, Math.max(0, (t - 0.05) / 0.28));
    const on = sa > 0.01 && sp > 0 && sp < 1;
    sheet.visible = on;
    scanEdge.visible = on;
    if (on) {
      const x = -78 + 156 * sp;
      sheet.position.x = x;
      scanEdge.position.x = x;
      sheet.material.opacity = 0.16 * sa;
      scanEdge.material.opacity = 0.5 * sa;
    }
    if (reduced) { setScanX(1e6); return; }
    if (swapWipe.active) setScanX(swapWipe.x);
    else setScanX(-78 + 156 * sp);
  }

  /* ---- highlights ----------------------------------------------------------- */
  function applyHighlight(k) {
    for (const key of Object.keys(houses)) {
      const h = houses[key];
      const on = key === styleKey;
      h.hl.ridge.visible = on && k === "ridge";
      h.hl.eave.visible = on && k === "eave";
      if (h.hl.roof) h.hl.roof.visible = on && k === "roof";
      (h.roofLineMats || []).forEach((m) => m.color.setHex(on && k === "roof" ? AMBER : INK));
      if (h.hl.damage) h.hl.damage.mat.color.setHex(on && k === "damage" ? 0xff2612 : 0xff5030);
    }
  }

  /* ---- loop ------------------------------------------------------------------ */
  let visible = true;
  if ("IntersectionObserver" in window) new IntersectionObserver((en) => { visible = en[0].isIntersecting; }).observe(cvs);
  let frameCb = null;
  let prev = performance.now();
  renderer.setAnimationLoop((now) => {
    const dt = Math.min(now - prev, 50);
    prev = now;
    if (!visible) return;
    if (!reduced && !dragging) {
      vel += (0.00115 - vel) * 0.02;
      az += (vel * dt) / 16.7;
    }
    placeCamera();
    scanUpdate();
    if (frameCb) frameCb();
    renderer.render(scene, camera);
  });

  return {
    el: cvs,
    onFrame(cb) { frameCb = cb; },
    onDragStart(cb) { onDrag = cb; },
    resize(w, h) {
      W = Math.max(2, Math.round(w));
      H = Math.max(2, Math.round(h));
      renderer.setSize(W, H, false);
      camera.aspect = W / H;
      camera.updateProjectionMatrix();
    },
    measure({ style }) {
      return houses[style]?.measure || houses.gable.measure;
    },
    anchors() { return houses[styleKey].anchors; },
    project(v3) {
      const p = v3.clone().project(camera);
      return { x: (p.x * 0.5 + 0.5) * W, y: (-p.y * 0.5 + 0.5) * H, behind: p.z > 1 };
    },
    viewSize() { return { w: W, h: H }; },
    stage(t) { stageT = t; },
    highlight(k) { applyHighlight(k || null); },
    /* replay the plotter wipe on the current model (first-load draw-in) */
    wipe() {
      if (reduced) return;
      gsap.killTweensOf(swapWipe);
      swapWipe.active = true;
      swapWipe.x = -80;
      gsap.to(swapWipe, {
        x: 85, duration: 1.1, ease: "power2.inOut",
        onComplete() { swapWipe.active = false; },
      });
    },
    set(cfg) {
      const next = cfg.style && houses[cfg.style] ? cfg.style : "gable";
      if (next === styleKey) return;
      houses[styleKey].group.visible = false;
      styleKey = next;
      houses[next].group.visible = true;
      applyHighlight(null);
      if (reduced) return;
      /* the new model plots itself in, left to right */
      gsap.killTweensOf(swapWipe);
      swapWipe.active = true;
      swapWipe.x = -80;
      gsap.to(swapWipe, {
        x: 85, duration: 0.9, ease: "power2.inOut",
        onComplete() { swapWipe.active = false; },
      });
    },
  };
}
