/**
 * LICENSE
 *
 *   This software is dual-licensed to the public domain and under the following
 *   license: you are granted a perpetual, irrevocable license to copy, modify,
 *   publish, and distribute this file as you see fit.
 *
 * VERSION
 *   0.1.0  (2016-03-28)  Initial release
 *
 * AUTHOR
 *   Forrest Smith
 *
 * CONTRIBUTORS
 *   J�rgen Tjern� - async helper
 *   Anurag Awasthi - updated to 0.2.0
 */

export const DEFAULT_WEIGHTS = {
    sequentialBonus: 15, // bonus for adjacent matches
    separatorBonus: 30, // bonus if match occurs after a separator
    camelBonus: 30, // bonus if match is uppercase and prev is lower
    firstLetterBonus: 15, // bonus if the first letter is matched

    leadingLetterPenalty: -5, // penalty applied for every letter in str before the first match
    maxLeadingLetterPenalty: -15, // maximum penalty for leading letters
    unmatchedLetterPenalty: -1
};

/**
 * Does a fuzzy search to find pattern inside a string.
 * @param {string} pattern        pattern to search for
 * @param {string} str            string which is being searched
 * @param {boolean} global        whether to search for all matches or just one
 * @returns [boolean, number]       a boolean which tells if pattern was
 *                                  found or not and a search score
 */
export function fuzzyMatch(pattern, str, global=false, weights=DEFAULT_WEIGHTS) {
    const recursionCount = 0;
    const recursionLimit = 10;
    const matches = [];
    const maxMatches = 256;

    if (!global) {
        return fuzzyMatchRecursive(
            pattern,
            str,
            0 /* patternCurIndex */,
            0 /* strCurrIndex */,
            null /* srcMatches */,
            matches,
            maxMatches,
            0 /* nextMatch */,
            recursionCount,
            recursionLimit,
            weights
        );
    }

    // Return all matches
    let foundMatch = true,
        score,
        idxs,
        strCurrIndex = 0;
    const results = [];

    while (foundMatch) {
        [foundMatch, score, idxs] = fuzzyMatchRecursive(
            pattern,
            str,
            0 /* patternCurIndex */,
            strCurrIndex,
            null /* srcMatches */,
            matches,
            maxMatches,
            0 /* nextMatch */,
            recursionCount,
            recursionLimit,
            weights
        );
        if (foundMatch) results.push([foundMatch, score, [...idxs]]);
        strCurrIndex = idxs[idxs.length - 1] + 1;
    }
    return results;
}

/**
 * Recursive helper function
 */
function fuzzyMatchRecursive(
    pattern,
    str,
    patternCurIndex,
    strCurrIndex,
    srcMatches,
    matches,
    maxMatches,
    nextMatch,
    recursionCount,
    recursionLimit,
    weights
) {
    let outScore = 0;

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

    // Return if we reached ends of strings.
    if (patternCurIndex === pattern.length || strCurrIndex === str.length) {
        return [false, outScore, []];
    }

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

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

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

            const [matched, recursiveScore, recursiveMatches] = fuzzyMatchRecursive(
                pattern,
                str,
                patternCurIndex,
                strCurrIndex + 1,
                matches,
                recursiveMatches,
                maxMatches,
                nextMatch,
                recursionCount,
                recursionLimit,
                weights
            );

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

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

    const matched = patternCurIndex === pattern.length;

    if (matched) {
        outScore = 100;

        // Apply leading letter penalty
        let penalty = weights.leadingLetterPenalty * matches[0];
        penalty =
            penalty < weights.maxLeadingLetterPenalty ?
                weights.maxLeadingLetterPenalty :
                penalty;
        outScore += penalty;

        // Apply unmatched penalty
        const unmatched = str.length - nextMatch;
        outScore += weights.unmatchedLetterPenalty * 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 += weights.sequentialBonus;
                }
            }

            // Check for bonuses based on neighbor character value.
            if (currIdx > 0) {
                // Camel case
                const neighbor = str[currIdx - 1];
                const curr = str[currIdx];
                if (
                    neighbor !== neighbor.toUpperCase() &&
                    curr !== curr.toLowerCase()
                ) {
                    outScore += weights.camelBonus;
                }
                const isNeighbourSeparator = neighbor === "_" || neighbor === " ";
                if (isNeighbourSeparator) {
                    outScore += weights.separatorBonus;
                }
            } else {
                // First letter
                outScore += weights.firstLetterBonus;
            }
        }

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

/**
 * Turns a list of match indexes into a list of match ranges
 *
 * @author n1474335 [n1474335@gmail.com]
 * @param [number] matches
 * @returns [[number]]
 */
export function calcMatchRanges(matches) {
    const ranges = [];
    let start = matches[0],
        curr = start;

    matches.forEach(m => {
        if (m === curr || m === curr + 1) curr = m;
        else {
            ranges.push([start, curr - start + 1]);
            start = m;
            curr = m;
        }
    });

    ranges.push([start, curr - start + 1]);
    return ranges;
}
