﻿const { useState, useEffect, useRef, useCallback } = React;

function parsePlist(xmlString) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(xmlString, "text/xml");

  function parseValue(node) {
    switch (node.tagName) {
      case "string":
        return node.textContent;
      case "integer":
        return parseInt(node.textContent, 10);
      case "real":
        return parseFloat(node.textContent);
      case "true":
        return true;
      case "false":
        return false;
      case "array": {
        const items = [];
        for (const child of node.children) {
          items.push(parseValue(child));
        }
        return items;
      }
      case "dict": {
        const obj = {};
        const children = Array.from(node.children);
        for (let index = 0; index < children.length; index += 1) {
          if (children[index].tagName === "key") {
            const key = children[index].textContent;
            obj[key] = parseValue(children[index + 1]);
            index += 1;
          }
        }
        return obj;
      }
      default:
        return node.textContent;
    }
  }

  const rootDict = doc.querySelector("plist > dict");
  return parseValue(rootDict);
}

function parseRect(rectString) {
  const values = rectString.match(/-?\d+\.?\d*/g).map(Number);
  return { x: values[0], y: values[1], width: values[2], height: values[3] };
}

function parsePoint(pointString) {
  const values = pointString.match(/-?\d+\.?\d*/g).map(Number);
  return { x: values[0], y: values[1] };
}

function getCanvasPosition(frame) {
  const size = parsePoint(frame.spriteSize);
  const offset = parsePoint(frame.spriteOffset);
  const source = parsePoint(frame.spriteSourceSize);

  return {
    x: (source.x - size.x) / 2 + offset.x,
    y: (source.y - size.y) / 2 - offset.y,
  };
}

function loadImage(imagePath) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.crossOrigin = "anonymous";
    image.onload = () => resolve(image);
    image.onerror = () => reject(new Error(`Failed to load image: ${imagePath}`));
    // Append a cache-buster so CDN-cached non-CORS responses are bypassed;
    // Railway will always return Access-Control-Allow-Origin for allowed origins.
    const sep = imagePath.includes("?") ? "&" : "?";
    image.src = imagePath + sep + "_c=" + Date.now();
  });
}

async function parseApiResponse(response) {
  const text = await response.text();
  const contentType = response.headers.get("content-type") || "";

  if (contentType.includes("application/json")) {
    const data = text ? JSON.parse(text) : {};
    if (!response.ok) {
      throw new Error(data.error || `Request failed with status ${response.status}`);
    }
    return data;
  }

  if (!response.ok) {
    throw new Error(text || `Request failed with status ${response.status}`);
  }

  return text ? { message: text } : {};
}

function TestApp({ embedded = false, onGoToCharacters = null }) {
  const { canDelete } = React.useContext(window.UserPermsContext);

  // --- Plist state ---
  const [plistFiles, setPlistFiles] = useState([]);
  const [currentPlistIndex, setCurrentPlistIndex] = useState(0);
  const [plistData, setPlistData] = useState(null);
  const [expressionIndex, setExpressionIndex] = useState(0);
  const [isScanning, setIsScanning] = useState(false);
  const [isUploadingPlists, setIsUploadingPlists] = useState(false);
  const [plistUploadResult, setPlistUploadResult] = useState(null);
  const [isClearingPlists, setIsClearingPlists] = useState(false);
  const plistFileInputRef = useRef(null);

  // --- Spritesheet browsing state ---
  const [sheetId, setSheetId] = useState("");
  const [spritesheetList, setSpritesheetList] = useState([]); // [{file, slot, slotName, plistFile, hasPlist}]
  const [sheetCharacter, setSheetCharacter] = useState(null);
  const [selectedSpriteFile, setSelectedSpriteFile] = useState(null);
  const [loadingSheets, setLoadingSheets] = useState(false);

  const [isDeletingSprite, setIsDeletingSprite] = useState(false);

  // --- Upload state ---
  const [uploadFile, setUploadFile] = useState(null);
  const [uploadId, setUploadId] = useState("");
  const [isUploading, setIsUploading] = useState(false);
  const [uploadResult, setUploadResult] = useState(null);
  const fileInputRef = useRef(null);

  // --- Canvas ---
  const [spriteImage, setSpriteImage] = useState(null);
  const [isLoadingPlist, setIsLoadingPlist] = useState(false);
  const canvasRef = useRef(null);

  const [error, setError] = useState(null);

  // --- Assign panel ---
  const [showAssignPanel, setShowAssignPanel] = useState(false);
  const [assignLabel, setAssignLabel] = useState("");
  const [assignSpriteName, setAssignSpriteName] = useState("");
  const [isAssigning, setIsAssigning] = useState(false);
  const [assignResult, setAssignResult] = useState(null);

  const currentPlistFile = plistFiles[currentPlistIndex] || null;

  const scanForPlists = useCallback(async () => {
    setIsScanning(true);
    setError(null);
    try {
      const response = await fetch(window.API_BASE + "/api/test-plists", { credentials: "include" });
      if (!response.ok) throw new Error("Could not read plist/test/ directory");
      const data = await response.json();
      setPlistFiles(data.files);
      setCurrentPlistIndex(i => (data.files.length > 0 ? Math.min(i, data.files.length - 1) : 0));
    } catch (e) {
      setError(e.message);
    } finally {
      setIsScanning(false);
    }
  }, []);

  useEffect(() => { scanForPlists(); }, []);

  // Load plist when the current plist file changes
  useEffect(() => {
    if (!currentPlistFile) { setPlistData(null); return; }
    setIsLoadingPlist(true);
    setError(null);
    setExpressionIndex(0);
    async function load() {
      try {
        const r = await fetch(`${window.API_BASE}/public/plist/test/${currentPlistFile}`, { credentials: "include" });
        if (!r.ok) throw new Error(`Failed to load ${currentPlistFile}`);
        setPlistData(parsePlist(await r.text()));
      } catch (e) {
        setError(e.message);
        setPlistData(null);
      } finally {
        setIsLoadingPlist(false);
      }
    }
    load();
  }, [currentPlistFile]);

  // Load sprite image when selected spritesheet changes
  useEffect(() => {
    if (!selectedSpriteFile) { setSpriteImage(null); return; }
    loadImage(`${window.API_BASE}/public/sprites/spritesheet/${selectedSpriteFile}`)
      .then(setSpriteImage)
      .catch(() => setSpriteImage(null));
  }, [selectedSpriteFile]);

  const expressionNames = plistData
    ? Object.keys(plistData.frames).filter(n => /^\d+\.png$/.test(n)).sort()
    : [];

  // Draw canvas
  useEffect(() => {
    if (!plistData || !spriteImage || !canvasRef.current || expressionNames.length === 0) return;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d");

    const bodyFrame = plistData.frames["body.png"];
    const expressionFrame = plistData.frames[expressionNames[expressionIndex]];
    if (!bodyFrame || !expressionFrame) return;

    const bodyRect = parseRect(bodyFrame.textureRect);
    const bodyPos = getCanvasPosition(bodyFrame);
    const bodySize = parsePoint(bodyFrame.spriteSize);
    const exRect = parseRect(expressionFrame.textureRect);
    const exPos = getCanvasPosition(expressionFrame);
    const exSize = parsePoint(expressionFrame.spriteSize);

    const minX = Math.min(bodyPos.x, exPos.x);
    const minY = Math.min(bodyPos.y, exPos.y);
    const maxX = Math.max(bodyPos.x + bodySize.x, exPos.x + exSize.x);
    const maxY = Math.max(bodyPos.y + bodySize.y, exPos.y + exSize.y);
    const pad = 4;
    canvas.width = maxX - minX + pad * 2;
    canvas.height = maxY - minY + pad * 2;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(spriteImage, bodyRect.x, bodyRect.y, bodyRect.width, bodyRect.height,
      bodyPos.x - minX + pad, bodyPos.y - minY + pad, bodyRect.width, bodyRect.height);
    ctx.drawImage(spriteImage, exRect.x, exRect.y, exRect.width, exRect.height,
      exPos.x - minX + pad, exPos.y - minY + pad, exRect.width, exRect.height);
  }, [plistData, spriteImage, expressionIndex, expressionNames]);

  const showPrev = useCallback(() =>
    setExpressionIndex(i => (i - 1 + expressionNames.length) % expressionNames.length),
    [expressionNames.length]);
  const showNext = useCallback(() =>
    setExpressionIndex(i => (i + 1) % expressionNames.length),
    [expressionNames.length]);
  function prevPlist() { setCurrentPlistIndex(i => (i - 1 + plistFiles.length) % plistFiles.length); }
  function nextPlist() { setCurrentPlistIndex(i => (i + 1) % plistFiles.length); }

  // Keyboard navigation
  useEffect(() => {
    function onKey(e) {
      if (e.target.tagName === "INPUT") return;
      if (showAssignPanel) return;
      if (e.key === "ArrowLeft") showPrev();
      if (e.key === "ArrowRight") showNext();
      if (e.key === "ArrowUp") { e.preventDefault(); prevPlist(); }
      if (e.key === "ArrowDown") { e.preventDefault(); nextPlist(); }
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [showPrev, showNext, showAssignPanel]);

  // Load spritesheets for a given character ID
  async function loadSpritesheets(overrideId) {
    const id = (overrideId !== undefined ? overrideId : sheetId).trim();
    if (!id) return;
    setLoadingSheets(true);
    setError(null);
    setSpritesheetList([]);
    setSheetCharacter(null);
    setSelectedSpriteFile(null);
    try {
      const r = await fetch(`${window.API_BASE}/api/spritesheets?id=${encodeURIComponent(id)}`);
      const data = await parseApiResponse(r);
      setSpritesheetList(data.files || []);
      setSheetCharacter(data.character || null);
    } catch (e) {
      setError(e.message);
    } finally {
      setLoadingSheets(false);
    }
  }

  async function handleUploadSprite() {
    if (!uploadFile || !uploadId.trim()) return;
    setIsUploading(true);
    setUploadResult(null);
    setError(null);
    try {
      const id = uploadId.trim();
      const r = await fetch(`${window.API_BASE}/api/upload-sprite?id=${encodeURIComponent(id)}`, {
        method: "POST",
        headers: { "Content-Type": "application/octet-stream" },
        body: uploadFile,
      });
      const result = await parseApiResponse(r);
      setUploadResult(result);
      setUploadFile(null);
      if (fileInputRef.current) fileInputRef.current.value = "";
      // Auto-load this character's spritesheets
      setSheetId(id);
      await loadSpritesheets(id);
    } catch (e) {
      setError(e.message);
    } finally {
      setIsUploading(false);
    }
  }

  async function handleUploadPlists(e) {
    const files = e.target.files;
    if (!files || files.length === 0) return;
    setIsUploadingPlists(true);
    setPlistUploadResult(null);
    setError(null);
    try {
      const formData = new FormData();
      for (const file of files) formData.append("plists", file, file.name);
      const r = await fetch(window.API_BASE + "/api/upload-plists", { method: "POST", credentials: "include", body: formData });
      setPlistUploadResult(await parseApiResponse(r));
      await scanForPlists();
    } catch (e) {
      setError(e.message);
    } finally {
      setIsUploadingPlists(false);
      if (plistFileInputRef.current) plistFileInputRef.current.value = "";
    }
  }

  async function handleClearPlists() {
    if (isClearingPlists || plistFiles.length === 0) return;
    if (!window.confirm("Delete all plist files currently in public/plist/test/?")) return;
    setIsClearingPlists(true);
    setPlistUploadResult(null);
    setError(null);
    try {
      const r = await fetch(window.API_BASE + "/api/clear-test-plists", { method: "POST", credentials: "include" });
      const result = await parseApiResponse(r);
      setCurrentPlistIndex(0);
      setPlistData(null);
      setExpressionIndex(0);
      setPlistUploadResult({ cleared: result.cleared });
      await scanForPlists();
    } catch (e) {
      setError(e.message);
    } finally {
      setIsClearingPlists(false);
    }
  }

  async function handleDeleteSpritesheet() {
    if (!selectedSpriteFile) return;
    if (!window.confirm(`Remove slot "${selectedSpriteFile}" from this character's roster? The file will remain in storage.`)) return;
    setIsDeletingSprite(true);
    setError(null);
    try {
      const r = await fetch(`${window.API_BASE}/api/delete-spritesheet?file=${encodeURIComponent(selectedSpriteFile)}`, {
        method: "DELETE",
        credentials: "include",
      });
      await parseApiResponse(r);
      setSpritesheetList(prev => prev.filter(e => e.file !== selectedSpriteFile));
      setSelectedSpriteFile(null);
    } catch (e) {
      setError(e.message);
    } finally {
      setIsDeletingSprite(false);
    }
  }

  // The entry in spritesheetList for the currently selected sprite file
  const selectedEntry = spritesheetList.find(f => f.file === selectedSpriteFile) || null;

  // Determine dialog mode
  const assignMode = !sheetCharacter ? "new" : (selectedEntry?.hasPlist ? "replace" : "add");

  function handleAssignClick() {
    if (!selectedSpriteFile || !currentPlistFile) return;
    setAssignLabel("");
    setAssignSpriteName("");
    setAssignResult(null);
    setShowAssignPanel(true);
  }

  async function handleAssignConfirm() {
    if (!selectedSpriteFile || !currentPlistFile) return;
    setIsAssigning(true);
    setError(null);
    try {
      const id = sheetId.trim();
      const body = { plistFile: currentPlistFile, id, spriteFile: selectedSpriteFile };
      if (!sheetCharacter) body.label = assignLabel.trim();
      if (!selectedEntry?.hasPlist) body.spriteName = assignSpriteName.trim();
      const r = await fetch(window.API_BASE + "/api/assign-spritesheet", {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const result = await parseApiResponse(r);
      setAssignResult(result);
      window.dispatchEvent(new CustomEvent("characterAssigned", { detail: result }));
    } catch (e) {
      setError(e.message);
      setShowAssignPanel(false);
    } finally {
      setIsAssigning(false);
    }
  }

  function closeAssignPanel() {
    const didAssign = !!assignResult;
    setShowAssignPanel(false);
    setAssignResult(null);
    if (didAssign) {
      scanForPlists();
      if (sheetId.trim()) loadSpritesheets();
    }
  }

  return (
    <main className="app-shell">
      {false && embedded && onGoToCharacters && null}
      {!embedded && (
        <header className="app-header">
          <p className="app-kicker">Plist Tester</p>
          <h1>Quick Sprite Check</h1>
          <p className="app-description">
            Upload a spritesheet, pick a character's sprite slot, then assign a test plist to it.
            <br /><a href="/" className="test-back-link">Back to Home</a>
          </p>
        </header>
      )}

      {/* Upload Section */}
      <section className="upload-card">
        <p className="upload-desc">Upload a new spritesheet PNG. It will be saved as <code>id_xxxxxxxx.png</code>.</p>
        <div className="upload-row">
          <label htmlFor="upload-id">Character ID</label>
          <input
            id="upload-id"
            type="text"
            className="character-dropdown test-id-input"
            placeholder="e.g. levia"
            value={uploadId}
            onChange={e => setUploadId(e.target.value)}
          />
          <label htmlFor="upload-file" className="upload-file-label">
            {uploadFile ? uploadFile.name : "Choose PNG..."}
            <input
              id="upload-file"
              ref={fileInputRef}
              type="file"
              accept=".png"
              className="upload-file-input"
              onChange={e => setUploadFile(e.target.files[0] || null)}
            />
          </label>
          <button
            type="button"
            className="primary-button"
            onClick={handleUploadSprite}
            disabled={!uploadFile || !uploadId.trim() || isUploading}
          >
            {isUploading ? "Uploading..." : "Upload"}
          </button>
        </div>
        {uploadResult && (
          <p className="upload-success">&#10003; Saved as <strong>{uploadResult.fileName}</strong></p>
        )}
      </section>

      {/* Character Spritesheet Browser */}
      <section className="upload-card">
        <p className="upload-desc">Load a character's spritesheets and pick one as the target for plist assignment.</p>
        <div className="upload-row">
          <label htmlFor="sheet-id-input">Character ID</label>
          <input
            id="sheet-id-input"
            type="text"
            className="character-dropdown test-id-input"
            placeholder="e.g. dahlria"
            value={sheetId}
            onChange={e => setSheetId(e.target.value)}
            onKeyDown={e => { if (e.key === "Enter") loadSpritesheets(); }}
            autoFocus
          />
          <button
            type="button"
            className="primary-button"
            onClick={() => loadSpritesheets()}
            disabled={!sheetId.trim() || loadingSheets}
          >
            {loadingSheets ? "Loading..." : "Load Spritesheets"}
          </button>
        </div>
        {sheetCharacter && (
          <p className="upload-desc" style={{ marginTop: "4px" }}>
            <strong>{sheetCharacter.label}</strong> &mdash; {sheetCharacter.spriteCount} sprite slot{sheetCharacter.spriteCount !== 1 ? "s" : ""}
          </p>
        )}
        {!sheetCharacter && spritesheetList.length > 0 && (
          <p className="upload-desc" style={{ marginTop: "4px", color: "#e8a020" }}>
            No character record found for <strong>{sheetId}</strong> &mdash; assigning will create a new character.
          </p>
        )}
        {spritesheetList.length > 0 && (
          <div className="spritesheet-grid">
            {spritesheetList.map(entry => (
              <button
                key={entry.file}
                type="button"
                className={`spritesheet-thumb-btn${selectedSpriteFile === entry.file ? " spritesheet-thumb-selected" : ""}`}
                onClick={() => setSelectedSpriteFile(entry.file)}
                title={entry.file}
              >
                <img
                  src={`${window.API_BASE}/public/sprites/spritesheet/${entry.file}`}
                  alt={entry.file}
                  className="spritesheet-thumb-img"
                  loading="lazy"
                />
                <span className="spritesheet-thumb-label">
                  {entry.slot !== null
                    ? `Slot ${entry.slot}: ${entry.slotName || "unnamed"}${entry.hasPlist ? "" : " \u2014 no plist"}`
                    : "Unassigned"}
                </span>
              </button>
            ))}
          </div>
        )}
        {selectedSpriteFile && canDelete && (
          <div className="sprite-action-bar">
            <span className="sprite-action-label">
              {selectedEntry
                ? <>Slot {selectedEntry.slot}: <strong>{selectedEntry.slotName || selectedEntry.file}</strong></>
                : <strong>{selectedSpriteFile}</strong>
              }
            </span>
            <button
              type="button"
              className="secondary-button danger-button"
              style={{ padding: "6px 16px", fontSize: "0.8rem" }}
              onClick={handleDeleteSpritesheet}
              disabled={isDeletingSprite}
            >
              {isDeletingSprite ? "Removing…" : "🗑 Remove Slot"}
            </button>
          </div>
        )}
        {spritesheetList.length === 0 && !loadingSheets && sheetId.trim() && (
          <p className="status-message">No spritesheet files found for <code>{sheetId}</code>.</p>
        )}
      </section>

      {/* Plist toolbar */}
      <div className="plist-upload-row">
        <label htmlFor="plist-upload" className="primary-button plist-upload-btn plist-toolbar-btn">
          {isUploadingPlists ? "Uploading..." : "Upload Plist Files"}
          <input
            id="plist-upload"
            ref={plistFileInputRef}
            type="file"
            accept=".plist"
            multiple
            className="upload-file-input"
            onChange={handleUploadPlists}
            disabled={isUploadingPlists}
          />
        </label>
        <button type="button" className="primary-button plist-toolbar-btn" onClick={scanForPlists} disabled={isScanning}>
          {isScanning ? "Scanning..." : "Scan"}
        </button>
        <button
          type="button"
          className="primary-button plist-toolbar-btn"
          onClick={handleClearPlists}
          disabled={isClearingPlists || plistFiles.length === 0}
        >
          {isClearingPlists ? "Clearing..." : "Clear Test Plists"}
        </button>
        {plistUploadResult && (
          <span className="upload-success">
            {plistUploadResult.cleared
              ? `Cleared: ${plistUploadResult.cleared.length} file${plistUploadResult.cleared.length !== 1 ? "s" : ""}`
              : `Uploaded: ${plistUploadResult.saved?.length || 0} file${(plistUploadResult.saved?.length || 0) !== 1 ? "s" : ""}`}
          </span>
        )}
      </div>

      {plistFiles.length === 0 && !error && !isScanning && (
        <div className="status-message">No plist files in <code>plist/test/</code>. Upload some and press Scan.</div>
      )}
      {error && <div className="status-message status-message-error">Error: {error}</div>}

      {plistFiles.length > 0 && (
        <section className="viewer-card">
          <div className="test-plist-nav">
            <button type="button" className="primary-button" onClick={prevPlist}>&#9650; Prev Plist</button>
            <div className="test-plist-badge">
              <span className="test-plist-filename">{currentPlistFile}</span>
              <span className="test-plist-count">{currentPlistIndex + 1} / {plistFiles.length}</span>
            </div>
            <button type="button" className="primary-button" onClick={nextPlist}>Next Plist &#9660;</button>
          </div>

          <div className="canvas-frame">
            {isLoadingPlist && (
              <div className="viewer-loading-overlay">
                <div className="viewer-spinner" aria-hidden="true" />
                <span>Loading...</span>
              </div>
            )}
            {!selectedSpriteFile && !isLoadingPlist && (
              <div className="viewer-loading-overlay">
                <span>Select a spritesheet above to preview</span>
              </div>
            )}
            <canvas ref={canvasRef} className="viewer-canvas" />
          </div>

          {expressionNames.length > 0 && (
            <>
              <div className="controls-row">
                <button type="button" className="primary-button" onClick={showPrev} disabled={isLoadingPlist}>Prev</button>
                <div className="expression-badge">
                  {expressionNames[expressionIndex]?.replace(".png", "")} ({expressionIndex + 1}/{expressionNames.length})
                </div>
                <button type="button" className="primary-button" onClick={showNext} disabled={isLoadingPlist}>Next</button>
              </div>
              <div className="expression-grid">
                {expressionNames.map((name, idx) => (
                  <button
                    key={name}
                    type="button"
                    className={idx === expressionIndex ? "expression-chip expression-chip-active" : "expression-chip"}
                    onClick={() => setExpressionIndex(idx)}
                    disabled={isLoadingPlist}
                  >
                    {name.replace(".png", "")}
                  </button>
                ))}
              </div>
            </>
          )}

          <div className="assign-row">
            <button
              type="button"
              className="primary-button assign-button"
              onClick={handleAssignClick}
              disabled={!selectedSpriteFile || !currentPlistFile || isLoadingPlist}
            >
              Assign This Plist
            </button>
            {selectedEntry && (
              <span className="assign-hint">
                {selectedEntry.hasPlist
                  ? `Will replace plist for Slot ${selectedEntry.slot}: ${selectedEntry.slotName}`
                  : selectedEntry.slot !== null
                    ? `Will create plist for Slot ${selectedEntry.slot}: ${selectedEntry.slotName}`
                    : "Will create a new sprite slot"}
              </span>
            )}
          </div>
        </section>
      )}

      <div className="test-help">
        <p><strong>Keyboard shortcuts</strong> (when not typing in an input):</p>
        <p>&#9650;&#9660; &mdash; cycle plists &nbsp;&nbsp; &#9664;&#9654; &mdash; cycle expressions</p>
      </div>

      {showAssignPanel && (
        <div className="assign-overlay" onClick={() => !isAssigning && closeAssignPanel()}>
          <div className="assign-panel" onClick={e => e.stopPropagation()}>
            {assignResult ? (
              <>
                <h2 className="assign-success">
                  &#10003; {assignResult.action === "created" ? "Character Created" : assignResult.action === "replaced" ? "Plist Replaced" : "Sprite Slot Added"}
                </h2>
                <p>Plist {assignResult.action === "replaced" ? "replaced at" : "moved to"} <strong>{assignResult.replacedFile || assignResult.newFileName}</strong></p>
                {assignResult.character && (
                  <p>ID: <strong>{assignResult.character.id}</strong> &bull; Sprites: {assignResult.character.spriteCount}</p>
                )}
                <div className="assign-actions">
                  <button type="button" className="primary-button" onClick={closeAssignPanel}>Close</button>
                </div>
              </>
            ) : assignMode === "replace" ? (
              <>
                <h2>Replace Plist</h2>
                <p>Overwrite the plist for <strong>Slot {selectedEntry.slot}: {selectedEntry.slotName}</strong> ({sheetCharacter.label})</p>
                <p>Source: <code>{currentPlistFile}</code> &rarr; <code>{selectedEntry.plistFile}</code></p>
                <div className="assign-actions">
                  <button type="button" className="primary-button" onClick={handleAssignConfirm} disabled={isAssigning}>
                    {isAssigning ? "Replacing..." : "Replace"}
                  </button>
                  <button type="button" className="secondary-button" onClick={closeAssignPanel} disabled={isAssigning}>Cancel</button>
                </div>
              </>
            ) : assignMode === "add" ? (
              <>
                <h2>Add Sprite Slot</h2>
                <p>Assign plist to <strong>{sheetCharacter.label}</strong> as a new sprite slot.</p>
                <p>Plist: <code>{currentPlistFile}</code></p>
                <label htmlFor="assign-sprite-input">Sprite name (e.g. Beach)</label>
                <input
                  id="assign-sprite-input"
                  type="text"
                  value={assignSpriteName}
                  onChange={e => setAssignSpriteName(e.target.value)}
                  placeholder="e.g. Beach"
                  autoFocus
                />
                <div className="assign-actions">
                  <button type="button" className="primary-button" onClick={handleAssignConfirm} disabled={!assignSpriteName.trim() || isAssigning}>
                    {isAssigning ? "Assigning..." : "Confirm"}
                  </button>
                  <button type="button" className="secondary-button" onClick={closeAssignPanel} disabled={isAssigning}>Cancel</button>
                </div>
              </>
            ) : (
              <>
                <h2>New Character</h2>
                <p>Creating character <strong>{sheetId.trim()}</strong> with this spritesheet as slot 0.</p>
                <p>Plist: <code>{currentPlistFile}</code> &rarr; <code>{sheetId.trim()}.plist</code></p>
                <label htmlFor="assign-label-input">Character label</label>
                <input
                  id="assign-label-input"
                  type="text"
                  value={assignLabel}
                  onChange={e => setAssignLabel(e.target.value)}
                  placeholder="e.g. Dark Dragon Knight"
                  autoFocus
                />
                <div className="assign-actions">
                  <button type="button" className="primary-button" onClick={handleAssignConfirm} disabled={!assignLabel.trim() || isAssigning}>
                    {isAssigning ? "Creating..." : "Confirm"}
                  </button>
                  <button type="button" className="secondary-button" onClick={closeAssignPanel} disabled={isAssigning}>Cancel</button>
                </div>
              </>
            )}
          </div>
        </div>
      )}
    </main>
  );
}

window.TestApp = TestApp;

