<script setup lang="ts">
import type { MenuRecordRaw } from '@easyflow/types';

import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';

import { SearchX } from '@easyflow/icons';
import { $t } from '@easyflow/locales';
import { mapTree, traverseTreeValues, uniqueByField } from '@easyflow/utils';

import { EasyFlowIcon, EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
import { isHttpUrl } from '@easyflow-core/shared/utils';

import { onKeyStroke, useThrottleFn } from '@vueuse/core';

defineOptions({
  name: 'SearchPanel',
});

const props = withDefaults(
  defineProps<{ keyword?: string; menus?: MenuRecordRaw[] }>(),
  {
    keyword: '',
    menus: () => [],
  },
);
const emit = defineEmits<{ close: []; resultsChange: [count: number] }>();

const router = useRouter();
const activeIndex = ref(-1);
const searchItems = shallowRef<MenuRecordRaw[]>([]);
const searchResults = ref<MenuRecordRaw[]>([]);
const displayResults = computed(() =>
  uniqueByField(searchResults.value, 'path'),
);

const handleSearch = useThrottleFn(search, 200);

function search(searchKey: string) {
  const normalizedKey = searchKey.trim();
  if (!normalizedKey) {
    searchResults.value = [];
    activeIndex.value = -1;
    return;
  }

  const reg = createSearchReg(normalizedKey);
  const results: MenuRecordRaw[] = [];

  traverseTreeValues(searchItems.value, (item) => {
    if (reg.test(item.name?.toLowerCase())) {
      results.push(item);
    }
  });

  searchResults.value = uniqueByField(results, 'path');
  activeIndex.value = searchResults.value.length > 0 ? 0 : -1;
}

function scrollIntoView() {
  const element = document.querySelector(
    `[data-search-item="${activeIndex.value}"]`,
  );

  if (element) {
    element.scrollIntoView({ block: 'nearest' });
  }
}

async function handleEnter() {
  if (displayResults.value.length === 0 || activeIndex.value < 0) {
    return;
  }
  await handleSelect(activeIndex.value);
}

async function handleSelect(index: number) {
  const to = displayResults.value[index];
  if (!to) {
    return;
  }
  emit('close');
  await nextTick();
  if (isHttpUrl(to.path)) {
    window.open(to.path, '_blank');
  } else {
    router.push({ path: to.path, replace: true });
  }
}

function handleUp() {
  if (displayResults.value.length === 0) {
    return;
  }
  activeIndex.value--;
  if (activeIndex.value < 0) {
    activeIndex.value = displayResults.value.length - 1;
  }
  scrollIntoView();
}

function handleDown() {
  if (displayResults.value.length === 0) {
    return;
  }
  activeIndex.value++;
  if (activeIndex.value > displayResults.value.length - 1) {
    activeIndex.value = 0;
  }
  scrollIntoView();
}

function handleClose() {
  emit('close');
}

function handleMouseenter(index: number) {
  activeIndex.value = index;
}

function getHighlightedChunks(text: string, keyword: string) {
  const sourceChars = [...text];
  const lowerChars = [...text.toLowerCase()];
  const queryChars = [...keyword.trim().toLowerCase()];

  if (queryChars.length === 0) {
    return [{ text, matched: false }];
  }

  const chunks: Array<{ matched: boolean; text: string }> = [];
  let queryIndex = 0;

  sourceChars.forEach((char, index) => {
    const matched =
      queryIndex < queryChars.length &&
      lowerChars[index] === queryChars[queryIndex];
    if (matched) {
      queryIndex++;
    }
    const previousChunk = chunks[chunks.length - 1];
    if (previousChunk && previousChunk.matched === matched) {
      previousChunk.text += char;
    } else {
      chunks.push({ matched, text: char });
    }
  });

  return chunks;
}

const code = new Set([
  '$',
  '(',
  ')',
  '*',
  '+',
  '.',
  '?',
  '[',
  '\\',
  ']',
  '^',
  '{',
  '|',
  '}',
]);

function transform(c: string) {
  return code.has(c) ? `\\${c}` : c;
}

function createSearchReg(key: string) {
  const keys = [...key].map((item) => transform(item)).join('.*');
  return new RegExp(`.*${keys}.*`);
}

watch(
  () => props.keyword,
  (val) => {
    if (val) {
      handleSearch(val);
    } else {
      searchResults.value = [];
      activeIndex.value = -1;
    }
  },
);

watch(
  () => displayResults.value.length,
  (count) => {
    emit('resultsChange', count);
  },
  { immediate: true },
);

onMounted(() => {
  searchItems.value = mapTree(props.menus, (item) => {
    return {
      ...item,
      name: $t(item?.name),
    };
  });
  onKeyStroke('Enter', handleEnter);
  onKeyStroke('ArrowUp', handleUp);
  onKeyStroke('ArrowDown', handleDown);
  onKeyStroke('Escape', handleClose);
});
</script>

<template>
  <EasyFlowScrollbar>
    <div class="px-2 py-1 sm:max-h-[420px]">
      <ul v-if="displayResults.length > 0" class="space-y-2">
        <li
          v-for="(item, index) in displayResults"
          :key="item.path"
          :class="
            activeIndex === index
              ? 'bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.22,hsl(var(--glass-tint))/0.1_18%,hsl(var(--surface-panel))/0.12)] shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.34,0_18px_30px_-24px_hsl(var(--primary)/0.12)] ring-1 ring-[hsl(var(--glass-border))/0.26]'
              : ''
          "
          :data-index="index"
          :data-search-item="index"
          class="group flex w-full cursor-pointer items-center gap-3 rounded-2xl bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.14,hsl(var(--glass-tint))/0.08_18%,hsl(var(--surface-panel))/0.08)] px-4 py-3.5 backdrop-blur-2xl transition-[transform,background-color,box-shadow] hover:-translate-y-0.5 hover:bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.18,hsl(var(--glass-tint))/0.12_18%,hsl(var(--surface-panel))/0.1)] hover:shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.28,0_16px_28px_-24px_hsl(var(--primary)/0.1)]"
          @click="handleSelect(index)"
          @mouseenter="handleMouseenter(index)"
        >
          <div
            class="flex size-10 flex-shrink-0 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.24,hsl(var(--nav-item-active))/0.09_22%,hsl(var(--glass-tint))/0.14)] text-[hsl(var(--nav-item-active-foreground))] shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.36]"
          >
            <EasyFlowIcon :icon="item.icon" class="size-5" fallback />
          </div>

          <div class="min-w-0 flex-1">
            <span
              class="block truncate text-sm font-medium leading-6 text-[hsl(var(--text-strong))]"
            >
              <template
                v-for="chunk in getHighlightedChunks(item.name || '', keyword)"
                :key="`${item.path}-${chunk.text}-${chunk.matched}`"
              >
                <span
                  :class="
                    chunk.matched
                      ? activeIndex === index
                        ? 'rounded-md bg-[hsl(var(--primary))/0.1] px-0.5 font-semibold text-current'
                        : 'rounded-md bg-[hsl(var(--primary))/0.08] px-0.5 font-semibold text-[hsl(var(--primary))]'
                      : ''
                  "
                >
                  {{ chunk.text }}
                </span>
              </template>
            </span>
          </div>
        </li>
      </ul>
      <div v-else class="hidden">
        <SearchX class="size-0" />
      </div>
    </div>
  </EasyFlowScrollbar>
</template>
