"use client";

import { useEffect, useRef, useState } from "react";
import { H264AnnexBStreamParser, type FrameThumb, type ParsedH264 } from "@/lib/h264";
import {
  clearCachedFrameSession,
  readCachedFrameSession,
  type CachedFrameRecord,
  type CachedFrameSession,
  writeCachedFrameSession,
} from "@/lib/frame-store";
import { clearCachedVideoResponse, openCachedVideoResponse } from "@/lib/video-cache";

const DEFAULT_VIDEO_URL =
  process.env.NEXT_PUBLIC_VIDEO_URL ?? "http://localhost:8080/video.h264";
const TARGET_FPS = 30;
const FRAME_EXPORT_WIDTH = 1600;
const PRELOAD_AHEAD = 6;
const FRAME_STEP_US = Math.round(1_000_000 / TARGET_FPS);

type LoadState = "idle" | "loading" | "ready" | "error";
type StreamSummary = Pick<ParsedH264, "codec" | "frameCount" | "keyFrameCount">;
type CacheState = "idle" | "hit" | "miss" | "released";
type StorageRequestState = "idle" | "requesting" | "granted" | "denied" | "unsupported" | "error";
type ProgressState = {
  bytesRead: number;
  bytesTotal: number | null;
  decoded: number;
  decodedTotal: number | null;
};

type StorageInfo = {
  usageBytes: number | null;
  quotaBytes: number | null;
  persisted: boolean | null;
};

type QueuedAccessUnit = {
  type: "key" | "delta";
  data: Uint8Array;
  timestampUs: number;
};

type PlaybackAnchor = {
  index: number;
  timestampUs: number;
  startedAt: number;
};

export default function VideoFrameViewer() {
  const [videoUrl, setVideoUrl] = useState(DEFAULT_VIDEO_URL);
  const [loadState, setLoadState] = useState<LoadState>("idle");
  const [videoCacheState, setVideoCacheState] = useState<CacheState>("idle");
  const [frameCacheState, setFrameCacheState] = useState<CacheState>("idle");
  const [storageInfo, setStorageInfo] = useState<StorageInfo>({
    usageBytes: null,
    quotaBytes: null,
    persisted: null,
  });
  const [storageRequestState, setStorageRequestState] = useState<StorageRequestState>("idle");
  const [status, setStatus] = useState("等待开始。建议使用 Chromium / Edge 浏览器。");
  const [error, setError] = useState<string | null>(null);
  const [frames, setFrames] = useState<FrameThumb[]>([]);
  const [streamInfo, setStreamInfo] = useState<StreamSummary | null>(null);
  const [progress, setProgress] = useState<ProgressState>({
    bytesRead: 0,
    bytesTotal: null,
    decoded: 0,
    decodedTotal: null,
  });
  const [playing, setPlaying] = useState(false);
  const [playhead, setPlayhead] = useState(0);
  const [scrubValue, setScrubValue] = useState(0);

  const abortRef = useRef<AbortController | null>(null);
  const playbackCanvasRef = useRef<HTMLCanvasElement | null>(null);
  const rafRef = useRef<number | null>(null);
  const framesRef = useRef<FrameThumb[]>([]);
  const playheadRef = useRef(0);
  const imageCacheRef = useRef(new Map<number, Promise<HTMLImageElement>>());
  const frameObjectUrlsRef = useRef(new Set<string>());
  const loadTokenRef = useRef(0);
  const playbackAnchorRef = useRef<PlaybackAnchor>({
    index: 0,
    timestampUs: 0,
    startedAt: 0,
  });
  const codecRef = useRef<string | null>(null);

  useEffect(() => {
    framesRef.current = frames;
  }, [frames]);

  useEffect(() => {
    playheadRef.current = playhead;
  }, [playhead]);

  useEffect(() => {
    setScrubValue(playhead);
  }, [playhead]);

  useEffect(() => {
    return () => {
      abortRef.current?.abort();
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
      }
      imageCacheRef.current.clear();
      revokeFrameObjectUrls();
    };
  }, []);

  useEffect(() => {
    const token = ++loadTokenRef.current;
    let cancelled = false;

    async function restoreCachedSession() {
      setStatus("正在检查本地帧缓存...");
      const cachedSession = await readCachedFrameSession(videoUrl);
      if (cancelled || token !== loadTokenRef.current) {
        return;
      }

      if (!cachedSession) {
        setFrameCacheState("miss");
        setStatus("本地帧缓存未命中，可以点击开始切帧。");
        return;
      }

      applyFrameSession(cachedSession);
      await clearCachedVideoResponse(videoUrl);
      setFrameCacheState("hit");
      setVideoCacheState("released");
      await refreshStorageInfo();
      setStatus("已从本地帧缓存恢复，刷新后可以直接播放。");
    }

    void restoreCachedSession();

    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    void refreshStorageInfo();
  }, []);

  const progressValue = (() => {
    if (loadState === "loading" && progress.bytesTotal && progress.bytesTotal > 0) {
      return Math.min(100, (progress.bytesRead / progress.bytesTotal) * 100);
    }

    if (progress.decodedTotal && progress.decodedTotal > 0) {
      return Math.min(100, (progress.decoded / progress.decodedTotal) * 100);
    }

    return 0;
  })();

  function resetPlaybackSurface() {
    if (rafRef.current !== null) {
      cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    }
    imageCacheRef.current.clear();
    const canvas = playbackCanvasRef.current;
    const context = canvas?.getContext("2d");
    if (canvas && context) {
      context.clearRect(0, 0, canvas.width, canvas.height);
    }
  }

  function revokeFrameObjectUrls() {
    for (const objectUrl of frameObjectUrlsRef.current) {
      URL.revokeObjectURL(objectUrl);
    }

    frameObjectUrlsRef.current.clear();
  }

  function registerFrameObjectUrl(objectUrl: string) {
    frameObjectUrlsRef.current.add(objectUrl);
    return objectUrl;
  }

  function clearDisplayedFrames() {
    revokeFrameObjectUrls();
    framesRef.current = [];
    setFrames([]);
  }

  function clearDisplayedSession() {
    resetPlaybackSurface();
    clearDisplayedFrames();
    setStreamInfo(null);
    setProgress({
      bytesRead: 0,
      bytesTotal: null,
      decoded: 0,
      decodedTotal: null,
    });
    setPlaying(false);
    setPlayhead(0);
    setScrubValue(0);
    playheadRef.current = 0;
    playbackAnchorRef.current = {
      index: 0,
      timestampUs: 0,
      startedAt: 0,
    };
  }

  function applyFrameSession(session: CachedFrameSession) {
    resetPlaybackSurface();
    clearDisplayedFrames();

    const nextFrames = session.frames.map((record) => ({
      index: record.index,
      src: registerFrameObjectUrl(URL.createObjectURL(record.blob)),
      width: record.width,
      height: record.height,
      timestampUs: record.timestampUs,
    }));

    framesRef.current = nextFrames;
    setFrames(nextFrames);
    setStreamInfo({
      codec: session.codec,
      frameCount: session.frameCount,
      keyFrameCount: session.keyFrameCount,
    });
    setProgress({
      bytesRead: session.sourceSize ?? 0,
      bytesTotal: session.sourceSize,
      decoded: session.frameCount,
      decodedTotal: session.frameCount,
    });
    setPlaying(false);
    setPlayhead(0);
    setScrubValue(0);
    playheadRef.current = 0;
    playbackAnchorRef.current = {
      index: 0,
      timestampUs: 0,
      startedAt: 0,
    };
    setLoadState("ready");
    setError(null);
  }

  function describeCacheState(state: CacheState) {
    if (state === "hit") {
      return "命中";
    }

    if (state === "miss") {
      return "未命中";
    }

    if (state === "released") {
      return "已释放";
    }

    return "待查";
  }

  function describeStorageState() {
    if (storageInfo.persisted === true) {
      return "已持久化";
    }

    if (storageInfo.persisted === false) {
      return "可申请持久化";
    }

    return "等待检测";
  }

  function getStorageUsageText() {
    if (storageInfo.quotaBytes && storageInfo.quotaBytes > 0) {
      const used = storageInfo.usageBytes ?? 0;
      const percent = Math.min(100, Math.round((used / storageInfo.quotaBytes) * 100));
      return `${formatBytes(used)} / ${formatBytes(storageInfo.quotaBytes)} · ${percent}%`;
    }

    if (storageInfo.usageBytes !== null) {
      return formatBytes(storageInfo.usageBytes);
    }

    return "不可用";
  }

  function jumpToFrame(index: number) {
    if (framesRef.current.length === 0) {
      return;
    }

    const nextIndex = Math.max(0, Math.min(framesRef.current.length - 1, index));
    setPlaying(false);
    setPlayhead(nextIndex);
    setScrubValue(nextIndex);
    playheadRef.current = nextIndex;
    void drawFrameToCanvas(nextIndex);
  }

  function getPersistentButtonLabel() {
    if (storageRequestState === "requesting") {
      return "申请中...";
    }

    if (storageInfo.persisted === true) {
      return "已持久化";
    }

    if (storageRequestState === "unsupported") {
      return "浏览器不支持";
    }

    return "申请持久化";
  }

  function isPersistentButtonDisabled() {
    return storageRequestState === "requesting" || storageInfo.persisted === true;
  }

  async function clearAllCaches() {
    abortRef.current?.abort();
    loadTokenRef.current += 1;
    resetPlaybackSurface();
    clearDisplayedSession();
    setLoadState("idle");
    setError(null);
    setStatus("正在清理缓存...");
    setFrameCacheState("idle");
    setVideoCacheState("idle");

    try {
      await Promise.all([
        clearCachedFrameSession(videoUrl),
        clearCachedVideoResponse(videoUrl),
      ]);
      await refreshStorageInfo();
      setStatus("缓存已清空，点击开始切帧会重新请求后端。");
      setFrameCacheState("miss");
      setVideoCacheState("miss");
    } catch (cause) {
      const message = cause instanceof Error ? cause.message : String(cause);
      setError(message);
      setStatus("清理缓存失败。");
    }
  }

  function startPlaybackFromCurrentFrame() {
    if (framesRef.current.length === 0) {
      return;
    }

    const nextIndex = Math.max(0, Math.min(framesRef.current.length - 1, playheadRef.current));
    const nextTimestampUs = framesRef.current[nextIndex]?.timestampUs ?? 0;
    playbackAnchorRef.current = {
      index: nextIndex,
      timestampUs: nextTimestampUs,
      startedAt: performance.now(),
    };
    setPlaying(true);
  }

  async function refreshStorageInfo() {
    if (typeof navigator === "undefined" || !navigator.storage) {
      setStorageInfo({
        usageBytes: null,
        quotaBytes: null,
        persisted: null,
      });
      return;
    }

    try {
      const [estimate, persisted] = await Promise.all([
        navigator.storage.estimate(),
        navigator.storage.persisted(),
      ]);

      setStorageInfo({
        usageBytes: typeof estimate.usage === "number" ? estimate.usage : null,
        quotaBytes: typeof estimate.quota === "number" ? estimate.quota : null,
        persisted,
      });
    } catch {
      setStorageInfo({
        usageBytes: null,
        quotaBytes: null,
        persisted: null,
      });
    }
  }

  async function requestPersistentStorage() {
    if (typeof navigator === "undefined" || !navigator.storage || !navigator.storage.persist) {
      setStorageRequestState("unsupported");
      setStatus("当前浏览器不支持持久化存储申请。");
      return;
    }

    setStorageRequestState("requesting");

    try {
      const granted = await navigator.storage.persist();
      setStorageRequestState(granted ? "granted" : "denied");
      await refreshStorageInfo();
      setStatus(
        granted
          ? "已申请持久化存储，长期缓存更不容易被浏览器回收。"
          : "浏览器没有授予持久化存储，仍可继续使用当前缓存。",
      );
    } catch {
      setStorageRequestState("error");
      setStatus("持久化存储申请失败。");
    }
  }

  function preloadFrameWindow(startIndex: number) {
    const endIndex = Math.min(framesRef.current.length, startIndex + PRELOAD_AHEAD);
    for (let index = startIndex; index < endIndex; index += 1) {
      void loadFrameImage(index).catch(() => undefined);
    }
  }

  function loadFrameImage(index: number): Promise<HTMLImageElement> {
    const cached = imageCacheRef.current.get(index);
    if (cached) {
      return cached;
    }

    const frame = framesRef.current[index];
    if (!frame) {
      return Promise.reject(new Error(`找不到第 ${index + 1} 帧`));
    }

    const promise = new Promise<HTMLImageElement>((resolve, reject) => {
      const image = new Image();
      image.decoding = "async";
      let settled = false;

      const settle = (fn: () => void) => {
        if (settled) {
          return;
        }
        settled = true;
        fn();
      };

      image.onload = () => settle(() => resolve(image));
      image.onerror = () => settle(() => reject(new Error(`第 ${index + 1} 帧加载失败`)));
      image.src = frame.src;

      if (typeof image.decode === "function") {
        image
          .decode()
          .then(() => settle(() => resolve(image)))
          .catch(() => {
            // Fallback to onload/onerror.
          });
      }
    });

    imageCacheRef.current.set(index, promise);
    return promise;
  }

  async function drawFrameToCanvas(index: number) {
    try {
      const canvas = playbackCanvasRef.current;
      if (!canvas) {
        return;
      }

      const frame = framesRef.current[index];
      if (!frame) {
        return;
      }

      const context = canvas.getContext("2d");
      if (!context) {
        return;
      }

      const image = await loadFrameImage(index);
      if (playheadRef.current !== index) {
        return;
      }

      const nextWidth = Math.max(1, image.naturalWidth || frame.width);
      const nextHeight = Math.max(1, image.naturalHeight || frame.height);
      if (canvas.width !== nextWidth || canvas.height !== nextHeight) {
        canvas.width = nextWidth;
        canvas.height = nextHeight;
      }

      context.clearRect(0, 0, canvas.width, canvas.height);
      context.drawImage(image, 0, 0, canvas.width, canvas.height);
    } catch {
      // Ignore transient frame load failures and keep the previous frame visible.
    }
  }

  async function loadAndDecode() {
    const token = ++loadTokenRef.current;
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    setError(null);
    setPlaying(false);
    setPlayhead(0);
    setScrubValue(0);
    playheadRef.current = 0;
    playbackAnchorRef.current = {
      index: 0,
      timestampUs: 0,
      startedAt: 0,
    };
    setStreamInfo(null);
    setProgress({
      bytesRead: 0,
      bytesTotal: null,
      decoded: 0,
      decodedTotal: null,
    });
    setFrameCacheState("idle");
    setVideoCacheState("idle");
    codecRef.current = null;
    setStatus("正在查找本地帧缓存...");
    const decoderRef = { current: null as VideoDecoder | null };

    const closeDecoder = () => {
      const decoder = decoderRef.current;
      if (!decoder) {
        return;
      }

      decoderRef.current = null;
      if (decoder.state !== "closed") {
        decoder.close();
      }
    };

    try {
      const cachedSession = await readCachedFrameSession(videoUrl);
      if (token !== loadTokenRef.current || controller.signal.aborted) {
        return;
      }

      if (cachedSession) {
        applyFrameSession(cachedSession);
        await clearCachedVideoResponse(videoUrl);
        setFrameCacheState("hit");
        setVideoCacheState("released");
        await refreshStorageInfo();
        setStatus("已从本地帧缓存恢复，刷新后可以直接播放。");
        return;
      }

      setFrameCacheState("miss");
      clearDisplayedSession();
      setLoadState("loading");
      setStatus("本地帧缓存未命中，正在请求后端 video.h264...");

      const { response, cacheState: nextCacheState } = await openCachedVideoResponse(
        videoUrl,
        controller.signal,
      );
      if (token !== loadTokenRef.current || controller.signal.aborted) {
        return;
      }

      setVideoCacheState(nextCacheState);

      const contentLength = Number(response.headers.get("content-length") ?? "");
      const bytesTotal = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : null;
      setProgress((current) => ({ ...current, bytesTotal }));

      if (!response.body) {
        throw new Error("当前浏览器不支持流式读取响应体。");
      }

      const reader = response.body.getReader();
      const parser = new H264AnnexBStreamParser();
      const decodedFrames: FrameThumb[] = [];
      const persistedFrames: CachedFrameRecord[] = [];
      const pendingFrames = new Map<number, { thumb: FrameThumb; record: CachedFrameRecord }>();
      const pendingUnits: QueuedAccessUnit[] = [];
      const materializationTasks: Promise<void>[] = [];
      let materializationError: Error | null = null;
      let latestSummary: StreamSummary | null = null;
      let bytesRead = 0;
      let decodedCount = 0;
      let readyFrameCount = 0;
      let nextFrameToCommit = 0;
      let timestampCursorUs = 0;
      let firstFrameSeen = false;
      const thumbnailCanvas = document.createElement("canvas");
      const thumbnailContext = thumbnailCanvas.getContext("2d");

      if (!thumbnailContext) {
        throw new Error("无法创建缩略图 canvas 2D 上下文。");
      }

      const flushUi = (force = false) => {
        if (force || readyFrameCount <= 4 || readyFrameCount % 8 === 0) {
          framesRef.current = decodedFrames;
          setFrames(decodedFrames.slice());
          setProgress({
            bytesRead,
            bytesTotal,
            decoded: readyFrameCount,
            decodedTotal: null,
          });
        }
      };

      const flushPendingFrames = (force = false) => {
        let changed = false;

        while (pendingFrames.has(nextFrameToCommit)) {
          const entry = pendingFrames.get(nextFrameToCommit);
          if (!entry) {
            break;
          }

          pendingFrames.delete(nextFrameToCommit);
          decodedFrames.push(entry.thumb);
          persistedFrames.push(entry.record);
          nextFrameToCommit += 1;
          readyFrameCount += 1;
          changed = true;

          if (!firstFrameSeen) {
            firstFrameSeen = true;
            setStatus("首帧已到达，页面正在边下边渲染。");
          }
        }

        if (changed || force) {
          flushUi(force);
        }
      };

      const queueFrameMaterialization = (frame: VideoFrame) => {
        const frameIndex = decodedCount;
        const width = frame.displayWidth;
        const height = frame.displayHeight;
        const timestampUs =
          typeof frame.timestamp === "number" ? frame.timestamp : frameIndex * FRAME_STEP_US;
        const scale = Math.min(1, FRAME_EXPORT_WIDTH / width);
        const exportCanvas = document.createElement("canvas");
        exportCanvas.width = Math.max(1, Math.round(width * scale));
        exportCanvas.height = Math.max(1, Math.round(height * scale));
        const exportContext = exportCanvas.getContext("2d");
        if (!exportContext) {
          frame.close();
          materializationError = materializationError ?? new Error("无法创建帧导出上下文。");
          return;
        }

        exportContext.clearRect(0, 0, exportCanvas.width, exportCanvas.height);
        exportContext.drawImage(frame, 0, 0, exportCanvas.width, exportCanvas.height);
        const blobPromise = canvasToWebpBlob(exportCanvas);
        frame.close();

        const task = blobPromise
          .then((blob) => {
            if (controller.signal.aborted || token !== loadTokenRef.current) {
              return;
            }

            const objectUrl = URL.createObjectURL(blob);
            if (controller.signal.aborted || token !== loadTokenRef.current) {
              URL.revokeObjectURL(objectUrl);
              return;
            }

            frameObjectUrlsRef.current.add(objectUrl);
            pendingFrames.set(frameIndex, {
              thumb: {
                index: frameIndex,
                src: objectUrl,
                width,
                height,
                timestampUs,
              },
              record: {
                index: frameIndex,
                width,
                height,
                timestampUs,
                blob,
              },
            });
            flushPendingFrames();
          })
          .catch((error) => {
            materializationError =
              materializationError ?? (error instanceof Error ? error : new Error(String(error)));
          });

        materializationTasks.push(task);
      };

      const decodeQueuedUnit = async (unit: QueuedAccessUnit) => {
        if (!decoderRef.current) {
          pendingUnits.push(unit);
          return;
        }

        while (decoderRef.current.decodeQueueSize > 12) {
          await sleep(8);
        }

        decoderRef.current.decode(
          new EncodedVideoChunk({
            type: unit.type,
            timestamp: unit.timestampUs,
            data: unit.data,
          }),
        );
      };

      const flushPendingUnits = async () => {
        while (pendingUnits.length > 0) {
          const next = pendingUnits.shift();
          if (next) {
            await decodeQueuedUnit(next);
          }
        }
      };

      const ensureDecoder = async (config: { codec: string; description: Uint8Array }) => {
        if (decoderRef.current) {
          return;
        }

        const support = await VideoDecoder.isConfigSupported(config);
        if (!support.supported || !support.config) {
          throw new Error("当前浏览器返回了不支持的 H.264 配置。");
        }

        decoderRef.current = new VideoDecoder({
          output(frame) {
            if (controller.signal.aborted || token !== loadTokenRef.current) {
              frame.close();
              return;
            }

            queueFrameMaterialization(frame);
            decodedCount += 1;
          },
          error(error) {
            materializationError =
              materializationError ?? (error instanceof Error ? error : new Error(String(error)));
          },
        });

        decoderRef.current.configure(support.config);
        setStatus("解码器已就绪，正在等待更多分片。");
        await flushPendingUnits();
      };

      const processChunk = async (chunk: Uint8Array) => {
        bytesRead += chunk.length;
        setProgress((current) => ({
          ...current,
          bytesRead,
          bytesTotal,
        }));

        const result = parser.push(chunk);
        if (result.config) {
          codecRef.current = result.config.codec;
          latestSummary = {
            codec: result.config.codec,
            frameCount: result.frameCount,
            keyFrameCount: result.keyFrameCount,
          };
          setStreamInfo(latestSummary);
          await ensureDecoder(result.config);
        } else if (codecRef.current) {
          latestSummary = {
            codec: codecRef.current,
            frameCount: result.frameCount,
            keyFrameCount: result.keyFrameCount,
          };
          setStreamInfo(latestSummary);
        }

        for (const unit of result.accessUnits) {
          const queued: QueuedAccessUnit = {
            type: unit.type,
            data: unit.data,
            timestampUs: timestampCursorUs,
          };
          timestampCursorUs += FRAME_STEP_US;
          await decodeQueuedUnit(queued);
        }

        flushPendingFrames();
      };

      setStatus(nextCacheState === "hit" ? "缓存命中，开始流式读取..." : "网络流式下载中...");

      while (true) {
        if (controller.signal.aborted || token !== loadTokenRef.current) {
          closeDecoder();
          return;
        }

        const { done, value } = await reader.read();
        if (done) {
          break;
        }

        if (value && value.length > 0) {
          await processChunk(value);
        }
      }

      const finalResult = parser.flush();
      if (finalResult.config) {
        codecRef.current = finalResult.config.codec;
        latestSummary = {
          codec: finalResult.config.codec,
          frameCount: finalResult.frameCount,
          keyFrameCount: finalResult.keyFrameCount,
        };
        setStreamInfo(latestSummary);
        await ensureDecoder(finalResult.config);
      }

      for (const unit of finalResult.accessUnits) {
        const queued: QueuedAccessUnit = {
          type: unit.type,
          data: unit.data,
          timestampUs: timestampCursorUs,
        };
        timestampCursorUs += FRAME_STEP_US;
        await decodeQueuedUnit(queued);
      }

      if (!decoderRef.current) {
        throw new Error("没有拿到可用的 SPS / PPS，无法初始化解码器。");
      }

      await flushPendingUnits();
      await decoderRef.current.flush();
      await Promise.all(materializationTasks);

      if (materializationError) {
        throw materializationError;
      }

      flushPendingFrames(true);
      closeDecoder();

      const summary = latestSummary ?? {
        codec: codecRef.current ?? "avc1.000000",
        frameCount: decodedFrames.length,
        keyFrameCount: decodedFrames.length > 0 ? 1 : 0,
      };

      let frameCacheSaved = false;
      let videoCacheReleased = false;

      try {
        await writeCachedFrameSession({
          version: 1,
          url: videoUrl,
          sourceSize: bytesTotal,
          codec: summary.codec,
          frameCount: decodedFrames.length,
          keyFrameCount: summary.keyFrameCount,
          frames: persistedFrames.slice(),
        });
        frameCacheSaved = true;
        setFrameCacheState("hit");
      } catch {
        setFrameCacheState("miss");
      }

      try {
        await clearCachedVideoResponse(videoUrl);
        videoCacheReleased = true;
        setVideoCacheState("released");
        await refreshStorageInfo();
      } catch {
        setVideoCacheState(nextCacheState);
      }

      setStatus(
        frameCacheSaved
          ? `完成，共切出 ${decodedFrames.length} 帧。${videoCacheReleased ? "原始 video.h264 已释放，仅保留帧缓存。" : "帧缓存已保存，但原始视频缓存清理失败。"}`
          : `完成，共切出 ${decodedFrames.length} 帧，但帧缓存写入失败。`,
      );

      framesRef.current = decodedFrames.slice();
      setFrames(decodedFrames.slice());
      setProgress({
        bytesRead,
        bytesTotal,
        decoded: decodedFrames.length,
        decodedTotal: decodedFrames.length,
      });
      setLoadState("ready");
      setPlaying(false);
    } catch (cause) {
      closeDecoder();
      if (controller.signal.aborted || token !== loadTokenRef.current) {
        return;
      }

      const message = cause instanceof Error ? cause.message : String(cause);
      setLoadState("error");
      setError(message);
      setStatus("切帧失败。");
    }
  }

  useEffect(() => {
    if (!playing || frames.length === 0) {
      return;
    }

    let cancelled = false;
    const startIndex = Math.min(playbackAnchorRef.current.index, frames.length - 1);
    const startTimestampUs = playbackAnchorRef.current.timestampUs;
    const startTime = playbackAnchorRef.current.startedAt || performance.now();

    void drawFrameToCanvas(startIndex);
    preloadFrameWindow(startIndex + 1);

    const tick = (now: number) => {
      if (cancelled) {
        return;
      }

      const elapsedUs = (now - startTime) * 1000;
      const targetTimestampUs = startTimestampUs + elapsedUs;

      let nextIndex = startIndex;
      const currentFrames = framesRef.current;
      while (
        nextIndex + 1 < currentFrames.length &&
        currentFrames[nextIndex + 1].timestampUs <= targetTimestampUs
      ) {
        nextIndex += 1;
      }

      if (nextIndex !== playheadRef.current) {
        playheadRef.current = nextIndex;
        setPlayhead(nextIndex);
        void drawFrameToCanvas(nextIndex);
        preloadFrameWindow(nextIndex + 1);
      }

      if (nextIndex >= currentFrames.length - 1) {
        setPlaying(false);
        return;
      }

      rafRef.current = requestAnimationFrame(tick);
    };

    rafRef.current = requestAnimationFrame(tick);

    return () => {
      cancelled = true;
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      }
    };
  }, [playing, frames]);

  const hasFrames = frames.length > 0;
  const canPlay = loadState === "ready" && frames.length > 0;

  return (
    <main className="viewer-shell">
      <div className="viewer-inner">
        <section className="hero-card">
          <div className="hero-copy">
            <p className="eyebrow">Rust backend · Next.js frontend · WebCodecs</p>
            <h1 className="hero-title">边下边看，最后再顺滑播放整段帧序列</h1>
            <p>
              前端先从 Cache Storage 里找视频，命中就直接流式解码；没命中则边下边解码，
              先把前几帧送到页面。全部帧到齐后，可以切到单画布播放模式，避免一堆图片
              同时刷新造成的卡顿。
            </p>
          </div>

          <div className="hero-metrics">
            <div className="metric">
              <p className="metric-label">视频地址</p>
              <p className="metric-value">{videoUrl}</p>
              <p className="metric-note">默认指向 Rust 服务的 `/video.h264`。</p>
            </div>
            <div className="metric">
              <p className="metric-label">缓存</p>
              <p className="metric-value">
                帧 {describeCacheState(frameCacheState)} · 视频 {describeCacheState(videoCacheState)}
              </p>
              <p className="metric-note">
                帧缓存命中就能直接播放；原始视频会在切帧完成后尽量释放。
              </p>
            </div>
            <div className="metric">
              <p className="metric-label">进度</p>
              <p className="metric-value">
                {progress.bytesTotal ? `${Math.round(progressValue)}%` : `${progress.decoded} 帧`}
              </p>
              <p className="metric-note">
                {progress.bytesTotal
                  ? `${formatBytes(progress.bytesRead)} / ${formatBytes(progress.bytesTotal)}`
                  : status}
              </p>
            </div>
            <div className="metric">
              <p className="metric-label">存储</p>
              <p className="metric-value">{getStorageUsageText()}</p>
              <p className="metric-note">
                {storageInfo.persisted === true
                  ? "已启用持久化，长期缓存更不容易被清理。"
                  : storageInfo.persisted === false
                    ? "可申请持久化存储，减少浏览器回收概率。"
                    : "统计的是整个 origin 的占用，包含帧缓存与视频缓存。"}
              </p>
            </div>
          </div>
        </section>

        <section className="panel-card">
          <div className="toolbar">
            <label className="field">
              <span className="field-label">Video URL</span>
              <input
                className="field-input"
                value={videoUrl}
                onChange={(event) => setVideoUrl(event.target.value)}
                spellCheck={false}
              />
            </label>

            <div className="toolbar-actions">
              <button
                className="primary-button"
                onClick={loadAndDecode}
                disabled={loadState === "loading"}
              >
                {loadState === "loading" ? "解码中..." : "开始切帧"}
              </button>
              <button
                className="ghost-button"
                onClick={requestPersistentStorage}
                disabled={isPersistentButtonDisabled()}
              >
                {getPersistentButtonLabel()}
              </button>
              <button
                className="ghost-button"
                onClick={clearAllCaches}
                disabled={loadState === "loading"}
              >
                清空缓存
              </button>
            </div>
          </div>

          <div className="status-row">
            <div>
              <div className="status-label">运行提示</div>
              <div className="status-copy">
                这页会先从本地帧缓存恢复；没有命中时才去请求后端 video.h264 并边下边切帧。
                切帧完成后会尽量只保留帧缓存，减少重复占用存储。
              </div>
            </div>

            {streamInfo ? (
              <div className="pager">
                <span className="ghost-button" style={{ cursor: "default" }}>
                  {streamInfo.codec}
                </span>
                <span className="ghost-button" style={{ cursor: "default" }}>
                  {streamInfo.frameCount} 帧
                </span>
                <span className="ghost-button" style={{ cursor: "default" }}>
                  {streamInfo.keyFrameCount} 关键帧
                </span>
              </div>
            ) : null}
          </div>

          <div className="progress-track" aria-label="decode progress">
            <div className="progress-bar" style={{ width: `${progressValue}%` }} />
          </div>

          {error ? <div className="error-state">{error}</div> : null}

          <div className="player-card">
            <div className="gallery-header">
              <div>
                <h2 className="gallery-title">播放模式</h2>
                <div className="gallery-meta">
                  {canPlay ? `${playhead + 1} / ${frames.length} · 1x` : "等待可播放帧"}
                </div>
              </div>

              <div className="pager">
                <button
                  onClick={() => {
                    if (playing) {
                      setPlaying(false);
                      return;
                    }

                    startPlaybackFromCurrentFrame();
                  }}
                  disabled={!canPlay}
                >
                  {playing ? "暂停" : "播放"}
                </button>
                <button
                  onClick={() => {
                    setPlaying(false);
                    setPlayhead(0);
                    setScrubValue(0);
                    playheadRef.current = 0;
                    void drawFrameToCanvas(0);
                  }}
                  disabled={!canPlay}
                >
                  重播
                </button>
              </div>
            </div>

            <div className="scrubber">
              <div className="scrubber-header">
                <span className="scrubber-label">帧拖动条</span>
                <span className="scrubber-value">
                  {canPlay ? `Frame ${scrubValue + 1} / ${frames.length}` : "等待可播放帧"}
                </span>
              </div>
              <input
                className="scrubber-input"
                type="range"
                min={0}
                max={Math.max(0, frames.length - 1)}
                value={Math.min(scrubValue, Math.max(0, frames.length - 1))}
                disabled={!canPlay}
                aria-label="拖动到指定帧"
                onPointerDown={() => setPlaying(false)}
                onChange={(event) => {
                  const nextIndex = event.currentTarget.valueAsNumber;
                  if (!Number.isFinite(nextIndex)) {
                    return;
                  }

                  jumpToFrame(nextIndex);
                }}
              />
            </div>

            <div className="player-surface">
              {canPlay ? (
                <canvas ref={playbackCanvasRef} className="playback-canvas" />
              ) : (
                <div className="player-placeholder">
                  {loadState === "loading"
                    ? "正在切帧，第一批缩略图出来后会直接显示在下面。"
                    : hasFrames
                      ? "帧已恢复，准备播放。"
                      : "这里会显示单画布播放区。"}
                </div>
              )}
            </div>
          </div>
        </section>
      </div>
    </main>
  );
}

function formatBytes(value: number | null): string {
  if (value === null) {
    return "--";
  }

  if (value < 1024) {
    return `${value} B`;
  }

  const units = ["KB", "MB", "GB"];
  let size = value / 1024;
  let unit = 0;

  while (size >= 1024 && unit < units.length - 1) {
    size /= 1024;
    unit += 1;
  }

  return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[unit]}`;
}

function sleep(ms: number) {
  return new Promise<void>((resolve) => {
    window.setTimeout(resolve, ms);
  });
}

function canvasToWebpBlob(canvas: HTMLCanvasElement): Promise<Blob> {
  return new Promise((resolve, reject) => {
    canvas.toBlob(
      (blob) => {
        if (!blob) {
          reject(new Error("无法生成 WebP 帧。"));
          return;
        }

        resolve(blob);
      },
      "image/webp",
      0.92,
    );
  });
}
