import stringWidth from 'string-width';
import { tokenize, styledCharsFromTokens, } from '@alcalzone/ansi-tokenize';
import { DataLimitedLruMap } from './data-limited-lru-map.js';
const defaultStringWidth = stringWidth;
let currentStringWidth = defaultStringWidth;
// This cache must be cleared each time the string width function is changed.
// The strings passed as input are single characters so there is no need to
// limit the size of the cache as there are only a limited number of valid
// characters.
const widthCache = new Map();
// This cache can persist for the lifetime of the application.
// The keys for this cache can be very large so we need to limit the size
// of the data cached as well as the number of keys cached to prevent
// memory issues.
const toStyledCharactersCache = new DataLimitedLruMap(10_000, 1_000_000);
export function setStringWidthFunction(fn) {
    currentStringWidth = fn;
    // Clear the width cache to avoid stale values.
    clearStringWidthCache();
}
export function clearStringWidthCache() {
    widthCache.clear();
}
export function toStyledCharacters(text) {
    const cached = toStyledCharactersCache.get(text);
    if (cached !== undefined) {
        return cached;
    }
    const tokens = tokenize(text);
    const characters = styledCharsFromTokens(tokens);
    const combinedCharacters = [];
    for (let i = 0; i < characters.length; i++) {
        const character = characters[i];
        if (!character) {
            continue;
        }
        if (character.value === '\t') {
            const spaceCharacter = { ...character, value: ' ' };
            combinedCharacters.push(spaceCharacter, spaceCharacter, spaceCharacter, spaceCharacter);
            continue;
        }
        if (character.value === '\b') {
            continue;
        }
        let { value } = character;
        let isCombined = false;
        const firstCodePoint = value.codePointAt(0);
        // 1. Regional Indicators (Flags)
        // These combine in pairs.
        // See: https://en.wikipedia.org/wiki/Regional_indicator_symbol
        if (firstCodePoint &&
            firstCodePoint >= 0x1_f1_e6 &&
            firstCodePoint <= 0x1_f1_ff &&
            i + 1 < characters.length) {
            const nextCharacter = characters[i + 1];
            if (nextCharacter) {
                const nextFirstCodePoint = nextCharacter.value.codePointAt(0);
                if (nextFirstCodePoint &&
                    nextFirstCodePoint >= 0x1_f1_e6 &&
                    nextFirstCodePoint <= 0x1_f1_ff) {
                    value += nextCharacter.value;
                    i++;
                    combinedCharacters.push({ ...character, value });
                    continue;
                }
            }
        }
        // 2. Other combining characters
        // See: https://en.wikipedia.org/wiki/Combining_character
        while (i + 1 < characters.length) {
            const nextCharacter = characters[i + 1];
            if (!nextCharacter) {
                break;
            }
            const codePoints = [...nextCharacter.value].map(char => char.codePointAt(0));
            const nextFirstCodePoint = codePoints[0];
            if (!nextFirstCodePoint) {
                break;
            }
            // Unicode Mark category includes:
            // - Combining Diacritical Marks (U+0300-036F)
            // - Thai combining characters (U+0E31-0E3A, U+0E47-0E4E)
            // - Variation selectors (U+FE00-FE0F)
            // - Combining enclosing keycap (U+20E3)
            // - And many other combining marks across Unicode
            const isUnicodeMark = /\p{Mark}/u.test(nextCharacter.value);
            // Skin tone modifiers (emoji modifiers, not in Mark category)
            const isSkinToneModifier = nextFirstCodePoint >= 0x1_f3_fb && nextFirstCodePoint <= 0x1_f3_ff;
            // Zero-width joiner (used in emoji sequences)
            const isZeroWidthJoiner = nextFirstCodePoint === 0x20_0d;
            // Tags block (U+E0000 - U+E007F, used for flag emoji)
            const isTagsBlock = nextFirstCodePoint >= 0xe_00_00 && nextFirstCodePoint <= 0xe_00_7f;
            const isCombining = isUnicodeMark || isSkinToneModifier || isZeroWidthJoiner || isTagsBlock;
            if (!isCombining) {
                break;
            }
            // Merge with previous character
            value += nextCharacter.value;
            i++; // Consume next character.
            isCombined = true;
            // If it was a ZWJ, also consume the character after it.
            if (isZeroWidthJoiner && i + 1 < characters.length) {
                const characterAfterZwj = characters[i + 1];
                if (characterAfterZwj) {
                    value += characterAfterZwj.value;
                    i++; // Consume character after ZWJ.
                }
            }
        }
        if (isCombined) {
            combinedCharacters.push({ ...character, value });
        }
        else {
            combinedCharacters.push(character);
        }
    }
    toStyledCharactersCache.set(text, combinedCharacters);
    return combinedCharacters;
}
export function styledCharsWidth(styledChars) {
    let length = 0;
    for (const char of styledChars) {
        length += inkCharacterWidth(char.value);
    }
    return length;
}
export function inkCharacterWidth(text) {
    const width = widthCache.get(text);
    if (width !== undefined) {
        return width;
    }
    let calculatedWidth;
    try {
        calculatedWidth = currentStringWidth(text);
    }
    catch {
        // Ignore errors and use default width of 1.
        // We catch this result to avoid throwing exceptions repeatedly.
        calculatedWidth = 1;
        console.warn(`Failed to calculate string width for ${JSON.stringify(text)}`);
    }
    widthCache.set(text, calculatedWidth);
    return calculatedWidth;
}
export function wordBreakStyledChars(styledChars) {
    const words = [];
    let currentWord = [];
    for (const char of styledChars) {
        if (char.value === '\n' || char.value === ' ') {
            if (currentWord.length > 0) {
                words.push(currentWord);
            }
            currentWord = [];
            words.push([char]);
        }
        else {
            currentWord.push(char);
        }
    }
    if (currentWord.length > 0) {
        words.push(currentWord);
    }
    return words;
}
export function splitStyledCharsByNewline(styledChars) {
    const lines = [[]];
    for (const char of styledChars) {
        if (char.value === '\n') {
            lines.push([]);
        }
        else {
            lines.at(-1).push(char);
        }
    }
    return lines;
}
export function widestLineFromStyledChars(lines) {
    let maxWidth = 0;
    for (const line of lines) {
        maxWidth = Math.max(maxWidth, styledCharsWidth(line));
    }
    return maxWidth;
}
export function styledCharsToString(styledChars) {
    let result = '';
    for (const char of styledChars) {
        result += char.value;
    }
    return result;
}
export function measureStyledChars(styledChars) {
    if (styledChars.length === 0) {
        return {
            width: 0,
            height: 0,
        };
    }
    const lines = splitStyledCharsByNewline(styledChars);
    const width = widestLineFromStyledChars(lines);
    const height = lines.length;
    const dimensions = { width, height };
    return dimensions;
}
/**
 * Calculate row and column position at a given character offset.
 * This is the unified cursor position calculation logic used by both
 * render-node-to-output.ts and output.ts.
 *
 * The character offset counting method matches CharOffsetMap used in
 * selection.ts and squash-text-nodes.ts, ensuring consistent behavior
 * between cursor positioning and text selection.
 *
 * Character counting rules:
 * - Each StyledChar counts by its value.length (handles combining marks)
 * - Newlines ('\n') advance row and reset column
 * - Other characters add their visual width to column
 */
export function getPositionAtOffset(styledChars, targetOffset) {
    let row = 0;
    let col = 0;
    let charCount = 0;
    for (const char of styledChars) {
        if (charCount >= targetOffset) {
            break;
        }
        if (char.value === '\n') {
            row++;
            col = 0;
        }
        else {
            col += inkCharacterWidth(char.value);
        }
        charCount += char.value.length;
    }
    return { row, col };
}
//# sourceMappingURL=measure-text.js.map