/**
 * LANE-RENDER — orthographic isometric camera rig.
 * Pan: middle-drag or WASD. Zoom: wheel (clamped). Resize handled.
 */
import * as THREE from 'three';

const ISO_DIR = new THREE.Vector3(1, 1.15, 1).normalize();
/**
 * Ortho: this affects near/far clipping, not apparent size. It DOES set how far every
 * object sits from the camera, so scene fog must be banded around it — see index.ts.
 */
export const CAM_DIST = 90;
const ZOOM_MIN = 8;
const ZOOM_MAX = 40;
const PAN_KEY_SPEED = 14; // world units/sec at default zoom

export const WORLD_HALF = 32; // 64x64 world, origin at centre (CONTRACTS.md)

export class CameraRig {
  readonly camera: THREE.OrthographicCamera;
  private target = new THREE.Vector3(0, 0, 0);
  /** Half-height of the ortho frustum in world units — our zoom level. */
  private zoomD = 16;
  private keys = new Set<string>();
  private dragging = false;
  private lastX = 0;
  private lastY = 0;
  private disposers: Array<() => void> = [];

  constructor(private container: HTMLElement, private dom: HTMLCanvasElement) {
    this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 400);
    this.applyFrustum();
    this.place();
    this.bind();
  }

  private bind(): void {
    const onWheel = (e: WheelEvent) => {
      e.preventDefault();
      this.zoomD = THREE.MathUtils.clamp(this.zoomD * Math.exp(e.deltaY * 0.0012), ZOOM_MIN, ZOOM_MAX);
      this.applyFrustum();
    };
    const onDown = (e: PointerEvent) => {
      if (e.button === 0) return; // left stays free for LANE-UI's click-to-place
      // main.ts places on ANY pointerdown over #game; keep our pan from also building.
      e.stopPropagation();
      e.preventDefault();
      this.dragging = true;
      this.lastX = e.clientX;
      this.lastY = e.clientY;
      this.dom.setPointerCapture(e.pointerId);
    };
    const onMove = (e: PointerEvent) => {
      if (!this.dragging) return;
      const wpp = (this.zoomD * 2) / this.container.clientHeight; // world units per pixel
      this.panScreen((e.clientX - this.lastX) * wpp, (e.clientY - this.lastY) * wpp);
      this.lastX = e.clientX;
      this.lastY = e.clientY;
    };
    const onUp = (e: PointerEvent) => {
      this.dragging = false;
      if (this.dom.hasPointerCapture(e.pointerId)) this.dom.releasePointerCapture(e.pointerId);
    };
    const onCtx = (e: Event) => e.preventDefault(); // right-drag pan without a menu
    const isTyping = (t: EventTarget | null) => {
      const el = t as HTMLElement | null;
      return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
    };
    const onKeyDown = (e: KeyboardEvent) => {
      if (isTyping(e.target)) return; // don't eat LANE-UI's typing
      this.keys.add(e.key.toLowerCase());
    };
    const onKeyUp = (e: KeyboardEvent) => this.keys.delete(e.key.toLowerCase());
    const onBlur = () => this.keys.clear();

    this.dom.addEventListener('wheel', onWheel, { passive: false });
    this.dom.addEventListener('pointerdown', onDown);
    this.dom.addEventListener('pointermove', onMove);
    this.dom.addEventListener('pointerup', onUp);
    this.dom.addEventListener('pointercancel', onUp);
    this.dom.addEventListener('contextmenu', onCtx);
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    window.addEventListener('blur', onBlur);

    this.disposers.push(() => {
      this.dom.removeEventListener('wheel', onWheel);
      this.dom.removeEventListener('pointerdown', onDown);
      this.dom.removeEventListener('pointermove', onMove);
      this.dom.removeEventListener('pointerup', onUp);
      this.dom.removeEventListener('pointercancel', onUp);
      this.dom.removeEventListener('contextmenu', onCtx);
      window.removeEventListener('keydown', onKeyDown);
      window.removeEventListener('keyup', onKeyUp);
      window.removeEventListener('blur', onBlur);
    });
  }

  /** Screen-space drag delta (world units) -> target motion on the ground plane. */
  private panScreen(dxWorld: number, dyWorld: number): void {
    const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrix, 0).setY(0).normalize();
    const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrix, 1).setY(0).normalize();
    this.target.addScaledVector(right, -dxWorld);
    this.target.addScaledVector(up, dyWorld);
    this.clampTarget();
    this.place();
  }

  update(dtSec: number): void {
    let kx = 0;
    let ky = 0;
    if (this.keys.has('a') || this.keys.has('arrowleft')) kx -= 1;
    if (this.keys.has('d') || this.keys.has('arrowright')) kx += 1;
    if (this.keys.has('w') || this.keys.has('arrowup')) ky += 1;
    if (this.keys.has('s') || this.keys.has('arrowdown')) ky -= 1;
    if (!kx && !ky) return;
    // Scale with zoom so keyboard panning feels the same when zoomed in.
    const step = PAN_KEY_SPEED * dtSec * (this.zoomD / 16);
    const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrix, 0).setY(0).normalize();
    const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrix, 1).setY(0).normalize();
    this.target.addScaledVector(right, kx * step);
    this.target.addScaledVector(up, ky * step);
    this.clampTarget();
    this.place();
  }

  private clampTarget(): void {
    this.target.x = THREE.MathUtils.clamp(this.target.x, -WORLD_HALF, WORLD_HALF);
    this.target.z = THREE.MathUtils.clamp(this.target.z, -WORLD_HALF, WORLD_HALF);
    this.target.y = 0;
  }

  private place(): void {
    this.camera.position.copy(this.target).addScaledVector(ISO_DIR, CAM_DIST);
    this.camera.lookAt(this.target);
    this.camera.updateMatrixWorld();
  }

  private applyFrustum(): void {
    const aspect = Math.max(0.0001, this.container.clientWidth / Math.max(1, this.container.clientHeight));
    const d = this.zoomD;
    this.camera.left = -d * aspect;
    this.camera.right = d * aspect;
    this.camera.top = d;
    this.camera.bottom = -d;
    this.camera.updateProjectionMatrix();
  }

  resize(): void {
    this.applyFrustum();
  }

  /** Jump the rig to frame a specific spot — the showroom's camera preset. */
  frame(x: number, z: number, zoom: number): void {
    this.target.set(x, 0, z);
    this.zoomD = THREE.MathUtils.clamp(zoom, ZOOM_MIN, ZOOM_MAX);
    this.applyFrustum();
    this.place();
  }

  dispose(): void {
    for (const d of this.disposers) d();
    this.disposers = [];
  }
}
