import { useCallback, useEffect, useRef, useState } from "react";

import { subtitleLabel } from "./format";
import { SUB_FONTS, SUB_PLACES, SUB_SIZES, type SubStyle } from "../types";

/**
 * Only the tracks this app attached.
 *
 * A downloaded file carries its own subtitle stream and WebKit exposes that
 * here too. Listing it would offer the same captions twice, and the in-band
 * copy is the one the media pipeline draws wherever the container's text box
 * points, out of reach of any styling. The app reads that stream out of the
 * file and attaches it itself instead.
 */
function listSubs(v: HTMLVideoElement | null): TextTrack[] {
  if (!v) return [];
  const own = new Set(Array.from(v.querySelectorAll("track")).map((el) => el.track));
  return Array.from(v.textTracks).filter(
    (t) => own.has(t) && (t.kind === "subtitles" || t.kind === "captions"),
  );
}

interface Props {
  videoRef: React.RefObject<HTMLVideoElement | null>;
  /** Element to fullscreen — the stage, so letterboxing travels with it. */
  stageRef: React.RefObject<HTMLDivElement | null>;
  /** Nudges the auto-hide timer whenever the user does something. */
  onActivity?: () => void;
  /** Preferred subtitle language, or "off" to start with none. */
  subLang: string;
  /** Persists a choice made here, so the next video matches. */
  onSubLang: (l: string) => void;
  subStyle: SubStyle;
  onSubStyle: (s: SubStyle) => void;
  /** Captions are being fetched, so the menu is not empty for lack of any. */
  subsLoading?: boolean;
}

/** One line of the appearance section: a label and a row of small choices. */
function StyleRow<T extends string | number>({
  label, value, options, onChange,
}: {
  label: string;
  value: T;
  options: Array<{ value: T; label: string }>;
  onChange: (v: T) => void;
}) {
  return (
    <div className="flex items-center gap-2 px-2 py-1">
      <span className="w-10 shrink-0 text-[11px] text-slate-400">{label}</span>
      <div className="flex min-w-0 flex-1 gap-1">
        {options.map((o) => (
          <button
            key={String(o.value)}
            onClick={() => onChange(o.value)}
            className={
              "min-w-0 flex-1 cursor-pointer truncate rounded px-1 py-1 text-[11px] " +
              "transition-colors " +
              (o.value === value
                ? "bg-sky-500 text-white"
                : "text-slate-300 hover:bg-slate-800")
            }
          >
            {o.label}
          </button>
        ))}
      </div>
    </div>
  );
}

const SKIP_S = 10;

/** The speeds shift-comma and shift-full-stop step through, as on YouTube. */
const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];

function nearestSpeed(rate: number): number {
  return SPEEDS.reduce((best, s) =>
    Math.abs(s - rate) < Math.abs(best - rate) ? s : best,
  );
}

function clock(seconds: number): string {
  if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = Math.floor(seconds % 60);
  const mm = h > 0 ? String(m).padStart(2, "0") : String(m);
  return `${h > 0 ? `${h}:` : ""}${mm}:${String(s).padStart(2, "0")}`;
}

const btn =
  "grid size-10 shrink-0 place-items-center rounded-lg text-white/90 cursor-pointer " +
  "hover:bg-white/15 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed";

/**
 * Our own transport bar.
 *
 * WebKit's native controls put fullscreen, Picture-in-Picture and volume as
 * overlay buttons in the top corners of the video and give no way to move them.
 * Owning the bar is the only way to get every control into one strip along the
 * bottom, so the native ones are switched off entirely.
 */
export default function PlayerControls({
  videoRef, stageRef, onActivity, subLang, onSubLang, subStyle, onSubStyle, subsLoading,
}: Props) {
  const [playing, setPlaying] = useState(false);
  const [time, setTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(1);
  const [muted, setMuted] = useState(false);
  // Shown only when it is not 1×, since nothing else in the bar would say so.
  const [rate, setRate] = useState(1);
  const [pip, setPip] = useState(false);
  const [full, setFull] = useState(false);
  const [menu, setMenu] = useState(false);
  const [subs, setSubs] = useState<TextTrack[]>([]);
  const [, bump] = useState(0);
  const menuRef = useRef<HTMLDivElement>(null);
  // The track that should be showing, and the set of tracks that choice was
  // made for.
  const desired = useRef<TextTrack | null>(null);
  const decided = useRef("");

  // Tracks arrive with the manifest, after metadata rather than on mount.
  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    const read = () => setSubs(listSubs(v));
    read();

    // Which track should be showing, or null for none. Decided once per set of
    // tracks — from the preference, or from a choice made in the menu — and
    // then held.
    //
    // Holding it is the point. Deciding once and walking away was not enough:
    // WebKit switches a newly added track on by itself, following the system's
    // caption settings, and it does so after the track is added. Off would come
    // back on at the next video, and there was nothing watching to undo it.
    decided.current = "";
    let corrections = 0;

    const enforce = () => {
      // A cap, so that if something insists on its own choice the two do not
      // sit there flipping it at each other forever.
      if (corrections > 24) return;
      let changed = false;
      for (const t of Array.from(v.textTracks)) {
        const want = t === desired.current ? "showing" : "disabled";
        if (t.mode !== want) {
          t.mode = want;
          changed = true;
        }
      }
      if (changed) corrections++;
      read();
    };

    const decide = () => {
      // Keyed on the tracks themselves, not their number. Changing where
      // subtitles sit re-cuts the same one track, and a count would not notice.
      const key = `${v.currentSrc}|${Array.from(v.querySelectorAll("track"))
        .map((el) => el.src)
        .join("|")}`;
      if (key !== decided.current) {
        decided.current = key;
        corrections = 0;
        desired.current =
          subLang === "off"
            ? null
            : (listSubs(v).find((t) =>
                (t.language || "").toLowerCase().startsWith(subLang.toLowerCase()),
              ) ?? null);
      }
      enforce();
    };

    decide();
    v.addEventListener("loadedmetadata", decide);
    // HLS subtitle renditions arrive after metadata, so decide as they land.
    v.textTracks.addEventListener?.("addtrack", decide);
    // And hold that decision against anything that changes a mode behind us.
    v.textTracks.addEventListener?.("change", enforce);

    v.addEventListener("loadedmetadata", read);
    v.textTracks.addEventListener?.("addtrack", read);
    // Manifests can take a moment to surface renditions.
    const id = setInterval(read, 1000);
    const stop = setTimeout(() => clearInterval(id), 8000);
    return () => {
      v.removeEventListener("loadedmetadata", decide);
      v.textTracks.removeEventListener?.("addtrack", decide);
      v.textTracks.removeEventListener?.("change", enforce);
      v.removeEventListener("loadedmetadata", read);
      v.textTracks.removeEventListener?.("addtrack", read);
      clearInterval(id);
      clearTimeout(stop);
    };
  }, [videoRef, subLang]);

  // Close the menu on any outside click.
  useEffect(() => {
    if (!menu) return;
    const onDown = (e: MouseEvent) => {
      if (!menuRef.current?.contains(e.target as Node)) setMenu(false);
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [menu]);

  const chooseSub = (track: TextTrack | null) => {
    const v = videoRef.current;
    if (!v) return;
    // Recorded first, so the holding above enforces this rather than undoing it.
    desired.current = track;
    for (const t of Array.from(v.textTracks)) {
      t.mode = t === track ? "showing" : "disabled";
    }
    // The choice becomes the preference, so the next video matches without
    // going back to Settings.
    onSubLang(track ? (track.language || "off").split("-")[0] : "off");
    bump((n) => n + 1);
    onActivity?.();
  };

  /** What `c` does: off if anything is showing, otherwise the best match. */
  const toggleSubs = useCallback(() => {
    const v = videoRef.current;
    if (!v) return;
    const available = listSubs(v);
    if (available.length === 0) return;
    const showing = available.find((t) => t.mode === "showing");
    chooseSub(
      showing
        ? null
        : (available.find((t) =>
            (t.language || "").toLowerCase().startsWith(subLang.toLowerCase()),
          ) ?? available[0]),
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [videoRef, subLang]);

  // Mirror the element's state rather than assuming ours is authoritative —
  // playback can change from the keyboard, the system, or the video ending.
  useEffect(() => {
    const v = videoRef.current;
    if (!v) return;
    const sync = () => {
      setPlaying(!v.paused);
      setTime(v.currentTime);
      setDuration(Number.isFinite(v.duration) ? v.duration : 0);
      setVolume(v.volume);
      setMuted(v.muted);
      setRate(v.playbackRate);
    };
    const onPip = () => setPip(!!document.pictureInPictureElement);
    const onFull = () => setFull(!!document.fullscreenElement);
    sync();
    for (const e of ["play", "pause", "timeupdate", "durationchange", "volumechange", "loadedmetadata", "ended", "ratechange"]) {
      v.addEventListener(e, sync);
    }
    v.addEventListener("enterpictureinpicture", onPip);
    v.addEventListener("leavepictureinpicture", onPip);
    document.addEventListener("fullscreenchange", onFull);
    return () => {
      for (const e of ["play", "pause", "timeupdate", "durationchange", "volumechange", "loadedmetadata", "ended", "ratechange"]) {
        v.removeEventListener(e, sync);
      }
      v.removeEventListener("enterpictureinpicture", onPip);
      v.removeEventListener("leavepictureinpicture", onPip);
      document.removeEventListener("fullscreenchange", onFull);
    };
  }, [videoRef]);

  const act = useCallback(
    (fn: (v: HTMLVideoElement) => void) => () => {
      const v = videoRef.current;
      if (v) fn(v);
      onActivity?.();
    },
    [videoRef, onActivity],
  );

  const togglePlay = act((v) => {
    if (v.paused) void v.play().catch(() => {});
    else v.pause();
  });

  const togglePip = useCallback(async () => {
    const v = videoRef.current;
    if (!v) return;
    onActivity?.();
    try {
      if (document.pictureInPictureElement) await document.exitPictureInPicture();
      else await v.requestPictureInPicture();
    } catch {
      /* not available for this source */
    }
  }, [videoRef, onActivity]);

  const toggleFull = useCallback(async () => {
    onActivity?.();
    try {
      if (document.fullscreenElement) await document.exitFullscreen();
      else await stageRef.current?.requestFullscreen();
    } catch {
      /* element fullscreen unavailable */
    }
  }, [stageRef, onActivity]);

  /**
   * YouTube's keyboard shortcuts, because this is a video player and those are
   * the ones fingers already know.
   *
   * space/k play, j/l jump ten seconds, arrows five, up and down are volume,
   * m mutes, f is fullscreen, i is Picture in Picture (YouTube's miniplayer),
   * c toggles subtitles, digits jump to that tenth of the video, Home and End
   * go to the ends, and shift with comma or full stop changes speed.
   *
   * Escape leaves fullscreen: WebKit does not do it for an element made
   * fullscreen this way, so without this there was no way out but the button.
   */
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const v = videoRef.current;
      if (!v) return;

      // Typing is typing. A range input is not: leaving arrows to the slider
      // would make them seek by a tenth of a second instead of five.
      const t = e.target as HTMLElement | null;
      const typing =
        t?.isContentEditable ||
        t?.tagName === "TEXTAREA" ||
        (t?.tagName === "INPUT" &&
          !["range", "checkbox", "button", "submit"].includes((t as HTMLInputElement).type));
      if (typing) return;
      // Leave the system's own combinations alone. Shift is ours: it carries
      // the speed controls.
      if (e.metaKey || e.ctrlKey || e.altKey) return;

      const seek = (to: number) => {
        v.currentTime = Math.min(Math.max(to, 0), v.duration || 0);
      };
      const setVol = (to: number) => {
        v.volume = Math.min(Math.max(to, 0), 1);
        v.muted = v.volume === 0;
      };
      const speed = (dir: 1 | -1) => {
        const i = SPEEDS.indexOf(nearestSpeed(v.playbackRate));
        v.playbackRate = SPEEDS[Math.min(Math.max(i + dir, 0), SPEEDS.length - 1)];
        setRate(v.playbackRate);
      };

      // A digit jumps to that tenth of the way through, as on YouTube.
      if (/^[0-9]$/.test(e.key) && !e.shiftKey) {
        seek(((v.duration || 0) * Number(e.key)) / 10);
      } else if (e.key === " " || e.code === "Space" || e.key === "k" || e.key === "K") {
        // Or space would also press whichever button has focus.
        e.preventDefault();
        if (v.paused) void v.play().catch(() => {});
        else v.pause();
      } else if (e.key === "j" || e.key === "J") {
        seek(v.currentTime - 10);
      } else if (e.key === "l" || e.key === "L") {
        seek(v.currentTime + 10);
      } else if (e.key === "ArrowLeft" && !e.shiftKey) {
        e.preventDefault();
        seek(v.currentTime - 5);
      } else if (e.key === "ArrowRight" && !e.shiftKey) {
        e.preventDefault();
        seek(v.currentTime + 5);
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setVol(v.volume + 0.05);
      } else if (e.key === "ArrowDown") {
        e.preventDefault();
        setVol(v.volume - 0.05);
      } else if (e.key === "Home") {
        seek(0);
      } else if (e.key === "End") {
        seek(v.duration || 0);
      } else if (e.key === "m" || e.key === "M") {
        v.muted = !v.muted;
      } else if (e.key === "f" || e.key === "F") {
        void toggleFull();
      } else if (e.key === "i" || e.key === "I") {
        void togglePip();
      } else if (e.key === "c" || e.key === "C") {
        toggleSubs();
      } else if (e.key === "<" || (e.key === "," && e.shiftKey)) {
        speed(-1);
      } else if (e.key === ">" || (e.key === "." && e.shiftKey)) {
        speed(1);
      } else if ((e.key === "," || e.key === ".") && v.paused) {
        // Frame stepping while paused. A video element will not say what its
        // frame rate is, so a frame is taken as a thirtieth of a second.
        seek(v.currentTime + (e.key === "." ? 1 / 30 : -1 / 30));
      } else if (e.key === "Escape" && document.fullscreenElement) {
        void document.exitFullscreen();
      } else {
        return;
      }
      onActivity?.();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [videoRef, toggleFull, togglePip, toggleSubs, onActivity]);

  const pct = duration > 0 ? (time / duration) * 100 : 0;

  return (
    <div
      // Clicks here must not reach the video's own play/pause handler.
      onClick={(e) => e.stopPropagation()}
      className="absolute inset-x-0 bottom-0 z-20 flex items-center gap-2.5 bg-gradient-to-t
                 from-slate-950/90 via-slate-950/65 to-transparent px-5 pb-4 pt-10"
    >
      <button onClick={togglePlay} className={btn} title={playing ? "Pause (space)" : "Play (space)"}>
        {playing ? (
          <svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
            <rect x="6" y="5" width="4" height="14" rx="1" />
            <rect x="14" y="5" width="4" height="14" rx="1" />
          </svg>
        ) : (
          <svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
            <path d="M8 5.5v13l11-6.5z" />
          </svg>
        )}
      </button>

      <button
        onClick={act((v) => (v.currentTime = Math.max(0, v.currentTime - SKIP_S)))}
        className={btn}
        title={`Back ${SKIP_S}s (j)`}
      >
        <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
          <path strokeLinecap="round" strokeLinejoin="round" d="M11 17l-5-5 5-5M18 17l-5-5 5-5" />
        </svg>
      </button>
      <button
        onClick={act((v) => (v.currentTime = Math.min(v.duration || 0, v.currentTime + SKIP_S)))}
        className={btn}
        title={`Forward ${SKIP_S}s (l)`}
      >
        <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
          <path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5-5 5M6 7l5 5-5 5" />
        </svg>
      </button>

      {rate !== 1 && (
        <button
          onClick={act((v) => {
            v.playbackRate = 1;
            setRate(1);
          })}
          title="Back to normal speed"
          className="shrink-0 cursor-pointer rounded bg-white/15 px-1.5 py-0.5 text-[11px]
                     font-medium text-white/90 hover:bg-white/25"
        >
          {rate}×
        </button>
      )}

      <span className="shrink-0 font-mono text-[12px] tabular-nums text-white/85">{clock(time)}</span>

      <input
        type="range"
        min={0}
        max={duration || 0}
        step={0.1}
        value={time}
        onChange={(e) => {
          const v = videoRef.current;
          if (v) v.currentTime = Number(e.target.value);
          onActivity?.();
        }}
        aria-label="Seek"
        className="h-1.5 min-w-0 flex-1 cursor-pointer appearance-none rounded-full bg-white/25 accent-sky-500"
        style={{
          background: `linear-gradient(to right, var(--color-sky-500) ${pct}%, rgba(255,255,255,0.25) ${pct}%)`,
        }}
      />

      <span className="shrink-0 font-mono text-[12px] tabular-nums text-white/85">{clock(duration)}</span>

      <button
        onClick={act((v) => (v.muted = !v.muted))}
        className={btn}
        title={muted || volume === 0 ? "Unmute (m)" : "Mute (m)"}
      >
        <svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
          <path d="M4 9v6h4l5 4V5L8 9H4z" />
          {muted || volume === 0 ? (
            <path d="M16 9l5 6M21 9l-5 6" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" />
          ) : (
            <path
              d="M16.5 8.5a5 5 0 010 7M19 6a8.5 8.5 0 010 12"
              stroke="currentColor"
              strokeWidth="2"
              fill="none"
              strokeLinecap="round"
            />
          )}
        </svg>
      </button>
      <input
        type="range"
        min={0}
        max={1}
        step={0.02}
        value={muted ? 0 : volume}
        onChange={(e) => {
          const v = videoRef.current;
          if (!v) return;
          v.volume = Number(e.target.value);
          v.muted = Number(e.target.value) === 0;
          onActivity?.();
        }}
        aria-label="Volume"
        className="h-1.5 w-24 shrink-0 cursor-pointer appearance-none rounded-full accent-sky-500"
        style={{
          background: `linear-gradient(to right, rgba(255,255,255,0.85) ${
            (muted ? 0 : volume) * 100
          }%, rgba(255,255,255,0.25) ${(muted ? 0 : volume) * 100}%)`,
        }}
      />

      {(subs.length > 0 || subsLoading) && (
        <div ref={menuRef} className="relative shrink-0">
          <button
            onClick={() => { setMenu((m) => !m); onActivity?.(); }}
            className={btn}
            title="Subtitles (c)"
          >
            <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
              <rect x="3" y="5" width="18" height="14" rx="2" />
              <path strokeLinecap="round" d="M7 15h4M14 15h3" />
            </svg>
          </button>

          {menu && (
            <div
              className="absolute bottom-full right-0 mb-2 max-h-80 w-64 overflow-y-auto rounded-lg
                         border border-slate-700 bg-slate-900/95 p-1 shadow-2xl backdrop-blur"
            >
              {subs.length === 0 && subsLoading && (
                <>
                  <div className="mt-1 px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
                    Subtitles
                  </div>
                  <div className="px-2 py-1.5 text-[12px] text-slate-400">Fetching…</div>
                </>
              )}

              {subs.length > 0 && (
                <>
                  <div className="mt-1 px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
                    Subtitles
                  </div>
                  <button
                    onClick={() => chooseSub(null)}
                    className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left
                                text-[12px] cursor-pointer hover:bg-slate-800 ${
                                  subs.every((t) => t.mode !== "showing")
                                    ? "text-white"
                                    : "text-slate-300"
                                }`}
                  >
                    <span className="w-3 shrink-0 text-sky-400">
                      {subs.every((t) => t.mode !== "showing") ? "✓" : ""}
                    </span>
                    Off
                  </button>
                  {subs.map((t, i) => (
                    <button
                      key={t.id || `${t.language}-${i}`}
                      onClick={() => chooseSub(t)}
                      className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left
                                  text-[12px] cursor-pointer hover:bg-slate-800 ${
                                    t.mode === "showing" ? "text-white" : "text-slate-300"
                                  }`}
                    >
                      <span className="w-3 shrink-0 text-sky-400">
                        {t.mode === "showing" ? "✓" : ""}
                      </span>
                      <span className="truncate">
                        {t.language
                          ? subtitleLabel(t.language)
                          : t.label || `Subtitles ${i + 1}`}
                      </span>
                    </button>
                  ))}
                </>
              )}

              {(subs.length > 0 || subsLoading) && (
                <>
                  <div className="mt-1 px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400">
                    Appearance
                  </div>
                  <StyleRow
                    label="Size"
                    value={subStyle.size}
                    options={SUB_SIZES}
                    onChange={(size) => onSubStyle({ ...subStyle, size })}
                  />
                  <StyleRow
                    label="Font"
                    value={subStyle.font}
                    options={SUB_FONTS.map((f) => ({ value: f.value, label: f.label }))}
                    onChange={(font) => onSubStyle({ ...subStyle, font })}
                  />
                  <StyleRow
                    label="Place"
                    value={subStyle.place}
                    options={SUB_PLACES.map((p) => ({ value: p.value, label: p.label }))}
                    onChange={(place) => onSubStyle({ ...subStyle, place })}
                  />
                </>
              )}
            </div>
          )}
        </div>
      )}

      <button onClick={togglePip} className={btn} title={pip ? "Leave Picture in Picture (i)" : "Picture in Picture (i)"}>
        <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
          <rect x="3" y="5" width="18" height="14" rx="2" />
          <rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none" />
        </svg>
      </button>

      <button onClick={toggleFull} className={btn} title={full ? "Leave full screen (f)" : "Full screen (f)"}>
        <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
          {full ? (
            <path strokeLinecap="round" strokeLinejoin="round" d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" />
          ) : (
            <path strokeLinecap="round" strokeLinejoin="round" d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
          )}
        </svg>
      </button>
    </div>
  );
}
