import { assert } from "../../assert.js";
import { BitSet } from "../../binary/BitSet.js";
import { max2 } from "../../math/max2.js";
import { min2 } from "../../math/min2.js";

/**
 * Calculate Jaro distance between two strings, this is a measure of string similarity.
 * Higher value means more similarity.
 * @param {string} first
 * @param {string} second
 * @param {number} first_length
 * @param {number} second_length
 * @return {number}
 */
export function string_jaro_distance(
    first, second,
    first_length, second_length
) {
    assert.isString(first,'first');
    assert.isString(second,'second');

    // TODO can do better for short strings under 32 characters by using int32 bitmask. This would avoid allocation entirely
    const matches1 = BitSet.fixedSize(first_length);
    const matches2 = BitSet.fixedSize(second_length);

    const matches = getMatching(first, second, matches1, matches2);

    if (matches <= 0) {
        return 0;
    }

    const transpositions = getTranspositions(first, second, matches1, matches2);
    return (matches / first_length + matches / second_length + (matches - transpositions) / matches) / 3;

}

/**
 * Find matching characters in both strings according to Jaro algorithm
 * @param {string} a1
 * @param {string} a2
 * @param {BitSet} matches1
 * @param {BitSet} matches2
 * @return {number}
 */
function getMatching(a1, a2, matches1, matches2) {
    const a1_length = a1.length;
    const a2_length = a2.length;

    // Window is modified to work with string of length 1
    const matchWindow = max2(
        0,
        Math.floor(max2(a1_length, a2_length) * 0.5) - 1
    );

    let matches = 0;

    // Loop to find matched characters:
    for (let index1 = 0; index1 < a1_length; index1++) {

        // Use the highest of the window diff and the min of the window and string 2 length:
        const start = max2(0, index1 - matchWindow);
        const end = min2(index1 + matchWindow + 1, a2_length);

        // Iterate second string index:
        for (let index2 = start; index2 < end; index2++) {

            // If second string character already matched, skip:
            if (matches2.get(index2)) {
                continue;
            }

            // If the characters don't match, skip:
            if (a1.charAt(index1) !== a2.charAt(index2)) {
                continue;
            }

            // Assume match if the above 2 checks don't continue:
            matches1.set(index1, true);
            matches2.set(index2, true);

            // Add matches by 1, break inner loop:
            ++matches;
            break;
        }
    }

    return matches;
}

/**
 * Calculate the number of transpositions between the two words
 * @param {string} a1 The first string to compare
 * @param {string} a2 The second string to compare
 * @param {BitSet} matches1
 * @param {BitSet} matches2
 * @returns {number}
 */
function getTranspositions(
    a1, a2,
    matches1, matches2
) {
    let transpositions = 0;

    // Loop to find transpositions:
    const a1_length = a1.length;
    const a2_length = a2.length;

    for (let i1 = 0, i2 = 0; i1 < a1_length; i1++) {
        // If a non-matching character was found, skip:
        if (matches1.get(i1) === false) {
            continue;
        }

        // Move i2 index to the next match:
        while (
            i2 < a2_length
            && matches2.get(i2) === false
            ) {
            i2++;
        }

        // If the characters don't match, increase transposition:
        if (a1.charAt(i1) !== a2.charAt(i2)) {
            transpositions++;
        }

        // Iterate i2 index normally:
        i2++;
    }

    return Math.floor(transpositions * 0.5);
}