# -*- coding: utf-8 -*-
"""Exporta o CONTEUDO do banco para arquivos de texto em Data/.

    python tools/exportar_data.py            mostra o que faria
    python tools/exportar_data.py --aplicar  grava os arquivos

A divisao e a mesma do servidor de Cabal: conteudo estatico -- o que vem do client e nunca
muda sozinho -- vive em arquivo, e o banco fica so com o que se perde se sumir (contas,
personagens, inventario, sessao).

Formato: uma linha por registro, campos separados por TAB, como os .scp do Cabal.
A primeira linha comeca com # e nomeia as colunas. Linhas em branco e linhas que comecam
com # sao ignoradas na leitura. Campo vazio vira NULL.

Escolhi TAB em vez de .ini porque .ini precisa de uma secao por registro -- 19806 secoes
so pro Item -- e fica pesado de ler e de editar. TAB abre no Excel e no bloco de notas,
e e o que voce ja edita no Cabal.
"""
import os
import sys

import pyodbc

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

BARRA = chr(92)

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DESTINO = os.path.join(RAIZ, "server", "Data")

# Data/Common guarda o que o dono do servidor MEXE -- multiplicadores de evento e bonus.
# O resto de Data/ e conteudo vindo do client ou regra medida no oficial: da pra editar,
# mas nao e "configuracao", e o jogo em si.
COMMON = "Common"

# arquivo -> (tabela, ordenacao, subpasta)
# Estes NAO saem do banco: sao anotacao de engenharia reversa, escrita a mao, e vivem
# so em Data/. Nunca tiveram tabela de origem porque nao vem do client.
#     LevelColor  Formula  SocialAction  Opcode  Rule

CONTEUDO = [
    # arquivo             tabela                 ordenacao      subpasta
    ("Rate",              "taxa",                None,          COMMON),
    ("ExpModifier",    "exp_modificador",     None,          COMMON),
    ("Quest",            "quest",               "quest_id",    None),
    ("QuestReward",      "quest_premio",        "quest_id, classe", None),
    ("Npc",          "npc",                 "npc_id",      None),
    ("Item",              "item",                "item_id",     None),
    ("ItemOption",        "item_atributo",       None,          None),
    ("Class",            "classe",              "classe_id",   None),
    ("ClassEvolution",    "classe_evolucao",     "classe, grau", None),
    ("ClassStartItem", "classe_item_inicial", "classe_id, slot", None),
    ("Level",             "nivel_exp",           "nivel",       None),
    ("Fleta",             "fleta",               None,          None),
    ("Skill",             "skill",               None,          None),
    ("SuperSkill",        "super_skill",         None,          None),
    ("Map",              "mapa",                None,          None),
    ("ExpPenalty",     "exp_penalidade",      None,          None),
    ("ReservedName",     "nome_reservado",      None,          None),
    ("Spawn",             "npc_spawn_guardado",  None,          None),
    # Ponto onde o personagem nasce. Quase passou batido: 1 linha so, e a conferencia
    # antes de apagar as tabelas foi o que revelou que ela nao tinha arquivo nenhum.
    ("StartPoint",        "spawn",               None,          None),
    ("Mob",               "mob",                 "mob_id",      None),
    # npc_spawn pode estar VAZIA de proposito ("mundo sem mob, pra medir no oficial"), e
    # isso e diferente de nao existir. O arquivo preserva a distincao: existe com zero
    # linhas != nao existe. Ver o docstring de db.spawns().
    ("NpcSpawn",          "npc_spawn",           "spawn_id",    None),
]

# estas NAO saem do banco: sao o progresso do jogador
PROGRESSO = ("conta", "personagem", "personagem_item", "sessao")


def conectar():
    return pyodbc.connect("DRIVER={ODBC Driver 18 for SQL Server};SERVER=localhost;"
                          "DATABASE=Rosh;Trusted_Connection=yes;TrustServerCertificate=yes")


def escapar(v):
    """Um valor por linha. TAB e quebra de linha viram escape.

    A coluna descricao de dbo.item tem quebra de linha em 1652 itens; sem escapar, o valor
    ocuparia varias linhas e o leitor rapido nao funcionaria. Escapado, cada campo cabe
    numa linha so. O preco: a quebra aparece como barra-n no arquivo em vez de quebrar.
    """
    if v is None:
        return None
    if isinstance(v, bytes):
        return v.hex()
    s = str(v)
    s = s.replace(BARRA, BARRA + BARRA)
    s = s.replace(chr(9), BARRA + "t")
    s = s.replace(chr(13), "")
    s = s.replace(chr(10), BARRA + "n")
    s = s.replace(chr(133), BARRA + "n")
    return s


# Colunas que servem de descricao: viram COMENTARIO acima da chave, no estilo plano.
COLUNA_NOTA = ("descricao", "nota", "texto", "relacao")
# Metadado do banco que nao faz sentido em arquivo: a data de alteracao e o mtime do
# proprio arquivo, e mante-la aqui so gera ruido e diferenca falsa a cada exportacao.
COLUNA_IGNORADA = ("alterada_em",)
REGUA = ";" + "-" * 100


def quebrar(texto, largura):
    """Quebra em pedacos sem partir palavra no meio."""
    saida, atual = [], ""
    for palavra in texto.split(" "):
        if atual and len(atual) + 1 + len(palavra) > largura:
            saida.append(atual)
            atual = palavra
        else:
            atual = (atual + " " + palavra) if atual else palavra
    if atual:
        saida.append(atual)
    return saida


def escrever_plano(fp, arquivo, tabela, cols, linhas):
    """Estilo chave = valor, com um bloco de comentario explicando cada uma.

    E o formato dos .ini de servidor que o usuario mostrou como referencia. Usado nos
    arquivos de Common/, que tem poucas linhas e sao editados a mao: ali a legibilidade
    vale mais que a compactacao.
    """
    chave, valor = cols[0], None
    for c in cols[1:]:
        if c not in COLUNA_NOTA:
            valor = c
            break
    nota = next((c for c in cols if c in COLUNA_NOTA), None)
    fp.write("; %s -- exportado de dbo.%s" % (arquivo, tabela) + chr(10))
    fp.write("; %s = %s" % (chave, valor) + chr(10) + chr(10))
    for r in linhas:
        d = dict(zip(cols, r))
        if nota and d.get(nota):
            fp.write(REGUA + chr(10))
            txt = escapar(d[nota])
            for pedaco in quebrar(txt, 96):
                fp.write(";-- " + pedaco + chr(10))
            fp.write(REGUA + chr(10))
        outros = [c for c in cols if c not in (chave, nota) and c not in COLUNA_IGNORADA]
        for c in outros:
            e = escapar(d.get(c))
            if e is not None:
                nome = str(d[chave]) if c == valor else "%s.%s" % (d[chave], c)
                fp.write("%s = %s" % (nome, e) + chr(10))
        fp.write(chr(10))


def escrever_secoes(fp, arquivo, tabela, cols, linhas):
    """Uma secao por registro. Para as tabelas grandes, onde o estilo plano nao cabe."""
    chaves = [str(r[0]) for r in linhas]
    unica = len(set(chaves)) == len(chaves)
    fp.write("; %s -- exportado de dbo.%s" % (arquivo, tabela) + chr(10))
    fp.write("; colunas: %s" % ", ".join(cols) + chr(10) + chr(10))
    for n, r in enumerate(linhas):
        fp.write("[%s]" % (chaves[n] if unica else n) + chr(10))
        for c, v in zip(cols, r):
            if c in COLUNA_IGNORADA:
                continue
            e = escapar(v)
            if e is not None:
                fp.write("%s = %s" % (c, e) + chr(10))
        fp.write(chr(10))


def exportar(cur, arquivo, tabela, ordem, sub, aplicar):
    cur.execute("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS "
                "WHERE TABLE_NAME=? ORDER BY ORDINAL_POSITION", tabela)
    cols = [r[0] for r in cur.fetchall()]
    if not cols:
        return None
    sql = "SELECT %s FROM dbo.%s" % (", ".join(cols), tabela)
    if ordem:
        sql += " ORDER BY " + ordem
    cur.execute(sql)
    linhas = cur.fetchall()
    if not aplicar:
        return (arquivo, tabela, len(cols), len(linhas), 0)
    pasta = os.path.join(DESTINO, sub) if sub else DESTINO
    if not os.path.isdir(pasta):
        os.makedirs(pasta)
    # Segue onde a tabela JA esta, se ela existir. O `sub` desta ferramenta e anterior a
    # reorganizacao de 25/08 e nao conhece as subpastas por sistema; sem isto, exportar
    # criaria uma segunda copia no lugar velho.
    cam = D.caminho(arquivo) if D.existe(arquivo) else os.path.join(pasta, arquivo + ".ini")
    with open(cam, "w", encoding="utf-8", newline=chr(10)) as fp:
        if sub == COMMON:
            escrever_plano(fp, arquivo, tabela, cols, linhas)
        else:
            escrever_secoes(fp, arquivo, tabela, cols, linhas)
    return (arquivo, tabela, len(cols), len(linhas), os.path.getsize(cam))


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    c = conectar()
    cur = c.cursor()
    if aplicar and not os.path.isdir(DESTINO):
        os.makedirs(DESTINO)

    print("%-24s %-22s %5s %8s %10s" % ("arquivo", "tabela", "cols", "linhas", "bytes"))
    print("-" * 74)
    total = 0
    for arquivo, tabela, ordem, sub in CONTEUDO:
        r = exportar(cur, arquivo, tabela, ordem, sub, aplicar)
        if r is None:
            print("%-20s %-22s  tabela nao existe" % (arquivo, tabela))
            continue
        rotulo = (sub + "/" if sub else "") + r[0] + ".ini"
        print("%-24s %-22s %5d %8d %10s" % (rotulo, r[1], r[2], r[3],
                                            "{:,}".format(r[4]) if r[4] else "-"))
        total += r[4]
    print("-" * 74)
    print("%-53s %10s" % ("total", "{:,}".format(total) if total else "-"))
    print()
    print("fica no banco (progresso): %s" % ", ".join(PROGRESSO))
    if not aplicar:
        print()
        print("nada gravado. rode com --aplicar")
    else:
        print()
        print("gravado em %s" % DESTINO)
    return 0


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