/**
 * C++ port of FZF Fuzzy Matching Algorithm (V2)
 * Original Go implementation: https://github.com/junegunn/fzf
 *
 * The port was mostly generated by Claude Sonnet 4.5, minus a few performance improvements.
 */

#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <iterator>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

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;
  // 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;
};

enum class Scheme { Default, Path, History };

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 R>
    requires std::same_as<std::ranges::range_value_t<R>, WeightedString>
  int fuzzy_match_v2_score_query(R &&weightedStrs, std::string_view query,
                                 bool case_sensitive = false) const {
    int globalScore = 0;
    int globalCount = 0;
    auto words = std::views::split(query, std::string_view{" "}) |
                 std::views::transform([](auto &&part) { return std::string_view{part}; }) |
                 std::views::filter([](auto &&s) { return !s.empty(); });

    for (auto word : words) {
      int maxScore = std::ranges::max(
          weightedStrs | std::views::transform([&](const WeightedString &str) {
            return static_cast<int>(fuzzy_match_v2(str.str, word, case_sensitive, false).score * str.weight);
          }));

      if (!maxScore) return 0;

      globalScore += maxScore;
      ++globalCount;
    }

    return globalCount != 0 ? globalScore / globalCount : 0;
  }

  int fuzzy_match_v2_score_query(std::string_view text, std::string_view query,
                                 bool case_sensitive = false) const {
    std::initializer_list<WeightedString> lst{{text, 1.0f}};
    return fuzzy_match_v2_score_query(std::views::all(lst), query, case_sensitive);
  }

  Result fuzzy_match_v2(std::string_view text, std::string_view pattern, bool case_sensitive = false,
                        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, case_sensitive);
    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] = case_sensitive ? c : 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 (optional)
    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;

      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))) {
          result.positions.push_back(min_idx + j);
          if (i == 0) break;
          i--;
        }

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

      // Positions were added in reverse order
      std::reverse(result.positions.begin(), result.positions.end());
    }

    return result;
  }

private:
  // 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,
                                        bool case_sensitive) 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
      size_t found = std::string_view::npos;
      if (case_sensitive) {
        found = text.find(p, idx);
      } else {
        // Search for both cases
        size_t lower_pos = text.find(p_lower, idx);
        size_t upper_pos = text.find(p_upper, idx);
        if (lower_pos != std::string_view::npos && upper_pos != std::string_view::npos) {
          found = std::min(lower_pos, upper_pos);
        } else if (lower_pos != std::string_view::npos) {
          found = lower_pos;
        } else {
          found = 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_char = pattern.back();
    char last_lower = std::tolower(static_cast<unsigned char>(last_char));
    char last_upper = std::toupper(static_cast<unsigned char>(last_char));

    for (int i = static_cast<int>(text.length()) - 1; i > last_idx; --i) {
      char c = text[i];
      if (c == last_char || (!case_sensitive && (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 vectors 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
};

/**
 * 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;
}

} // namespace fzf
