<script lang="ts">
  import {
    Braces,
    ChevronDown,
    ChevronRight,
    CodeXml,
    Database,
    FileArchive,
    FileAudio,
    FileCode,
    FileCog,
    FileImage,
    FileJson,
    FileSpreadsheet,
    FileText,
    FileType,
    FileVideo,
    Folder,
    FolderOpen,
    ExternalLink,
    History,
    Terminal,
  } from "@lucide/svelte";
  import { languageIconForPath } from "../languageIcons";
  import type { ExplorerNode, ExplorerNodeKind, FileStatusKind, GitRepositoryFile } from "../types";
  import LanguageIcon from "./LanguageIcon.svelte";

  interface Props {
    repoFiles: GitRepositoryFile[];
    expandedExplorerPaths: Set<string>;
    selectedExplorerPath: string;
    selectedExplorerKind: ExplorerNodeKind;
    hasRepository: boolean;
    isBusy: boolean;
    onToggleFolder: (node: ExplorerNode) => void;
    onExpandAllFolders: () => void;
    onCollapseAllFolders: () => void;
    onSelectNode: (node: ExplorerNode) => void;
    onOpenFile: (node: ExplorerNode) => void;
    onBlame: (node: ExplorerNode) => void;
    collapsed?: boolean;
    onToggleCollapsed?: () => void;
  }

  let {
    repoFiles = [],
    expandedExplorerPaths = new Set(),
    selectedExplorerPath = "",
    selectedExplorerKind = "file",
    hasRepository = false,
    isBusy = false,
    onToggleFolder = () => {},
    onExpandAllFolders = () => {},
    onCollapseAllFolders = () => {},
    onSelectNode = () => {},
    onOpenFile = () => {},
    onBlame = () => {},
    collapsed = false,
    onToggleCollapsed = () => {},
  }: Props = $props();

  let contextNode = $state<ExplorerNode | null>(null);
  let contextMenuX = $state(0);
  let contextMenuY = $state(0);

  function mergeExplorerStatus(current: FileStatusKind | null, next: FileStatusKind | null): FileStatusKind | null {
    if (!next) return current;
    if (!current) return next;
    const priority: FileStatusKind[] = ["conflicted", "modified", "renamed", "deleted", "added", "untracked", "unknown"];
    return priority.indexOf(next) < priority.indexOf(current) ? next : current;
  }

  function sortExplorerNodes(nodes: ExplorerNode[]) {
    nodes.sort((a, b) => {
      if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
      return a.name.localeCompare(b.name);
    });
    for (const node of nodes) sortExplorerNodes(node.children);
  }

  function buildExplorerTree(files: GitRepositoryFile[]): ExplorerNode[] {
    const roots: ExplorerNode[] = [];
    const folders = new Map<string, ExplorerNode>();

    for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
      const parts = file.path.split(/[\\/]+/).filter(Boolean);
      let currentPath = "";
      let siblings = roots;
      let ancestors: ExplorerNode[] = [];

      for (let i = 0; i < parts.length - 1; i++) {
        currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
        let folderNode = folders.get(currentPath);
        if (!folderNode) {
          folderNode = { name: parts[i], path: currentPath, kind: "folder", status: null, tracked: true, depth: i, children: [] };
          folders.set(currentPath, folderNode);
          siblings.push(folderNode);
        }
        ancestors = [...ancestors, folderNode];
        siblings = folderNode.children;
      }

      const fileNode: ExplorerNode = {
        name: parts[parts.length - 1] ?? file.path,
        path: file.path,
        kind: "file",
        status: file.status,
        tracked: file.tracked,
        depth: Math.max(parts.length - 1, 0),
        children: [],
      };
      siblings.push(fileNode);

      for (const folderNode of ancestors) {
        folderNode.status = mergeExplorerStatus(folderNode.status, file.status);
        folderNode.tracked = folderNode.tracked && file.tracked;
      }
    }

    sortExplorerNodes(roots);
    return roots;
  }

  function flattenExplorerTree(nodes: ExplorerNode[], expanded: Set<string>): ExplorerNode[] {
    const visible: ExplorerNode[] = [];
    for (const node of nodes) {
      visible.push(node);
      if (node.kind === "folder" && expanded.has(node.path)) {
        visible.push(...flattenExplorerTree(node.children, expanded));
      }
    }
    return visible;
  }

  function statusLabel(kind: FileStatusKind | null): string {
    return kind ?? "none";
  }

  function extensionFor(path: string): string {
    const name = path.split(/[\\/]/).pop()?.toLowerCase() ?? "";
    const index = name.lastIndexOf(".");
    return index > 0 ? name.slice(index + 1) : "";
  }

  function fileIconKind(node: ExplorerNode): string {
    const name = node.name.toLowerCase();
    const ext = extensionFor(node.path);

    if (["package.json", "tsconfig.json", "jsconfig.json", "composer.json", "deno.json", "tauri.conf.json"].includes(name)) return "json";
    if (["cargo.toml", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"].includes(name)) return "config";
    if (["dockerfile", "makefile", "justfile", "rakefile", "gemfile", ".env", ".gitignore", ".gitattributes", ".npmrc", ".editorconfig"].includes(name)) return "config";

    if (["js", "jsx", "ts", "tsx", "svelte", "vue", "astro", "rs", "go", "java", "kt", "kts", "cs", "cpp", "cxx", "cc", "c", "h", "hpp", "swift", "php", "rb", "py", "lua", "dart", "scala", "zig", "ex", "exs", "erl", "hrl", "fs", "fsx", "fsi", "clj", "cljs"].includes(ext)) return "code";
    if (["html", "htm", "xml", "xaml", "svg"].includes(ext)) return "markup";
    if (["css", "scss", "sass", "less", "postcss"].includes(ext)) return "style";
    if (["json", "jsonc", "json5"].includes(ext)) return "json";
    if (["toml", "yaml", "yml", "ini", "conf", "config", "properties", "env"].includes(ext)) return "config";
    if (["sh", "bash", "zsh", "fish", "ps1", "bat", "cmd"].includes(ext)) return "script";
    if (["md", "mdx", "txt", "log", "rst", "adoc", "tex"].includes(ext)) return "text";
    if (["png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "avif", "tif", "tiff"].includes(ext)) return "image";
    if (["mp3", "wav", "ogg", "flac", "m4a", "aac"].includes(ext)) return "audio";
    if (["mp4", "mov", "avi", "mkv", "webm", "wmv"].includes(ext)) return "video";
    if (["zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "xz"].includes(ext)) return "archive";
    if (["csv", "tsv", "xls", "xlsx", "ods"].includes(ext)) return "sheet";
    if (["sql", "sqlite", "sqlite3", "db"].includes(ext)) return "database";
    if (["ttf", "otf", "woff", "woff2", "eot"].includes(ext)) return "font";

    return "text";
  }

  function openFileContextMenu(event: MouseEvent, node: ExplorerNode) {
    if (node.kind !== "file") return;
    event.preventDefault();
    event.stopPropagation();

    contextNode = node;
    contextMenuX = Math.max(8, Math.min(event.clientX + 2, window.innerWidth - 192));
    contextMenuY = Math.max(8, Math.min(event.clientY + 2, window.innerHeight - 56));
  }

  function closeFileContextMenu() {
    contextNode = null;
  }

  function openContextFile() {
    const node = contextNode;
    if (!node || node.kind !== "file") return;
    closeFileContextMenu();
    onOpenFile(node);
  }

  function openContextBlame() {
    const node = contextNode;
    if (!node || node.kind !== "file") return;
    closeFileContextMenu();
    onBlame(node);
  }

  function handleWindowKeydown(event: KeyboardEvent) {
    if (event.key === "Escape") closeFileContextMenu();
  }

  let explorerTree = $derived(buildExplorerTree(repoFiles));
  let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths));
  let hasFolders = $derived(explorerTree.some((node) => node.kind === "folder"));
</script>

<svelte:window on:click={closeFileContextMenu} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeFileContextMenu} />

<section class="panel explorer-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="File explorer">
  <div class="section-head">
    <div>
      <span class="eyebrow">Explorer</span>
      <h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Files</h2>
    </div>
    <div class="explorer-head-actions">
      <button
        class="explorer-bulk-button"
        type="button"
        onclick={onExpandAllFolders}
        disabled={isBusy || !hasRepository || !hasFolders}
        title="Alle Ordner aufklappen"
        aria-label="Alle Ordner aufklappen"
      >
        <FolderOpen size={14} aria-hidden="true" />
      </button>
      <button
        class="explorer-bulk-button"
        type="button"
        onclick={onCollapseAllFolders}
        disabled={isBusy || !hasRepository || !hasFolders || expandedExplorerPaths.size === 0}
        title="Alle Ordner zuklappen"
        aria-label="Alle Ordner zuklappen"
      >
        <Folder size={14} aria-hidden="true" />
      </button>
      <span class="pill pill-count">{repoFiles.length}</span>
      <button
        class="explorer-bulk-button panel-collapse-toggle"
        type="button"
        onclick={onToggleCollapsed}
        aria-expanded={!collapsed}
        title={collapsed ? "Expand file explorer" : "Collapse file explorer"}
        aria-label={collapsed ? "Expand file explorer panel" : "Collapse file explorer panel"}
      >
        {#if collapsed}
          <ChevronRight size={14} aria-hidden="true" />
        {:else}
          <ChevronDown size={14} aria-hidden="true" />
        {/if}
      </button>
    </div>
  </div>

  {#if collapsed}
    <!-- collapsed -->
  {:else if !hasRepository}
    <p class="blank-state">Open a repository to browse files.</p>
  {:else if repoFiles.length === 0}
    <p class="blank-state">No files returned.</p>
  {:else}
    <div class="explorer-list overflow-auto p-2">
      {#each visibleNodes as node (`${node.kind}:${node.path}`)}
        <div
          class="explorer-row"
          class:active={selectedExplorerPath === node.path && selectedExplorerKind === node.kind}
          class:folder={node.kind === "folder"}
          style={`--depth: ${node.depth}`}
          title={node.path}
        >
          {#if node.kind === "folder"}
            <button
              class="tree-toggle"
              type="button"
              onclick={() => onToggleFolder(node)}
              disabled={isBusy}
              title={expandedExplorerPaths.has(node.path) ? "Collapse folder" : "Expand folder"}
            >
              {#if expandedExplorerPaths.has(node.path)}
                <ChevronDown size={14} aria-hidden="true" />
              {:else}
                <ChevronRight size={14} aria-hidden="true" />
              {/if}
            </button>
            {#if expandedExplorerPaths.has(node.path)}
              <FolderOpen size={15} aria-hidden="true" />
            {:else}
              <Folder size={15} aria-hidden="true" />
            {/if}
          {:else}
            <span class="tree-spacer"></span>
            {@const languageIcon = languageIconForPath(node.path)}
            {@const iconKind = fileIconKind(node)}
            {#if languageIcon}
              <LanguageIcon icon={languageIcon.icon} title={languageIcon.title} />
            {:else if iconKind === "code"}
              <FileCode class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "markup"}
              <CodeXml class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "style"}
              <Braces class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "json"}
              <FileJson class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "config"}
              <FileCog class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "script"}
              <Terminal class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "image"}
              <FileImage class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "audio"}
              <FileAudio class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "video"}
              <FileVideo class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "archive"}
              <FileArchive class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "sheet"}
              <FileSpreadsheet class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "database"}
              <Database class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else if iconKind === "font"}
              <FileType class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {:else}
              <FileText class={`file-icon ${iconKind}`} size={15} aria-hidden="true" />
            {/if}
          {/if}

          <button
            class="explorer-select"
            type="button"
            onclick={() => onSelectNode(node)}
            oncontextmenu={(event) => openFileContextMenu(event, node)}
            disabled={isBusy}
            title={`Show history for ${node.path}`}
          >
            <span>{node.name}</span>
          </button>

          {#if node.status}
            <small class={`status-badge ${node.status}`}>{statusLabel(node.status)}</small>
          {:else if !node.tracked}
            <small class="status-badge untracked">untracked</small>
          {/if}
        </div>
      {/each}
    </div>
  {/if}

</section>

{#if contextNode}
  <div
    class="explorer-context-menu"
    style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
    role="menu"
    tabindex="-1"
    aria-label={`Actions for ${contextNode.path}`}
  >
    <button
      type="button"
      role="menuitem"
      onclick={openContextFile}
      disabled={contextNode.status === "deleted"}
      title={contextNode.status === "deleted" ? "Deleted files cannot be revealed in Explorer" : "Reveal this file in Explorer"}
    >
      <ExternalLink size={14} aria-hidden="true" />
      Open in Explorer
    </button>
    <button
      type="button"
      role="menuitem"
      onclick={openContextBlame}
      disabled={!contextNode.tracked || contextNode.status === "deleted"}
      title={!contextNode.tracked || contextNode.status === "deleted" ? "Blame is only available for tracked files" : "Show blame for this file"}
    >
      <History size={14} aria-hidden="true" />
      Blame
    </button>
  </div>
{/if}
