"""
generate_patterns.py
Genere des motifs de cesure (patterns TeX, algorithme de Liang) pour le
malgache a partir d'un corpus de mots annotes au format "ma1la2ga3sy".

Convention d'annotation :
  - un chiffre entre deux lettres = point de cesure autorise, pondere
    par sa valeur
  - chiffres impairs = coupure encouragee, chiffres pairs = coupure
    interdite (convention Liang standard, cf. hyphen.tex / patgen)

Sorties :
  - tex/hyph-mg.tex        : motifs bruts (\\patterns{...})
  - tex/hyphmg-test.ini     : format INITEX autonome pour les tests
                              (test/*.tex), avec \\newlanguage dynamique.
                              La PRODUCTION utilise \\addlanguage via
                              l'integration hyph-utf8, pas ce fichier.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import NamedTuple


# ---------------------------------------------------------------------------
# Parsing partage de la notation Liang
# ---------------------------------------------------------------------------

def parse_liang_notation(raw: str) -> tuple[str, list[int]]:
    """Decompose une chaine annotee a la Liang (ex: 'ma1la2ga3sy' ou un
    motif de PatternRule) en (lettres_seules, vecteur_de_poids)."""
    letters = re.sub(r"\d", "", raw)
    values = [0] * (len(letters) + 1)
    pos, i = 0, 0
    while i < len(raw):
        ch = raw[i]
        if ch.isdigit():
            values[pos] = int(ch)
            i += 1
        else:
            pos += 1
            i += 1
    return letters, values


# ---------------------------------------------------------------------------
# Modeles de donnees
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class AnnotatedWord:
    """Un mot malgache annote avec ses points de cesure (notation Liang)."""
    raw: str  # ex: "ma1la2ga3sy"

    def letters(self) -> str:
        return parse_liang_notation(self.raw)[0]

    def hyphen_values(self) -> list[int]:
        return parse_liang_notation(self.raw)[1]


@dataclass
class PatternRule:
    """Un motif de cesure au format TeX, ex: 'ndr1a' ou '.ma1la'."""
    pattern: str
    source_words: set[str] = field(default_factory=set)

    def to_tex(self) -> str:
        return self.pattern


class ClusterViolation(NamedTuple):
    """Une coupure encouragee trouvee a l'interieur d'un cluster protege."""
    pattern: str
    cluster: str
    position: int  # index (en lettres) ou le cluster commence


class OverrideContradiction(NamedTuple):
    """Un override manuel qui contredit un motif regenere depuis le corpus
    pour la meme sequence de lettres."""
    override_pattern: str
    corpus_pattern: str
    position: int
    override_value: int
    corpus_value: int


# ---------------------------------------------------------------------------
# Extraction des sous-motifs (algorithme de Liang simplifie)
# ---------------------------------------------------------------------------

class PatternExtractor:
    """
    Extrait, a partir de mots annotes, un ensemble de motifs de cesure.

    Deux modes, choisis via whole_word_only :

    - whole_word_only=True (DEFAUT, sur depuis le 2026-09-02) : emet un
      seul motif par mot, ancre aux deux bouts par le caractere de
      frontiere (".mot."). Un tel motif ne peut matcher QUE ce mot
      exact dans son integralite -- aucune contamination croisee
      possible entre mots. Contrepartie assumee : generalisation NULLE
      a des mots absents du corpus (ils restent simplement non
      hyphenes, ce qui est le comportement de repli sur si TeX ne
      trouve aucun motif).

    - whole_word_only=False (ANCIEN COMPORTEMENT, DANGEREUX EN L'ETAT) :
      extrait toutes les sous-chaines de longueur min_len a max_len.
      CONFIRME PAR EXECUTION REELLE (2026-09-02) : provoque une
      contamination croisee -- des mots absents du corpus se
      retrouvent hyphenes a tort par coincidence de sous-chaines
      courtes (ex. "malagasy" -> "ma-la-ga-sy" via un fragment "la"
      extrait de "vola"). A ne reactiver qu'apres implementation d'un
      vrai algorithme d'elagage (patgen ou equivalent) -- voir
      docs/methodology.md section 4.
    """

    def __init__(
        self, min_len: int = 2, max_len: int = 6, boundary_char: str = ".",
        whole_word_only: bool = True,
    ) -> None:
        self.min_len = min_len
        self.max_len = max_len
        self.boundary_char = boundary_char
        self.whole_word_only = whole_word_only
        self._patterns: dict[str, PatternRule] = {}

    def add_word(self, word: AnnotatedWord) -> None:
        letters = self.boundary_char + word.letters() + self.boundary_char
        values = [0] + word.hyphen_values() + [0]
        n = len(letters)

        if self.whole_word_only:
            pattern_str = self._interleave(letters, values, 0, n)
            if pattern_str is not None:
                rule = self._patterns.setdefault(pattern_str, PatternRule(pattern=pattern_str))
                rule.source_words.add(word.raw)
            return

        for length in range(self.min_len, self.max_len + 1):
            for start in range(0, n - length + 1):
                substr = letters[start:start + length]
                if substr.count(self.boundary_char) > 1:
                    continue
                pattern_str = self._interleave(substr, values, start, length)
                if pattern_str is None:
                    continue
                rule = self._patterns.setdefault(pattern_str, PatternRule(pattern=pattern_str))
                rule.source_words.add(word.raw)

    @staticmethod
    def _interleave(substr: str, values: list[int], start: int, length: int) -> str | None:
        out = []
        has_info = False
        for i, ch in enumerate(substr):
            out.append(ch)
            v = values[start + i + 1]
            if v != 0:
                out.append(str(v))
                has_info = True
        return "".join(out) if has_info else None

    def patterns(self) -> list[PatternRule]:
        return sorted(self._patterns.values(), key=lambda r: r.pattern)


# ---------------------------------------------------------------------------
# Regles specifiques au malgache (clusters, morphologie)
# ---------------------------------------------------------------------------

class MalagasyPhonotactics:
    """
    Contraintes phonotactiques du malgache : clusters consonantiques
    jamais scindes. Base sur Dahl (1952) / O'Neill (2015) -- les
    consonnes prenasalisees (mp, mb, nk, ng, nd, ndr, nj, nts) et les
    digraphes/trigraphes (ts, tr, dr) forment des unites phonemiques
    uniques en attaque de syllabe.

    Le cluster "ntsar" mentionne a l'origine du projet n'a pas ete
    confirme comme unite autonome dans la litterature -- traite comme
    n+ts (deja couvert) en attendant validation par un locuteur natif
    ou l'Academia Malgache.
    """

    UNBREAKABLE_CLUSTERS: tuple[str, ...] = (
        "ts", "dr", "tr", "ndr", "nd", "mp", "mb", "nk", "ng", "nj", "nts",
    )

    KNOWN_PREFIXES: tuple[str, ...] = (
        "an", "am", "mank", "maha", "mpi", "fi", "faha", "voa", "man", "mi",
    )

    def force_cluster_integrity(self, rules: list[PatternRule]) -> list[PatternRule]:
        return [r for r in rules if not self.find_violations(r.pattern)]

    def find_violations(self, pattern: str) -> list[ClusterViolation]:
        """Renvoie toutes les violations de clusters proteges dans un
        motif OU un mot annote complet (meme format)."""
        letters_only = re.sub(r"\d", "", pattern)
        violations = []
        for cluster in self.UNBREAKABLE_CLUSTERS:
            start = 0
            while (idx := letters_only.find(cluster, start)) != -1:
                if self.has_forbidden_break(pattern, idx, len(cluster)):
                    violations.append(ClusterViolation(pattern, cluster, idx))
                start = idx + 1
        return violations

    @staticmethod
    def has_forbidden_break(pattern: str, start: int, cluster_len: int) -> bool:
        """True si un chiffre impair (coupure encouragee) tombe
        strictement entre les lettres [start, start+cluster_len) du motif."""
        letter_pos = 0
        i = 0
        while i < len(pattern):
            ch = pattern[i]
            if ch.isdigit():
                if start < letter_pos < start + cluster_len and int(ch) % 2 == 1:
                    return True
            else:
                letter_pos += 1
            i += 1
        return False


def find_override_contradictions(
    manual_overrides: list[str], regenerated_patterns: list[str]
) -> list[OverrideContradiction]:
    """Detecte les cas ou un override manuel indique, pour EXACTEMENT la
    meme sequence de lettres qu'un motif du corpus, une decision de
    coupure de parite opposee.

    Limite assumee : seules les correspondances lettre-pour-lettre
    exactes sont comparees ; un chevauchement partiel (override sur une
    sous-chaine plus longue/courte) n'est pas detecte. Voir
    docs/methodology.md.
    """
    corpus_by_letters: dict[str, list[int]] = {}
    for p in regenerated_patterns:
        letters, values = parse_liang_notation(p)
        corpus_by_letters[letters] = values

    contradictions = []
    for override in manual_overrides:
        letters, override_values = parse_liang_notation(override)
        corpus_values = corpus_by_letters.get(letters)
        if corpus_values is None:
            continue
        for pos, (ov, cv) in enumerate(zip(override_values, corpus_values)):
            if ov == 0 or cv == 0:
                continue
            if (ov % 2) != (cv % 2):
                contradictions.append(OverrideContradiction(override, letters, pos, ov, cv))
    return contradictions


# ---------------------------------------------------------------------------
# Chargement corpus / overrides
# ---------------------------------------------------------------------------

def load_annotated_words(path: Path) -> list[AnnotatedWord]:
    words = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.split("#", 1)[0].strip()
        if line:
            words.append(AnnotatedWord(raw=line))
    return words


def load_manual_overrides(path: Path) -> list[str]:
    if not path.exists():
        return []
    overrides = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.split("#", 1)[0].strip()
        if line:
            overrides.append(line)
    return overrides


# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------

def export_tex_patterns(patterns: list[str], output_path: Path) -> None:
    """Ecrit tex/hyph-mg.tex : motifs bruts uniquement (\\patterns{...}).
    NE PAS EDITER A LA MAIN -- voir source/manual-overrides.txt pour les
    correctifs ponctuels non encore integres au corpus."""
    lines = [
        "% Fichier genere automatiquement par source/generate_patterns.py",
        "% NE PAS EDITER A LA MAIN -- voir source/manual-overrides.txt",
        "\\patterns{%",
    ]
    lines += patterns
    lines.append("}")
    output_path.write_text("\n".join(lines), encoding="utf-8")


def export_test_format_ini(patterns: list[str], output_path: Path) -> None:
    """Ecrit tex/hyphmg-test.ini : format INITEX autonome pour les tests
    pdfTeX (test/basic.tex, test/words.tex), avec \\newlanguage
    dynamique. Valide par execution reelle : le malgache DOIT etre
    isole sur son propre \\language (plain.tex precharge l'anglais sur
    \\language0 et \\patterns{} AJOUTE au lieu de remplacer)."""
    lines = [
        "% Genere automatiquement -- format de TEST uniquement.",
        "% La production utilise \\addlanguage via l'integration hyph-utf8.",
        "\\catcode`\\@=11",
        "\\input plain",
        "\\newlanguage\\malagasylang",
        "\\language=\\malagasylang",
        "\\lefthyphenmin=2",
        "\\righthyphenmin=2",
        "\\patterns{%",
    ]
    lines += patterns
    lines += ["}", "\\patternsused=1", "\\dump"]
    output_path.write_text("\n".join(lines), encoding="utf-8")


# ---------------------------------------------------------------------------
# Point d'entree
# ---------------------------------------------------------------------------

def main() -> None:
    source = Path("source/malagasy-hyphenation.txt")
    overrides_path = Path("source/manual-overrides.txt")

    words = load_annotated_words(source)
    extractor = PatternExtractor(min_len=2, max_len=6)
    for w in words:
        extractor.add_word(w)

    phonotactics = MalagasyPhonotactics()
    rules = phonotactics.force_cluster_integrity(extractor.patterns())
    overrides = load_manual_overrides(overrides_path)

    contradictions = find_override_contradictions(overrides, [r.pattern for r in rules])
    for c in contradictions:
        print(f"[AVERTISSEMENT] override '{c.override_pattern}' contredit "
              f"le corpus '{c.corpus_pattern}' en position {c.position}")

    all_patterns = [r.pattern for r in rules] + overrides

    export_tex_patterns(all_patterns, Path("tex/hyph-mg.tex"))
    export_test_format_ini(all_patterns, Path("tex/hyphmg-test.ini"))
    print(f"{len(rules)} motif(s) du corpus + {len(overrides)} override(s) "
          f"-> tex/hyph-mg.tex + tex/hyphmg-test.ini")


if __name__ == "__main__":
    main()
