<script lang="ts">
  import * as THREE from "three";
  import Viewport from "./Viewport.svelte";
  import { CameraSyncManager } from "./cameraSyncManager.js";
  import { loadAsset } from "./assetLoader.js";
  import { computeAutoCamera, mergeBoxes } from "./boundingBox.js";
  import type { LoadedAsset } from "./assetLoader.js";
  import type { Sync3DCompareValue, AssetDescriptor, RenderMode, CameraState } from "../types";

  interface Props {
    value: Sync3DCompareValue | null;
    root: string;
    api_prefix: string;
    interactive: boolean;
    max_file_size: number | null;
    onchange?: (value: Sync3DCompareValue | null) => void;
  }

  let props: Props = $props();
  let value = $derived(props.value);
  let root = $derived(props.root);
  let api_prefix = $derived(props.api_prefix);
  let interactive = $derived(props.interactive);
  let max_file_size = $derived(props.max_file_size);
  let onchange = $derived(props.onchange);

  // Config from value or defaults
  let syncCamera = $derived(value?.sync_camera ?? true);
  let height = $derived(value?.height ?? 500);
  let maxViews = $derived(value?.max_views ?? 4);
  let pointSize = $state(2.0);
  let currentRenderMode = $state<RenderMode>("points");

  // Slot state
  let numSlots = $state(2);
  let slotAssets = $state<(AssetDescriptor | null)[]>([null, null]);
  let loadedAssets = $state<(LoadedAsset | null)[]>([null, null]);
  let slotLoading = $state<boolean[]>([false, false]);
  let slotErrors = $state<(string | null)[]>([null, null]);
  let slotDragOver = $state<boolean[]>([false, false]);

  // Camera & sync
  let syncManager = new CameraSyncManager();
  let initialCamera = $state<CameraState | null>(null);

  // Upload
  let uploadError = $state<string | null>(null);
  let fileInputs: HTMLInputElement[] = [];

  // Prevent feedback loop on dispatched values
  let lastDispatchedKey = "";

  // Slot count options: 2 .. maxViews
  let slotOptions = $derived(
    Array.from({ length: Math.max(maxViews - 1, 1) }, (_, i) => i + 2)
  );

  // ── Sync from backend value ──────────────────────────────────────────
  $effect(() => {
    if (value?.assets && value.assets.length > 0) {
      const key = value.assets.map(a => a.url || a.path).join("|");
      if (key !== lastDispatchedKey) {
        const n = value.assets.length;
        numSlots = n;
        slotAssets  = value.assets.map(a => ({ ...a }));
        loadedAssets = new Array(n).fill(null);
        slotLoading  = new Array(n).fill(false);
        slotErrors   = new Array(n).fill(null);
        slotDragOver = new Array(n).fill(false);
        if (value.point_size != null) pointSize = value.point_size;
        if (value.render_mode) currentRenderMode = value.render_mode;
        loadAllSlots();
      }
    }
  });

  $effect(() => {
    syncManager.setEnabled(syncCamera);
  });

  // ── Slot count ───────────────────────────────────────────────────────
  function setNumSlots(n: number) {
    if (n === numSlots) return;
    if (n > numSlots) {
      const extra = n - numSlots;
      slotAssets   = [...slotAssets,   ...new Array(extra).fill(null)];
      loadedAssets = [...loadedAssets,  ...new Array(extra).fill(null)];
      slotLoading  = [...slotLoading,  ...new Array(extra).fill(false)];
      slotErrors   = [...slotErrors,   ...new Array(extra).fill(null)];
      slotDragOver = [...slotDragOver,  ...new Array(extra).fill(false)];
    } else {
      slotAssets   = slotAssets.slice(0, n);
      loadedAssets = loadedAssets.slice(0, n);
      slotLoading  = slotLoading.slice(0, n);
      slotErrors   = slotErrors.slice(0, n);
      slotDragOver = slotDragOver.slice(0, n);
    }
    numSlots = n;
    recomputeCamera();
    dispatchChange();
  }

  // ── Loading ──────────────────────────────────────────────────────────
  async function loadSlot(i: number) {
    const asset = slotAssets[i];
    if (!asset) { loadedAssets[i] = null; return; }

    slotLoading[i] = true;
    slotErrors[i]  = null;
    try {
      loadedAssets[i] = await loadAsset(asset, currentRenderMode, pointSize);
    } catch {
      slotErrors[i]  = `Failed to load: ${asset.name || "asset"}`;
      loadedAssets[i] = null;
    } finally {
      slotLoading[i] = false;
    }
  }

  async function loadAllSlots() {
    await Promise.allSettled(slotAssets.map((_, i) => loadSlot(i)));
    recomputeCamera();
  }

  function recomputeCamera() {
    const boxes = loadedAssets
      .filter(la => la !== null)
      .map(la => la!.bounds);
    if (boxes.length > 0) {
      initialCamera = computeAutoCamera(mergeBoxes(boxes));
      syncManager.resetAll(initialCamera);
    }
  }

  function handleReset() {
    if (initialCamera) syncManager.resetAll(initialCamera);
  }

  async function handleRenderModeChange(mode: RenderMode) {
    currentRenderMode = mode;
    await loadAllSlots();
  }

  // ── Upload helpers ───────────────────────────────────────────────────
  function detectType(filename: string): "ply" | "glb" | null {
    const ext = filename.split(".").pop()?.toLowerCase();
    if (ext === "ply") return "ply";
    if (ext === "glb") return "glb";
    return null;
  }

  async function uploadFileToSlot(slotIndex: number, file: File) {
    const type = detectType(file.name);
    if (!type) {
      uploadError = `Unsupported file: ${file.name}. Only .ply and .glb are supported.`;
      return;
    }
    if (max_file_size && file.size > max_file_size) {
      uploadError = `File ${file.name} exceeds the maximum allowed size.`;
      return;
    }

    uploadError = null;
    slotLoading[slotIndex] = true;
    slotErrors[slotIndex]  = null;

    try {
      const formData = new FormData();
      formData.append("files", file);

      const resp = await fetch(`${root}${api_prefix}/upload`, {
        method: "POST",
        body: formData,
      });
      if (!resp.ok) throw new Error(`Upload failed: ${resp.statusText}`);

      const [serverPath] = await resp.json();

      slotAssets[slotIndex] = {
        name: file.name,
        path: serverPath,
        url: `${root}${api_prefix}/file=${serverPath}`,
        type,
        visible: true,
      };

      await loadSlot(slotIndex);
      recomputeCamera();
      dispatchChange();
    } catch (err: any) {
      slotErrors[slotIndex]  = err.message || "Upload failed";
      slotLoading[slotIndex] = false;
    }
  }

  function openFilePicker(index: number) {
    fileInputs[index]?.click();
  }

  function handleFileSelect(index: number, event: Event) {
    const input = event.target as HTMLInputElement;
    if (input.files?.[0]) {
      const file = input.files[0];
      input.value = "";
      uploadFileToSlot(index, file);
    }
  }

  function handleSlotDrop(index: number, event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
    slotDragOver[index] = false;
    if (event.dataTransfer?.files?.[0]) {
      uploadFileToSlot(index, event.dataTransfer.files[0]);
    }
  }

  function handleSlotDragOver(index: number, event: DragEvent) {
    event.preventDefault();
    slotDragOver[index] = true;
  }

  function handleSlotDragLeave(index: number) {
    slotDragOver[index] = false;
  }

  function removeFromSlot(index: number) {
    slotAssets[index]   = null;
    loadedAssets[index]  = null;
    slotLoading[index]  = false;
    slotErrors[index]   = null;
    recomputeCamera();
    dispatchChange();
  }

  // ── Dispatch value to parent ─────────────────────────────────────────
  function dispatchChange() {
    const filled = slotAssets.filter(a => a !== null) as AssetDescriptor[];
    lastDispatchedKey = filled.map(a => a.url || a.path).join("|");

    onchange?.(filled.length > 0 ? {
      assets: filled,
      render_mode: currentRenderMode,
      sync_camera: syncCamera,
      point_size: pointSize,
      height,
      max_views: maxViews,
    } : null);
  }
</script>

<div class="sync3d-root">
  <!-- Toolbar -->
  <div class="toolbar">
    {#if interactive}
      <div class="toolbar-group">
        <span class="toolbar-label">Viewports:</span>
        {#each slotOptions as n}
          <button
            class="toolbar-btn {numSlots === n ? 'active' : ''}"
            onclick={() => setNumSlots(n)}
          >{n}</button>
        {/each}
      </div>
      <div class="toolbar-sep"></div>
    {/if}

    <button class="toolbar-btn" onclick={handleReset} title="Reset cameras">&#x27F3; Reset</button>

    <div class="toolbar-group">
      <span class="toolbar-label">Mode:</span>
      <button
        class="toolbar-btn {currentRenderMode === 'points' ? 'active' : ''}"
        onclick={() => handleRenderModeChange('points')}
      >Points</button>
      <button
        class="toolbar-btn {currentRenderMode === 'native' ? 'active' : ''}"
        onclick={() => handleRenderModeChange('native')}
      >Native</button>
    </div>

    <div class="toolbar-group">
      <span class="toolbar-label">Size: {pointSize.toFixed(1)}</span>
      <input
        type="range" min="0.5" max="10" step="0.5"
        bind:value={pointSize}
        class="point-slider"
      />
    </div>
  </div>

  {#if uploadError}
    <div class="error-banner">{uploadError}</div>
  {/if}

  <!-- Slot row -->
  <div class="slots-row" style="height: {height}px;">
    {#each {length: numSlots} as _, i}
      <div class="slot">
        {#if slotAssets[i]}
          <!-- ── filled slot ── -->
          {#if interactive}
            <button class="remove-btn" onclick={() => removeFromSlot(i)} title="Remove">&#x2715;</button>
          {/if}
          <Viewport
            asset={loadedAssets[i] ?? null}
            label={slotAssets[i]?.name ?? `Asset ${i + 1}`}
            {height}
            {syncManager}
            {initialCamera}
            {pointSize}
            loading={slotLoading[i] ?? false}
            error={slotErrors[i] ?? null}
          />
        {:else if interactive}
          <!-- ── empty slot: upload zone ── -->
          <input
            bind:this={fileInputs[i]}
            type="file"
            style="display: none;"
            onchange={(e) => handleFileSelect(i, e)}
          />
          <div
            class="slot-upload"
            class:drag-over={slotDragOver[i]}
            ondragover={(e) => handleSlotDragOver(i, e)}
            ondragleave={() => handleSlotDragLeave(i)}
            ondrop={(e) => handleSlotDrop(i, e)}
            onclick={() => openFilePicker(i)}
            onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePicker(i); } }}
            role="button"
            tabindex="0"
          >
            {#if slotLoading[i]}
              <div class="slot-status">
                <div class="spinner"></div>
                <span>Uploading&#x2026;</span>
              </div>
            {:else if slotErrors[i]}
              <div class="slot-err">
                <span>{slotErrors[i]}</span>
                <span class="hint">Click to retry</span>
              </div>
            {:else}
              <svg class="slot-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32">
                <path d="M12 16V4m0 0L8 8m4-4l4 4" stroke-linecap="round" stroke-linejoin="round"/>
                <path d="M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
              <span class="slot-label">Upload</span>
              <span class="hint">.ply / .glb</span>
            {/if}
          </div>
        {:else}
          <!-- ── non-interactive empty ── -->
          <div class="slot-empty">
            <span>No asset</span>
          </div>
        {/if}
      </div>
    {/each}
  </div>
</div>

<style>
  .sync3d-root {
    display: flex;
    flex-direction: column;
    gap: 8px;
    font-family: system-ui, sans-serif;
  }

  /* ── toolbar ── */
  .toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 10px;
    background: #111827;
    border-radius: 6px;
    flex-wrap: wrap;
  }
  .toolbar-group {
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .toolbar-sep {
    width: 1px;
    height: 18px;
    background: #374151;
  }
  .toolbar-label {
    font-size: 12px;
    color: #9ca3af;
    white-space: nowrap;
  }
  .toolbar-btn {
    background: #1f2937;
    color: #d1d5db;
    border: 1px solid #374151;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 12px;
    cursor: pointer;
    transition: background 0.15s;
  }
  .toolbar-btn:hover { background: #374151; }
  .toolbar-btn.active {
    background: #2563eb;
    border-color: #3b82f6;
    color: #fff;
  }
  .point-slider {
    width: 100px;
    accent-color: #3b82f6;
  }

  .error-banner {
    color: #f87171;
    font-size: 12px;
    padding: 2px 10px;
  }

  /* ── slot row ── */
  .slots-row {
    display: flex;
    gap: 6px;
    overflow: hidden;
  }
  .slot {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    display: flex;
  }

  /* remove button */
  .remove-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 30;
    background: rgba(0,0,0,.6);
    color: #f87171;
    border: 1px solid rgba(239,68,68,.3);
    border-radius: 50%;
    width: 22px; height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 10px;
    padding: 0;
    transition: background .15s;
  }
  .remove-btn:hover {
    background: rgba(239,68,68,.8);
    color: #fff;
  }

  /* ── upload zone inside a slot ── */
  .slot-upload {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: #1a1a2e;
    border: 2px dashed #374151;
    border-radius: 6px;
    cursor: pointer;
    color: #6b7280;
    transition: border-color .2s, background .2s;
    user-select: none;
  }
  .slot-upload:hover,
  .slot-upload.drag-over {
    border-color: #3b82f6;
    background: #1e293b;
    color: #9ca3af;
  }
  .slot-icon { opacity: .4; }
  .slot-upload:hover .slot-icon,
  .slot-upload.drag-over .slot-icon { opacity: .7; }
  .slot-label {
    font-size: 13px;
    font-weight: 500;
  }
  .hint {
    font-size: 11px;
    opacity: .6;
  }

  .slot-status {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    color: #60a5fa;
    font-size: 12px;
  }
  .slot-err {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    color: #f87171;
    font-size: 12px;
    text-align: center;
    padding: 0 16px;
  }

  .slot-empty {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1a1a2e;
    border: 1px solid #374151;
    border-radius: 6px;
    color: #4b5563;
    font-size: 12px;
  }

  .spinner {
    width: 24px; height: 24px;
    border: 3px solid #444;
    border-top-color: #7eb8f7;
    border-radius: 50%;
    animation: spin .8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
</style>
