#!/usr/bin/env python3
"""Devuelve los saltos de linea a los versos citados dentro de los comentarios.

El problema. En 202 sitios —201 de ellos en el canto 4 del Bhagavatam— un verso
citado dentro de un purport quedo aplanado en una sola linea corrida:

    mamaivāṁśo jīva-loke jīva-bhūtaḥ sanātanaḥ manaḥ ṣaṣṭhānīndriyāṇi prakṛti-sthāni karṣati

cuando el impreso lo trae en cuatro padas. Un lector lo reporto desde
https://vedabase.cc/sb/4/29/74/ y tiene razon: se lee como una frase, no como un
sloka.

De donde salen los saltos. NO se inventan. El verso citado casi siempre ES un
verso del propio corpus —«mamaivāṁśo» es Bg 15.7, «dharmaḥ svanuṣṭhitaḥ» es
SB 1.2.8— y alli esta ya partido en padas y cotejado contra el escaneo. Se
indexan los 21.453 versos del repo por su texto aplanado y se busca. Donde la
grafia varia un poco («pitḷn» por «pitṝn») se acepta un parecido alto.

Que marcado se escribe. Un parrafo donde TODAS las lineas empiezan por «> ».
Es la unica forma que el renderizador convierte en lineas sueltas: mira
`isBlockquote` en src/pages/[...path].astro, que une las lineas con <br/>. Un
salto de linea a secas NO vale —se colapsa— y un «\\» al final se imprime tal
cual; las dos cosas estan comprobadas en la web.

La comprobacion. Antes de escribir, el texto reconstruido sin los «> » tiene que
ser IGUAL, caracter a caracter, a la linea que habia. Solo se anaden saltos.

Uso:
    python3 partir_versos_citados.py            # ensayo
    python3 partir_versos_citados.py --aplicar
"""
import difflib
import pathlib
import re
import sys
import unicodedata

REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
SALTA = ('translations', 'legacy-vedabase-site', 'letters', 'lectures-and-conversations')
# Se admiten comas y puntos: las canciones en bengali los llevan («karma-kāṇḍa,
# jñāna-kāṇḍa, kevala viṣera bhāṇḍa»), y sin ellos diez versos aplanados no se
# detectaban.
IAST = re.compile(r"^[A-Za-zĀ-ſāīūṛṝḷḹṅñṭḍṇśṣṁḥ'’,.\- ]+$")
DIAC = re.compile(r'[āīūṛṅñṭḍṇśṣṁḥ]')
ING = set('the of and to in is a that it for as with by not this his he was are '
          'or from be on all we you they many people have'.split())


def pel(s):
    s = ''.join(c for c in unicodedata.normalize('NFD', str(s)) if not unicodedata.combining(c))
    return re.sub(r'\s+', ' ', re.sub(r'[^A-Za-z ]', ' ', s)).strip().lower()


def ficheros(legacy=False):
    for f in REPO.rglob('*.md'):
        raiz = f.relative_to(REPO).parts[0]
        if legacy:
            if raiz == 'legacy-vedabase-site':
                yield f
        elif raiz not in SALTA:
            yield f


def indice(legacy=False):
    """texto aplanado del verso -> sus lineas, tal como el repo las tiene.

    Con `legacy` se lee `legacy-vedabase-site/`, el juego de texto anterior que
    el repo guarda como referencia. De ahi NO se copia una sola palabra: solo
    se mira DONDE parte las lineas, cuando el corpus propio no tiene el verso.
    El texto que se escribe sigue siendo el de la cita.
    """
    idx = {}
    for f in ficheros(legacy):
        bloque = []
        for l in f.read_text(encoding='utf-8').splitlines() + ['']:
            s = l.strip()
            if s.startswith('>') and not re.search(r'[ऀ-ॿঀ-৿]', s):
                bloque.append(s.lstrip('> ').rstrip('\\').strip())
            else:
                if len(bloque) >= 2:
                    idx.setdefault(pel(' '.join(bloque)), [x for x in bloque if x])
                bloque = []
    return idx


def colapsada(s):
    # 58 caracteres, no 85: la mitad de los casos son DOS padas en una linea,
    # no cuatro, y con el umbral alto no se veian. Por debajo de eso ya suele
    # ser un pada suelto, que esta bien como esta.
    if len(s) < 58 or s.startswith(('>', '#', '*', '|')):
        return False
    if not DIAC.search(s) or not IAST.match(s):
        return False
    pal = s.lower().split()
    # Cinco palabras es el minimo normal, pero un verso de compuestos largos
    # cabe en menos: el Siksastaka 1 son cuatro palabras de veinte letras
    # («ceto-darpaṇa-mārjanaṁ bhava-mahā-dāvāgni-nirvāpaṇaṁ…») y se escapaba.
    minimo = 3 if len(s) >= 85 else 4
    if len(pal) < minimo or any(w.strip("'’-.,") in ING for w in pal):
        return False
    # Una linea de transliteracion va cargada de diacriticos; una frase inglesa
    # con un «Kṛṣṇa» dentro, no. Sin esta prueba se colaba «Without approaching
    # an authorized person, one cannot understand Kṛṣṇa».
    con = sum(1 for w in pal if DIAC.search(w))
    return con / len(pal) >= 0.4


def buscar(idx, claves, s):
    k = pel(s)
    if k in idx:
        return idx[k], 'exacta'
    # la grafia puede variar poco entre la cita y el verso original
    cerca = difflib.get_close_matches(k, claves, n=1, cutoff=0.93)
    if cerca:
        return idx[cerca[0]], f'parecido {difflib.SequenceMatcher(None, k, cerca[0]).ratio():.3f}'
    # Segunda via: por el PRIMER pada. El parecido global falla cuando el bloque
    # del corpus trae mas de un verso, o cuando la cita cambia el orden de dos
    # palabras («adyaiva me viśatu» por «adyaiva viśatu me»): la cadena entera se
    # aleja aunque el verso sea el mismo. El primer pada, en cambio, ancla bien.
    letras = lambda x: re.sub(r'[^a-z]', '', pel(x))
    lk = letras(s)
    mejor, punt = None, 0.0
    for kk, v in idx.items():
        pl = letras(v[0])
        if len(pl) < 12 or not lk.startswith(pl):
            continue
        r = difflib.SequenceMatcher(None, lk, letras(' '.join(v))).ratio()
        if r > punt:
            mejor, punt = v, r
    if mejor and punt >= 0.85:
        return mejor, f'primer pada, parecido {punt:.3f}'
    return None, None


def main():
    aplicar = '--aplicar' in sys.argv
    idx = indice()
    idx_legacy = indice(legacy=True)
    for k, v in idx_legacy.items():
        idx.setdefault(k, v)
    claves = list(idx)
    print(f'  versos indexados: {len(idx)}  (de ellos {len(idx_legacy)} del juego anterior)')
    hechos = fallos = 0
    sin = []
    for f in ficheros():
        lin = f.read_text(encoding='utf-8').splitlines()
        cambio = False
        for i, l in enumerate(lin):
            s = l.strip()
            if not colapsada(s):
                continue
            partes, como = buscar(idx, claves, s)
            if not partes:
                sin.append((f.name, s[:80]))
                continue
            # El verso de referencia dice DONDE cortar, no QUE escribir. No se
            # pueden contar palabras: la cita parte compuestos que el original
            # une —«manaḥ ṣaṣṭhānīndriyāṇi» por «manaḥ-ṣaṣṭhānīndriyāṇi»— y el
            # numero baila. Se comparan LETRAS: se van consumiendo palabras de
            # la cita hasta que las letras acumuladas son las del pada.
            def letras(x):
                return re.sub(r'[^a-z]', '', pel(x))
            palabras = s.split()
            trozos, j, roto = [], 0, False
            for pada in partes:
                # La cita puede traer solo el principio del verso: en
                # sb/4/13/8-9 se citan los dos primeros padas del Siksastaka 1 y
                # el bloque de referencia tiene cuatro. Se paran los padas
                # cuando la cita se acaba.
                if j >= len(palabras):
                    break
                meta = letras(pada)
                acc, k = '', j
                while k < len(palabras) and len(acc) < len(meta):
                    acc += letras(palabras[k]); k += 1
                if acc != meta:
                    roto = True; break
                trozos.append(' '.join(palabras[j:k])); j = k
            if roto or j != len(palabras) or ' '.join(trozos) != s:
                # Respaldo: repartir por NUMERO de palabras. Hace falta cuando la
                # cita trae una grafia distinta del verso original —«ekena» por
                # «ekeha», «pitḷn» por «pitṝn», «vāñcā» por «vāñchā»—, que es del
                # impreso y hay que respetar. El corte por letras no puede casar
                # ahi, pero el reparto por palabras si, y el texto no se toca.
                largos = [len(x.split()) for x in partes]
                if sum(largos) != len(palabras):
                    fallos += 1
                    print(f'  !! {f.name}: el reparto no reconstruye la linea')
                    continue
                trozos, j = [], 0
                for n in largos:
                    trozos.append(' '.join(palabras[j:j + n])); j += n
                if ' '.join(trozos) != s:
                    fallos += 1
                    print(f'  !! {f.name}: el reparto por palabras tampoco cuadra')
                    continue
            lin[i] = '\n'.join('> ' + x for x in trozos)
            cambio = True
            hechos += 1
        if cambio and aplicar:
            f.write_text('\n'.join(lin) + '\n', encoding='utf-8')
    print(f'  versos partidos: {hechos}   descartados por no coincidir: {fallos}')
    print(f'  sin verso de referencia: {len(sin)}')
    for n, s in sin[:10]:
        print(f'    {n:22} {s}')
    if not aplicar:
        print('\n  (ensayo: anade --aplicar)')
    return 0


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