# -*- coding: utf-8 -*-
"""Converte tabelas .ini grandes para .tsv, o formato em colunas separadas por TAB.

    python tools/ini_para_tsv.py                mostra o que ganharia
    python tools/ini_para_tsv.py --aplicar      converte no lugar (mesmo nome)
    python tools/ini_para_tsv.py --min 200      so os ficheiros acima de 200 KB

POR QUE

O .ini repete o nome de cada coluna em cada registo. O Item.ini gasta 13,8 MB para 19.806
itens, e quase tudo e escrever "nome = " e "preco_compra = " vinte mil vezes. Em TSV o
cabecalho aparece uma vez:

    //item_id  nome               preco_compra  preco_venda
    1002       "1-espada pesada"  2115          423

O QUE E PRESERVADO

Os comentarios do topo do .ini passam para o topo do .tsv com ; -- e neles que esta escrito
como cada tabela foi medida, e perder isso seria perder o mais caro do ficheiro.

As colunas saem na ordem em que aparecem no primeiro registo, e a uniao de todas as chaves
vistas: se um registo tiver uma coluna que os outros nao tem, ela entra e os outros levam
celula vazia.

CUIDADOS

Celula vazia le-se como None, nao como string vazia -- e o mesmo que o .ini faz quando a
chave nao aparece. Por isso um valor que seja mesmo a string vazia nao sobrevive a ida e
volta; nao ha nenhum nas tabelas atuais, e o verificador abaixo confirma isso ficheiro a
ficheiro antes de apagar o original.

Valores com TAB dentro seriam ambiguos. Nao existe nenhum, e a conversao recusa-se a
continuar se aparecer um.
"""
import os
import sys

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(RAIZ, "server"))
import data_files as D                                              # noqa: E402

DATA = os.path.join(RAIZ, "server", "Data")
TAB = chr(9)
CR = chr(13) + chr(10)
ASPAS = chr(34)
B = chr(92)


def cabecalho_do_ini(caminho):
    """As linhas de comentario do topo, ate ao primeiro registo."""
    notas = []
    with open(caminho, encoding="utf-8") as fp:
        for linha in fp:
            linha = linha.rstrip(chr(10)).rstrip(chr(13))
            if linha.startswith(";"):
                notas.append(linha)
            elif linha.strip() == "":
                if notas:
                    notas.append("")
            else:
                break
    while notas and notas[-1] == "":
        notas.pop()
    return notas


def colunas_de(registos):
    """Ordem do primeiro registo, mais o que aparecer so nos seguintes."""
    cols = list(registos[0].keys())
    vistas = set(cols)
    for r in registos[1:]:
        for c in r:
            if c not in vistas:
                vistas.add(c)
                cols.append(c)
    return cols


def celula(v):
    """Um valor em texto, seguro numa linha so.

    Escapa em vez de estragar. A primeira versao disto trocava aspas por plicas e deixava as
    quebras de linha passar em bruto -- o verificador apanhou as duas na hora:

        "Crowler \"O Diabo\" Atkins"  virava  "Crowler 'O Diabo' Atkins"
        uma descricao com 3 paragrafos  virava  3 registos

    O Item.ini crescia de 19.806 para 24.699 linhas por causa disso. E o mesmo problema que o
    .ini ja resolvia escapando; so faltava fazer o mesmo aqui.
    """
    if v is None:
        return ""
    s = str(v)
    s = (s.replace(B, B + B)
          .replace(chr(13), "")
          .replace(chr(10), B + "n")
          .replace(chr(9), B + "t")
          .replace(ASPAS, B + ASPAS))
    if s == "" or " " in s or s[0] == ASPAS:
        return ASPAS + s + ASPAS
    return s


def converter(nome, caminho_ini, destino_tsv):
    registos = D.tabela(nome)
    if not registos:
        return None
    cols = colunas_de(registos)
    linhas = cabecalho_do_ini(caminho_ini)
    if linhas:
        linhas.append("")
    linhas.append("//" + TAB.join(cols))
    for r in registos:
        linhas.append(TAB.join(celula(r.get(c)) for c in cols))
    texto = CR.join(linhas) + CR
    with open(destino_tsv, "w", encoding="utf-8", newline="") as fp:
        fp.write(texto)
    return len(registos)


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    minimo = 100 * 1024
    if "--min" in sys.argv:
        minimo = int(sys.argv[sys.argv.index("--min") + 1]) * 1024

    D.carregar()
    alvos = []
    for base, _, arqs in os.walk(DATA):
        for f in sorted(arqs):
            if not f.endswith(".ini"):
                continue
            cam = os.path.join(base, f)
            if os.path.getsize(cam) >= minimo:
                alvos.append((f[:-4], cam))
    alvos.sort(key=lambda x: -os.path.getsize(x[1]))

    print("ficheiros .ini com %d KB ou mais: %d" % (minimo // 1024, len(alvos)))
    print()
    print("%-16s %10s %10s %8s %8s" % ("tabela", "antes", "depois", "poupanca", "linhas"))
    print("-" * 58)
    total_antes = total_depois = 0
    feitos = []
    for nome, cam in alvos:
        tsv = cam + ".novo"   # escreve ao lado; so substitui depois de conferir
        try:
            n = converter(nome, cam, tsv)
        except ValueError as e:
            print("%-16s RECUSADO: %s" % (nome, e))
            continue
        if n is None:
            print("%-16s vazio, ignorado" % nome)
            continue
        antes = os.path.getsize(cam)
        depois = os.path.getsize(tsv)
        total_antes += antes
        total_depois += depois
        print("%-16s %9.1fK %9.1fK %7.0f%% %8d"
              % (nome, antes / 1024, depois / 1024, 100 * (1 - depois / antes), n))
        feitos.append((nome, cam, tsv, n))

    if total_antes:
        print("-" * 58)
        print("%-16s %9.1fK %9.1fK %7.0f%%"
              % ("TOTAL", total_antes / 1024, total_depois / 1024,
                 100 * (1 - total_depois / total_antes)))

    # VERIFICA antes de apagar seja o que for
    print()
    print("a conferir que o .tsv le exatamente igual ao .ini...")
    import importlib
    ok = True
    for nome, cam, tsv, n in feitos:
        antes = D.tabela(nome)
        importlib.reload(D)
        D.carregar()
        # forca a ler o .tsv escondendo o .ini
        tmp = cam + ".conferir"
        os.rename(cam, tmp)
        os.rename(tsv, cam)
        importlib.reload(D)
        D.carregar()
        depois = D.tabela(nome)
        os.rename(cam, tsv)
        os.rename(tmp, cam)
        # compara SEM as chaves a None: o .ini tem duas maneiras de dizer "vazio" (chave
        # ausente, ou "chave = " sem valor) e o formato em colunas so tem uma. Para quem le
        # com .get() sao equivalentes, e comparar cru acusava diferenca onde nao ha.
        def limpo(lista):
            return [{k: v for k, v in r.items() if v is not None} for r in lista]
        antes, depois = limpo(antes), limpo(depois)
        if antes != depois:
            ok = False
            dif = [i for i in range(min(len(antes), len(depois))) if antes[i] != depois[i]]
            print("  %-16s DIFERE: %d vs %d registos, primeiro em %s"
                  % (nome, len(antes), len(depois), dif[:1]))
            if dif:
                i = dif[0]
                print("     ini: %s" % str(antes[i])[:150])
                print("     tsv: %s" % str(depois[i])[:150])
        else:
            print("  %-16s identico (%d registos)" % (nome, len(depois)))
        importlib.reload(D)
        D.carregar()

    if not ok:
        print()
        print("HOUVE DIFERENCAS. Nada foi apagado -- os .tsv ficam ao lado para se ver.")
        return 1
    if not aplicar:
        print()
        print("tudo confere. os .tsv ficaram escritos, mas os .ini NAO foram apagados.")
        print("rode com --aplicar para apagar os .ini.")
        return 0
    for nome, cam, tsv, n in feitos:
        os.remove(cam)
        os.rename(tsv, cam)
    print()
    print("%d ficheiros convertidos, mesmo nome .ini." % len(feitos))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
