#!/usr/bin/env python3
"""Compara el VERSO en transliteracion: la region que el alineador principal salta.

`diff_align_surya.py` compara la prosa (translation + purport) y, en modo
`--synonyms`, la glosa palabra por palabra. En los libros por-verso —Caitanya-
caritamrta y Srimad-Bhagavatam— salta a proposito el verso, y con razon: el
repo lo formatea distinto que el impreso y alinearlo todo junto metia ruido
masivo en cada frontera.

Pero eso deja SIN COMPARAR el verso mismo, que es la parte mas delicada del
corpus: si una vocal larga se perdio ahi, nadie lo ha mirado.

Aqui se comparan solo los versos, extraidos de los dos lados:

  REPO   las lineas de cita (`> ...`) que NO son devanagari ni bengali. La
         transliteracion va en su propio bloque, separada del indio por una
         linea en blanco.
  SCAN   la linea que sigue al indio y precede a SYNONYMS/TRANSLATION. El
         impreso rotula los versos con «TEXT N»; ese marcador abre el bloque.

Se descarta de los dos lados lo mismo: numeros de pagina, titulillos y los
rotulos. Y se compara con la normalizacion del alineador —sin diacriticos— por
una razon: los diacriticos NO son fiables en el OCR, asi que una diferencia de
diacritico seria ruido. Lo que esto caza son consonantes y silabas cambiadas,
que es donde el OCR si acierta y donde una corrupcion del texto digital duele.

Uso: python3 diff_verso.py <libro> [--todos]
"""
import json, re, sys, unicodedata, difflib, pathlib, collections

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
SURYA = HERE / 'surya'
OUT = HERE / 'out_verso'
OUT_IND = HERE / 'out_indio'
INDIO_MODE = False

INDIO = re.compile(r'[ऀ-ॿঀ-৿]')
MARCA = re.compile(r'^(SYNONYMS|TRANSLATION|PURPORT|TEXTS?\s+\d+.*|CHAPTER\b.*)\s*$', re.I)
PAGINA = re.compile(r'^\d+$')
GLOSA = re.compile(r'\S\s*[—–]\s*\w')          # linea de glosa: guion LARGO
ANCLA = 3                                       # palabras identicas a cada lado

# Pares de letras que el OCR confunde en transliteracion. Una diferencia que se
# explica SOLO por uno de estos no es una divergencia del texto: es el escaneo.
CONFUSAS = [('y', 'v'), ('i', 'l'), ('c', 'e'), ('n', 'u'), ('o', '0'),
            ('rn', 'm'), ('t', 'r'), ('s', 'a'), ('h', 'b'), ('d', 'cl')]


def solo_confusion(a, b):
    """True si `a` y `b` solo difieren por una confusion tipica del OCR."""
    if a == b: return True
    if abs(len(a) - len(b)) > 1: return False
    for x, y in CONFUSAS:
        if a.replace(x, y) == b.replace(x, y): return True
        if a.replace(y, x) == b.replace(y, x): return True
    # una sola letra distinta, misma longitud
    if len(a) == len(b):
        d = [(u, v) for u, v in zip(a, b) if u != v]
        if len(d) == 1 and tuple(sorted(d[0])) in {tuple(sorted(p)) for p in CONFUSAS}:
            return True
    return False


def sd(s):
    s = unicodedata.normalize('NFD', s or '')
    s = ''.join(c for c in s if unicodedata.category(c) != 'Mn')
    return re.sub(r'[^A-Za-z0-9]+', ' ', s.lower()).split()


def indio_repo(md):
    """Las lineas en devanagari/bengali del repo, sin puntuacion latina.

    Se comparan como CADENA DE CARACTERES, no por palabras: el escaneo junta el
    verso entero en un renglon y el repo lo parte en dos, asi que a nivel de
    palabra no alinearia nada. A nivel de caracter, si."""
    out = []
    for l in md.splitlines():
        s = l.strip()
        if s.startswith('>') and INDIO.search(s):
            out.append(re.sub(r'[^\u0900-\u097F\u0980-\u09FF]+', '', s))
    return out


def indio_scan(txt):
    out = []
    for l in txt.splitlines():
        if INDIO.search(l):
            out.append(re.sub(r'[^\u0900-\u097F\u0980-\u09FF]+', '', l))
    return out


def verso_repo(md):
    """Las lineas de cita en transliteracion. El indio queda fuera: vive en otra
    columna de D1 y el OCR no lo lee de forma comparable."""
    out = []
    for l in md.splitlines():
        s = l.strip()
        if not s.startswith('>'): continue
        s = s.lstrip('> ').rstrip('\\').strip()
        if not s or INDIO.search(s): continue
        if GLOSA.search(s): continue            # alguna glosa se cuela en cita
        out.append(s)
    return out


def verso_scan(txt):
    """Las lineas de verso de una pagina del escaneo.

    Maquina de estados: «TEXT N» o una linea en indio abren el verso; SYNONYMS,
    TRANSLATION o PURPORT lo cierran. Sin esto entra la prosa entera."""
    out = []
    modo = False
    lineas = txt.splitlines()
    for i, l in enumerate(lineas):
        s = l.strip()
        if not s: continue
        if PAGINA.match(s): continue
        if MARCA.match(s):
            modo = bool(re.match(r'^TEXTS?\s+\d+', s, re.I))
            continue
        if INDIO.search(s):
            modo = True                          # tras el indio viene el verso
            continue
        if not modo: continue
        if GLOSA.search(s): modo = False; continue
        out.append(s)
    return out


def candidatos(libro):
    carpeta = REPO / libro
    ficheros = sorted(carpeta.rglob('*.md'))
    # el orden del repo tiene que seguir el del impreso; se usa el mismo criterio
    # que el alineador: por segmento y dentro de el por numero natural
    def clave(p):
        n = [int(x) for x in re.findall(r'\d+', str(p.relative_to(carpeta)))]
        return (str(p.parent), n)
    ficheros.sort(key=clave)

    rep = []; mapa = []
    for f in ficheros:
        lineas = (indio_repo if INDIO_MODE else verso_repo)(f.read_text(encoding='utf-8'))
        for l in lineas:
            # en modo indio se compara caracter a caracter; en el otro, palabra
            for w in (list(l) if INDIO_MODE else sd(l)):
                rep.append(w); mapa.append(str(f.relative_to(REPO)))

    esc = []; pag = []
    for d in sorted((SURYA / libro).glob('*')):
        if not d.is_dir(): continue
        for p in sorted(d.glob('p*.surya.txt')):
            lineas = (indio_scan if INDIO_MODE else verso_scan)(p.read_text(encoding='utf-8'))
            for l in lineas:
                for w in (list(l) if INDIO_MODE else sd(l)):
                    esc.append(w); pag.append(f'{d.name}#p{p.stem[1:5]}')

    print(f'  {libro}: {len(rep)} palabras de verso en el repo | {len(esc)} en el escaneo')
    if not rep or not esc: return []

    sm = difflib.SequenceMatcher(None, rep, esc, autojunk=False)
    # frecuencia de cada insercion, para reconocer las cabeceras
    heads = collections.Counter()
    for op, i1, i2, j1, j2 in sm.get_opcodes():
        if op == 'insert': heads[' '.join(esc[j1:j2])] += 1
    out = []
    ops = sm.get_opcodes()
    for k, (op, i1, i2, j1, j2) in enumerate(ops):
        if op == 'equal': continue
        # ANCLA: exigir contexto identico a los dos lados, o es ruido de borde
        izq = ops[k - 1] if k else None
        der = ops[k + 1] if k + 1 < len(ops) else None
        if not izq or izq[0] != 'equal' or izq[2] - izq[1] < ANCLA: continue
        if not der or der[0] != 'equal' or der[4] - der[3] < ANCLA: continue
        if (i2 - i1) > 8 or (j2 - j1) > 8: continue      # tramos largos: ruido
        sep = '' if INDIO_MODE else ' '
        r = sep.join(rep[i1:i2]); e = sep.join(esc[j1:j2])
        # SEPARACION DE PALABRA. El repo escribe los compuestos con guion
        # ('sva-janam') y el escaneo los entrega juntos ('svajanam'); al
        # normalizar, el guion se vuelve espacio y parecen distintos. Si quitando
        # los espacios coinciden, no hay diferencia de texto.
        if r.replace(' ', '') == e.replace(' ', ''): continue
        # TITULILLO del impreso metido en el verso. Se reconocen por frecuencia:
        # un texto que el escaneo inserta en muchos versos distintos es la
        # cabecera de la pagina, no texto del verso.
        if not r and heads.get(e, 0) >= 3: continue
        if re.search(r'contents of the|summary|^ch \d|^teext|^\d+$', e): continue
        # CONFUSION DEL OCR: 'yajne' -> 'vajne', 'nanyad' -> 'nanvad'. No es una
        # divergencia del texto, es como leyo el escaneo.
        if r and e and solo_confusion(r.replace(' ', ''), e.replace(' ', '')): continue
        out.append({'file': mapa[i1] if i1 < len(mapa) else (mapa[-1] if mapa else ''),
                    'page': pag[j1] if j1 < len(pag) else (pag[-1] if pag else ''),
                    'repo': r, 'scan': e,
                    'context': sep.join(rep[max(0, i1 - 12):i1]) + '[[' + r + ']]'
                               + sep.join(rep[i2:i2 + 12]),
                    'op': op, 'region': 'indio' if INDIO_MODE else 'verso'})
    return out


def main():
    global INDIO_MODE
    if len(sys.argv) < 2: print(__doc__); return 1
    INDIO_MODE = '--indio' in sys.argv
    destino = OUT_IND if INDIO_MODE else OUT
    destino.mkdir(exist_ok=True)
    libros = ([d.name for d in SURYA.iterdir() if d.is_dir() and (REPO / d.name).exists()]
              if sys.argv[1] == '--todos' else [sys.argv[1]])
    tot = 0
    for b in sorted(libros):
        c = candidatos(b)
        if c:
            with open(destino / f'{b}.candidates.jsonl', 'w', encoding='utf-8') as fh:
                for x in c: fh.write(json.dumps(x, ensure_ascii=False) + '\n')
        print(f'     -> {len(c)} candidatos de verso')
        tot += len(c)
    print(f'\ntotal: {tot}')
    return 0


if __name__ == '__main__':
    sys.exit(main())
