<script lang="ts">
  import type {NodePaletteItem} from '../utils/nodePalette';

  const { nodes = [], onSelect, placeholder = '搜索节点、插件、工作流' }: {
    nodes: NodePaletteItem[];
    onSelect?: (node: NodePaletteItem) => void;
    placeholder?: string;
  } = $props();

  let keyword = $state('');
  type HighlightPart = { text: string; hit: boolean };

  const PINYIN_CHAR_MAP: Record<string, string> = {
    大: 'da', 模: 'mo', 型: 'xing',
    开: 'kai', 始: 'shi', 节: 'jie', 点: 'dian',
    循: 'xun', 环: 'huan',
    条: 'tiao', 件: 'jian', 判: 'pan', 断: 'duan',
    知: 'zhi', 识: 'shi', 库: 'ku',
    搜: 'sou', 索: 'suo', 引: 'yin', 擎: 'qing',
    请: 'qing', 求: 'qiu',
    动: 'dong', 态: 'tai', 代: 'dai', 码: 'ma',
    内: 'nei', 容: 'rong', 板: 'ban',
    用: 'yong', 户: 'hu', 确: 'que', 认: 'ren',
    结: 'jie', 束: 'shu',
    文: 'wen', 档: 'dang', 提: 'ti', 取: 'qu',
    生: 'sheng', 成: 'cheng',
    查: 'cha', 询: 'xun', 数: 'shu', 据: 'ju',
    插: 'cha', 子: 'zi',
    流: 'liu', 程: 'cheng',
    素: 'su', 材: 'cai', 同: 'tong', 步: 'bu',
    保: 'bao', 存: 'cun',
    基: 'ji', 础: 'chu',
    扩: 'kuo', 展: 'zhan', 工: 'gong', 具: 'ju',
    与: 'yu', 输: 'shu', 入: 'ru',
    其: 'qi', 他: 'ta'
  };

  const categoryOrder = ['模型与流程', '逻辑', '输入输出', '数据与集成', '基础节点', '扩展工具', '其他'];

  const normalizedKeyword = $derived(keyword.trim().toLowerCase());
  const compactKeyword = $derived(normalizeForMatch(keyword));

  function normalizeForMatch(text: string) {
    return text.toLowerCase().replace(/\s+/g, '');
  }

  function toPinyinToken(text: string) {
    let full = '';
    let initials = '';
    for (const char of text) {
      const py = PINYIN_CHAR_MAP[char];
      if (py) {
        full += py;
        initials += py[0];
        continue;
      }
      const code = char.codePointAt(0) || 0;
      const isAsciiDigit = code >= 48 && code <= 57;
      const isAsciiUpper = code >= 65 && code <= 90;
      const isAsciiLower = code >= 97 && code <= 122;
      if (isAsciiDigit || isAsciiUpper || isAsciiLower) {
        const lowerChar = char.toLowerCase();
        full += lowerChar;
        initials += lowerChar;
      }
    }
    return { full, initials };
  }

  function isNodeMatched(item: NodePaletteItem) {
    if (!compactKeyword) {
      return true;
    }
    const directHaystack = normalizeForMatch(`${item.title} ${item.type} ${item.description || ''}`);
    if (directHaystack.includes(compactKeyword)) {
      return true;
    }
    const pinyinToken = toPinyinToken(item.title);
    return pinyinToken.full.includes(compactKeyword) || pinyinToken.initials.includes(compactKeyword);
  }

  function getHighlightParts(title: string): HighlightPart[] {
    if (!normalizedKeyword) {
      return [{ text: title, hit: false }];
    }
    const source = title.toLowerCase();
    const index = source.indexOf(normalizedKeyword);
    if (index < 0) {
      return [{ text: title, hit: false }];
    }
    const head = title.slice(0, index);
    const middle = title.slice(index, index + normalizedKeyword.length);
    const tail = title.slice(index + normalizedKeyword.length);
    const parts: HighlightPart[] = [];
    if (head) {
      parts.push({ text: head, hit: false });
    }
    if (middle) {
      parts.push({ text: middle, hit: true });
    }
    if (tail) {
      parts.push({ text: tail, hit: false });
    }
    return parts;
  }

  const filteredNodes = $derived.by(() => {
    if (!compactKeyword) {
      return nodes;
    }
    return nodes.filter((item) => isNodeMatched(item));
  });

  const groupedNodes = $derived.by(() => {
    const map = new Map<string, NodePaletteItem[]>();
    for (const item of filteredNodes) {
      const category = item.category || '其他';
      if (!map.has(category)) {
        map.set(category, []);
      }
      map.get(category)?.push(item);
    }
    return [...map.entries()].sort((a, b) => {
      const ai = categoryOrder.indexOf(a[0]);
      const bi = categoryOrder.indexOf(b[0]);
      if (ai === -1 && bi === -1) {
        return a[0].localeCompare(b[0]);
      }
      if (ai === -1) {
        return 1;
      }
      if (bi === -1) {
        return -1;
      }
      return ai - bi;
    });
  });

  function onDragStart(event: DragEvent, node: NodePaletteItem) {
    if (!event.dataTransfer) {
      return;
    }
    const payload = {
      type: node.type,
      data: {
        title: node.title,
        description: node.description,
        ...(node.extra || {})
      }
    };
    event.dataTransfer.setData('application/tinyflow', JSON.stringify(payload));
    event.dataTransfer.effectAllowed = 'move';
  }

  function handleSelect(node: NodePaletteItem) {
    onSelect?.(node);
  }
</script>

<div class="tf-node-picker">
  <div class="tf-node-picker-search">
    <span class="tf-node-picker-search-icon">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
        <path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path>
      </svg>
    </span>
    <input
      type="text"
      class="tf-node-picker-search-input"
      bind:value={keyword}
      placeholder={placeholder}
    />
  </div>

  <div class="tf-node-picker-content">
    {#if groupedNodes.length === 0}
      <div class="tf-node-picker-empty">暂无可用节点</div>
    {:else}
      {#each groupedNodes as [category, categoryNodes]}
        <div class="tf-node-picker-group">
          <div class="tf-node-picker-group-title">{category}</div>
          <div class="tf-node-picker-grid">
            {#each categoryNodes as node}
              <button
                type="button"
                class="tf-node-picker-item nopan nodrag"
                draggable={true}
                ondragstart={(event) => onDragStart(event, node)}
                onclick={() => handleSelect(node)}
              >
                <span class="tf-node-picker-item-icon">
                  {#if node.icon}
                    {@html node.icon}
                  {:else}
                    <span class="tf-node-picker-item-icon-fallback">N</span>
                  {/if}
                </span>
                <span class="tf-node-picker-item-title">
                  {#each getHighlightParts(node.title) as part}
                    <span class:tf-node-picker-item-title-hit={part.hit}>{part.text}</span>
                  {/each}
                </span>
              </button>
            {/each}
          </div>
        </div>
      {/each}
    {/if}
  </div>
</div>

<style lang="less">
  .tf-node-picker {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .tf-node-picker-search {
    display: flex;
    align-items: center;
    border: 1px solid var(--tf-border-color);
    border-radius: 10px;
    background: var(--tf-bg-surface);
    padding: 0 10px;
    height: 40px;
  }

  .tf-node-picker-search-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    color: var(--tf-text-muted);

    svg {
      width: 16px;
      height: 16px;
      fill: currentColor;
    }
  }

  .tf-node-picker-search-input {
    flex: 1;
    margin-left: 8px;
    border: none;
    background: transparent;
    outline: none;
    color: var(--tf-text-primary);
    font-size: 14px;
  }

  .tf-node-picker-content {
    max-height: min(300px, 38vh);
    overflow: auto;
    padding-right: 2px;
  }

  .tf-node-picker-empty {
    padding: 18px 4px;
    font-size: 14px;
    color: var(--tf-text-muted);
  }

  .tf-node-picker-group {
    margin-bottom: 14px;
  }

  .tf-node-picker-group-title {
    font-size: 13px;
    color: var(--tf-text-secondary);
    font-weight: 600;
    margin-bottom: 8px;
  }

  .tf-node-picker-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px 12px;
  }

  .tf-node-picker-item {
    border: none;
    border-radius: 10px;
    background: transparent;
    min-height: 34px;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    max-width: none;
    padding: 5px 8px;
    text-align: left;
    cursor: pointer;
    color: var(--tf-text-primary);
  }

  .tf-node-picker-item:hover {
    background: var(--tf-bg-hover);
  }

  .tf-node-picker-item:active {
    transform: translateY(1px);
  }

  .tf-node-picker-item-icon {
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--tf-primary-color);
  }

  .tf-node-picker-item-icon :global(svg) {
    width: 20px;
    height: 20px;
    fill: currentColor;
  }

  .tf-node-picker-item-icon-fallback {
    width: 20px;
    height: 20px;
    border-radius: 999px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--tf-bg-hover);
    color: var(--tf-primary-color);
    font-size: 12px;
    font-weight: 700;
  }

  .tf-node-picker-item-title {
    font-size: 13px;
    color: var(--tf-text-primary);
    line-height: 1.2;
  }

  .tf-node-picker-item-title-hit {
    color: var(--tf-primary-color);
    font-weight: 700;
  }
</style>
