/*
 * Constants and utility functions used for searching for node types in node creator component
 * based on https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js
 */

const SEQUENTIAL_BONUS = 60; // bonus for adjacent matches
const SEPARATOR_BONUS = 38; // bonus if match occurs after a separator
const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower
const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched

const LEADING_LETTER_PENALTY = -20; // penalty applied for every letter in str before the first match
const MAX_LEADING_LETTER_PENALTY = -200; // maximum penalty for leading letters
const UNMATCHED_LETTER_PENALTY = -2.5;

export const DEFAULT_KEYS = [
	{ key: 'properties.displayName', weight: 1.3 },
	{ key: 'properties.codex.alias', weight: 1 },
];

/**
 * Returns true if each character in pattern is found sequentially within target
 * @param {*} pattern string
 * @param {*} target string
 */
function fuzzyMatchSimple(pattern: string, target: string): boolean {
	let patternIdx = 0;
	let strIdx = 0;

	while (patternIdx < pattern.length && strIdx < target.length) {
		const patternChar = pattern.charAt(patternIdx).toLowerCase();
		const targetChar = target.charAt(strIdx).toLowerCase();
		if (patternChar === targetChar) {
			patternIdx++;
		}
		++strIdx;
	}

	return pattern.length !== 0 && target.length !== 0 && patternIdx === pattern.length;
}

function fuzzyMatchRecursive(
	pattern: string,
	target: string,
	patternCurIndex: number,
	targetCurrIndex: number,
	targetMatches: null | number[],
	matches: number[],
	maxMatches: number,
	nextMatch: number,
	recursionCount: number,
	recursionLimit: number,
): { matched: boolean; outScore: number } {
	let outScore = 0;

	// Return if recursion limit is reached.
	if (++recursionCount >= recursionLimit) {
		return { matched: false, outScore };
	}

	// Return if we reached ends of strings.
	if (patternCurIndex === pattern.length || targetCurrIndex === target.length) {
		return { matched: false, outScore };
	}

	// Recursion params
	let recursiveMatch = false;
	let bestRecursiveMatches: number[] = [];
	let bestRecursiveScore = 0;

	// Loop through pattern and str looking for a match.
	let firstMatch = true;
	while (patternCurIndex < pattern.length && targetCurrIndex < target.length) {
		// Match found.
		if (pattern[patternCurIndex].toLowerCase() === target[targetCurrIndex].toLowerCase()) {
			if (nextMatch >= maxMatches) {
				return { matched: false, outScore };
			}

			if (firstMatch && targetMatches) {
				matches = [...targetMatches];
				firstMatch = false;
			}

			const recursiveMatches: number[] = [];
			const recursiveResult = fuzzyMatchRecursive(
				pattern,
				target,
				patternCurIndex,
				targetCurrIndex + 1,
				matches,
				recursiveMatches,
				maxMatches,
				nextMatch,
				recursionCount,
				recursionLimit,
			);

			const recursiveScore = recursiveResult.outScore;
			if (recursiveResult.matched) {
				// Pick best recursive score.
				if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
					bestRecursiveMatches = [...recursiveMatches];
					bestRecursiveScore = recursiveScore;
				}
				recursiveMatch = true;
			}

			matches[nextMatch++] = targetCurrIndex;
			++patternCurIndex;
		}
		++targetCurrIndex;
	}

	const matched = patternCurIndex === pattern.length;

	if (matched) {
		outScore = 100;

		// Apply leading letter penalty (if not n8n-prefixed)
		if (!target.toLowerCase().startsWith('n8n')) {
			let penalty = LEADING_LETTER_PENALTY * matches[0];
			penalty = penalty < MAX_LEADING_LETTER_PENALTY ? MAX_LEADING_LETTER_PENALTY : penalty;
			outScore += penalty;
		}

		//Apply unmatched penalty
		const unmatched = target.length - nextMatch;
		outScore += UNMATCHED_LETTER_PENALTY * unmatched;

		// Apply ordering bonuses
		for (let i = 0; i < nextMatch; i++) {
			const currIdx = matches[i];

			if (i > 0) {
				const prevIdx = matches[i - 1];
				if (currIdx === prevIdx + 1) {
					outScore += SEQUENTIAL_BONUS;
				}
			}

			// Check for bonuses based on neighbor character value.
			if (currIdx > 0) {
				// Camel case
				const neighbor = target[currIdx - 1];
				const curr = target[currIdx];
				if (neighbor !== neighbor.toUpperCase() && curr !== curr.toLowerCase()) {
					outScore += CAMEL_BONUS;
				}
				const isNeighbourSeparator = neighbor === '_' || neighbor === ' ';
				if (isNeighbourSeparator) {
					outScore += SEPARATOR_BONUS;
				}
			} else {
				// First letter
				outScore += FIRST_LETTER_BONUS;
			}
		}

		// Return best result
		if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
			// Recursive score is better than "this"
			matches = [...bestRecursiveMatches];
			outScore = bestRecursiveScore;
			return { matched: true, outScore };
		} else if (matched) {
			// "this" score is better than recursive
			return { matched: true, outScore };
		} else {
			return { matched: false, outScore };
		}
	}
	return { matched: false, outScore };
}

/**
 * Does a fuzzy search to find pattern inside a string.
 * @param {*} pattern string        pattern to search for
 * @param {*} target     string        string which is being searched
 * @returns [boolean, number]       a boolean which tells if pattern was
 *                                  found or not and a search score
 */
function fuzzyMatch(pattern: string, target: string): { matched: boolean; outScore: number } {
	const recursionCount = 0;
	const recursionLimit = 5;
	const matches: number[] = [];
	const maxMatches = 256;

	return fuzzyMatchRecursive(
		pattern,
		target,
		0 /* patternCurIndex */,
		0 /* strCurrIndex */,
		null /* srcMatces */,
		matches,
		maxMatches,
		0 /* nextMatch */,
		recursionCount,
		recursionLimit,
	);
}

// prop = 'key'
// prop = 'key1.key2'
// prop = ['key1', 'key2']
function getValue<T extends object>(obj: T, prop: string): unknown {
	if (obj.hasOwnProperty(prop)) {
		return obj[prop as keyof T];
	}

	const segments = prop.split('.');

	let result = obj;
	let i = 0;
	while (result && i < segments.length) {
		const key = segments[i] as keyof T;
		result = result[key] as T;
		i++;
	}
	return result;
}

function scoreSearchValue(filter: string, value: string, weight: number): number | undefined {
	if (!fuzzyMatchSimple(filter, value)) return undefined;

	const match = fuzzyMatch(filter, value);
	if (!match.matched) return undefined;

	return match.outScore * weight;
}

export function sublimeSearch<T extends object>(
	filter: string,
	data: readonly T[],
	keys: Array<{ key: string; weight: number }> = DEFAULT_KEYS,
	limit?: number,
): Array<{ score: number; item: T }> {
	const results: Array<{ score: number; item: T }> = [];

	for (const item of data) {
		let itemMatchScore: number | undefined;

		for (const { key, weight } of keys) {
			const value = getValue(item, key);
			if (Array.isArray(value)) {
				for (const entry of value) {
					if (typeof entry !== 'string') continue;

					const score = scoreSearchValue(filter, entry, weight);
					if (score !== undefined && (itemMatchScore === undefined || score > itemMatchScore)) {
						itemMatchScore = score;
					}
				}
			} else if (typeof value === 'string') {
				const score = scoreSearchValue(filter, value, weight);
				if (score !== undefined && (itemMatchScore === undefined || score > itemMatchScore)) {
					itemMatchScore = score;
				}
			}
		}

		if (itemMatchScore !== undefined) {
			const result: { score: number; item: T } = {
				score: itemMatchScore,
				item,
			};

			if (limit === undefined || results.length < limit) {
				results.push(result);
			} else {
				let lowestIndex = 0;
				let lowestScore = results[0].score;
				for (let i = 1; i < results.length; i++) {
					if (results[i].score < lowestScore) {
						lowestIndex = i;
						lowestScore = results[i].score;
					}
				}
				if (result.score > lowestScore) {
					results[lowestIndex] = result;
				}
			}
		}
	}

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

	return results;
}
