import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";
import Preview from "./components/Preview";
import { generateEffect } from "./engine/aiMock";
import { buildShader } from "./engine/shaderBuilder";
import { createUniforms, updateUniformValues } from "./engine/uniformManager";
import baseVertexShader from "./shaders/base.vert?raw";
import { templateMap, templates } from "./shaders/templates";
import type { EffectParams } from "./shaders/templates/types";

function downloadFile(filename: string, content: string, type: string): void {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(url);
}

type EffectItem = {
  id: string;
  name: string;
  templateName: string;
  params: EffectParams;
  shaderDraft: string;
  shaderApplied: string;
};

function makeEffect(templateName: string, serial: number): EffectItem {
  const template = templateMap.get(templateName) ?? templates[0];
  const shader = buildShader(template, template.defaultParams);
  return {
    id: `effect-${Date.now()}-${Math.floor(Math.random() * 10000)}`,
    name: `effect_${serial}`,
    templateName: template.name,
    params: { ...template.defaultParams },
    shaderDraft: shader,
    shaderApplied: shader
  };
}

export default function App() {
  const channelIndices = [0, 1, 2, 3] as const;
  const channelKeys = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"] as const;
  const [effects, setEffects] = useState<EffectItem[]>([makeEffect(templates[0].name, 1)]);
  const [activeEffectId, setActiveEffectId] = useState<string>("");
  const [prompt, setPrompt] = useState<string>("hot fire");
  const [isPlaying, setIsPlaying] = useState<boolean>(true);
  const previewWrapRef = useRef<HTMLDivElement | null>(null);

  const activeEffect = useMemo(() => {
    const direct = effects.find((effect) => effect.id === activeEffectId);
    return direct ?? effects[0];
  }, [effects, activeEffectId]);

  const [uniforms, setUniforms] = useState(() => createUniforms(effects[0].params));
  const [channelTextures, setChannelTextures] = useState<Array<THREE.Texture | null>>([
    null,
    null,
    null,
    null
  ]);
  const [channelFileNames, setChannelFileNames] = useState<string[]>(["None", "None", "None", "None"]);
  const channelTexturesRef = useRef<Array<THREE.Texture | null>>([null, null, null, null]);

  useEffect(() => {
    if (!activeEffectId && effects[0]) {
      setActiveEffectId(effects[0].id);
    }
  }, [activeEffectId, effects]);

  useEffect(() => {
    if (activeEffect) {
      setUniforms(createUniforms(activeEffect.params));
    }
  }, [activeEffect?.id]);

  useEffect(() => {
    if (!activeEffect) {
      return;
    }
    updateUniformValues(uniforms, isPlaying ? activeEffect.params : { ...activeEffect.params, speed: 0 });
  }, [uniforms, activeEffect, isPlaying]);

  useEffect(() => {
    channelTexturesRef.current = channelTextures;
    channelIndices.forEach((idx) => {
      const channelKey = channelKeys[idx];
      const texture = channelTextures[idx];
      uniforms[channelKey].value = texture;
      if (texture && texture.image) {
        const image = texture.image as { width?: number; height?: number };
        const width = image.width ?? 1;
        const height = image.height ?? 1;
        uniforms.iChannelResolution.value[idx].set(width, height, 1);
      } else {
        uniforms.iChannelResolution.value[idx].set(1, 1, 1);
      }
    });
  }, [channelTextures, uniforms]);

  useEffect(() => {
    return () => {
      channelTexturesRef.current.forEach((texture) => texture?.dispose());
    };
  }, []);

  const updateActiveEffect = (updater: (current: EffectItem) => EffectItem) => {
    setEffects((prev) => prev.map((effect) => (effect.id === activeEffect.id ? updater(effect) : effect)));
  };

  const onCreateEffect = () => {
    const next = makeEffect(templates[0].name, effects.length + 1);
    setEffects((prev) => [...prev, next]);
    setActiveEffectId(next.id);
    setUniforms(createUniforms(next.params));
  };

  const onDeleteEffect = () => {
    if (!activeEffect || effects.length <= 1) {
      return;
    }
    const currentIndex = effects.findIndex((effect) => effect.id === activeEffect.id);
    const nextEffects = effects.filter((effect) => effect.id !== activeEffect.id);
    const fallback = nextEffects[Math.max(0, currentIndex - 1)] ?? nextEffects[0];
    setEffects(nextEffects);
    setActiveEffectId(fallback.id);
    setUniforms(createUniforms(fallback.params));
  };

  const onSelectTemplate = (templateName: string) => {
    const selected = templateMap.get(templateName);
    if (!selected) {
      return;
    }
    const nextShader = buildShader(selected, selected.defaultParams);
    updateActiveEffect((current) => ({
      ...current,
      templateName: selected.name,
      params: { ...selected.defaultParams },
      shaderDraft: nextShader,
      shaderApplied: nextShader
    }));
    setUniforms(createUniforms(selected.defaultParams));
  };

  const onGenerate = () => {
    const result = generateEffect(prompt);
    const selected = templateMap.get(result.template);
    if (!selected) {
      return;
    }
    const nextShader = buildShader(selected, result.params);
    updateActiveEffect((current) => ({
      ...current,
      templateName: selected.name,
      params: { ...result.params },
      shaderDraft: nextShader,
      shaderApplied: nextShader
    }));
    setUniforms(createUniforms(result.params));
  };

  const onParamChange = (key: keyof EffectParams, value: number) => {
    updateActiveEffect((current) => ({
      ...current,
      params: { ...current.params, [key]: value }
    }));
  };

  const onApplyShader = () => {
    updateActiveEffect((current) => ({ ...current, shaderApplied: current.shaderDraft }));
  };

  const onExport = () => {
    downloadFile("fragment.glsl", activeEffect.shaderApplied, "text/plain;charset=utf-8");
    downloadFile("vertex.glsl", baseVertexShader, "text/plain;charset=utf-8");
    downloadFile(
      "material.json",
      JSON.stringify(
        {
          type: "ShaderMaterial",
          effect: activeEffect.name,
          template: activeEffect.templateName,
          uniforms: {
            u_time: 0,
            u_speed: activeEffect.params.speed,
            u_scale: activeEffect.params.scale,
            u_intensity: activeEffect.params.intensity
          }
        },
        null,
        2
      ),
      "application/json;charset=utf-8"
    );
  };

  const onFullscreen = () => {
    previewWrapRef.current?.requestFullscreen?.();
  };

  const onUploadChannelTexture = (channelIndex: number, file: File) => {
    const objectUrl = URL.createObjectURL(file);
    const loader = new THREE.TextureLoader();
    loader.load(
      objectUrl,
      (texture) => {
        texture.needsUpdate = true;
        setChannelTextures((prev) => {
          const next = [...prev];
          next[channelIndex]?.dispose();
          next[channelIndex] = texture;
          return next;
        });
        setChannelFileNames((prev) => {
          const next = [...prev];
          next[channelIndex] = file.name;
          return next;
        });
        URL.revokeObjectURL(objectUrl);
      },
      undefined,
      () => {
        URL.revokeObjectURL(objectUrl);
      }
    );
  };

  const onClearChannelTexture = (channelIndex: number) => {
    setChannelTextures((prev) => {
      const next = [...prev];
      next[channelIndex]?.dispose();
      next[channelIndex] = null;
      return next;
    });
    setChannelFileNames((prev) => {
      const next = [...prev];
      next[channelIndex] = "None";
      return next;
    });
  };

  const onSampleRateChange = (value: number) => {
    uniforms.iSampleRate.value = Number.isFinite(value) && value > 0 ? value : 44100;
  };

  return (
    <main className="app-shell">
      <header className="topbar">
        <h1>AI VFX Shader Tool</h1>
      </header>

      <section className="content-grid">
        <section className="left-column">
          <article className="card player" ref={previewWrapRef}>
            <Preview
              vertexShader={baseVertexShader}
              fragmentShader={activeEffect.shaderApplied}
              uniforms={uniforms}
            />
            <div className="player-bar">
              <span className="status-text">
                Active Effect: <strong>{activeEffect.name}</strong>
              </span>
              <div className="player-actions">
                <button type="button" className="secondary" onClick={() => setIsPlaying((prev) => !prev)}>
                  {isPlaying ? "Pause" : "Play"}
                </button>
                <button type="button" onClick={onFullscreen}>
                  Fullscreen
                </button>
              </div>
            </div>
          </article>

          <article className="card editor-shell">
            <div className="effect-manager">
              {effects.map((effect) => (
                <button
                  key={effect.id}
                  type="button"
                  className={`effect-tab ${activeEffect.id === effect.id ? "active" : ""}`}
                  onClick={() => {
                    setActiveEffectId(effect.id);
                    setUniforms(createUniforms(effect.params));
                  }}
                >
                  {effect.name}
                </button>
              ))}
              <div className="effect-actions">
                <button type="button" className="small secondary" onClick={onCreateEffect}>
                  + New Effect
                </button>
                <button
                  type="button"
                  className="small secondary"
                  onClick={onDeleteEffect}
                  disabled={effects.length <= 1}
                >
                  Delete Effect
                </button>
              </div>
            </div>

            <label className="field shader-editor">
              <span>Fragment Shader (Editable)</span>
              <textarea
                value={activeEffect.shaderDraft}
                rows={16}
                onChange={(e) =>
                  updateActiveEffect((current) => ({ ...current, shaderDraft: e.target.value }))
                }
              />
            </label>

            <div className="editor-actions">
              <button type="button" onClick={onApplyShader}>
                Apply Shader
              </button>
              <button type="button" className="secondary" onClick={onExport}>
                Export
              </button>
            </div>
          </article>
        </section>

        <aside className="right-column card panel">
          <h3>Effect Settings</h3>

          <label className="field">
            <span>Effect Name</span>
            <input
              value={activeEffect.name}
              onChange={(e) => updateActiveEffect((current) => ({ ...current, name: e.target.value }))}
              placeholder="输入特效名称"
            />
          </label>

          <label className="field">
            <span>Template</span>
            <select value={activeEffect.templateName} onChange={(e) => onSelectTemplate(e.target.value)}>
              {templates.map((t) => (
                <option key={t.name} value={t.name}>
                  {t.name}
                </option>
              ))}
            </select>
          </label>

          <label className="field">
            <span>AI Prompt (Mock)</span>
            <input
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
              placeholder="例如: hot fire / water ripple / glitch distortion"
            />
          </label>
          <button type="button" className="secondary" onClick={onGenerate}>
            Generate
          </button>

          {(["speed", "scale", "intensity"] as Array<keyof EffectParams>).map((key) => {
            const ranges: Record<keyof EffectParams, [number, number, number]> = {
              speed: [0, 3, 0.01],
              scale: [0.5, 12, 0.1],
              intensity: [0, 2, 0.01]
            };
            const [min, max, step] = ranges[key];
            return (
              <label key={key} className="slider-row">
                <span>{key}</span>
                <input
                  type="range"
                  min={min}
                  max={max}
                  step={step}
                  value={activeEffect.params[key]}
                  onChange={(e) => onParamChange(key, Number(e.target.value))}
                />
                <strong>{activeEffect.params[key].toFixed(2)}</strong>
              </label>
            );
          })}

          <label className="field">
            <span>iSampleRate</span>
            <input
              type="number"
              min={1}
              value={uniforms.iSampleRate.value}
              onChange={(e) => onSampleRateChange(Number(e.target.value))}
            />
          </label>

          <div className="channel-panel">
            <h4>Shadertoy Channels</h4>
            {channelIndices.map((channelIndex) => (
              <div key={channelIndex} className="channel-row">
                <span>{`iChannel${channelIndex}`}</span>
                <input
                  type="file"
                  accept="image/*"
                  onChange={(e) => {
                    const file = e.target.files?.[0];
                    if (file) {
                      onUploadChannelTexture(channelIndex, file);
                    }
                  }}
                />
                <small>{channelFileNames[channelIndex]}</small>
                <button
                  type="button"
                  className="small secondary"
                  onClick={() => onClearChannelTexture(channelIndex)}
                >
                  Clear
                </button>
              </div>
            ))}
          </div>
        </aside>
      </section>
    </main>
  );
}
