#!/usr/bin/env python3
"""Busca los PARENTESIS del impreso que la edicion digital dejo fuera.

De donde sale. Adjudicando la clase `op=insert` aparecieron cuatro huecos del
mismo tipo en cuatro capitulos distintos de *Teachings of Queen Kunti*:

    (padaṁ padaṁ yad vipadām)
    (aṇḍāntara-stha-paramāṇu-cayāntara-stham)
    (anyābhilāṣitā-śūnyam)
    (sac-cid-ānanda-vigraha)

Siempre igual: una frase en ingles seguida del sanscrito entre parentesis, y el
texto digital se come el parentesis entero. Cuatro casos en cuatro capitulos no
es casualidad, asi que esto barre el libro completo —o el que se le diga— en vez
de esperar a que el alineador los vaya sacando de uno en uno.

Metodo: se sacan todos los parentesis del OCR de las paginas, se normalizan
(sin diacriticos, sin puntuacion) y se mira cuales NO aparecen en el texto del
repo. Lo que salga hay que confirmarlo mirando la imagen: el OCR se inventa
parentesis donde hay una mancha, y parte los que cruzan un salto de linea.

Uso: python3 barrer_parentesis.py <libro> [--min 2]
     python3 barrer_parentesis.py teachings-of-queen-kunti
"""
import re, sys, difflib, unicodedata, pathlib, collections

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
SURYA = HERE / 'surya'

# lo que no es un hueco aunque venga entre parentesis
RUIDO = re.compile(r'^[\d\s.,:;-]*$|^(bg|sb|cc|text|verse|ibid|cf)\b', re.I)
# una CITA no es un hueco: el repo la escribe de otra forma (enlace, nota) y
# saldria como falta en todas las paginas. Fuente + numeros de capitulo/verso.
CITA = re.compile(r'^\s*(p{1,2}\.|bh[āa]g\.|bg\.|cc\.|[A-ZŚ][\w\u0100-\u017f-]+\s*)?'
                  r'[\d\s.,:;–-]+\s*(vols?\.?)?\s*$', re.I)
DIACRITICO = re.compile(r'[\u0100-\u017f\u1e00-\u1eff]')


def es_sanscrito(s):
    """El patron que buscamos: transliteracion, sin cifras.

    Lleva diacriticos o un compuesto con guion, y NO lleva numeros — con
    numeros es una cita, y la cita el repo la escribe a su manera."""
    if re.search(r'\d', s): return False
    return bool(DIACRITICO.search(s)) or ('-' in s and len(s.split()) <= 6)


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


def parecido_en(n, pal, indice, tope=400):
    """El mejor parecido entre `n` y una ventana del repo del mismo tamano.

    Se ancla por la palabra MENOS frecuente del fragmento —si se anclara por una
    cualquiera, 'the' obligaria a comparar decenas de miles de ventanas— y solo
    se miran las primeras `tope` posiciones."""
    w = n.split()
    presentes = [x for x in w if x in indice]
    if not presentes: return 0.0
    ancla = min(presentes, key=lambda x: len(indice[x]))
    mejor = 0.0
    L = len(w)
    for pos in indice[ancla][:tope]:
        # ventanas del MISMO tamano que el fragmento, deslizando el ancla por
        # cada posicion. Comparar contra una ventana del doble de largo hunde
        # el parecido y no filtraba nada.
        for d in range(-L + 1, 1):
            a = pos + d
            if a < 0 or a + L > len(pal): continue
            v = ' '.join(pal[a:a + L])
            if difflib.SequenceMatcher(None, n, v).real_quick_ratio() <= mejor: continue
            mejor = max(mejor, difflib.SequenceMatcher(None, n, v).ratio())
            if mejor > 0.85: return mejor
    return mejor


def paginas_del_texto(paginas, repo, V=8):
    """Solo las paginas que pertenecen al cuerpo del libro.

    El glosario, el indice y la lista de libros del autor no estan en el repo, y
    sus parentesis —«(see also Bhakti-yoga)», «(mā —not; yā —this)»— salen todos
    como huecos. Se reconocen porque casi nada de la pagina aparece en el repo.
    En Light of the Bhagavata eso era 11 de los 11 «huecos»."""
    ng = {tuple(repo[i:i + V]) for i in range(len(repo) - V + 1)}
    buenas = set()
    for f in paginas:
        w = [x for x in sd_lista(f.read_text(encoding='utf-8')) if not x.isdigit()]
        if len(w) < V + 2: continue
        m = [tuple(w[i:i + V]) in ng for i in range(len(w) - V + 1)]
        if sum(m) / len(m) >= 0.33: buenas.add(f)
    return buenas


def sd_lista(s):
    return sd(s).split()


def main():
    if len(sys.argv) < 2:
        print(__doc__); return 1
    libro = sys.argv[1]
    minpal = int(sys.argv[sys.argv.index('--min') + 1]) if '--min' in sys.argv else 2

    dirs = list((SURYA / libro).glob('*'))
    paginas = sorted(p for d in dirs for p in d.glob('p*.surya.txt'))
    if not paginas:
        print('sin OCR para', libro); return 1

    carpeta = REPO / libro
    texto = sd('\n'.join(f.read_text(encoding='utf-8')
                         for f in sorted(carpeta.rglob('*.md'))))
    pal_repo = texto.split()
    vocab = set(pal_repo)
    indice = collections.defaultdict(list)
    for i, x in enumerate(pal_repo): indice[x].append(i)
    delcuerpo = paginas_del_texto(paginas, texto.split())
    paginas = [p for p in paginas if p in delcuerpo]
    print(f'{libro}: {len(paginas)} paginas de cuerpo, '
          f'{len(list(carpeta.rglob("*.md")))} ficheros en el repo')

    faltan = []; total = 0
    for f in paginas:
        pag = int(re.search(r'p(\d+)', f.name).group(1))
        t = f.read_text(encoding='utf-8')
        for m in re.finditer(r'\(([^()]{3,120})\)', t):
            dentro = m.group(1).strip()
            total += 1
            if RUIDO.match(dentro) or CITA.match(dentro): continue
            # se admite la parte sanscrita de una cita mixta:
            # «(alola-candraka..., Brahma-samhita 5.31)»
            trozos = [y.strip() for y in dentro.split(',')]
            sanscrito = [y for y in trozos if es_sanscrito(y)]
            if not sanscrito: continue
            dentro = ', '.join(sanscrito)
            n = sd(dentro)
            if len(n.split()) < minpal: continue
            if n in texto: continue
            # el impreso PARTE palabras con guion y el OCR entrega los trozos:
            # 'jan-mādy asya yataḥ' donde el repo escribe 'janmādy asya yataḥ',
            # 'bhak-ti-rasa' donde escribe 'bhakti-rasa'. Se prueba tambien sin
            # los espacios que dejo el guion.
            if n.replace(' ', '') in texto.replace(' ', ''): continue
            # BASURA DEL OCR. Los titulillos y las portadillas salen ilegibles
            # ('Califanya: carifāmyta', 'če Dovotional šorvico') y no son huecos.
            # Si la mayoria de las palabras no existen en el libro, no es texto.
            w = n.split()
            if sum(1 for x in w if x in vocab) / len(w) < 0.5: continue
            # CASI IGUAL = el OCR leyendo mal algo que SI esta. El repo escribe
            # «bhaktir ucyate» y el OCR entrega «bhaktir ucvate»; como la
            # comparacion va sin diacriticos, la unica diferencia es una letra.
            # Se busca la ventana mas parecida del repo y se descarta si el
            # parecido pasa de 0,85.
            if parecido_en(n, pal_repo, indice) > 0.85: continue
            ini = max(0, m.start() - 90)
            faltan.append({'pagina': pag, 'dentro': dentro,
                           'contexto': ' '.join(t[ini:m.end() + 40].split())})

    # deduplicar por contenido
    vistos = set(); u = []
    for x in faltan:
        k = sd(x['dentro'])
        if k in vistos: continue
        vistos.add(k); u.append(x)

    print(f'parentesis en el OCR: {total} | no estan en el repo: {len(u)}\n')
    for x in sorted(u, key=lambda y: y['pagina']):
        print(f"  p{x['pagina']:<4} ({x['dentro'][:70]})")
        print(f"        ...{x['contexto'][-110:]}")
    import json
    json.dump(u, open(HERE / f'parentesis_{libro}.json', 'w'), ensure_ascii=False, indent=1)
    print(f'\n-> parentesis_{libro}.json')
    return 0


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