'use client';
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import {
  buildObject,
  createTexture,
  disposeObject,
  makeMaterial,
  materialPresets,
  texturePresets,
  type MaterialSettings,
} from '@/lib/objects';
import { objects } from '@/lib/catalog';
import { sceneEffects } from '@/lib/showcase';
import { attachEffect } from '@/lib/scene-effects';
import {
  Download,
  Pause,
  Play,
  RotateCcw,
  Box,
  ArrowUpRight,
} from 'lucide-react';

function saveBlob(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob),
    a = document.createElement('a');
  a.href = url;
  a.download = name;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 3000);
}
export default function Scene({
  compact = false,
  initial = 'bird',
  initialMaterial,
  effect = 'pointer',
}: {
  compact?: boolean;
  initial?: string;
  initialMaterial?: string;
  effect?: string;
}) {
  const host = useRef<HTMLDivElement>(null);
  const model = useRef<THREE.Group | null>(null);
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
  const materialRef = useRef<THREE.MeshPhysicalMaterial | null>(null);
  const [object, setObject] = useState(initial);
  const [preset, setPreset] = useState(
    initialMaterial ||
      (initial === 'bird'
        ? 'glass'
        : initial === 'wrench'
          ? 'steel'
          : 'plastic'),
  );
  const [settings, setSettings] = useState<MaterialSettings>({
    ...materialPresets.find(
      (m) =>
        m.id ===
        (initialMaterial ||
          (initial === 'bird'
            ? 'glass'
            : initial === 'wrench'
              ? 'steel'
              : 'plastic')),
    )!,
  });
  const [activeEffect, setActiveEffect] = useState(effect);
  const [mode, setMode] = useState(
      effect === 'scroll'
        ? 'scroll'
        : effect === 'turntable'
          ? 'auto'
          : 'pointer',
    ),
    [playing, setPlaying] = useState(
      effect !== 'pointer' && effect !== 'scroll',
    ),
    [light, setLight] = useState('studio'),
    [error, setError] = useState(''),
    [exporting, setExporting] = useState(false),
    [angle, setAngle] = useState(0);
  const live = useRef({ mode, playing, angle });
  live.current = { mode, playing, angle };
  useEffect(() => {
    if (compact) return;
    const id = new URLSearchParams(location.search).get('object');
    if (id && objects.some((o) => o.id === id)) {
      setObject(id);
      const preset = materialPresets.find(
        (m) =>
          m.id ===
          (['bird', 'gem'].includes(id)
            ? 'glass'
            : ['wrench', 'bolt', 'chip'].includes(id)
              ? 'steel'
              : 'ceramic'),
      )!;
      setPreset(preset.id);
      setSettings({ ...preset });
    }
  }, [compact]);
  const settingsRef = useRef(settings);
  settingsRef.current = settings;
  useEffect(() => {
    const el = host.current;
    if (!el) return;
    let disposed = false,
      visible = true,
      last = 0,
      elapsed = 0;
    let renderer: THREE.WebGLRenderer;
    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: false,
        preserveDrawingBuffer: true,
        powerPreference: 'high-performance',
      });
    } catch {
      setError(
        'WebGL недоступен. Исходник и параметры модели можно скачать ниже.',
      );
      return;
    }
    setError('');
    rendererRef.current = renderer;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75));
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = light === 'dramatic' ? 0.8 : 1.1;
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    el.appendChild(renderer.domElement);
    renderer.domElement.setAttribute(
      'aria-label',
      `Интерактивная 3D-модель ${object}`,
    );
    renderer.domElement.setAttribute('role', 'img');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(
      light === 'dark'
        ? '#16191c'
        : light === 'dramatic'
          ? '#252126'
          : '#d6d8d3',
    );
    const camera = new THREE.PerspectiveCamera(33, 1, 0.1, 60);
    camera.position.set(0, 0.35, 5.7);
    camera.lookAt(0, 0, 0);
    const pmrem = new THREE.PMREMGenerator(renderer);
    const room = new RoomEnvironment();
    const env = pmrem.fromScene(room, 0.035);
    scene.environment = env.texture;
    room.dispose();
    pmrem.dispose();
    const ambient = new THREE.HemisphereLight(0xffffff, 0x45403a, 2);
    scene.add(ambient);
    const key = new THREE.DirectionalLight(0xffffff, 4);
    key.position.set(3, 5, 4);
    scene.add(key);
    const rim = new THREE.DirectionalLight(
      light === 'dramatic' ? 0x9da6ff : 0xffffff,
      2.5,
    );
    rim.position.set(-4, 1, -2);
    scene.add(rim);
    const material = makeMaterial(settingsRef.current);
    materialRef.current = material;
    const effectTime = attachEffect(material, activeEffect);
    const group = buildObject(object, material);
    scene.add(group);
    model.current = group;
    const pieces: {
      mesh: THREE.Mesh;
      start: THREE.Vector3;
      direction: THREE.Vector3;
    }[] = [];
    group.traverse((node) => {
      if (node instanceof THREE.Mesh) {
        node.geometry.computeBoundingBox();
        const center = node.geometry
          .boundingBox!.getCenter(new THREE.Vector3())
          .add(node.position);
        pieces.push({
          mesh: node,
          start: node.position.clone(),
          direction:
            center.length() > 0.01
              ? center.normalize()
              : new THREE.Vector3(0, 1, 0),
        });
      }
    });
    const ground = new THREE.Mesh(
      new THREE.PlaneGeometry(200, 200),
      new THREE.MeshStandardMaterial({
        color:
          light === 'dark'
            ? 0x181b1d
            : light === 'dramatic'
              ? 0x272329
              : 0xcbcec8,
        roughness: 0.72,
      }),
    );
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -1.65;
    scene.add(ground);
    // A real surface behind transmitted objects makes refraction legible without an external HDRI.
    const board = new THREE.Mesh(
      new THREE.PlaneGeometry(5, 5),
      new THREE.MeshStandardMaterial({
        color: light === 'studio' ? 0xf1f2e8 : 0x48434c,
        roughness: 0.7,
      }),
    );
    board.position.set(0, 0, -2.5);
    board.rotation.z = 0.4;
    scene.add(board);
    const pointer = { x: 0, y: 0 };
    const move = (e: PointerEvent) => {
      if (!matchMedia('(pointer:fine)').matches) return;
      const b = el.getBoundingClientRect();
      pointer.x = ((e.clientX - b.left) / b.width - 0.5) * 2;
      pointer.y = ((e.clientY - b.top) / b.height - 0.5) * 2;
    };
    const leave = () => {
      pointer.x = pointer.y = 0;
    };
    el.addEventListener('pointermove', move);
    el.addEventListener('pointerleave', leave);
    const resize = new ResizeObserver(() => {
      const w = el.clientWidth,
        h = el.clientHeight;
      if (w && h) {
        renderer.setSize(w, h);
        camera.aspect = w / h;
        camera.position.z = w / h < 0.8 ? 7 : 5.7;
        camera.updateProjectionMatrix();
      }
    });
    resize.observe(el);
    const intersection = new IntersectionObserver(([entry]) => {
      visible = entry.isIntersecting;
    });
    intersection.observe(el);
    const reduced = matchMedia('(prefers-reduced-motion:reduce)');
    const lost = (e: Event) => {
      e.preventDefault();
      setError(
        'Графический контекст потерян. Смените объект, чтобы перезапустить сцену.',
      );
    };
    renderer.domElement.addEventListener('webglcontextlost', lost);
    renderer.setAnimationLoop((t) => {
      if (disposed) return;
      const dt = Math.min((t - last) / 1000 || 0.016, 0.05);
      last = t;
      if (!visible || document.hidden) return;
      const { mode, playing, angle } = live.current;
      let rx = 0.12,
        ry = angle;
      if (!reduced.matches) {
        if (playing) elapsed += dt * 0.45;
        if (mode === 'pointer') {
          rx += pointer.y * 0.28;
          ry += pointer.x * 0.65 + (activeEffect === 'pointer' ? elapsed : 0);
        } else if (mode === 'scroll') {
          const rect = el.getBoundingClientRect();
          const p = THREE.MathUtils.clamp(
            (window.innerHeight - rect.top) /
              (window.innerHeight + rect.height),
            0,
            1,
          );
          ry += (p - 0.5) * Math.PI * 2;
        } else {
          ry += elapsed;
        }
      }
      effectTime.value = reduced.matches ? 0 : elapsed * 2;
      if (!reduced.matches) {
        if (activeEffect === 'gyroscope') {
          rx += elapsed * 0.55;
          ry += elapsed * 0.8;
          group.rotation.z = Math.sin(elapsed * 0.5) * 0.4;
        }
        if (activeEffect === 'pendulum') {
          group.rotation.z = Math.sin(elapsed * 1.7) * 0.36;
          group.position.x = Math.sin(elapsed * 1.7) * 0.2;
        }
        if (activeEffect === 'orbit-path') {
          group.position.set(
            Math.cos(elapsed) * 0.4,
            Math.sin(elapsed) * 0.23,
            Math.sin(elapsed) * 0.3,
          );
          ry += elapsed * 0.25;
        }
        if (activeEffect === 'scroll-dolly') {
          const r = el.getBoundingClientRect();
          const p = THREE.MathUtils.clamp(
            (innerHeight - r.top) / (innerHeight + r.height),
            0,
            1,
          );
          camera.position.z = 8 - p * 4;
          camera.lookAt(0, 0, 0);
        }
        if (activeEffect === 'levitate') {
          group.position.y = Math.sin(elapsed * 2) * 0.2;
          rx += Math.sin(elapsed) * 0.08;
        }
        if (activeEffect === 'explode') {
          const amount = (Math.sin(elapsed * 1.8 - Math.PI / 2) + 1) * 0.3;
          for (const part of pieces)
            part.mesh.position
              .copy(part.start)
              .addScaledVector(part.direction, amount);
        }
        if (activeEffect === 'wire-scan')
          material.wireframe = Math.sin(elapsed * 1.4) > 0.25;
        if (activeEffect === 'orbit-camera') {
          camera.position.set(
            Math.sin(elapsed * 0.65) * 4.9,
            0.75,
            Math.cos(elapsed * 0.65) * 4.9,
          );
          camera.lookAt(0, 0, 0);
        }
      }
      const a = reduced.matches ? 1 : 1 - Math.exp(-7 * dt);
      group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, rx, a);
      group.rotation.y = THREE.MathUtils.lerp(group.rotation.y, ry, a);
      renderer.render(scene, camera);
    });
    return () => {
      disposed = true;
      renderer.setAnimationLoop(null);
      resize.disconnect();
      intersection.disconnect();
      el.removeEventListener('pointermove', move);
      el.removeEventListener('pointerleave', leave);
      renderer.domElement.removeEventListener('webglcontextlost', lost);
      disposeObject(group);
      ground.geometry.dispose();
      ground.material.dispose();
      board.geometry.dispose();
      board.material.dispose();
      env.dispose();
      renderer.dispose();
      renderer.forceContextLoss();
      renderer.domElement.remove();
      model.current = null;
      rendererRef.current = null;
    };
  }, [object, light, activeEffect]);
  useEffect(() => {
    const m = materialRef.current;
    if (!m) return;
    m.color.set(settings.color);
    m.metalness = settings.metalness;
    m.roughness = settings.roughness;
    m.transmission = settings.transmission;
    m.ior = settings.ior;
    m.thickness = settings.thickness;
    m.wireframe = !!settings.wireframe;
    m.iridescence = settings.iridescence ?? 0;
    if (m.userData.texture !== settings.texture) {
      m.map?.dispose();
      m.bumpMap?.dispose();
      m.map = createTexture(settings.texture);
      m.bumpMap = m.map?.clone() ?? null;
      if (m.bumpMap) {
        m.bumpMap.colorSpace = THREE.NoColorSpace;
        m.bumpMap.needsUpdate = true;
      }
      m.bumpScale = settings.texture === 'brushed' ? 0.012 : 0.045;
      m.userData.texture = settings.texture;
    }
    m.needsUpdate = true;
  }, [settings]);
  const applyPreset = (id: string) => {
    const p = materialPresets.find((m) => m.id === id)!;
    setPreset(id);
    setSettings({ ...p, iridescence: id === 'iridescent' ? 1 : 0 });
  };
  const choose = (id: string) => {
    setObject(id);
    setAngle(0);
    if (id === 'bird' || id === 'gem') applyPreset('glass');
    else if (['wrench', 'bolt', 'chip'].includes(id)) applyPreset('steel');
  };
  const exportGLB = async () => {
    if (!model.current) return;
    setExporting(true);
    try {
      const out = await new GLTFExporter().parseAsync(model.current, {
        binary: true,
      });
      saveBlob(
        new Blob([out as ArrayBuffer], { type: 'model/gltf-binary' }),
        `webcraft-${object}-${preset}.glb`,
      );
    } catch {
      setError(
        'Не удалось экспортировать модель. Попробуйте материал без текстуры.',
      );
    } finally {
      setExporting(false);
    }
  };
  const downloadTexture = () => {
    const tex = createTexture(settings.texture, 1024);
    if (!tex) return;
    const canvas = tex.image as HTMLCanvasElement;
    canvas.toBlob((blob) => {
      if (blob) saveBlob(blob, `webcraft-${settings.texture}-1024.png`);
      tex.dispose();
    });
  };
  return (
    <div className={`scene-lab ${compact ? 'compact' : ''}`}>
      <div className="scene-main">
        <div ref={host} className="scene-canvas" />
        <div className="scene-caption">
          <span>
            <span className="live-dot" /> LIVE WEBGL
          </span>
          <span>
            {objects.find((o) => o.id === object)?.name} / {preset}
          </span>
        </div>
        {error && (
          <div className="scene-error" role="status">
            {error}
          </div>
        )}
        <div className="scene-toolbar">
          <button
            className="scene-icon"
            onClick={() => setPlaying(!playing)}
            aria-label={playing ? 'Остановить вращение' : 'Включить вращение'}
          >
            {playing ? <Pause size={16} /> : <Play size={16} />}
          </button>
          <button
            className="scene-icon"
            onClick={() => setAngle((a) => a + Math.PI / 4)}
            aria-label="Повернуть объект на 45 градусов"
          >
            <RotateCcw size={16} />
          </button>
          <span>
            {mode === 'scroll'
              ? 'Прокрутите страницу'
              : playing
                ? 'Анимация включена'
                : 'Двигайте курсором'}
          </span>
          {!compact && (
            <button
              className="scene-icon"
              onClick={() => {
                const r = rendererRef.current;
                if (r)
                  r.domElement.toBlob((b) => {
                    if (b) saveBlob(b, `webcraft-${object}.png`);
                  });
              }}
              aria-label="Скачать PNG"
            >
              <Download size={16} />
            </button>
          )}
        </div>
      </div>
      {!compact && (
        <aside className="scene-settings">
          <h3>Мастерская 3D</h3>
          <p>Геометрия, поверхность, свет.</p>
          <label>
            Эффект
            <select
              value={activeEffect}
              onChange={(e) => {
                const id = e.target.value;
                setActiveEffect(id);
                setMode(
                  id === 'scroll'
                    ? 'scroll'
                    : id === 'turntable'
                      ? 'auto'
                      : 'pointer',
                );
                setPlaying(id !== 'pointer' && id !== 'scroll');
              }}
            >
              {sceneEffects.map((e) => (
                <option value={e.id} key={e.id}>
                  {e.name}
                </option>
              ))}
            </select>
          </label>
          <label>
            Объект
            <select value={object} onChange={(e) => choose(e.target.value)}>
              {objects.map((o) => (
                <option key={o.id} value={o.id}>
                  {o.name} — {o.type}
                </option>
              ))}
            </select>
          </label>
          <label>
            Материал
            <select
              value={preset}
              onChange={(e) => applyPreset(e.target.value)}
            >
              {materialPresets.map((m) => (
                <option value={m.id} key={m.id}>
                  {m.name}
                </option>
              ))}
            </select>
          </label>
          <div className="material-swatches">
            {materialPresets.map((m) => (
              <button
                key={m.id}
                title={m.name}
                aria-label={m.name}
                aria-pressed={preset === m.id}
                className={preset === m.id ? 'selected' : ''}
                style={{ background: m.color }}
                onClick={() => applyPreset(m.id)}
              />
            ))}
          </div>
          <div className="setting-pair">
            <label>
              Цвет
              <input
                type="color"
                value={settings.color}
                onChange={(e) =>
                  setSettings({ ...settings, color: e.target.value })
                }
              />
            </label>
            <label>
              Текстура
              <select
                value={settings.texture}
                onChange={(e) =>
                  setSettings({ ...settings, texture: e.target.value })
                }
              >
                {texturePresets.map((t) => (
                  <option key={t}>{t}</option>
                ))}
              </select>
            </label>
          </div>
          {(
            [
              ['metalness', 'Металличность', 0, 1],
              ['roughness', 'Шероховатость', 0, 1],
              ['transmission', 'Пропускание', 0, 1],
              ['ior', 'Преломление (IOR)', 1, 2.33],
              ['thickness', 'Толщина', 0.05, 2],
            ] as const
          ).map(([key, title, min, max]) => (
            <label className="range-label" key={key}>
              <span>
                {title}
                <output>{settings[key].toFixed(2)}</output>
              </span>
              <input
                type="range"
                min={min}
                max={max}
                step="0.01"
                value={settings[key]}
                onChange={(e) =>
                  setSettings({ ...settings, [key]: Number(e.target.value) })
                }
              />
            </label>
          ))}
          <div className="setting-pair">
            <label>
              Свет
              <select value={light} onChange={(e) => setLight(e.target.value)}>
                <option value="studio">Студийный</option>
                <option value="dark">Тёмный</option>
                <option value="dramatic">Контрастный</option>
              </select>
            </label>
            <label>
              Движение
              <select value={mode} onChange={(e) => setMode(e.target.value)}>
                <option value="pointer">От курсора</option>
                <option value="scroll">От скролла</option>
                <option value="auto">Автовращение</option>
              </select>
            </label>
          </div>
          <label className="check-line">
            <input
              type="checkbox"
              checked={settings.wireframe ?? false}
              onChange={(e) =>
                setSettings({ ...settings, wireframe: e.target.checked })
              }
            />{' '}
            Показать сетку
          </label>
          <button
            className="primary full"
            onClick={exportGLB}
            disabled={exporting || !!error}
          >
            <Box size={16} />
            {exporting ? 'Экспорт…' : 'Базовая модель .glb'}
          </button>
          <p className="export-note">
            Шейдерные эффекты передаются исходным кодом.
          </p>
          <div className="button-row">
            <a className="subtle" href="/source/lib/objects.ts" download>
              Исходник <ArrowUpRight size={14} />
            </a>
            <button
              className="subtle"
              disabled={settings.texture === 'none'}
              onClick={downloadTexture}
            >
              Текстура PNG <Download size={14} />
            </button>
          </div>
        </aside>
      )}
    </div>
  );
}
