import { Folder, FolderOpen } from "lucide-react";
import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
import { i18nMessage } from "../i18n/LanguageContext";

export type ExplorerTreeNodeContext = {
  depth: number;
  active: boolean;
  expanded: boolean;
  hasChildren: boolean;
  disabled: boolean;
};

type ExplorerTreeCommonProps<T> = {
  nodes: T[];
  getNodeId: (node: T) => string;
  getNodeLabel: (node: T) => string;
  getNodeChildren: (node: T) => T[];
  activeId?: string;
  onOpen: (node: T, context: ExplorerTreeNodeContext) => void;
  disabled?: boolean;
  depth?: number;
  className?: string;
  childrenBaseClassName?: string;
  nodeContainerClassName?: string;
  nodeWrapBaseClassName?: string;
  toggleBaseClassName?: string;
  nodeButtonBaseClassName?: string;
  renderToggleIcon?: (node: T, context: ExplorerTreeNodeContext) => ReactNode;
  renderNodeContent?: (node: T, context: ExplorerTreeNodeContext) => ReactNode;
  renderNodeActions?: (node: T, context: ExplorerTreeNodeContext) => ReactNode;
  getNodeWrapClassName?: (node: T, context: ExplorerTreeNodeContext) => string | undefined;
  getNodeWrapStyle?: (node: T, context: ExplorerTreeNodeContext) => CSSProperties | undefined;
  getNodeButtonClassName?: (node: T, context: ExplorerTreeNodeContext) => string | undefined;
  getNodeDraggable?: (node: T, context: ExplorerTreeNodeContext) => boolean;
  onContextMenu?: (event: ReactMouseEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
  onDragStart?: (event: ReactDragEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
  onDragEnd?: (event: ReactDragEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
  onDragOver?: (event: ReactDragEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
  onDragLeave?: (event: ReactDragEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
  onDrop?: (event: ReactDragEvent<HTMLElement>, node: T, context: ExplorerTreeNodeContext) => void;
};

type CollapsibleExplorerTreeProps<T> = {
  collapsible?: true;
  expandedIds: ReadonlySet<string>;
  onToggle: (node: T, context: ExplorerTreeNodeContext) => void;
};

type StaticExplorerTreeProps = {
  collapsible: false;
  expandedIds?: never;
  onToggle?: never;
};

export type ExplorerTreeProps<T> = ExplorerTreeCommonProps<T> & (
  CollapsibleExplorerTreeProps<T> | StaticExplorerTreeProps
);

type SafeNode<T> = {
  id: string;
  node: T;
};

const EMPTY_EXPANDED_IDS: ReadonlySet<string> = new Set();

export default function ExplorerTree<T>(props: ExplorerTreeProps<T>) {
  const {
    nodes,
    getNodeId,
    getNodeLabel,
    getNodeChildren,
    activeId = "",
    onOpen,
    disabled = false,
    depth = 1,
    className = "",
    childrenBaseClassName = "explorer-tree-children file-tree-children",
    nodeContainerClassName = "",
    nodeWrapBaseClassName = "explorer-tree-node-wrap file-tree-node-wrap",
    toggleBaseClassName = "explorer-tree-toggle file-tree-toggle",
    nodeButtonBaseClassName = "explorer-tree-node file-tree-node",
    renderToggleIcon,
    renderNodeContent,
    renderNodeActions,
    getNodeWrapClassName,
    getNodeWrapStyle,
    getNodeButtonClassName,
    getNodeDraggable,
    onContextMenu,
    onDragStart,
    onDragEnd,
    onDragOver,
    onDragLeave,
    onDrop
  } = props;
  const collapsible = props.collapsible !== false;
  const expandedIds = props.collapsible === false ? EMPTY_EXPANDED_IDS : props.expandedIds;
  const onToggle = props.collapsible === false ? undefined : props.onToggle;

  function pathSafeNodes(levelNodes: T[], ancestorIds: ReadonlySet<string>): SafeNode<T>[] {
    const siblingIds = new Set<string>();
    const safeNodes: SafeNode<T>[] = [];
    for (const node of levelNodes) {
      const id = getNodeId(node);
      if (ancestorIds.has(id) || siblingIds.has(id)) continue;
      siblingIds.add(id);
      safeNodes.push({ id, node });
    }
    return safeNodes;
  }

  function renderLevel(levelNodes: T[], currentDepth: number, ancestorIds: ReadonlySet<string>, root = false): ReactNode {
    const safeNodes = pathSafeNodes(levelNodes, ancestorIds);
    if (safeNodes.length === 0) return null;

    return (
      <div className={[childrenBaseClassName, root ? className : ""].filter(Boolean).join(" ")}>
        {safeNodes.map(({ id: nodeId, node }) => {
          const nextAncestorIds = new Set(ancestorIds);
          nextAncestorIds.add(nodeId);
          const children = getNodeChildren(node);
          const hasChildren = pathSafeNodes(children, nextAncestorIds).length > 0;
          const expanded = collapsible ? expandedIds.has(nodeId) : hasChildren;
          const active = activeId === nodeId;
          const context: ExplorerTreeNodeContext = { depth: currentDepth, active, expanded, hasChildren, disabled };
          const draggable = getNodeDraggable?.(node, context) ?? false;
          const nodeActions = renderNodeActions?.(node, context);
          const hasNodeActions = nodeActions !== null && nodeActions !== undefined && nodeActions !== false;

          return (
            <div key={nodeId} className={nodeContainerClassName}>
              <div
                className={[
                  nodeWrapBaseClassName,
                  active ? "is-active" : "",
                  hasNodeActions ? "has-actions" : "",
                  getNodeWrapClassName?.(node, context) ?? ""
                ].filter(Boolean).join(" ")}
                style={getNodeWrapStyle?.(node, context) ?? { paddingLeft: `${Math.min(currentDepth * 14, 56)}px` }}
                draggable={draggable && !disabled}
                onContextMenu={onContextMenu ? (event) => onContextMenu(event, node, context) : undefined}
                onDragStart={draggable && onDragStart ? (event) => onDragStart(event, node, context) : undefined}
                onDragEnd={draggable && onDragEnd ? (event) => onDragEnd(event, node, context) : undefined}
                onDragOver={onDragOver ? (event) => onDragOver(event, node, context) : undefined}
                onDragLeave={onDragLeave ? (event) => onDragLeave(event, node, context) : undefined}
                onDrop={onDrop ? (event) => onDrop(event, node, context) : undefined}
              >
                {collapsible ? (
                  <button
                    type="button"
                    className={toggleBaseClassName}
                    onClick={(event) => {
                      event.stopPropagation();
                      if (hasChildren) onToggle?.(node, context);
                    }}
                    disabled={disabled || !hasChildren}
                    aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", {
                      value0: expanded ? "i18n:govoplan-core.collapse.9cf188d3" : "i18n:govoplan-core.expand.9869e506",
                      value1: getNodeLabel(node)
                    })}
                    aria-expanded={hasChildren ? expanded : undefined}
                  >
                    {renderToggleIcon?.(node, context) ?? (expanded ? <FolderOpen size={15} aria-hidden="true" /> : <Folder size={15} aria-hidden="true" />)}
                  </button>
                ) : (
                  <span className={`${toggleBaseClassName} explorer-tree-toggle-static`} aria-hidden="true">
                    {renderToggleIcon?.(node, context) ?? (hasChildren ? <FolderOpen size={15} aria-hidden="true" /> : <Folder size={15} aria-hidden="true" />)}
                  </span>
                )}
                <button
                  type="button"
                  className={[nodeButtonBaseClassName, getNodeButtonClassName?.(node, context) ?? ""].filter(Boolean).join(" ")}
                  onClick={() => onOpen(node, context)}
                  disabled={disabled}
                  aria-current={active ? "true" : undefined}
                >
                  {renderNodeContent?.(node, context) ?? <span>{getNodeLabel(node)}</span>}
                </button>
                {hasNodeActions && (
                  <div
                    className="explorer-tree-actions"
                    role="group"
                    aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", {
                      value0: "i18n:govoplan-core.actions.c3cd636a",
                      value1: getNodeLabel(node)
                    })}
                  >
                    {nodeActions}
                  </div>
                )}
              </div>
              {hasChildren && expanded && renderLevel(children, currentDepth + 1, nextAncestorIds)}
            </div>
          );
        })}
      </div>
    );
  }

  return renderLevel(nodes, depth, new Set(), true);
}
