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

  interface Props {
    repoFiles: GitRepositoryFile[];
    expandedExplorerPaths: Set<string>;
    selectedExplorerPath: string;
    selectedExplorerKind: ExplorerNodeKind;
    hasRepository: boolean;
    isBusy: boolean;
    language?: "en" | "de";
    editorName?: string;
    diffName?: string;
    onToggleFolder: (node: ExplorerNode) => void;
    onExpandAllFolders: () => void;
    onCollapseAllFolders: () => void;
    onSelectNode: (node: ExplorerNode) => void;
    onOpenFile: (node: ExplorerNode) => void;
    onOpenInEditor: (node: ExplorerNode) => void;
    onExternalDiff: (node: ExplorerNode) => void;
    onFileHistory: (node: ExplorerNode) => void;
    onBlame: (node: ExplorerNode) => void;
    onIgnore: (target: string, kind: GitIgnoreKind) => void;
    onStopTracking: (target: string, kind: "file" | "folder") => void;
    collapsed?: boolean;
    onToggleCollapsed?: () => void;
  }

  let {
    repoFiles = [],
    expandedExplorerPaths = new Set(),
    selectedExplorerPath = "",
    selectedExplorerKind = "file",
    hasRepository = false,
    isBusy = false,
    language = "en",
    editorName = "Editor",
    diffName = "diff tool",
    onToggleFolder = () => {},
    onExpandAllFolders = () => {},
    onCollapseAllFolders = () => {},
    onSelectNode = () => {},
    onOpenFile = () => {},
    onOpenInEditor = () => {},
    onExternalDiff = () => {},
    onFileHistory = () => {},
    onBlame = () => {},
    onIgnore = () => {},
    onStopTracking = () => {},
    collapsed = false,
    onToggleCollapsed = () => {},
  }: Props = $props();

  let contextNode = $state<ExplorerNode | null>(null);
  let contextMenuX = $state(0);
  let contextMenuY = $state(0);
  let contextMenuElement = $state<HTMLDivElement | null>(null);
  const isGerman = $derived(language === "de");

  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 openNodeContextMenu(event: MouseEvent, node: ExplorerNode) {
    event.preventDefault();
    event.stopPropagation();

    contextNode = node;
    contextMenuX = Math.max(8, Math.min(event.clientX + 2, window.innerWidth - 248));
    contextMenuY = Math.max(8, Math.min(event.clientY + 2, window.innerHeight - 320));
    requestAnimationFrame(() => {
      if (!contextMenuElement) return;
      const bounds = contextMenuElement.getBoundingClientRect();
      contextMenuX = Math.max(8, Math.min(event.clientX + 2, window.innerWidth - bounds.width - 8));
      contextMenuY = Math.max(8, Math.min(event.clientY + 2, window.innerHeight - bounds.height - 8));
    });
  }

  function closeFileContextMenu() {
    contextNode = null;
  }

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

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

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

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

  function openContextFileHistory() {
    const node = contextNode;
    if (!node || node.kind !== "file" || !node.tracked) return;
    closeFileContextMenu();
    onFileHistory(node);
  }

  function explorerFileNodes(node: ExplorerNode): ExplorerNode[] {
    if (node.kind === "file") return [node];
    return node.children.flatMap(explorerFileNodes);
  }

  function isIgnoreableExplorerFile(node: ExplorerNode): boolean {
    return node.kind === "file" && !node.tracked && node.path.replace(/\\/g, "/").toLowerCase() !== ".gitignore";
  }

  function runContextIgnore(kind: GitIgnoreKind) {
    const node = contextNode;
    if (!node) return;
    if (kind === "folder" && node.kind !== "folder") return;
    if ((kind === "file" || kind === "extension") && node.kind !== "file") return;
    closeFileContextMenu();
    onIgnore(node.path, kind);
  }

  function runContextStopTracking() {
    const node = contextNode;
    if (!node) return;
    closeFileContextMenu();
    onStopTracking(node.path, node.kind);
  }

  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"));
  let contextFiles = $derived(contextNode ? explorerFileNodes(contextNode) : []);
  let contextCanIgnore = $derived(contextFiles.some(isIgnoreableExplorerFile));
  let contextCanStopTracking = $derived(contextFiles.some((node) => node.tracked));
  let contextIgnoreExtension = $derived(contextNode?.kind === "file" && contextCanIgnore ? extensionFor(contextNode.path) : "");
  let selectedFileNode = $derived(
    selectedExplorerKind === "file"
      ? visibleNodes.find((node) => node.kind === "file" && node.path === selectedExplorerPath) ?? null
      : null,
  );
</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 explorer-tool-action"
        type="button"
        onclick={() => selectedFileNode && onOpenInEditor(selectedFileNode)}
        disabled={isBusy || !selectedFileNode || selectedFileNode.status === "deleted"}
        title={isGerman
          ? `Ausgewählte Datei in ${editorName} öffnen`
          : `Open selected file in ${editorName}`}
        aria-label={isGerman
          ? `Ausgewählte Datei in ${editorName} öffnen`
          : `Open selected file in ${editorName}`}
      >
        <FileCode size={14} aria-hidden="true" />
      </button>
      <button
        class="explorer-bulk-button explorer-tool-action"
        type="button"
        onclick={() => selectedFileNode && onExternalDiff(selectedFileNode)}
        disabled={isBusy || !selectedFileNode || !selectedFileNode.tracked || selectedFileNode.status === "deleted"}
        title={isGerman
          ? `Ausgewählte Datei mit HEAD in ${diffName} vergleichen`
          : `Compare selected file with HEAD in ${diffName}`}
        aria-label={isGerman
          ? `Ausgewählte Datei mit HEAD in ${diffName} vergleichen`
          : `Compare selected file with HEAD in ${diffName}`}
      >
        <GitCompare size={14} aria-hidden="true" />
      </button>
      <span class="explorer-action-divider" aria-hidden="true"></span>
      <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}`)}
        <!-- svelte-ignore a11y_no_static_element_interactions -->
        <div
          class="explorer-row"
          class:active={selectedExplorerPath === node.path && selectedExplorerKind === node.kind}
          class:folder={node.kind === "folder"}
          style={`--depth: ${node.depth}`}
          title={node.path}
          oncontextmenu={(event) => openNodeContextMenu(event, node)}
        >
          {#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)}
            disabled={isBusy}
            title={`Select ${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
    bind:this={contextMenuElement}
    class="explorer-context-menu"
    style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
    role="menu"
    tabindex="-1"
    aria-label={`Actions for ${contextNode.path}`}
  >
    {#if contextNode.kind === "file"}
      <button type="button" role="menuitem" onclick={openContextFileInEditor} disabled={contextNode.status === "deleted"}>
        <FileCode size={14} aria-hidden="true" />
        {isGerman ? `In ${editorName} öffnen` : `Open in ${editorName}`}
      </button>
      <button type="button" role="menuitem" onclick={openContextExternalDiff} disabled={!contextNode.tracked || contextNode.status === "deleted"}>
        <GitCompare size={14} aria-hidden="true" />
        {isGerman ? `Mit HEAD in ${diffName} vergleichen` : `Compare with HEAD in ${diffName}`}
      </button>
      <button type="button" role="menuitem" onclick={openContextFileHistory} disabled={!contextNode.tracked} title={contextNode.tracked ? "Show the commit history for this file" : "File history is only available for tracked files"}>
        <History size={14} aria-hidden="true" />
        {isGerman ? "Dateiverlauf" : "File history"}
      </button>
      <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" />
        {isGerman ? "Im Explorer öffnen" : "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"}>
        <FileSearch size={14} aria-hidden="true" />
        Blame
      </button>
    {/if}
    {#if contextCanStopTracking || contextCanIgnore}
      <div class="menu-separator" role="separator"></div>
    {/if}
    {#if contextCanStopTracking}
      <button class="untrack" type="button" role="menuitem" onclick={runContextStopTracking} disabled={isBusy} title="Keep the working-tree content and remove it from the Git index">
        {#if contextNode.kind === "folder"}<FolderMinus size={14} aria-hidden="true" />{:else}<FileMinus2 size={14} aria-hidden="true" />{/if}
        {isGerman ? `${contextNode.kind === "folder" ? "Ordner" : "Datei"} nicht mehr tracken` : `Stop tracking ${contextNode.kind}`}
      </button>
    {/if}
    {#if contextCanIgnore && contextNode.kind === "file"}
      <button class="ignore" type="button" role="menuitem" onclick={() => runContextIgnore("file")} disabled={isBusy} title={`Add /${contextNode.path.replace(/\\/g, "/")} to .gitignore`}>
        <FileX size={14} aria-hidden="true" />
        {isGerman ? "Datei ignorieren" : "Ignore file"}
      </button>
      {#if contextIgnoreExtension}
        <button class="ignore" type="button" role="menuitem" onclick={() => runContextIgnore("extension")} disabled={isBusy} title={`Add *.${contextIgnoreExtension} to .gitignore`}>
          <FileType size={14} aria-hidden="true" />
          {isGerman ? `Alle *.${contextIgnoreExtension}-Dateien ignorieren` : `Ignore all *.${contextIgnoreExtension} files`}
        </button>
      {/if}
    {:else if contextCanIgnore && contextNode.kind === "folder"}
      <button class="ignore" type="button" role="menuitem" onclick={() => runContextIgnore("folder")} disabled={isBusy} title={`Add /${contextNode.path.replace(/\\/g, "/").replace(/\/+$/, "")}/ to .gitignore`}>
        <FolderX size={14} aria-hidden="true" />
        {isGerman ? "Ordner ignorieren" : "Ignore folder"}
      </button>
    {/if}
  </div>
{/if}
