import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { SKY_VERT, SKY_FRAG } from './shaders/sky';
import { OCEAN_VERT, OCEAN_FRAG } from './shaders/ocean';
import { MIST_VERT, MIST_FRAG } from './shaders/mist';
import { FINAL_VERT, FINAL_FRAG } from './shaders/post';
import { PALETTES, Palette } from './palettes';

// ---------------------------------------------------------------------------
// renderer + composer
// ---------------------------------------------------------------------------
let renderer: THREE.WebGLRenderer;
try {
  renderer = new THREE.WebGLRenderer({
    antialias: false,
    powerPreference: 'high-performance',
    stencil: false,
    depth: true,
  });
} catch (e) {
  const el = document.getElementById('hints');
  if (el) el.textContent = 'BMO is sorry — this needs WebGL.';
  throw e;
}
renderer.toneMapping = THREE.NoToneMapping; // graded manually in the final pass
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(58, 1, 0.1, 6000);

const BASE_PR = Math.min(window.devicePixelRatio || 1, 1.5);
const QUALITY_STEPS = [1.0, 0.85, 0.72, 0.6, 0.5];
let qualityIdx = 0;

const composer = new EffectComposer(
  renderer,
  new THREE.WebGLRenderTarget(2, 2, { type: THREE.HalfFloatType })
);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(1, 1), 0.55, 0.5, 0.9);
composer.addPass(bloom);
const finalPass = new ShaderPass({
  uniforms: {
    tDiffuse: { value: null },
    uTime: { value: 0 },
    uRes: { value: new THREE.Vector2(1, 1) },
    uAberration: { value: 0.045 },
    uExposure: { value: 1.1 },
  },
  vertexShader: FINAL_VERT,
  fragmentShader: FINAL_FRAG,
});
composer.addPass(finalPass);

// ---------------------------------------------------------------------------
// palette (live colors lerp toward the target palette each frame)
// ---------------------------------------------------------------------------
const cloneP = (p: Palette): Palette => ({
  name: p.name,
  skyTop: p.skyTop.clone(), skyHorizon: p.skyHorizon.clone(), nebula: p.nebula.clone(),
  auroraA: p.auroraA.clone(), auroraB: p.auroraB.clone(),
  moonA: p.moonA.clone(), moonB: p.moonB.clone(),
  deep: p.deep.clone(), glint: p.glint.clone(),
  mist: p.mist.clone(), spark: p.spark.clone(),
});
const live: Palette = cloneP(PALETTES[0]);
let paletteIdx = 0;
let target = PALETTES[0];

function lerpPalette(dt: number) {
  const k = 1 - Math.exp(-dt * 1.6);
  (Object.keys(target) as (keyof Palette)[]).forEach((key) => {
    const a = live[key], b = target[key];
    if (a instanceof THREE.Color && b instanceof THREE.Color) a.lerp(b, k);
  });
}

// ---------------------------------------------------------------------------
// shared sky uniforms (same {value} objects bound into sky + ocean materials)
// ---------------------------------------------------------------------------
const skyUniforms = {
  uTime: { value: 0 },
  uMoonADir: { value: new THREE.Vector3(0.3, 0.35, 0.9).normalize() },
  uMoonBDir: { value: new THREE.Vector3(-0.55, 0.52, 0.65).normalize() },
  uMoonACol: { value: live.moonA },
  uMoonBCol: { value: live.moonB },
  uSkyTop: { value: live.skyTop },
  uSkyHorizon: { value: live.skyHorizon },
  uNebulaCol: { value: live.nebula },
  uAuroraA: { value: live.auroraA },
  uAuroraB: { value: live.auroraB },
  uAuroraAmp: { value: 1.0 },
  uFlash: { value: 0 },
  uFlashDir: { value: new THREE.Vector3(0.8, 0, 0.6).normalize() },
};

// ---------------------------------------------------------------------------
// sky dome
// ---------------------------------------------------------------------------
const skyMat = new THREE.ShaderMaterial({
  uniforms: skyUniforms,
  vertexShader: SKY_VERT,
  fragmentShader: SKY_FRAG,
  side: THREE.BackSide,
  depthWrite: false,
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(3000, 48, 32), skyMat);
sky.frustumCulled = false;
sky.renderOrder = -1;
scene.add(sky);

// ---------------------------------------------------------------------------
// mercury ocean
// ---------------------------------------------------------------------------
const OCEAN_SIZE = 600;
const OCEAN_SEGS = 380;
const oceanGeo = new THREE.PlaneGeometry(OCEAN_SIZE, OCEAN_SIZE, OCEAN_SEGS, OCEAN_SEGS);
oceanGeo.rotateX(-Math.PI / 2);
const oceanMat = new THREE.ShaderMaterial({
  uniforms: {
    ...skyUniforms,
    uWaveAmp: { value: 0.8 },
    uDeepCol: { value: live.deep },
    uGlintCol: { value: live.glint },
    uMirror: { value: 0.92 },
  },
  vertexShader: OCEAN_VERT,
  fragmentShader: OCEAN_FRAG,
});
const ocean = new THREE.Mesh(oceanGeo, oceanMat);
ocean.frustumCulled = false;
scene.add(ocean);
const OCEAN_CELL = OCEAN_SIZE / OCEAN_SEGS;

// ---------------------------------------------------------------------------
// mist motes
// ---------------------------------------------------------------------------
const MIST_COUNT = 1400;
const seeds = new Float32Array(MIST_COUNT * 3);
for (let i = 0; i < seeds.length; i++) seeds[i] = Math.random() - 0.5;
const mistGeo = new THREE.BufferGeometry();
mistGeo.setAttribute('position', new THREE.BufferAttribute(seeds, 3));
const mistMat = new THREE.ShaderMaterial({
  uniforms: {
    uTime: skyUniforms.uTime,
    uPixelRatio: { value: BASE_PR },
    uMistCol: { value: live.mist },
    uSparkCol: { value: live.spark },
  },
  vertexShader: MIST_VERT,
  fragmentShader: MIST_FRAG,
  transparent: true,
  blending: THREE.AdditiveBlending,
  depthWrite: false,
});
const mist = new THREE.Points(mistGeo, mistMat);
mist.frustumCulled = false;
mist.renderOrder = 2;
scene.add(mist);

// ---------------------------------------------------------------------------
// camera rig: slow forward drift on a lazily curving heading + wave bob,
// mouse/touch adds a look offset
// ---------------------------------------------------------------------------
let simTime = 0;
let paused = false;
const camPos = new THREE.Vector3(0, 6, 0);
let yawLook = 0, pitchLook = 0;          // eased look offsets
let yawTarget = 0, pitchTarget = 0;
let dragYaw = 0, dragPitch = 0;          // persistent drag offsets

window.addEventListener('mousemove', (e) => {
  if (dragging) return;
  yawTarget = (e.clientX / window.innerWidth - 0.5) * 0.55;
  pitchTarget = (e.clientY / window.innerHeight - 0.5) * 0.3;
});

let dragging = false;
let lastPX = 0, lastPY = 0;
window.addEventListener('pointerdown', (e) => {
  dragging = true;
  lastPX = e.clientX; lastPY = e.clientY;
});
window.addEventListener('pointermove', (e) => {
  if (!dragging) return;
  dragYaw -= (e.clientX - lastPX) * 0.0035;
  dragPitch -= (e.clientY - lastPY) * 0.0022;
  dragPitch = Math.max(-0.5, Math.min(0.45, dragPitch));
  lastPX = e.clientX; lastPY = e.clientY;
});
window.addEventListener('pointerup', () => (dragging = false));
window.addEventListener('pointercancel', () => (dragging = false));

window.addEventListener('keydown', (e) => {
  if (e.code === 'Space') {
    paused = !paused;
    e.preventDefault();
  } else if (e.key === 'c' || e.key === 'C') {
    paletteIdx = (paletteIdx + 1) % PALETTES.length;
    target = PALETTES[paletteIdx];
  }
});

// ---------------------------------------------------------------------------
// distant heat lightning scheduler
// ---------------------------------------------------------------------------
let nextStorm = 5 + Math.random() * 8;
let pulses: number[] = [];
function updateLightning(t: number) {
  if (t > nextStorm) {
    const n = 2 + Math.floor(Math.random() * 3);
    for (let i = 0; i < n; i++) pulses.push(t + Math.random() * 0.7);
    const az = Math.random() * Math.PI * 2;
    (skyUniforms.uFlashDir.value as THREE.Vector3).set(Math.sin(az), 0, Math.cos(az));
    nextStorm = t + 9 + Math.random() * 16;
  }
  let f = 0;
  for (const tp of pulses) {
    if (t >= tp) f += Math.exp(-(t - tp) * 9);
  }
  pulses = pulses.filter((tp) => t - tp < 1.2);
  skyUniforms.uFlash.value = Math.min(f, 1.4);
}

// ---------------------------------------------------------------------------
// adaptive resolution: keep 60fps by dropping internal scale if needed
// ---------------------------------------------------------------------------
let emaDt = 16.7;
let lastQualityCheck = 0;
let goodStreak = 0;
function applySize() {
  const w = window.innerWidth, h = window.innerHeight;
  const pr = BASE_PR * QUALITY_STEPS[qualityIdx];
  renderer.setPixelRatio(pr);
  renderer.setSize(w, h);
  composer.setPixelRatio(pr);
  composer.setSize(w, h);
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  finalPass.uniforms.uRes.value.set(w * pr, h * pr);
  mistMat.uniforms.uPixelRatio.value = pr;
}
window.addEventListener('resize', applySize);
applySize();

function adaptQuality(now: number) {
  if (now - lastQualityCheck < 1500) return;
  lastQualityCheck = now;
  const fps = 1000 / emaDt;
  if (fps < 50 && qualityIdx < QUALITY_STEPS.length - 1) {
    qualityIdx++;
    goodStreak = 0;
    applySize();
  } else if (fps > 58 && qualityIdx > 0) {
    if (++goodStreak >= 3) {
      qualityIdx--;
      goodStreak = 0;
      applySize();
    }
  } else {
    goodStreak = 0;
  }
}

// ---------------------------------------------------------------------------
// hints fade
// ---------------------------------------------------------------------------
setTimeout(() => {
  document.getElementById('hints')?.classList.add('faded');
  document.getElementById('title')?.classList.add('faded');
}, 9000);

// ---------------------------------------------------------------------------
// main loop
// ---------------------------------------------------------------------------
const fwd = new THREE.Vector3();
const lookAt = new THREE.Vector3();
let lastT = performance.now();

renderer.setAnimationLoop((now: number) => {
  const dt = Math.min((now - lastT) / 1000, 0.1);
  lastT = now;
  emaDt = emaDt * 0.95 + (now ? Math.min(dt * 1000, 100) : 16.7) * 0.05;

  if (!paused) simTime += dt;
  const t = simTime;
  skyUniforms.uTime.value = t;
  finalPass.uniforms.uTime.value = t;

  // palette + slow mood drift
  lerpPalette(dt);
  skyUniforms.uAuroraAmp.value = 0.65 + 0.5 * Math.sin(t * 0.045 + 1.2);
  updateLightning(t);

  // moons creep across the sky
  const azA = 0.32 + t * 0.0022;
  const elA = 0.34 + 0.05 * Math.sin(t * 0.01);
  skyUniforms.uMoonADir.value.set(Math.sin(azA) * Math.cos(elA), Math.sin(elA), Math.cos(azA) * Math.cos(elA));
  const azB = -0.62 - t * 0.0016;
  const elB = 0.52 + 0.04 * Math.sin(t * 0.008 + 2.0);
  skyUniforms.uMoonBDir.value.set(Math.sin(azB) * Math.cos(elB), Math.sin(elB), Math.cos(azB) * Math.cos(elB));

  // camera: forward drift + bob
  if (!paused) {
    const heading = 0.3 * Math.sin(t * 0.013);
    fwd.set(Math.sin(heading), 0, Math.cos(heading));
    camPos.addScaledVector(fwd, dt * 2.2);
  }
  camPos.y = 6.0 + 0.45 * Math.sin(t * 0.4) + 0.2 * Math.sin(t * 0.97 + 2.0);
  camera.position.copy(camPos);

  // eased look
  yawLook += (yawTarget - yawLook) * Math.min(dt * 2.5, 1);
  pitchLook += (pitchTarget - pitchLook) * Math.min(dt * 2.5, 1);
  const heading = 0.3 * Math.sin(t * 0.013);
  const yaw = heading + yawLook + dragYaw;
  const pitch = -0.055 - pitchLook - dragPitch;
  lookAt.set(
    camPos.x + Math.sin(yaw) * Math.cos(pitch),
    camPos.y + Math.sin(pitch),
    camPos.z + Math.cos(yaw) * Math.cos(pitch)
  );
  camera.lookAt(lookAt);

  // infinite ocean: plane follows camera, snapped to the vertex grid
  ocean.position.set(
    Math.round(camPos.x / OCEAN_CELL) * OCEAN_CELL,
    0,
    Math.round(camPos.z / OCEAN_CELL) * OCEAN_CELL
  );
  sky.position.copy(camPos);

  adaptQuality(now);
  composer.render();
});

// expose a tiny status hook for the automated check
(window as unknown as Record<string, unknown>).__ATS__ = {
  get fps() { return 1000 / emaDt; },
  get quality() { return QUALITY_STEPS[qualityIdx]; },
};
