/**
 * C++ port of FZF Fuzzy Matching Algorithm (V2), with many additions
 * Original Go implementation: https://github.com/junegunn/fzf
 */

#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <iterator>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include "fuzzy/normalize.hpp"

namespace fzf {

// Scoring constants
constexpr const int SCORE_MATCH = 16;
constexpr const int SCORE_GAP_START = -3;
constexpr const int SCORE_GAP_EXTENSION = -1;

constexpr const int BONUS_BOUNDARY = SCORE_MATCH / 2;                             // 8
constexpr const int BONUS_NON_WORD = SCORE_MATCH / 2;                             // 8
constexpr const int BONUS_CAMEL123 = BONUS_BOUNDARY + SCORE_GAP_EXTENSION;        // 7
constexpr const int BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENSION); // 4
constexpr const int BONUS_FIRST_CHAR_MULTIPLIER = 2;

enum class CharClass : uint8_t { White = 0, NonWord, Delimiter, Lower, Upper, Letter, Number };

struct Result {
  int start = -1;
  int end = -1;
  int score = 0;
  // false when the alignment spreads over several words while some run of consecutive matched
  // characters starts mid-word ("time" in "S[t]art [I]nput [Me]thod"): a match nobody would recognize.
  // Substrings, in-word abbreviations ("kbd"/"Keyboard") and acronyms ("sim"/"Start Input Method") are
  // coherent.
  bool coherent = true;
  // not populated if position is not requested
  std::vector<int> positions;

  bool matched() const { return start >= 0; }
};

struct WeightedString {
  std::string_view str;
  float weight;
};

// `weighted` is the raw fzf score with field weights applied, averaged over query words;
// `score` (0-100) is `weighted` relative to a perfect match of the whole query (use for ranking);
// `quality` (0-100) is the unweighted score of the worst-matched query word relative to a perfect
// match of that word, so it can be thresholded independently of query length (use for filtering).
// Only coherent alignments (see Result::coherent) count toward `quality`.
struct QueryScore {
  int weighted = 0;
  int score = 0;
  int quality = 0;
};

enum class Scheme { Default, Path, History };

class Matcher;
inline const Matcher &threadLocalMatcher();

// A query prepared for scoring many items: split into words, each with the best score it can
// reach (`self`, the word matched against itself), so per-item scoring never recomputes it.
// Ceilings depend on the matcher scheme; the default matcher is used unless one is given.
struct Query {
  struct Word {
    std::string text;
    int self = 0;
  };

  std::string text;
  std::vector<Word> words;

  Query() = default;
  explicit Query(std::string_view text, const Matcher &matcher = threadLocalMatcher());

  bool empty() const { return words.empty(); }
};

class Matcher {
public:
  explicit Matcher(Scheme scheme = Scheme::Default) : m_scheme_(scheme) {
    // Initialize scheme-specific parameters
    switch (scheme) {
    case Scheme::Default:
      m_bonus_boundary_white = BONUS_BOUNDARY + 2;
      m_bonus_boundary_delimiter = BONUS_BOUNDARY + 1;
      m_delimiter_chars = "/,:;|";
      m_initial_char_class = CharClass::White;
      break;
    case Scheme::Path:
      m_bonus_boundary_white = BONUS_BOUNDARY;
      m_bonus_boundary_delimiter = BONUS_BOUNDARY + 1;
      m_delimiter_chars = "/";
      m_initial_char_class = CharClass::Delimiter;
      break;
    case Scheme::History:
      m_bonus_boundary_white = BONUS_BOUNDARY;
      m_bonus_boundary_delimiter = BONUS_BOUNDARY;
      m_delimiter_chars = "/,:;|";
      m_initial_char_class = CharClass::White;
      break;
    }

    // Initialize ASCII character class lookup table
    for (int i = 0; i < 128; ++i) {
      char c = static_cast<char>(i);
      CharClass cls = CharClass::NonWord;

      if (c >= 'a' && c <= 'z') {
        cls = CharClass::Lower;
      } else if (c >= 'A' && c <= 'Z') {
        cls = CharClass::Upper;
      } else if (c >= '0' && c <= '9') {
        cls = CharClass::Number;
      } else if (std::isspace(static_cast<unsigned char>(c))) {
        cls = CharClass::White;
      } else if (m_delimiter_chars.find(c) != std::string::npos) {
        cls = CharClass::Delimiter;
      }

      ascii_char_classes_[i] = cls;
    }

    // Pre-compute bonus matrix
    for (int i = 0; i < 8; ++i) {
      for (int j = 0; j < 8; ++j) {
        bonus_matrix_[i][j] = bonus_for(static_cast<CharClass>(i), static_cast<CharClass>(j));
      }
    }
  }

  template <std::ranges::forward_range R1, std::ranges::forward_range R2>
    requires std::same_as<std::ranges::range_value_t<R1>, WeightedString> &&
             std::same_as<std::ranges::range_value_t<R2>, WeightedString>
  QueryScore score_query(R1 &&range1, R2 &&range2, const Query &query) const {
    int weightedSum = 0;
    int selfSum = 0;
    int minQuality = 100;

    for (const auto &word : query.words) {
      int maxWeighted = 0;
      int maxRaw = 0;
      auto score = [&](const WeightedString &str) {
        auto const r = match(str.str, word.text);
        if (r.coherent) { maxRaw = std::max(maxRaw, r.score); }
        maxWeighted = std::max(maxWeighted, static_cast<int>(r.score * str.weight));
      };
      for (const auto &s : range1)
        score(s);
      for (const auto &s : range2)
        score(s);

      if (!maxWeighted) return {};

      minQuality = std::min(minQuality, word.self > 0 ? maxRaw * 100 / word.self : 0);
      weightedSum += maxWeighted;
      selfSum += word.self;
    }

    if (query.words.empty() || selfSum == 0) return {};
    return {.weighted = weightedSum / static_cast<int>(query.words.size()),
            .score = std::min(100, weightedSum * 100 / selfSum),
            .quality = minQuality};
  }

  template <std::ranges::forward_range R>
    requires std::same_as<std::ranges::range_value_t<R>, WeightedString>
  QueryScore score_query(R &&weightedStrs, const Query &query) const {
    return score_query(std::forward<R>(weightedStrs), std::views::empty<WeightedString>, query);
  }

  QueryScore score_query(std::string_view text, const Query &query) const {
    std::initializer_list<WeightedString> lst{{text, 1.0f}};
    return score_query(std::views::all(lst), query);
  }

  Result match(std::string_view text, std::string_view pattern, bool with_pos = false) const {
    const auto match = [&](std::string_view candidate) {
      if (!has_non_ascii(text) && !has_non_ascii(candidate)) {
        return match_ascii(text, candidate, with_pos);
      }

      fold_text(text);
      const std::string_view ftext{m_fold_text.data(), m_fold_text.size()};
      const std::string_view fpat = fold_pattern(candidate);

      Result result = match_ascii(ftext, fpat, with_pos);
      if (result.matched()) {
        result.start = m_fold_off[result.start];
        result.end = m_fold_off[result.end];
        for (int &position : result.positions) {
          position = m_fold_off[position];
        }
      }
      return result;
    };

    Result best = match(pattern);
    if (!has_non_ascii(pattern) || !needsTransliteration(pattern)) { return best; }

    for (const auto scheme : {TranslitScheme::Primary, TranslitScheme::Alternate}) {
      if (!transliterate(pattern, m_translit_pattern, scheme) || m_translit_pattern == pattern) { continue; }

      Result candidate = match(m_translit_pattern);
      if (candidate.score > best.score) { best = std::move(candidate); }
    }

    return best;
  }

  Result match_ascii(std::string_view text, std::string_view pattern, bool with_pos = false) const {
    const int M = static_cast<int>(pattern.length());
    const int N = static_cast<int>(text.length());

    if (M == 0) { return Result{0, 0, 0, {}}; }
    if (M > N) { return Result{}; }

    // Phase 1: Find search boundaries
    auto [min_idx, max_idx] = ascii_fuzzy_index(text, pattern);
    if (min_idx < 0) { return Result{}; }

    const int search_len = max_idx - min_idx;

    H0.resize(search_len);
    C0.resize(search_len);
    B.resize(search_len);
    F.resize(M);
    T.resize(search_len);

    // Phase 2: Calculate bonus for each position and find first occurrences
    int max_score = 0;
    int max_score_pos = 0;
    int pidx = 0;
    int last_idx = 0;
    char pchar0 = std::tolower(static_cast<unsigned char>(pattern[0]));
    char pchar = pchar0;
    int prev_h0 = 0;
    CharClass prev_class = m_initial_char_class;
    bool in_gap = false;

    for (int off = 0; off < search_len; ++off) {
      char c = text[min_idx + off];
      T[off] = std::tolower(static_cast<unsigned char>(c));

      CharClass cls = char_class_of(c);
      int bonus = bonus_matrix_[static_cast<int>(prev_class)][static_cast<int>(cls)];
      B[off] = bonus;
      prev_class = cls;

      // Track first occurrence of each pattern character
      if (T[off] == pchar) {
        if (pidx < M) {
          F[pidx] = off;
          pidx++;
          pchar = pidx < M ? std::tolower(static_cast<unsigned char>(pattern[pidx])) : pchar;
        }
        last_idx = off;
      }

      // Calculate score for first pattern character
      if (T[off] == pchar0) {
        int score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER;
        H0[off] = score;
        C0[off] = 1;
        if (M == 1 && score > max_score) {
          max_score = score;
          max_score_pos = off;
        }
        in_gap = false;
      } else {
        H0[off] = std::max<int>(prev_h0 + (in_gap ? SCORE_GAP_EXTENSION : SCORE_GAP_START), 0);
        C0[off] = 0;
        in_gap = true;
      }
      prev_h0 = H0[off];
    }

    // Check if all pattern characters were found
    if (pidx != M) { return Result{}; }

    // Single character pattern - return early
    if (M == 1) {
      Result result;
      result.start = min_idx + max_score_pos;
      result.end = result.start + 1;
      result.score = max_score;
      if (with_pos) { result.positions.push_back(result.start); }
      return result;
    }

    // Phase 3: Fill in score matrix (dynamic programming)
    const int f0 = F[0];
    const int width = last_idx - f0 + 1;

    H.assign(static_cast<size_t>(width) * M, 0);
    C.assign(static_cast<size_t>(width) * M, 0);

    // Copy first row
    std::copy(H0.begin() + f0, H0.begin() + last_idx + 1, H.begin());
    std::copy(C0.begin() + f0, C0.begin() + last_idx + 1, C.begin());

    // Fill remaining rows
    for (int i = 1; i < M; ++i) {
      const int f = F[i];
      const char pc = std::tolower(static_cast<unsigned char>(pattern[i]));
      const int row = i * width;
      in_gap = false;

      for (int j = f; j <= last_idx; ++j) {
        const int col = j - f0;
        const int offset = row + col;

        int s1 = 0, s2;
        int consecutive = 0;

        // Gap extension vs gap start
        if (in_gap) {
          s2 = H[offset - 1] + SCORE_GAP_EXTENSION;
        } else {
          s2 = H[offset - 1] + SCORE_GAP_START;
        }

        // Character match
        if (T[j] == pc) {
          s1 = H[offset - width - 1] + SCORE_MATCH;
          int b = B[j];
          consecutive = C[offset - width - 1] + 1;

          // Handle consecutive bonus
          if (consecutive > 1) {
            int first_bonus = B[j - consecutive + 1];
            // Break consecutive chunk at strong boundaries
            if (b >= BONUS_BOUNDARY && b > first_bonus) {
              consecutive = 1;
            } else {
              b = std::max({b, BONUS_CONSECUTIVE, first_bonus});
            }
          }

          // Use gap score if it's better
          if (s1 + b < s2) {
            s1 += B[j];
            consecutive = 0;
          } else {
            s1 += b;
          }
        }

        C[offset] = consecutive;
        in_gap = s1 < s2;
        int score = std::max({s1, s2, static_cast<int>(0)});

        // Track best score in last row
        if (i == M - 1 && score > max_score) {
          max_score = score;
          max_score_pos = j;
        }

        H[offset] = score;
      }
    }

    // Phase 4: Backtracking. Always walked to classify the alignment (Result::coherent);
    // positions are only recorded on request.
    Result result;
    result.start = min_idx + f0;
    result.end = min_idx + max_score_pos + 1;
    result.score = max_score;

    if (with_pos) { result.positions.reserve(M); }

    {
      int i = M - 1;
      int j = max_score_pos;
      bool prefer_match = true;
      bool boundary_inside = false;
      bool mid_word_run_start = false;
      int next_pos = -1;

      while (true) {
        const int I = i * width;
        const int j0 = j - f0;
        const int s = H[I + j0];

        int s1 = 0, s2 = 0;
        if (i > 0 && j >= F[i]) { s1 = H[I - width + j0 - 1]; }
        if (j > F[i]) { s2 = H[I + j0 - 1]; }

        if (s > s1 && (s > s2 || (s == s2 && prefer_match))) {
          if (with_pos) { result.positions.push_back(min_idx + j); }
          if (next_pos >= 0 && next_pos != j + 1 && B[next_pos] == 0) { mid_word_run_start = true; }
          next_pos = j;
          if (i == 0) {
            if (B[j] == 0) { mid_word_run_start = true; }
            break;
          }
          i--;
        }

        if (B[j] > 0) { boundary_inside = true; }
        prefer_match =
            C[I + j0] > 1 || (I + width + j0 + 1 < static_cast<int>(C.size()) && C[I + width + j0 + 1] > 0);
        j--;
      }

      result.coherent = !boundary_inside || !mid_word_run_start;
      std::reverse(result.positions.begin(), result.positions.end());
    }

    return result;
  }

private:
  static bool has_non_ascii(std::string_view s) {
    unsigned char acc = 0;
    for (const char c : s) {
      acc |= static_cast<unsigned char>(c);
    }
    return (acc & 0x80) != 0;
  }

  void fold_text(std::string_view text) const {
    m_fold_text.clear();
    m_fold_off.clear();
    m_fold_text.reserve(text.size());
    m_fold_off.reserve(text.size() + 1);

    size_t i = 0;
    const size_t n = text.size();
    while (i < n) {
      const auto [cp, len] = decodeUtf8(text, i);
      const char folded = cp < 0x80 ? static_cast<char>(cp) : foldDiacritic(cp);
      if (folded != 0) {
        m_fold_text.emplace_back(folded);
        m_fold_off.emplace_back(static_cast<int>(i));
      } else {
        const char32_t caseFolded = foldScriptCase(cp);
        if (caseFolded != cp) {
          std::array<char, 4> encoded{};
          const int encodedLen = encodeUtf8(caseFolded, encoded);
          for (int k = 0; k < encodedLen; ++k) {
            m_fold_text.emplace_back(encoded[k]);
            m_fold_off.emplace_back(static_cast<int>(i + k));
          }
        } else {
          for (int k = 0; k < len; ++k) {
            m_fold_text.emplace_back(text[i + k]);
            m_fold_off.emplace_back(static_cast<int>(i + k));
          }
        }
      }
      i += len;
    }
    m_fold_off.emplace_back(static_cast<int>(n)); // sentinel for exclusive end remap
  }

  std::string_view fold_pattern(std::string_view pattern) const {
    if (!has_non_ascii(pattern)) { return pattern; }

    m_fold_pat.clear();
    m_fold_pat.reserve(pattern.size());

    size_t i = 0;
    const size_t n = pattern.size();
    while (i < n) {
      const auto [cp, len] = decodeUtf8(pattern, i);
      const char folded = cp < 0x80 ? static_cast<char>(cp) : foldDiacritic(cp);
      if (folded != 0) {
        m_fold_pat.emplace_back(folded);
      } else {
        const char32_t caseFolded = foldScriptCase(cp);
        if (caseFolded != cp) {
          std::array<char, 4> encoded{};
          const int encodedLen = encodeUtf8(caseFolded, encoded);
          m_fold_pat.insert(m_fold_pat.end(), encoded.begin(), encoded.begin() + encodedLen);
        } else {
          for (int k = 0; k < len; ++k) {
            m_fold_pat.emplace_back(pattern[i + k]);
          }
        }
      }
      i += len;
    }
    return {m_fold_pat.data(), m_fold_pat.size()};
  }

  // Helper: Get character class
  CharClass char_class_of(char c) const {
    if (static_cast<unsigned char>(c) < 128) { return ascii_char_classes_[static_cast<unsigned char>(c)]; }
    // For non-ASCII, fall back to simple classification
    if (std::isspace(static_cast<unsigned char>(c))) return CharClass::White;
    if (std::isdigit(static_cast<unsigned char>(c))) return CharClass::Number;
    if (std::islower(static_cast<unsigned char>(c))) return CharClass::Lower;
    if (std::isupper(static_cast<unsigned char>(c))) return CharClass::Upper;
    if (std::isalpha(static_cast<unsigned char>(c))) return CharClass::Letter;
    return CharClass::NonWord;
  }

  // Calculate bonus for character class transition
  int bonus_for(CharClass prev_class, CharClass class_) const {
    if (class_ > CharClass::NonWord) {
      switch (prev_class) {
      case CharClass::White:
        return m_bonus_boundary_white;
      case CharClass::Delimiter:
        return m_bonus_boundary_delimiter;
      case CharClass::NonWord:
        return BONUS_BOUNDARY;
      default:
        break;
      }
    }

    // CamelCase or letter123
    if ((prev_class == CharClass::Lower && class_ == CharClass::Upper) ||
        (prev_class != CharClass::Number && class_ == CharClass::Number)) {
      return BONUS_CAMEL123;
    }

    if (class_ == CharClass::NonWord || class_ == CharClass::Delimiter) { return BONUS_NON_WORD; }
    if (class_ == CharClass::White) { return m_bonus_boundary_white; }
    return 0;
  }

  // ASCII fast path to find pattern boundaries
  std::pair<int, int> ascii_fuzzy_index(std::string_view text, std::string_view pattern) const {
    if (pattern.empty()) return {0, static_cast<int>(text.length())};

    int first_idx = 0;
    int last_idx = 0;
    size_t idx = 0;

    for (size_t pidx = 0; pidx < pattern.length(); ++pidx) {
      char p = pattern[pidx];
      char p_lower = std::tolower(static_cast<unsigned char>(p));
      char p_upper = std::toupper(static_cast<unsigned char>(p));

      // Find next occurrence in either case
      size_t lower_pos = text.find(p_lower, idx);
      size_t upper_pos = text.find(p_upper, idx);
      size_t found = std::min(lower_pos, upper_pos);

      if (found == std::string_view::npos) { return {-1, -1}; }

      if (pidx == 0 && found > 0) { first_idx = static_cast<int>(found) - 1; }
      last_idx = static_cast<int>(found);
      idx = found + 1;
    }

    // Find last occurrence of last character
    char last_lower = std::tolower(static_cast<unsigned char>(pattern.back()));
    char last_upper = std::toupper(static_cast<unsigned char>(pattern.back()));

    for (int i = static_cast<int>(text.length()) - 1; i > last_idx; --i) {
      char c = text[i];
      if (c == last_lower || c == last_upper) { return {first_idx, i + 1}; }
    }

    return {first_idx, last_idx + 1};
  }

  size_t cacheKey(std::string_view txt, std::string_view pattern) const {
    std::string s;
    s.reserve(txt.size() + pattern.size() + 1);
    std::ranges::copy(txt, std::back_inserter(s));
    std::ranges::copy("\0", std::back_inserter(s));
    std::ranges::copy(pattern, std::back_inserter(s));
    return std::hash<std::string>()(s);
  }

  Scheme m_scheme_;
  int m_bonus_boundary_white;
  int m_bonus_boundary_delimiter;
  std::string m_delimiter_chars;
  CharClass m_initial_char_class;
  std::array<CharClass, 128> ascii_char_classes_;
  std::array<std::array<int, 8>, 8> bonus_matrix_;

  // we reuse these buffers across search in order to not allocate on every search
  // this is what makes this matcher non thread safe,

  mutable std::vector<int> H0; // Score for first pattern char
  mutable std::vector<int> C0; // Consecutive count for first char
  mutable std::vector<int> B;  // Bonus for each position
  mutable std::vector<int> F;  // First occurrence of each pattern char
  mutable std::vector<char> T; // Normalized text
  mutable std::vector<int> H;  // Score matrix
  mutable std::vector<int> C;  // Consecutive match matrix

  mutable std::vector<char> m_fold_text; // folded text
  mutable std::vector<int> m_fold_off;   // source byte offset per folded byte (+1 sentinel)
  mutable std::vector<char> m_fold_pat;  // folded pattern
  mutable std::string m_translit_pattern;
};

/**
 * Returns a thread-local default matcher.
 *
 * The matcher reuses internal vectors across calls, so it is not safe to share
 * across threads. Each thread gets its own instance via this accessor.
 */
inline const Matcher &threadLocalMatcher() {
  thread_local const Matcher matcher{};
  return matcher;
}

inline Query::Query(std::string_view text, const Matcher &matcher) : text(text) {
  for (auto &&part : std::views::split(std::string_view{this->text}, std::string_view{" "})) {
    std::string_view const word{part};
    if (word.empty()) continue;
    words.push_back({.text = std::string(word), .self = matcher.match(word, word).score});
  }
}

} // namespace fzf
