# -*- coding: utf-8 -*-
"""Reconstroi Data/Level.ini do zero, a partir das capturas.

    python tools/gerar_level_ini.py            mostra
    python tools/gerar_level_ini.py --aplicar  grava

Le o limiar de EXP direto do campo [77:81] do 08 01 em todas as capturas, preenche o resto
com a formula antiga como marcador de lugar, e escreve no formato de uma linha por nivel.

Existe porque editar o arquivo com regex foi um erro: em 25/08 uma reescrita herdou lixo do
arquivo anterior e o nivel 1 saiu com 20 em vez de 48. Gerar do zero a partir da fonte nao
tem esse risco -- o que a captura diz e o que vai pro arquivo.

O nivel 68 e a excecao: veio de print da ficha do jogador, nao de captura, e por isso entra
como constante aqui embaixo. E o unico ponto acima do nivel 16 que temos, e e ele que mostra
que a formula esta errada por ordem de grandeza.
"""
import collections
import glob
import os
import re
import struct
import sys

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

NL = chr(10)
# O caminho vem do data_files, nao escrito aqui: os arquivos foram pra subpastas por
# sistema em 25/08, e caminho fixo passaria a gravar um segundo arquivo na raiz da
# Data -- calado, enquanto o servidor seguia lendo o certo.
DESTINO = D.caminho("Level", "Character")
NIVEL_MAX = 200          # teto do jogo: 532 das 730 missoes usam nivel_max=200
DA_TELA = {
    # Prints da ficha do jogador. Os dois de 27/08 sao de personagens do oficial, e um deles
    # esta CONFERIDO em captura: a Feiticeira de nivel 48 morreu, a tela escreveu
    # "Experiencia perdida: 144425" e o 08 01 do fio mostrou a EXP a cair de 1.460.350 para
    # 1.315.925 -- exatamente 2 por cento de 7.221.295. O limiar lido do print bate com o que
    # o servidor mandou, o que valida de caminho o proprio metodo de ler numeros da tela.
    48: 7221295,        # ficha "1460350/7221295", conferido no fio
    68: 49753658,       # print da ficha, nao verificavel em captura
    90: 661261951,      # ficha "25407822/661261951"
    99: 44063677744,    # ficha "7023606774/44063677744"
}

# O nivel 99 nao cabe em 32 bits: 44.063.677.744 passa dos 4.294.967.295. E a prova pratica
# de que o campo [77:85] do 08 01 e u64 e nao u32 -- o que ja se sabia por o AMo.exe formatar
# a barra com "%I64d/%I64d", mas nunca se tinha visto um valor que obrigasse a isso.
#
# `do_fio` ainda le o limiar como u32 e descarta o que passa de 10^9, portanto nao apanha
# niveis destes sozinho. Nao se mexeu nisso agora: mudar a leitura sem uma captura de nivel
# alto para conferir seria trocar um limite conhecido por um risco novo.


def pacotes_08_01(cam):
    atual, blocos = None, []
    for l in open(cam, encoding="latin-1", errors="replace"):
        m = re.match(r"\[[\d:.]+\] (SEND|RECV) sock=", l)
        if m:
            atual = [m.group(1), bytearray()]
            blocos.append(atual)
            continue
        if atual is not None and re.fullmatch(r"(?:[0-9A-Fa-f]{2} )+\s*", l):
            atual[1].extend(bytes.fromhex(l.strip()))
    for d, b in blocos:
        if d != "RECV":
            continue
        pos = 0
        while pos + 4 <= len(b):
            (n,) = struct.unpack_from(">I", b, pos)
            if n == 0 or n > 16384 or pos + 4 + n > len(b):
                break
            c = bytes(b[pos + 4:pos + 4 + n])
            if len(c) >= 109 and c[0] == 8 and c[1] == 1:
                yield c
            pos += 4 + n


def do_fio():
    votos = collections.defaultdict(collections.Counter)
    caps = sorted(glob.glob(r"C:\RoshHook\traffic*.log"))
    # As capturas que os jogadores mandam pela ferramenta CapturaKaros contam igual: e o
    # mesmo hook a escrever o mesmo formato, so muda a pasta.
    caps += sorted(glob.glob(os.path.join(
        os.path.expanduser("~"), "Desktop", "CapturaKaros", "*.log")))
    for cam in caps:
        for c in pacotes_08_01(cam):
            nivel = c[43]
            # u64 e nao u32, e o teto sobe com ele.
            #
            # A ficha de um Destruidor de nivel 99 do oficial mostra "7023606774/44063677744".
            # O limiar sozinho passa dos 44 mil milhoes, dez vezes o que cabe em 32 bits, e o
            # teto antigo de 10^9 descartava-o em silencio -- ou seja, nenhuma captura de
            # personagem alto conseguia entrar aqui, por mais que se gravasse.
            #
            # Que o campo e de 64 bits ja se sabia (o AMo.exe formata a barra com
            # "%I64d/%I64d"), mas so agora apareceu um valor que obriga a isso.
            exp = struct.unpack_from("<Q", c, 69)[0]
            lim = struct.unpack_from("<Q", c, 77)[0]
            if 1 <= nivel <= NIVEL_MAX and 0 < lim < 10 ** 15 and exp <= lim:
                votos[nivel][lim] += 1
    saida = {}
    for n, v in votos.items():
        principal, quantas = v.most_common(1)[0]
        # exige maioria esmagadora: valor minoritario costuma ser pacote truncado
        if quantas / sum(v.values()) >= 0.9:
            saida[n] = principal
    return saida, len(caps)


def formula(n):
    return 68 * n * n - 36 * n + 16


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    fio, quantas_caps = do_fio()
    print("%d capturas lidas" % quantas_caps)
    print("medido no fio : %d niveis  %s" % (len(fio), sorted(fio)))
    print("medido na tela: %d niveis  %s" % (len(DA_TELA), sorted(DA_TELA)))
    print("resto ate %d  : formula (marcador de lugar)" % NIVEL_MAX)
    print()
    for n in sorted(fio)[:6]:
        print("   nivel %-4d %d" % (n, fio[n]))
    print("   ...")

    if not aplicar:
        print()
        print("nada gravado. rode com --aplicar")
        return 0

    with open(DESTINO, "w", encoding="utf-8", newline=NL) as fp:
        w = fp.write
        w("; Level -- EXP para SAIR de cada nivel (quanto falta pra chegar no seguinte)." + NL)
        w(";" + NL)
        w("; colunas: nivel, exp_para_subir, fonte" + NL)
        w(";" + NL)
        w("; medido  -- o servidor oficial mandou no campo [77:81] do 08 01. Confiavel." + NL)
        w("; tela    -- lido de print da ficha do jogador. Legitimo, mas nao reconferivel." + NL)
        w("; formula -- 68n^2 - 36n + 16, SABIDAMENTE ERRADA. Onde da pra comparar ela erra" + NL)
        w(";            muito: no nivel 10 dava 6456 e o real e 7099; no 68 dava 312000 e o" + NL)
        w(";            real passa de 49 milhoes. Marcador de lugar, nao estimativa." + NL)
        w(";" + NL)
        w("; A curva real DESCE do nivel 9 pro 10 (8648 -> 7099). Nao e erro de leitura:" + NL)
        w("; cinco personagens de classes diferentes mandaram os mesmos numeros. O pico" + NL)
        w("; coincide com o nivel 10, que e o da primeira promocao de classe." + NL)
        w(";" + NL)
        w("; Pra medir mais: uma captura de personagem de nivel alto preenche a faixa toda de" + NL)
        w("; uma vez, porque o limiar vai em TODA barra de heroi. Depois rode este script." + NL)
        w(NL)
        # FORMATO TABULAR, e nao "[ReqEXP]" com "nivel = exp".
        #
        # O leitor (db.exp_para_subir) procura as colunas `nivel` e `exp_para_subir` por
        # indice, e com o formato de seccao encontrava UMA linha com 200 chaves em vez de 200
        # linhas -- devolvia None em todos os niveis e o servidor caia na formula antiga, a
        # mesma que o cabecalho aqui em cima chama de sabidamente errada. Ou seja: a tabela
        # media certo e ninguem a usava.
        w("//nivel	exp_para_subir	fonte" + NL)
        for n in range(1, NIVEL_MAX + 1):
            if n in fio:
                v, marca = fio[n], "medido"
            elif n in DA_TELA:
                v, marca = DA_TELA[n], "tela"
            else:
                v, marca = formula(n), "formula"
            w("%d	%d	%s" % (n, v, marca) + NL)
    print()
    print("gravado: Level.ini com %d niveis (%d medidos, %d da tela)"
          % (NIVEL_MAX, len(fio), len(DA_TELA)))
    return 0


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