﻿import { useState, useEffect, useRef, useCallback } from "react";
import api from "../api/client";


function bellFrequency(bellNumber) {
  return 880 * Math.pow(Math.pow(2, 1 / 12), -2 * (bellNumber - 1));
}

function playStep(audioCtx, stepValue, beatDurationMs) {
  if (!stepValue || !audioCtx) return;
  const now = audioCtx.currentTime;
  const duration = beatDurationMs / 1000;
  const fadeIn = 0.005;
  const fadeOut = 0.03;

  for (let bit = 0; bit < 16; bit++) {
    if (stepValue & (1 << bit)) {
      const freq = bellFrequency(bit + 1);
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.type = "sine";
      osc.frequency.setValueAtTime(freq, now);
      gain.gain.setValueAtTime(0, now);
      gain.gain.linearRampToValueAtTime(0.3, now + fadeIn);
      gain.gain.setValueAtTime(0.3, now + Math.max(duration - fadeOut, fadeIn + 0.001));
      gain.gain.linearRampToValueAtTime(0, now + duration);
      osc.start(now);
      osc.stop(now + duration);
    }
  }
}

function parseBellNotation(notation) {
  const raw = String(notation || "").trim();
  if (raw === "0" || !raw) return 0;
  let value = 0;
  for (const part of raw.split("+")) {
    const n = Number.parseInt(part.trim(), 10);
    if (Number.isInteger(n) && n >= 1 && n <= 16) value |= 1 << (n - 1);
  }
  return value;
}

function parseStepsString(stepsStr) {
  if (!stepsStr || !String(stepsStr).trim()) return [];
  return String(stepsStr).trim().split(",").map((s) => parseBellNotation(s));
}

function normalizePlaybackUrl(url) {
  if (!url || typeof url !== "string") return null;
  if (url.startsWith("http") || url.startsWith("/api")) return url;
  if (url.startsWith("/")) return `/api${url}`;
  return `/api/${url}`;
}

async function fetchBinaryResponse(url) {
  const token = localStorage.getItem("access_token");
  try {
    const res = await fetch(url, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
    if (res.ok) return res;
    if (url.startsWith("http")) {
      const retry = await fetch(url);
      if (retry.ok) return retry;
      throw new Error(`Failed to fetch binary: ${retry.statusText || retry.status}`);
    }
    throw new Error(`Failed to fetch binary: ${res.statusText || res.status}`);
  } catch (err) {
    if (url.startsWith("http")) {
      const retry = await fetch(url);
      if (retry.ok) return retry;
      throw new Error(`Failed to fetch binary: ${retry.statusText || retry.status}`);
    }
    throw err;
  }
}

async function decodeBsmBinary(url) {
  const res = await fetchBinaryResponse(url);
  const buf = await res.arrayBuffer();
  const view = new DataView(buf);
  const steps = [];
  for (let i = 0; i + 1 < buf.byteLength; i += 2) {
    steps.push(view.getUint16(i, false));
  }
  return steps;
}

function mapPercentageToSpeed(percent, minSpeed, maxSpeed) {
  if (minSpeed == null || maxSpeed == null) return null;
  const t = Math.max(0, Math.min(100, percent)) / 100;
  const a = minSpeed;
  const b = maxSpeed;
  if (a <= 0 || b <= 0) return Math.round(a + (b - a) * t);
  return Math.round(a * Math.pow(b / a, t));
}

function applyNoteAssignments(rawStepValue, noteAssignments) {
  if (!noteAssignments || noteAssignments.length === 0) return rawStepValue;
  let result = 0;
  for (let bit = 0; bit < 16; bit++) {
    if (rawStepValue & (1 << bit)) {
      const assignedBell = noteAssignments[bit];
      if (assignedBell && assignedBell > 0) {
        result |= 1 << (assignedBell - 1);
      }
    }
  }
  return result;
}

function interpolateHue(t) {
  const stops = [
    [0.0, 190], // bright teal/blue
    [0.24, 140], // green
    [0.5, 56], // yellow
    [0.82, 30], // orange
    [1.0, 0], // red
  ];
  for (let i = 0; i < stops.length - 1; i++) {
    const [aPos, aHue] = stops[i];
    const [bPos, bHue] = stops[i + 1];
    if (t >= aPos && t <= bPos) {
      const local = (t - aPos) / (bPos - aPos || 1);
      return aHue + (bHue - aHue) * local;
    }
  }
  return stops[stops.length - 1][1];
}

function bellDotColor(assignedBell) {
  const bell = Number(assignedBell || 0);
  if (bell <= 0) return "rgba(148,163,184,0.7)";
  const t = Math.min(1, Math.max(0, (bell - 1) / 15));
  const hue = interpolateHue(t);
  return `hsl(${hue}, 78%, 68%)`;
}

function bellDotGlow(assignedBell) {
  const bell = Number(assignedBell || 0);
  if (bell <= 0) return "rgba(100,116,139,0.35)";
  const t = Math.min(1, Math.max(0, (bell - 1) / 15));
  const hue = interpolateHue(t);
  return `hsla(${hue}, 78%, 56%, 0.45)`;
}

function bellCustomGlow(color) {
  if (!color || typeof color !== "string") return null;
  return `${color}66`;
}

const mutedStyle = { color: "var(--text-muted)" };
const labelStyle = { color: "var(--text-secondary)" };
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";

export default function PlaybackModal({ open, melody, builtMelody, files, archetypeCsv, onClose }) {
  const info = melody?.information || {};
  const minSpeed = info.minSpeed || null;
  const maxSpeed = info.maxSpeed || null;
  const noteAssignments = melody?.default_settings?.noteAssignments || [];

  const [steps, setSteps] = useState([]);
  const [loading, setLoading] = useState(false);
  const [loadError, setLoadError] = useState("");

  const [playing, setPlaying] = useState(false);
  const [currentStep, setCurrentStep] = useState(-1);
  const [speedPercent, setSpeedPercent] = useState(50);
  const [toneLengthMs, setToneLengthMs] = useState(80);
  const [loopEnabled, setLoopEnabled] = useState(true);
  const [activeBells, setActiveBells] = useState(new Set());
  const [noteColors, setNoteColors] = useState([]);

  const audioCtxRef = useRef(null);
  const playbackRef = useRef(null);
  const stepsRef = useRef([]);
  const speedMsRef = useRef(500);
  const toneLengthRef = useRef(80);
  const noteAssignmentsRef = useRef(noteAssignments);
  const loopEnabledRef = useRef(true);

  const speedMs = mapPercentageToSpeed(speedPercent, minSpeed, maxSpeed) ?? 500;

  useEffect(() => { stepsRef.current = steps; }, [steps]);
  useEffect(() => { speedMsRef.current = speedMs; }, [speedMs]);
  useEffect(() => { toneLengthRef.current = toneLengthMs; }, [toneLengthMs]);
  useEffect(() => { noteAssignmentsRef.current = noteAssignments; }, [noteAssignments]);
  useEffect(() => { loopEnabledRef.current = loopEnabled; }, [loopEnabled]);
  useEffect(() => {
    if (!open) return;
    let canceled = false;
    api.get("/settings/melody")
      .then((s) => {
        if (canceled) return;
        setNoteColors((s?.note_assignment_colors || []).slice(0, 16));
      })
      .catch(() => {
        if (!canceled) setNoteColors([]);
      });
    return () => { canceled = true; };
  }, [open]);

  const stopPlayback = useCallback(() => {
    if (playbackRef.current) {
      clearTimeout(playbackRef.current.timer);
      if (playbackRef.current.flashTimer) clearTimeout(playbackRef.current.flashTimer);
      playbackRef.current = null;
    }
    setPlaying(false);
    setCurrentStep(-1);
    setActiveBells(new Set());
  }, []);

  useEffect(() => {
    if (!open) {
      stopPlayback();
      setSteps([]);
      setCurrentStep(-1);
      setLoadError("");
      setSpeedPercent(50);
      setLoopEnabled(true);
      setActiveBells(new Set());
      return;
    }

    const binaryUrlCandidate = builtMelody?.binary_url
      ? `/api${builtMelody.binary_url}`
      : files?.binary_url || melody?.url || null;
    const binaryUrl = normalizePlaybackUrl(binaryUrlCandidate);
    const csv = archetypeCsv || info.archetype_csv || null;

    if (binaryUrl) {
      setLoading(true);
      setLoadError("");
      decodeBsmBinary(binaryUrl)
        .then((decoded) => {
          setSteps(decoded);
          stepsRef.current = decoded;
        })
        .catch((err) => {
          if (csv) {
            const parsed = parseStepsString(csv);
            setSteps(parsed);
            stepsRef.current = parsed;
            setLoadError("");
            return;
          }
          setLoadError(err.message || "Failed to load melody data.");
        })
        .finally(() => setLoading(false));
      return;
    }

    if (csv) {
      const parsed = parseStepsString(csv);
      setSteps(parsed);
      stepsRef.current = parsed;
      setLoadError("");
      return;
    }

    setLoadError("No binary or archetype data available for this melody.");
  }, [open]); // eslint-disable-line react-hooks/exhaustive-deps

  const ensureAudioCtx = () => {
    if (!audioCtxRef.current || audioCtxRef.current.state === "closed") {
      audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (audioCtxRef.current.state === "suspended") {
      audioCtxRef.current.resume();
    }
    return audioCtxRef.current;
  };

  const scheduleStep = useCallback((stepIndex) => {
    const currentSteps = stepsRef.current;
    if (!currentSteps.length) return;

    const playFrom = stepIndex % currentSteps.length;
    const ctx = ensureAudioCtx();
    const rawStepValue = currentSteps[playFrom];
    const stepValue = applyNoteAssignments(rawStepValue, noteAssignmentsRef.current);

    setCurrentStep(playFrom);

    const bellsNow = new Set();
    for (let bit = 0; bit < 16; bit++) {
      if (stepValue & (1 << bit)) bellsNow.add(bit + 1);
    }
    setActiveBells(bellsNow);

    playStep(ctx, stepValue, toneLengthRef.current);

    const flashTimer = setTimeout(() => setActiveBells(new Set()), toneLengthRef.current);
    const timer = setTimeout(() => {
      const next = playFrom + 1;
      if (next >= stepsRef.current.length) {
        if (loopEnabledRef.current) scheduleStep(0);
        else stopPlayback();
        return;
      }
      scheduleStep(next);
    }, speedMsRef.current);

    playbackRef.current = { timer, flashTimer, stepIndex: playFrom };
  }, [stopPlayback]);

  const handlePlay = () => {
    if (!stepsRef.current.length) return;
    setPlaying(true);
    scheduleStep(0);
  };

  if (!open) return null;

  const totalSteps = steps.length;
  const allBellsUsed = steps.reduce((set, v) => {
    const mapped = applyNoteAssignments(v, noteAssignments);
    for (let bit = 0; bit < 16; bit++) {
      if (mapped & (1 << bit)) set.add(bit + 1);
    }
    return set;
  }, new Set());
  const maxBell = allBellsUsed.size > 0 ? Math.max(...Array.from(allBellsUsed)) : 0;

  const hasSpeedInfo = minSpeed != null && maxSpeed != null;
  const detectedNoteCount = steps.reduce((max, stepValue) => {
    let highest = 0;
    for (let bit = 15; bit >= 0; bit--) {
      if (stepValue & (1 << bit)) {
        highest = bit + 1;
        break;
      }
    }
    return Math.max(max, highest);
  }, 0);
  const configuredNoteCount = Number(info.totalNotes || noteAssignments.length || 0);
  const gridNoteCount = Math.max(1, Math.min(16, configuredNoteCount || detectedNoteCount || 1));

  return (
    <div
      className="fixed inset-0 flex items-center justify-center z-50"
      style={{ backgroundColor: "rgba(0,0,0,0.7)" }}
      onClick={(e) => e.target === e.currentTarget && !playing && onClose()}
    >
      <div
        className="w-full rounded-lg border shadow-xl"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          maxWidth: "1100px",
        }}
      >
        <div className="flex items-center justify-between px-6 py-4 border-b" style={{ borderColor: "var(--border-primary)" }}>
          <div>
            <h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>Melody Playback</h2>
            <p className="text-xs mt-0.5" style={mutedStyle}>{melody?.information?.name?.en || "Melody"} - looping</p>
          </div>
          <button onClick={() => { stopPlayback(); onClose(); }} className="text-xl leading-none" style={mutedStyle}>&times;</button>
        </div>

        <div className="px-6 py-5 space-y-5">
          {loading && <p className="text-sm text-center py-4" style={mutedStyle}>Loading binary...</p>}
          {loadError && (
            <div className="text-sm rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
              {loadError}
            </div>
          )}

          {!loading && !loadError && totalSteps > 0 && (
            <>
              <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
                <div className="space-y-4">
                  <div className="flex items-center justify-between">
                    <span className="text-xs" style={mutedStyle}>{totalSteps} steps · {allBellsUsed.size} bell{allBellsUsed.size !== 1 ? "s" : ""}</span>
                  </div>

                  {noteAssignments.length > 0 && (
                    <div>
                      <p className="text-xs mb-2" style={mutedStyle}>Note to Assigned Bell</p>
                      <div className="flex flex-wrap gap-1.5">
                        {noteAssignments.map((assignedBell, noteIdx) => {
                          const firesABell = assignedBell && assignedBell > 0;
                          const isActive = firesABell && activeBells.has(assignedBell);
                          return (
                            <div
                              key={noteIdx}
                              className="flex flex-col items-center rounded-md border transition-all"
                              style={{
                                minWidth: "36px",
                                padding: "4px 6px",
                                backgroundColor: isActive && firesABell ? "var(--accent)" : "var(--bg-card-hover)",
                                borderColor: isActive ? "var(--accent)" : "var(--border-primary)",
                              }}
                            >
                              <span className="text-xs font-bold leading-tight" style={{ color: isActive && firesABell ? "var(--bg-primary)" : "var(--text-secondary)" }}>{NOTE_LABELS[noteIdx]}</span>
                              <div className="w-full my-0.5" style={{ height: "1px", backgroundColor: isActive && firesABell ? "rgba(255,255,255,0.4)" : "var(--border-primary)" }} />
                              <span className="text-xs leading-tight" style={{ color: isActive && firesABell ? "var(--bg-primary)" : "var(--text-muted)" }}>{assignedBell > 0 ? assignedBell : "-"}</span>
                            </div>
                          );
                        })}
                      </div>
                    </div>
                  )}

                  {maxBell > 0 && (
                    <div>
                      <p className="text-xs mb-2" style={mutedStyle}>Active Bells</p>
                      <div className="flex flex-wrap gap-1.5">
                        {Array.from({ length: maxBell }, (_, i) => i + 1).map((b) => {
                          const isActive = activeBells.has(b);
                          const isUsed = allBellsUsed.has(b);
                          return (
                            <div
                              key={b}
                              className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold"
                              style={{
                                backgroundColor: isActive ? "#22c55e" : isUsed ? "var(--bg-card-hover)" : "var(--bg-primary)",
                                color: isActive ? "#fff" : isUsed ? "var(--text-secondary)" : "var(--border-primary)",
                                border: `2px solid ${isActive ? "#22c55e" : "var(--border-primary)"}`,
                              }}
                            >
                              {b}
                            </div>
                          );
                        })}
                      </div>
                    </div>
                  )}
                </div>

                <div className="space-y-4">
                  <div className="flex items-center gap-3">
                    {!playing ? (
                      <button onClick={handlePlay} className="px-5 py-2 text-sm rounded-md font-medium transition-colors" style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}>
                        Play
                      </button>
                    ) : (
                      <button onClick={stopPlayback} className="px-5 py-2 text-sm rounded-md font-medium transition-colors" style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}>
                        Stop
                      </button>
                    )}
                    <label className="inline-flex items-center gap-2 text-xs" style={mutedStyle}>
                      <input type="checkbox" checked={loopEnabled} onChange={(e) => setLoopEnabled(e.target.checked)} className="h-3.5 w-3.5 rounded" />
                      Loop
                    </label>
                    {currentStep >= 0 && (
                      <div className="ml-auto min-w-[180px]">
                        <div className="flex items-center justify-end gap-2">
                          <span className="text-xs font-mono" style={{ color: "var(--accent)" }}>
                            Step {currentStep + 1} / {totalSteps}
                          </span>
                        </div>
                        <div className="mt-1 h-1.5 rounded-full" style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)" }}>
                          <div
                            className="h-full rounded-full"
                            style={{ width: `${Math.max(0, Math.min(100, ((currentStep + 1) / totalSteps) * 100))}%`, backgroundColor: "var(--accent)" }}
                          />
                        </div>
                      </div>
                    )}
                  </div>

                  <div>
                    <div className="flex items-center justify-between mb-1.5">
                      <label className="text-sm font-medium" style={labelStyle}>Speed</label>
                      <div className="text-right">
                        <span className="text-sm font-bold" style={{ color: "var(--accent)" }}>{speedPercent}%</span>
                        {hasSpeedInfo && <span className="text-xs ml-2" style={mutedStyle}>({speedMs} ms/step)</span>}
                      </div>
                    </div>
                    <input type="range" min="1" max="100" step="1" value={speedPercent} onChange={(e) => setSpeedPercent(Number(e.target.value))} className="w-full h-2 rounded-lg appearance-none cursor-pointer" />
                  </div>

                  <div>
                    <div className="flex items-center justify-between mb-1.5">
                      <label className="text-sm font-medium" style={labelStyle}>Tone Length</label>
                      <span className="text-sm font-bold" style={{ color: "var(--accent)" }}>{toneLengthMs} ms</span>
                    </div>
                    <input type="range" min="20" max="400" step="10" value={toneLengthMs} onChange={(e) => setToneLengthMs(Number(e.target.value))} className="w-full h-2 rounded-lg appearance-none cursor-pointer" />
                  </div>
                </div>
              </div>

              <div>
                <p className="text-xs mb-2" style={mutedStyle}>Note/Step Matrix</p>
                <div className="rounded-md border overflow-auto" style={{ borderColor: "var(--border-primary)", maxHeight: "340px" }}>
                  <table className="min-w-max border-separate border-spacing-0 text-xs">
                    <thead>
                      <tr>
                        <th className="sticky top-0 left-0 z-20 px-2 py-1.5 text-left border-b border-r" style={{ backgroundColor: "var(--bg-primary)", borderColor: "var(--border-primary)", color: "var(--text-muted)" }}>Note \\ Step</th>
                        {steps.map((_, stepIdx) => (
                          <th
                            key={stepIdx}
                            className="sticky top-0 z-10 px-2 py-1.5 text-center border-b border-r"
                            style={{
                              minWidth: "40px",
                              backgroundColor: currentStep === stepIdx ? "rgba(116,184,22,0.2)" : "var(--bg-primary)",
                              borderColor: "var(--border-primary)",
                              color: currentStep === stepIdx ? "var(--accent)" : "var(--text-muted)",
                            }}
                          >
                            {stepIdx + 1}
                          </th>
                        ))}
                      </tr>
                    </thead>
                    <tbody>
                      {Array.from({ length: gridNoteCount }, (_, noteIdx) => (
                        <tr key={noteIdx}>
                          <th className="sticky left-0 z-[1] px-2 py-1.5 text-left border-b border-r" style={{ backgroundColor: "var(--bg-primary)", borderColor: "var(--border-primary)", color: "var(--text-secondary)" }}>{NOTE_LABELS[noteIdx]}</th>
                          {steps.map((stepValue, stepIdx) => {
                            const enabled = Boolean(stepValue & (1 << noteIdx));
                            const isCurrent = currentStep === stepIdx;
                            const assignedBell = Number(noteAssignments[noteIdx] || 0);
                            const assignedColor = assignedBell > 0 ? noteColors?.[assignedBell - 1] : null;
                            const dotLabel = assignedBell > 0 ? assignedBell : "";
                            const isUnassigned = assignedBell <= 0;
                            const dotVisible = enabled || (isUnassigned && Boolean(stepValue & (1 << noteIdx)));
                            return (
                              <td
                                key={`${noteIdx}-${stepIdx}`}
                                className="border-b border-r"
                                style={{
                                  width: "40px",
                                  height: "40px",
                                  borderColor: "var(--border-primary)",
                                  backgroundColor: isCurrent ? "rgba(116,184,22,0.06)" : "transparent",
                                }}
                              >
                                <span className="w-full h-full flex items-center justify-center" aria-hidden="true">
                                  <span
                                    className="flex items-center justify-center text-[10px] font-semibold"
                                    style={{
                                      width: "68%",
                                      height: "68%",
                                      borderRadius: "9999px",
                                      backgroundColor: isUnassigned ? "rgba(100,116,139,0.7)" : (assignedColor || bellDotColor(assignedBell)),
                                      color: "#111827",
                                      opacity: dotVisible ? 1 : 0,
                                      transform: dotVisible ? "scale(1)" : "scale(0.4)",
                                      boxShadow: dotVisible
                                        ? (isUnassigned ? "0 0 8px 2px rgba(100,116,139,0.35)" : `0 0 10px 3px ${bellCustomGlow(assignedColor) || bellDotGlow(assignedBell)}`)
                                        : "none",
                                      transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease",
                                    }}
                                  >
                                    {dotVisible ? dotLabel : ""}
                                  </span>
                                </span>
                              </td>
                            );
                          })}
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </div>

              {!hasSpeedInfo && (
                <p className="text-xs mt-1.5" style={{ color: "var(--warning, #f59e0b)" }}>
                  No MIN/MAX speed set for this melody - using linear fallback.
                </p>
              )}
            </>
          )}
        </div>

        <div className="flex justify-end px-6 py-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
          <button onClick={() => { stopPlayback(); onClose(); }} className="px-4 py-2 text-sm rounded-md transition-colors" style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}>
            Close
          </button>
        </div>
      </div>
    </div>
  );
}


