'use client';
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import { zipSync, strToU8 } from 'fflate';
import { Download, RotateCcw, Pause, Play } from 'lucide-react';
import {
  assets,
  getAssetFiles,
  selectFile,
  type Asset,
  type AssetFiles,
  type RemoteFile,
} from '@/lib/assets';
import { materialPresets } from '@/lib/materials';
import { makeMaterial } from '@/lib/objects';
const download = (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), 5000);
};
export default function AssetViewer({
  asset,
  files,
}: {
  asset: Asset;
  files: AssetFiles;
}) {
  const host = useRef<HTMLDivElement>(null),
    groupRef = useRef<THREE.Group | null>(null),
    renderRef = useRef<THREE.WebGLRenderer | null>(null),
    reset = useRef(() => {});
  const [status, setStatus] = useState('Загрузка геометрии и текстур…'),
    [error, setError] = useState(''),
    [surface, setSurface] = useState('original'),
    [motion, setMotion] = useState('orbit'),
    [playing, setPlaying] = useState(false),
    [shape, setShape] = useState('sphere'),
    [exposure, setExposure] = useState(1),
    [clip, setClip] = useState(''),
    [clips, setClips] = useState<string[]>([]),
    [environment, setEnvironment] = useState('studio'),
    [busy, setBusy] = useState(false),
    [retry, setRetry] = useState(0);
  const live = useRef({ motion, playing, exposure, clip });
  live.current = { motion, playing, exposure, clip };
  const envOptions = assets
    .filter(
      (a) =>
        a.kind === 'environments' &&
        a.categories.some((c) =>
          ['studio', 'indoor', 'sunset', 'forest'].includes(c),
        ),
    )
    .slice(0, 14);
  useEffect(() => {
    const el = host.current;
    if (!el) return;
    let stopped = false,
      visible = true,
      t = 0,
      last = 0,
      mixer: THREE.AnimationMixer | undefined,
      group = new THREE.Group(),
      env: THREE.WebGLRenderTarget | undefined;
    const textures = new Set<THREE.Texture>(),
      materials = new Set<THREE.Material>(),
      geometries = new Set<THREE.BufferGeometry>(),
      controller = new AbortController();
    let renderer: THREE.WebGLRenderer;
    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        preserveDrawingBuffer: true,
      });
    } catch {
      setError('WebGL недоступен. Файлы оригинала доступны под сценой.');
      return;
    }
    setError('');
    setStatus('Загрузка геометрии и текстур…');
    setClips([]);
    setClip('');
    renderRef.current = renderer;
    renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.localClippingEnabled = true;
    el.appendChild(renderer.domElement);
    renderer.domElement.setAttribute(
      'aria-label',
      `3D-просмотр: ${asset.name}`,
    );
    renderer.domElement.setAttribute('role', 'img');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color('#dfded8');
    const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 100);
    camera.position.set(3, 1.6, 4.5);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.enablePan = false;
    controls.minDistance = 1.5;
    controls.maxDistance = 12;
    controls.autoRotateSpeed = 1;
    reset.current = () => {
      camera.position.set(3, 1.6, 4.5);
      controls.target.set(0, 0, 0);
      controls.update();
    };
    const pmrem = new THREE.PMREMGenerator(renderer),
      room = new RoomEnvironment();
    env = pmrem.fromScene(room, 0.04);
    room.dispose();
    scene.environment = env.texture;
    scene.add(new THREE.HemisphereLight(0xffffff, 0x59534a, 0.8));
    const track = (g: THREE.Object3D) =>
      g.traverse((n) => {
        if (n instanceof THREE.Mesh) {
          geometries.add(n.geometry);
          for (const m of Array.isArray(n.material)
            ? n.material
            : [n.material]) {
            materials.add(m);
            for (const v of Object.values(m))
              if (v instanceof THREE.Texture) textures.add(v);
          }
        }
      });
    const cleanupLoaded = (g: THREE.Object3D) => {
      track(g);
      for (const x of geometries) x.dispose();
      for (const x of materials) x.dispose();
      for (const x of textures) x.dispose();
    };
    const loadTexture = async (file: RemoteFile | undefined, color = false) => {
      if (!file) return null;
      const tx = await new THREE.TextureLoader().loadAsync(file.url);
      if (stopped) {
        tx.dispose();
        throw new Error('cancelled');
      }
      textures.add(tx);
      tx.colorSpace = color ? THREE.SRGBColorSpace : THREE.NoColorSpace;
      tx.wrapS = tx.wrapT = THREE.RepeatWrapping;
      return tx;
    };
    const loadEnvironment = async (f: AssetFiles, background = false) => {
      const file = selectFile(f, ['hdri'], '1k', ['hdr']);
      if (!file) throw new Error('HDR-файл недоступен');
      const hdr = await new RGBELoader().loadAsync(file.url);
      if (stopped) {
        hdr.dispose();
        return;
      }
      const next = pmrem.fromEquirectangular(hdr);
      env?.dispose();
      env = next;
      scene.environment = env.texture;
      if (background) {
        hdr.mapping = THREE.EquirectangularReflectionMapping;
        scene.background = hdr;
        textures.add(hdr);
      } else hdr.dispose();
    };
    const ready = async () => {
      if (asset.kind === 'models') {
        const f = selectFile(files, ['gltf'], '1k', ['gltf']);
        if (!f)
          throw new Error(
            'Этот ассет доступен в исходных форматах на странице автора; glTF пока отсутствует.',
          );
        const r = await fetch(f.url, { signal: controller.signal });
        if (!r.ok) throw new Error('Не удалось загрузить glTF');
        const json = (await r.json()) as {
          buffers?: { uri?: string }[];
          images?: { uri?: string }[];
        };
        for (const collection of [json.buffers || [], json.images || []])
          for (const resource of collection) {
            if (!resource.uri || resource.uri.startsWith('data:')) continue;
            const include =
              f.include?.[resource.uri] ||
              f.include?.[decodeURIComponent(resource.uri)];
            resource.uri = include?.url || new URL(resource.uri, f.url).href;
          }
        const gltf = await new GLTFLoader().parseAsync(
          JSON.stringify(json),
          '',
        );
        if (stopped) {
          cleanupLoaded(gltf.scene);
          return;
        }
        const object = gltf.scene;
        track(object);
        group.add(object);
        const box = new THREE.Box3().setFromObject(object),
          center = box.getCenter(new THREE.Vector3()),
          size = box.getSize(new THREE.Vector3()),
          factor = 2.7 / Math.max(size.x, size.y, size.z, 0.01);
        object.position.sub(center);
        group.scale.setScalar(factor);
        if (gltf.animations.length) {
          mixer = new THREE.AnimationMixer(object);
          mixer.clipAction(gltf.animations[0]).play();
          setClips(gltf.animations.map((a) => a.name));
          setClip(gltf.animations[0].name);
          group.userData.animations = gltf.animations;
        }
      } else {
        const geometry =
          shape === 'cube'
            ? new THREE.BoxGeometry(1.9, 1.9, 1.9, 32, 32, 32)
            : shape === 'torus'
              ? new THREE.TorusKnotGeometry(0.8, 0.27, 160, 24)
              : new THREE.SphereGeometry(1.3, 96, 64);
        const mat = new THREE.MeshStandardMaterial({
          color: '#f0eee4',
          metalness: asset.kind === 'environments' ? 1 : 0,
          roughness: asset.kind === 'environments' ? 0.12 : 0.8,
        });
        if (asset.kind === 'pbr') {
          const [map, normal, rough, metal, ao] = await Promise.all([
            loadTexture(selectFile(files, ['diff', 'Diffuse']), true),
            loadTexture(selectFile(files, ['nor_gl'])),
            loadTexture(selectFile(files, ['rough', 'Rough'])),
            loadTexture(selectFile(files, ['metal', 'Metal'])),
            loadTexture(selectFile(files, ['AO'])),
          ]);
          if (stopped) {
            geometry.dispose();
            mat.dispose();
            return;
          }
          mat.map = map;
          mat.normalMap = normal;
          mat.roughnessMap = rough;
          mat.metalnessMap = metal;
          mat.metalness = metal ? 1 : 0;
          mat.aoMap = ao;
          geometry.setAttribute('uv1', geometry.getAttribute('uv').clone());
          mat.needsUpdate = true;
        } else await loadEnvironment(files, true);
        group.add(new THREE.Mesh(geometry, mat));
        track(group);
      }
      if (stopped) {
        cleanupLoaded(group);
        return;
      }
      if (surface !== 'original') {
        const m = makeMaterial(materialPresets.find((m) => m.id === surface)!);
        materials.add(m);
        group.traverse((n) => {
          if (n instanceof THREE.Mesh) n.material = m;
        });
      }
      if (environment !== 'studio' && asset.kind !== 'environments')
        await loadEnvironment(
          await getAssetFiles(environment, controller.signal),
        );
      if (stopped) {
        cleanupLoaded(group);
        return;
      }
      scene.add(group);
      groupRef.current = group;
      setStatus('');
    };
    ready().catch((e) => {
      if (!stopped) {
        setError(e.message);
        setStatus('');
      }
    });
    const pointer = { x: 0, y: 0 };
    const move = (e: PointerEvent) => {
      const b = el.getBoundingClientRect();
      pointer.x = (e.clientX - b.left) / b.width - 0.5;
      pointer.y = (e.clientY - b.top) / b.height - 0.5;
    };
    el.addEventListener('pointermove', move);
    let firstSize = true;
    const resize = new ResizeObserver(() => {
      const w = el.clientWidth,
        h = el.clientHeight;
      if (w && h) {
        renderer.setSize(w, h);
        camera.aspect = w / h;
        if (firstSize) {
          camera.position.setLength(5.7 / Math.min(camera.aspect, 1));
          firstSize = false;
        }
        camera.updateProjectionMatrix();
      }
    });
    resize.observe(el);
    const observer = new IntersectionObserver(
      ([e]) => (visible = e.isIntersecting),
    );
    observer.observe(el);
    const reduce = matchMedia('(prefers-reduced-motion:reduce)');
    let previousClip = '';
    renderer.setAnimationLoop((now) => {
      const dt = Math.min((now - last) / 1000 || 0, 0.05);
      last = now;
      if (stopped || !visible || document.hidden) return;
      const v = live.current;
      renderer.toneMappingExposure = v.exposure;
      const animated = v.playing && !reduce.matches;
      if (animated) t += dt;
      controls.autoRotate = animated && v.motion === 'orbit';
      controls.update();
      group.position.y =
        v.motion === 'float' && !reduce.matches ? Math.sin(t * 1.2) * 0.15 : 0;
      if (v.motion === 'pointer' && !reduce.matches) {
        group.rotation.y = THREE.MathUtils.damp(
          group.rotation.y,
          pointer.x * 1.4,
          6,
          dt,
        );
        group.rotation.x = THREE.MathUtils.damp(
          group.rotation.x,
          pointer.y * 0.5,
          6,
          dt,
        );
      } else {
        group.rotation.x = 0;
        group.rotation.y = 0;
      }
      if (mixer) {
        if (v.clip !== previousClip) {
          mixer.stopAllAction();
          const a = (group.userData.animations as THREE.AnimationClip[])?.find(
            (a) => a.name === v.clip,
          );
          if (a) mixer.clipAction(a).reset().play();
          previousClip = v.clip;
        }
        if (animated) mixer.update(dt);
      }
      renderer.render(scene, camera);
    });
    return () => {
      stopped = true;
      controller.abort();
      renderer.setAnimationLoop(null);
      resize.disconnect();
      observer.disconnect();
      el.removeEventListener('pointermove', move);
      controls.dispose();
      mixer?.stopAllAction();
      if (mixer) mixer.uncacheRoot(mixer.getRoot());
      cleanupLoaded(group);
      env?.dispose();
      pmrem.dispose();
      renderer.dispose();
      renderer.forceContextLoss();
      renderer.domElement.remove();
      groupRef.current = null;
      renderRef.current = null;
    };
  }, [asset, files, surface, shape, environment, retry]);
  const zipOriginal = async () => {
    const file = selectFile(files, ['gltf'], '1k', ['gltf']);
    if (!file) return;
    setBusy(true);
    try {
      const entries: Record<string, Uint8Array> = {};
      for (const [name, f] of [
        [`${asset.id}.gltf`, file],
        ...Object.entries(file.include || {}),
      ] as [string, RemoteFile][]) {
        if (name.includes('..') || name.startsWith('/'))
          throw new Error('Invalid asset path');
        const res = await fetch(f.url);
        if (!res.ok) throw new Error('Не удалось скачать один из файлов');
        entries[name] = new Uint8Array(await res.arrayBuffer());
      }
      entries['SOURCE.txt'] = strToU8(
        `${asset.name}\nAuthor: ${asset.author}\nSource: https://polyhaven.com/a/${asset.id}\nLicense: CC0 1.0 https://creativecommons.org/publicdomain/zero/1.0/\nOriginal files, unmodified.\n`,
      );
      download(new Blob([zipSync(entries) as BlobPart]), asset.id + '-1k.zip');
    } catch (e) {
      setError((e as Error).message);
    } finally {
      setBusy(false);
    }
  };
  return (
    <div className="asset-viewer">
      <div className="asset-webgl" ref={host} />
      {(status || error) && (
        <div className="asset-viewer-status" role="status">
          {error || status}
          {error && (
            <button onClick={() => setRetry((v) => v + 1)}>Повторить</button>
          )}
        </div>
      )}
      <div className="asset-viewer-bar">
        <button
          onClick={() => setPlaying((v) => !v)}
          aria-label={playing ? 'Пауза' : 'Запустить'}
        >
          {playing ? <Pause size={16} /> : <Play size={16} />}
        </button>
        <button onClick={() => reset.current()} aria-label="Сбросить камеру">
          <RotateCcw size={16} />
        </button>
        <span>Потяни для вращения · колесо для масштаба</span>
        <button
          aria-label="Скачать кадр PNG"
          onClick={() =>
            renderRef.current?.domElement.toBlob(
              (b) => b && download(b, asset.id + '.png'),
            )
          }
        >
          <Download size={16} />
        </button>
      </div>
      <div className="asset-viewer-controls">
        <label>
          Поверхность
          <select value={surface} onChange={(e) => setSurface(e.target.value)}>
            <option value="original">Оригинальные PBR-материалы</option>
            {materialPresets.map((m) => (
              <option key={m.id} value={m.id}>
                {m.name}
              </option>
            ))}
          </select>
        </label>
        <label>
          Движение
          <select value={motion} onChange={(e) => setMotion(e.target.value)}>
            <option value="orbit">Автовращение камеры</option>
            <option value="pointer">Реакция на курсор</option>
            <option value="float">Левитация</option>
          </select>
        </label>
        {asset.kind !== 'models' && (
          <label>
            Геометрия
            <select value={shape} onChange={(e) => setShape(e.target.value)}>
              <option value="sphere">Сфера</option>
              <option value="cube">Куб</option>
              <option value="torus">Узел</option>
            </select>
          </label>
        )}
        {asset.kind !== 'environments' && (
          <label>
            Окружение
            <select
              value={environment}
              onChange={(e) => setEnvironment(e.target.value)}
            >
              <option value="studio">Нейтральная студия</option>
              {envOptions.map((a) => (
                <option key={a.id} value={a.id}>
                  {a.name}
                </option>
              ))}
            </select>
          </label>
        )}
        <label>
          Экспозиция <output>{exposure.toFixed(1)}</output>
          <input
            type="range"
            min="0.2"
            max="2"
            step="0.1"
            value={exposure}
            onChange={(e) => setExposure(+e.target.value)}
          />
        </label>
        {clips.length > 0 && (
          <label>
            Анимация модели
            <select value={clip} onChange={(e) => setClip(e.target.value)}>
              {clips.map((c) => (
                <option key={c}>{c}</option>
              ))}
            </select>
          </label>
        )}
        {asset.kind === 'models' && (
          <>
            <button disabled={busy} onClick={zipOriginal}>
              {busy ? 'Скачиваем…' : 'Исходный glTF + текстуры ZIP'}
              <Download size={14} />
            </button>
            <button
              disabled={!!status || !!error || busy}
              onClick={async () => {
                if (!groupRef.current) return;
                setBusy(true);
                try {
                  const glb = await new GLTFExporter().parseAsync(
                    groupRef.current,
                    {
                      binary: true,
                      animations: groupRef.current.userData.animations || [],
                    },
                  );
                  download(
                    new Blob([glb as ArrayBuffer], {
                      type: 'model/gltf-binary',
                    }),
                    asset.id + '.glb',
                  );
                } catch {
                  setError(
                    'Экспорт не удался. Скачайте оригинальные файлы ZIP.',
                  );
                } finally {
                  setBusy(false);
                }
              }}
            >
              Экспорт сцены GLB
            </button>
          </>
        )}
      </div>
    </div>
  );
}
