/*
  Fast Search for Hugo — adapted from
  https://gist.github.com/cmod/5410eae147e4318164258742dd053993
  MIT License
*/

const DEFAULT_CONFIG = {
  shortcuts: {
    open: {
      key: '/',
      metaKey: false,
      altKey: false,
      ctrlKey: false,
      shiftKey: false
    }
  },
  search: {
    minChars: 2,
    maxResults: 8,
    fields: {
      title: true,
      description: true,
      section: true
    }
  }
};

function initSearch(userConfig = {}) {
  const CONFIG = mergeConfigs(DEFAULT_CONFIG, userConfig);

  const mainNav     = document.getElementById('mainNav');
  const fastSearch  = document.getElementById('fastSearch');
  const searchInput = document.getElementById('searchInput');
  const searchResults = document.getElementById('searchResults');

  let searchIndex = null;
  let searchVisible = false;
  let resultsAvailable = false;
  let firstRun = true;

  async function loadSearchIndex() {
    try {
      const response = await fetch('/index.json');
      if (!response.ok) throw new Error('Failed to load search index');
      const data = await response.json();
      searchIndex = data.map(item => ({
        ...item,
        searchableTitle:    item.title?.toLowerCase()    || '',
        searchableDesc:     item.desc?.toLowerCase()     || '',
        searchableSection:  item.section?.toLowerCase()  || '',
        searchableContents: item.contents?.toLowerCase() || ''
      }));
      if (searchInput.value) performSearch(searchInput.value);
    } catch (error) {
      console.error('Error loading search index:', error);
      searchResults.innerHTML = '<li class="search-message">Error loading search index.</li>';
    }
  }

  function simpleFuzzyMatch(text, term) {
    if (text.includes(term)) return true;
    if (term.length < 3) return false;
    let matches = 0, lastMatchIndex = -1;
    for (let i = 0; i < term.length; i++) {
      const found = text.indexOf(term[i], lastMatchIndex + 1);
      if (found > -1) { matches++; lastMatchIndex = found; }
    }
    return matches === term.length;
  }

  function matchesShortcut(event, sc) {
    return event.key === sc.key &&
           event.metaKey  === sc.metaKey  &&
           event.altKey   === sc.altKey   &&
           event.ctrlKey  === sc.ctrlKey  &&
           event.shiftKey === sc.shiftKey;
  }

  function openSearch() {
    searchVisible = true;
    mainNav.style.display    = 'none';
    fastSearch.style.display = 'flex';
    if (firstRun) { loadSearchIndex(); firstRun = false; }
    searchInput.focus();
    searchInput.value = '';
    searchResults.innerHTML = '';
  }

  function closeSearch() {
    searchVisible = false;
    fastSearch.style.display = 'none';
    mainNav.style.display    = '';
    searchInput.blur();
    searchInput.value = '';
    searchResults.innerHTML = '';
    resultsAvailable = false;
  }

  document.addEventListener('keydown', (event) => {
    const tag           = event.target.tagName;
    const inOtherInput  = tag === 'TEXTAREA' || (tag === 'INPUT' && event.target.id !== 'searchInput');
    const inSearchInput = tag === 'INPUT' && event.target.id === 'searchInput';

    // Never steal keystrokes from other inputs/textareas
    if (inOtherInput) return;

    // Toggle shortcut — not when the user is already typing in the search box
    if (!inSearchInput && matchesShortcut(event, CONFIG.shortcuts.open)) {
      event.preventDefault();
      searchVisible ? closeSearch() : openSearch();
      return;
    }

    // ESC closes from anywhere, including the search input
    if (event.key === 'Escape' && searchVisible) {
      closeSearch();
      return;
    }

    // Arrow navigation and Enter
    if (searchVisible && resultsAvailable) {
      const links = Array.from(searchResults.getElementsByTagName('a'));
      if (!links.length) return;
      const active = document.activeElement;
      const activeInResults = searchResults.contains(active) && active.tagName === 'A';
      const i = activeInResults ? links.indexOf(active) : -1;

      if (event.key === 'Enter') {
        // Follow the focused result, not always the first one
        const target = activeInResults ? active : links[0];
        event.preventDefault();
        window.location.href = target.href;
        return;
      }

      if (event.key === 'ArrowDown') {
        event.preventDefault();
        if (!activeInResults) { links[0].focus(); }          // from input or stale focus → first result
        else if (i < links.length - 1) { links[i + 1].focus(); }
        return;
      }

      if (event.key === 'ArrowUp') {
        event.preventDefault();
        if (!activeInResults || i === 0) { searchInput.focus(); }
        else { links[i - 1].focus(); }
        return;
      }
    }

    // Enter with no results: do nothing but swallow it so the form doesn't submit
    if (event.key === 'Enter' && searchVisible) {
      event.preventDefault();
      return;
    }
  });

  function performSearch(term) {
    term = term.toLowerCase().trim();
    if (!term || !searchIndex) { searchResults.innerHTML = ''; resultsAvailable = false; return; }

    if (term.length < CONFIG.search.minChars) {
      searchResults.innerHTML = '<li class="search-message">Type at least 2 characters…</li>';
      resultsAvailable = false;
      return;
    }

    const searchTerms = term.split(/\s+/).filter(t => t.length > 0);

    const results = searchIndex
      .map(item => {
        let score = 0;
        const matchesAll = searchTerms.every(t => {
          let matched = false;
          if (CONFIG.search.fields.title) {
            if (item.searchableTitle.startsWith(t))             { score += 3; matched = true; }
            else if (simpleFuzzyMatch(item.searchableTitle, t)) { score += 2; matched = true; }
          }
          if (!matched && CONFIG.search.fields.description && item.searchableDesc.includes(t))      { score += 0.5; matched = true; }
          if (!matched && CONFIG.search.fields.section    && item.searchableSection.includes(t))   { score += 0.5; matched = true; }
          if (!matched && item.searchableContents.includes(t))                                     { score += 0.1; matched = true; }
          return matched;
        });
        return { item, score: matchesAll ? score : 0 };
      })
      .filter(r => r.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, CONFIG.search.maxResults)
      .map(r => r.item);

    resultsAvailable = results.length > 0;

    if (!resultsAvailable) {
      searchResults.innerHTML = '<li class="search-message">No results found.</li>';
      return;
    }

    searchResults.innerHTML = results.map(item => `
      <li>
        <a href="${escapeHtml(item.permalink)}" tabindex="0">
          <span class="title">${escapeHtml(item.title)}</span>
          <span class="meta">${escapeHtml(item.section)} &mdash; ${escapeHtml(item.date)}</span>
          <span class="desc">${escapeHtml(item.desc)}</span>
        </a>
      </li>
    `).join('');
  }

  searchInput.addEventListener('input', function () {
    if (!searchIndex && !firstRun) {
      searchResults.innerHTML = '<li class="search-message">Loading…</li>';
      return;
    }
    performSearch(this.value);
  });
}

function mergeConfigs(defaultConfig, userConfig) {
  const merged = { ...defaultConfig };
  for (const [key, value] of Object.entries(userConfig)) {
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      merged[key] = mergeConfigs(defaultConfig[key] || {}, value);
    } else {
      merged[key] = value;
    }
  }
  return merged;
}

function escapeHtml(unsafe) {
  if (!unsafe) return '';
  return unsafe
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

initSearch();
