/**

* Aliki Search Implementation
*
* Search algorithm with the following priorities:
* 1. Exact full_name match always wins (for namespace/method queries)
* 2. Exact name match gets high priority
* 3. Match types:
*    - Namespace queries (::) and method queries (# or .) match against full_name
*    - Regular queries match against unqualified name
*    - Prefix (10000) > substring (5000) > fuzzy (1000)
* 4. First character determines type priority:
*    - Starts with lowercase: methods first
*    - Starts with uppercase: classes/modules/constants first
* 5. Within same type priority:
*    - Unqualified match > qualified match
*    - Shorter name > longer name
* 6. Class methods > instance methods
* 7. Result limit: 30
* 8. Minimum query length: 1 character
*/

var MAX_RESULTS = 30; var MIN_QUERY_LENGTH = 1;

/*

* Scoring constants - organized in tiers where each tier dominates lower tiers.
* This ensures match type always beats type priority, etc.
*
* Tier 0: Exact matches (immediate return)
* Tier 1: Match type (prefix > substring > fuzzy)
* Tier 2: Exact name bonus
* Tier 3: Type priority (method vs class based on query case)
* Tier 4: Minor bonuses (top-level, class method, name length)
*/

var SCORE_EXACT_FULL_NAME = 1000000; // Tier 0: Query exactly matches full_name var SCORE_MATCH_PREFIX = 10000; // Tier 1: Query is prefix of name var SCORE_MATCH_SUBSTRING = 5000; // Tier 1: Query is substring of name var SCORE_MATCH_FUZZY = 1000; // Tier 1: Query chars appear in order var SCORE_EXACT_NAME = 500; // Tier 2: Name exactly equals query var SCORE_TYPE_PRIORITY = 100; // Tier 3: Preferred type (method/class) var SCORE_TOP_LEVEL = 50; // Tier 4: Top-level over namespaced var SCORE_CLASS_METHOD = 10; // Tier 4: Class method over instance method

/**

* Check if all characters in query appear in order in target
* e.g., "addalias" fuzzy matches "add_foo_alias"
*/

function fuzzyMatch(target, query) {

var ti = 0;
for (var qi = 0; qi < query.length; qi++) {
  ti = target.indexOf(query[qi], ti);
  if (ti === -1) return false;
  ti++;
}
return true;

}

/**

* Parse and normalize a search query
* @param {string} query - The raw search query
* @returns {Object} Parsed query with normalized form and flags
*/

function parseQuery(query) {

// Lowercase for case-insensitive matching (so "hash" finds both Hash class and #hash methods)
var normalized = query.toLowerCase();
var isNamespaceQuery = query.includes('::');
var isMethodQuery = query.includes('#') || query.includes('.');

// Normalize . to :: (RDoc uses :: for class methods in full_name)
if (query.includes('.')) {
  normalized = normalized.replace(/\./g, '::');
}

return {
  original: query,
  normalized: normalized,
  isNamespaceQuery: isNamespaceQuery,
  isMethodQuery: isMethodQuery,
  // Namespace and method queries match against full_name instead of name
  matchesFullName: isNamespaceQuery || isMethodQuery,
  // If query starts with lowercase, prioritize methods; otherwise prioritize classes/modules/constants
  prioritizeMethod: !/^[A-Z]/.test(query)
};

}

/**

* Main search function
* @param {string} query - The search query
* @param {Array} index - The search index to search in
* @returns {Array} Array of matching entries, sorted by relevance
*/

function search(query, index) {

if (!query || query.length < MIN_QUERY_LENGTH) {
  return [];
}

var q = parseQuery(query);
var results = [];

for (var i = 0; i < index.length; i++) {
  var entry = index[i];
  var score = computeScore(entry, q);

  if (score !== null) {
    results.push({ entry: entry, score: score });
  }
}

results.sort(function(a, b) {
  return b.score - a.score;
});

return results.slice(0, MAX_RESULTS).map(function(r) {
  return r.entry;
});

}

/**

* Compute the relevance score for an entry
* @param {Object} entry - The search index entry
* @param {Object} q - Parsed query from parseQuery()
* @returns {number|null} Score or null if no match
*/

function computeScore(entry, q) {

var name = entry.name;
var fullName = entry.full_name;
var type = entry.type;

var nameLower = name.toLowerCase();
var fullNameLower = fullName.toLowerCase();

// Exact full_name match (e.g., "Array#filter" matches Array#filter)
if (q.matchesFullName && fullNameLower === q.normalized) {
  return SCORE_EXACT_FULL_NAME;
}

var matchScore = 0;
var target = q.matchesFullName ? fullNameLower : nameLower;

if (target.startsWith(q.normalized)) {
  matchScore = SCORE_MATCH_PREFIX;     // Prefix (e.g., "Arr" matches "Array")
} else if (target.includes(q.normalized)) {
  matchScore = SCORE_MATCH_SUBSTRING;  // Substring (e.g., "ray" matches "Array")
} else if (fuzzyMatch(target, q.normalized)) {
  matchScore = SCORE_MATCH_FUZZY;      // Fuzzy (e.g., "addalias" matches "add_foo_alias")
} else {
  return null;
}

var score = matchScore;
var isMethod = (type === 'instance_method' || type === 'class_method');

if (q.prioritizeMethod ? isMethod : !isMethod) {
  score += SCORE_TYPE_PRIORITY;
}

if (type === 'class_method') score += SCORE_CLASS_METHOD;
if (name === fullName) score += SCORE_TOP_LEVEL;  // Top-level (Hash) > namespaced (Foo::Hash)
if (nameLower === q.normalized) score += SCORE_EXACT_NAME;  // Exact name match
score -= name.length;

return score;

}

/**

* SearchRanker class for compatibility with the Search UI
* Provides ready() and find() interface
*/

function SearchRanker(index) {

this.index = index;
this.handlers = [];

}

SearchRanker.prototype.ready = function(fn) {

this.handlers.push(fn);

};

SearchRanker.prototype.find = function(query) {

var q = parseQuery(query);
var rawResults = search(query, this.index);
var results = rawResults.map(function(entry) {
  return formatResult(entry, q);
});

var _this = this;
this.handlers.forEach(function(fn) {
  fn.call(_this, results, true);
});

};

/**

* Format a search result entry for display
*/

function formatResult(entry, q) {

var result = {
  title: highlightMatch(entry.full_name, q),
  path: entry.path,
  type: entry.type
};

if (entry.snippet) {
  result.snippet = entry.snippet;
}

return result;

}

/**

* Add highlight markers (\u0001 and \u0002) to matching portions of text
* @param {string} text - The text to highlight
* @param {Object} q - Parsed query from parseQuery()
*/

function highlightMatch(text, q) {

if (!text || !q) return text;

var textLower = text.toLowerCase();
var query = q.normalized;

// Try contiguous match first (prefix or substring)
var matchIndex = textLower.indexOf(query);
if (matchIndex !== -1) {
  return text.substring(0, matchIndex) +
    '\u0001' + text.substring(matchIndex, matchIndex + query.length) + '\u0002' +
    text.substring(matchIndex + query.length);
}

// Fall back to fuzzy highlight (highlight each matched character)
var result = '';
var ti = 0;
for (var qi = 0; qi < query.length; qi++) {
  var charIndex = textLower.indexOf(query[qi], ti);
  if (charIndex === -1) return text;
  result += text.substring(ti, charIndex);
  result += '\u0001' + text[charIndex] + '\u0002';
  ti = charIndex + 1;
}
result += text.substring(ti);
return result;

}