<script lang="ts" setup>
import {ref, computed, watch, nextTick, useTemplateRef, onMounted, onUnmounted, type ShallowRef} from 'vue';
import {generateElemId} from '../utils/dom.ts';
import {GET} from '../modules/fetch.ts';
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
import {pathEscapeSegments} from '../utils/url.ts';
import {SvgIcon} from '../svg.ts';
import {throttle} from 'throttle-debounce';

const props = defineProps({
  repoLink: { type: String, required: true },
  currentRefNameSubURL: { type: String, required: true },
  treeListUrl: { type: String, required: true },
  noResultsText: { type: String, required: true },
  placeholder: { type: String, required: true },
});

const refElemInput = useTemplateRef('searchInput') as Readonly<ShallowRef<HTMLInputElement>>;
const refElemPopup = useTemplateRef('searchPopup') as Readonly<ShallowRef<HTMLDivElement>>;

const searchQuery = ref('');
const allFiles = ref<string[]>([]);
const selectedIndex = ref(0);
const isLoadingFileList = ref(false);
const hasLoadedFileList = ref(false);

const showPopup = computed(() => searchQuery.value.length > 0);

const filteredFiles = computed(() => {
  if (!searchQuery.value) return [];
  return filterRepoFilesWeighted(allFiles.value, searchQuery.value);
});

const applySearchQuery = throttle(300, () => {
  searchQuery.value = refElemInput.value.value;
  selectedIndex.value = 0;
});

const handleSearchInput = () => {
  loadFileListForSearch();
  applySearchQuery();
};

const handleKeyDown = (e: KeyboardEvent) => {
  if (e.isComposing) return;

  if (e.key === 'Escape') {
    clearSearch();
    nextTick(() => refElemInput.value.blur());
    return;
  }
  if (!searchQuery.value || filteredFiles.value.length === 0) return;

  const handleSelectedItem = (idx: number) => {
    e.preventDefault();
    selectedIndex.value = idx;
    const el = refElemPopup.value.querySelector(`.file-search-results > :nth-child(${idx+1} of .item)`);
    el?.scrollIntoView({ block: 'nearest', behavior: 'instant' });
  };

  if (e.key === 'ArrowDown') {
    handleSelectedItem(Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1));
  } else if (e.key === 'ArrowUp') {
    handleSelectedItem(Math.max(selectedIndex.value - 1, 0))
  } else if (e.key === 'Enter') {
    e.preventDefault();
    const selectedFile = filteredFiles.value[selectedIndex.value];
    if (selectedFile) {
      handleSearchResultClick(selectedFile.matchResult.join(''));
    }
  }
};

const clearSearch = () => {
  searchQuery.value = '';
  refElemInput.value.value = '';
};


const handleClickOutside = (e: MouseEvent) => {
  if (!searchQuery.value) return;

  const target = e.target as HTMLElement;
  const clickInside = refElemInput.value.contains(target) || refElemPopup.value.contains(target);
  if (!clickInside) clearSearch();
};

const loadFileListForSearch = async () => {
  if (hasLoadedFileList.value || isLoadingFileList.value) return;

  isLoadingFileList.value = true;
  try {
    const response = await GET(props.treeListUrl);
    allFiles.value = await response.json();
    hasLoadedFileList.value = true;
  } finally {
    isLoadingFileList.value = false;
  }
};

function handleSearchResultClick(filePath: string) {
  clearSearch();
  window.location.href = `${props.repoLink}/src/${pathEscapeSegments(props.currentRefNameSubURL)}/${pathEscapeSegments(filePath)}`;
}

const updatePosition = () => {
  if (!showPopup.value) return;

  const rectInput = refElemInput.value.getBoundingClientRect();
  const rectPopup = refElemPopup.value.getBoundingClientRect();
  const docElem = document.documentElement;
  const style = refElemPopup.value.style;
  style.top = `${docElem.scrollTop + rectInput.bottom + 4}px`;
  if (rectInput.x + rectPopup.width < docElem.clientWidth) {
    // enough space to align left with the input
    style.left = `${docElem.scrollLeft + rectInput.x}px`;
  } else {
    // no enough space, align right from the viewport right edge minus page margin
    const leftPos = docElem.scrollLeft + docElem.getBoundingClientRect().width - rectPopup.width;
    style.left = `calc(${leftPos}px - var(--page-margin-x))`;
  }
};

onMounted(() => {
  const searchPopupId = generateElemId('file-search-popup-');
  refElemPopup.value.setAttribute('id', searchPopupId);
  refElemInput.value.setAttribute('aria-controls', searchPopupId);
  document.addEventListener('click', handleClickOutside);
  window.addEventListener('resize', updatePosition);
});

onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside);
  window.removeEventListener('resize', updatePosition);
});

// Position search results below the input
watch([searchQuery, filteredFiles], async () => {
  if (searchQuery.value) {
    await nextTick();
    updatePosition();
  }
});
</script>

<template>
  <div>
    <div class="ui small input global-shortcut-wrapper">
      <input
        ref="searchInput" :placeholder="placeholder" autocomplete="off"
        role="combobox" aria-autocomplete="list" :aria-expanded="searchQuery ? 'true' : 'false'"
        @input="handleSearchInput" @keydown="handleKeyDown"
      >
      <kbd data-global-init="onGlobalShortcut" data-shortcut-keys="t">T</kbd>
    </div>

    <Teleport to="body">
      <div v-show="showPopup" ref="searchPopup" class="file-search-popup">
        <!-- always create the popup by v-show above to avoid null ref, only create the popup content if the popup should be displayed to save memory -->
        <template v-if="showPopup">
          <div v-if="filteredFiles.length" role="listbox" class="file-search-results flex-items-block">
            <div
              v-for="(result, idx) in filteredFiles" :key="result.matchResult.join('')"
              :class="['item', { 'selected': idx === selectedIndex }]"
              role="option" :aria-selected="idx === selectedIndex" @click="handleSearchResultClick(result.matchResult.join(''))"
              @mouseenter="selectedIndex = idx" :title="result.matchResult.join('')"
            >
              <SvgIcon name="octicon-file" class="file-icon"/>
              <span class="full-path">
                <span v-for="(part, index) in result.matchResult" :key="index">{{ part }}</span>
              </span>
            </div>
          </div>
          <div v-else-if="isLoadingFileList">
            <div class="is-loading"/>
          </div>
          <div v-else class="tw-p-4">
            {{ props.noResultsText }}
          </div>
        </template>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.file-search-popup {
  position: absolute;
  background: var(--color-box-body);
  border: 1px solid var(--color-secondary);
  border-radius: var(--border-radius);
  width: max-content;
  max-height: min(calc(100vw - 20px), 300px);
  max-width: min(calc(100vw - 40px), 600px);
  overflow-y: auto;
}

.file-search-popup .is-loading {
  width: 200px;
  height: 200px;
}

.file-search-results .item {
  align-items: flex-start;
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-bottom: 1px solid var(--color-secondary);
}

.file-search-results .item:last-child {
  border-bottom: none;
}

.file-search-results .item:hover,
.file-search-results .item.selected {
  background-color: var(--color-hover);
}

.file-search-results .item .file-icon {
  flex-shrink: 0;
  margin-top: 0.125rem;
}

.file-search-results .item .full-path {
  flex: 1;
  overflow-wrap: anywhere;
}

.file-search-results .item .full-path :nth-child(even) {
  color: var(--color-red);
  font-weight: var(--font-weight-semibold);
}
</style>
