/*
|--------------------------------------------------------------------------
| Découpage d'un message en jetons
|--------------------------------------------------------------------------
|
| Brique de base du correcteur : toutes les autres couches raisonnent sur des
| jetons positionnés, jamais sur des `split(' ')`.
|
| POURQUOI un scanner maison plutôt qu'un `text.split(/\W+/)` :
|   - `\W` est ASCII, il couperait « hôtel » en « h » + « tel » ;
|   - on a besoin des positions exactes (start/end) pour appliquer les
|     corrections sans réécrire le message à l'aveugle ;
|   - le français colle des apostrophes et des traits d'union AU MILIEU des
|     mots (« l'hôtel », « aujourd'hui », « rendez-vous ») : ces caractères ne
|     doivent couper un mot que lorsqu'ils ne sont pas entourés de lettres.
*/

export interface Token {
    /** Texte exact du jeton, tel qu'il apparaît dans le message. */
    text: string;
    /** Index du premier caractère dans le texte d'origine. */
    start: number;
    /** Index du caractère qui suit le jeton (exclu) : `text.slice(start, end) === token.text`. */
    end: number;
    /** `true` pour un mot, `false` pour un bloc de ponctuation / d'espaces. */
    isWord: boolean;
}

/**
 * Caractères qui composent un mot : toutes les lettres Unicode (donc « É », « à », « ç »)
 * et les chiffres. Le drapeau `u` est indispensable — sans lui, `\p{L}` n'est pas reconnu.
 */
const WORD_CHARACTER = /[\p{L}\p{N}]/u;

/**
 * Caractères qui peuvent se trouver À L'INTÉRIEUR d'un mot.
 *
 * POURQUOI deux apostrophes : les claviers physiques produisent « ' » (U+0027), iOS et les
 * traitements de texte produisent « ’ » (U+2019). Les deux arrivent dans la même conversation,
 * parfois dans le même message copié-collé depuis Airbnb.
 */
const INNER_JOINER = /['’‘ʼ-]/;

function isWordCharacter(character: string): boolean {
    return character !== '' && WORD_CHARACTER.test(character);
}

/**
 * Découpe `text` en jetons contigus : la concaténation des `token.text` redonne `text`
 * caractère pour caractère (aucune perte, aucun ajout).
 */
export function tokenize(text: string): Token[] {
    const tokens: Token[] = [];
    let index = 0;

    while (index < text.length) {
        const start = index;

        if (isWordCharacter(text[index])) {
            index += 1;

            while (index < text.length) {
                if (isWordCharacter(text[index])) {
                    index += 1;
                    continue;
                }

                // Un joncteur ne reste dans le mot que s'il est SUIVI d'une lettre : « l'hôtel »
                // reste un jeton, mais l'apostrophe finale de « les gars' » n'en fait pas partie.
                if (INNER_JOINER.test(text[index]) && isWordCharacter(text[index + 1] ?? '')) {
                    index += 2;
                    continue;
                }

                break;
            }

            tokens.push({ text: text.slice(start, index), start, end: index, isWord: true });
            continue;
        }

        while (index < text.length && !isWordCharacter(text[index])) {
            index += 1;
        }

        tokens.push({ text: text.slice(start, index), start, end: index, isWord: false });
    }

    return tokens;
}

/** Raccourci : uniquement les jetons « mot ». */
export function wordTokens(text: string): Token[] {
    return tokenize(text).filter((token) => token.isWord);
}

/**
 * Ponctuation qui ferme une phrase. Les deux-points en font partie : « Merci : Jenvois
 * l'adresse » ouvre bien une phrase, et c'est ainsi que les gérants écrivent leurs listes.
 */
const SENTENCE_TERMINATOR = /[.!?:]/u;

/**
 * `true` si le caractère d'index `index` ouvre une phrase : début du texte, ou premier
 * caractère non blanc après un `. ! ? :` ou un saut de ligne.
 *
 * POURQUOI cette fonction existe : une capitale ne dit RIEN à elle seule. « Jai bien reçu »
 * en tête de message est une élision oubliée (« J'ai ») ; « le chauffeur Sabri » au milieu
 * d'une phrase est un nom propre, et lui inventer une apostrophe donne « S'abri ». Seule la
 * position dans le texte sépare les deux, d'où ce test au niveau du texte et non du mot.
 */
export function startsSentence(text: string, index: number): boolean {
    for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
        const character = text[cursor];

        if (character === '\n' || character === '\r') {
            return true; // une nouvelle ligne vaut une nouvelle phrase, même sans ponctuation
        }

        if (character === ' ' || character === '\t') {
            continue;
        }

        return SENTENCE_TERMINATOR.test(character);
    }

    return true; // rien avant : c'est le tout début du texte
}

/**
 * `true` si le mot contient au moins une lettre latine.
 *
 * Un mot écrit entièrement dans une autre écriture — arabe, cyrillique, grec — n'a rien à
 * faire dans un dictionnaire français : il n'y sera jamais connu, et les suggestions sont
 * vides. Le signaler, c'est souligner en rouge un message arabe parfaitement écrit.
 */
export function hasLatinLetter(word: string): boolean {
    return /\p{Script=Latin}/u.test(word);
}

/**
 * Préfixes d'élision du français.
 *
 * POURQUOI ils vivent ici : l'élision est une question de STRUCTURE du mot, pas de
 * dictionnaire ni de typographie. Les deux couches s'en servent — le dictionnaire pour
 * savoir que « l'hôtel » se vérifie comme « hôtel », la typographie pour savoir que
 * « l'addresse » se corrige comme « addresse ».
 *
 * « aujourd'hui » n'est volontairement pas concerné : « aujourd » n'est pas un préfixe
 * d'élision, le mot reste donc entier.
 */
const ELISION_PREFIXES: ReadonlySet<string> = new Set([
    'c',
    'd',
    'j',
    'l',
    'm',
    'n',
    's',
    't',
    'qu',
    'jusqu',
    'lorsqu',
    'puisqu',
    'quoiqu',
    'quelqu',
]);

const APOSTROPHE = /['’‘ʼ]/u;

/**
 * « l'hôtel » → `{ prefix: "l'", rest: 'hôtel' }`. `null` si l'apostrophe n'est pas une élision
 * (« aujourd'hui ») ou si elle est en bord de mot. Les tranches renvoyées sont celles du mot
 * D'ORIGINE : l'apostrophe typographique éventuelle est préservée.
 */
export function splitElision(word: string): { prefix: string; rest: string } | null {
    const match = APOSTROPHE.exec(word);

    if (match === null || match.index === 0 || match.index === word.length - 1) {
        return null;
    }

    if (!ELISION_PREFIXES.has(word.slice(0, match.index).toLocaleLowerCase('fr'))) {
        return null;
    }

    return { prefix: word.slice(0, match.index + 1), rest: word.slice(match.index + 1) };
}
