# -*- coding: utf-8 -*-
"""Le a curva de EXP direto das capturas e grava em Data/Level.ini.

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

O servidor MANDA o limiar de cada nivel, no campo [77:81] do 08 01 (little-endian), logo
depois da EXP atual em [69:73]. Isso foi descoberto em 25/08 procurando o valor 10913 --
que o jogador viu na ficha como "Experiencia 764/10913" -- dentro dos pacotes: apareceu em
260 deles, sempre no mesmo offset.

Antes disso a curva era medida um nivel por vez, esperando o jogador subir e vendo a EXP
zerar. Com o campo, uma captura entrega todos os niveis por onde o personagem passou.

A CURVA TEM UM DEGRAU PRA BAIXO, e nao e erro de leitura:

    nivel  8    4374
    nivel  9    8648   <- quase dobra
    nivel 10    7099   <- CAI
    nivel 11   10913

Conferido em 5 personagens de classes diferentes, em 4 capturas: os mesmos valores. O pico
no nivel 9 coincide com o nivel da primeira promocao de classe, que e 10 -- o jogo cobra
mais caro justamente pra chegar la, e alivia logo depois.
"""
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")
OFF_NIVEL, OFF_EXP, OFF_LIMIAR = 43, 69, 77


def eventos(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 main() -> int:
    aplicar = "--aplicar" in sys.argv
    votos = collections.defaultdict(collections.Counter)
    capturas = sorted(glob.glob(r"C:\RoshHook\traffic*.log"))
    for cam in capturas:
        for c in eventos(cam):
            nivel = c[OFF_NIVEL]
            exp = struct.unpack_from("<I", c, OFF_EXP)[0]
            limiar = struct.unpack_from("<I", c, OFF_LIMIAR)[0]
            if 1 <= nivel <= 200 and 0 < limiar < 10 ** 9 and exp <= limiar:
                votos[nivel][limiar] += 1

    print("%d capturas lidas, %d niveis com dado" % (len(capturas), len(votos)))
    print()
    print("%-7s %-12s %-9s %s" % ("nivel", "limiar", "amostras", "divergencia"))
    print("-" * 52)
    medidos = {}
    for n in sorted(votos):
        v = votos[n]
        principal, quantas = v.most_common(1)[0]
        # so aceita se a maioria for esmagadora: valor minoritario costuma ser leitura de
        # um pacote truncado, nao um limiar diferente de verdade
        total = sum(v.values())
        if quantas / total < 0.9:
            print("%-7d %-12s %-9d DESCARTADO: %s" % (n, "?", total, dict(v)))
            continue
        medidos[n] = principal
        outros = {k: x for k, x in v.items() if k != principal}
        print("%-7d %-12d %-9d %s" % (n, principal, quantas, outros or ""))

    print()
    print("niveis medidos: %d  (de %d a %d)"
          % (len(medidos), min(medidos), max(medidos)) if medidos else "nenhum")

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

    # mantem o que ja existe pros niveis sem medida, marcando quem e medido
    antigos = {}
    if os.path.exists(DESTINO):
        atual = None
        for l in open(DESTINO, encoding="utf-8"):
            m = re.match(r"\[(\d+)\]", l)
            if m:
                atual = int(m.group(1))
                antigos[atual] = {}
                continue
            m = re.match(r"(\w+) = (.+)", l)
            if m and atual is not None:
                antigos[atual][m.group(1)] = m.group(2).strip()

    niveis = sorted(set(antigos) | set(medidos))
    with open(DESTINO, "w", encoding="utf-8", newline=NL) as fp:
        fp.write("; Level -- EXP para sair de cada nivel." + NL)
        fp.write("; medido=True veio do campo [77:81] do 08 01, que o servidor manda em toda" + NL)
        fp.write("; barra de heroi. medido=False e interpolacao e nao merece confianca --" + NL)
        fp.write("; a formula que a gerou erra feio: no nivel 10 ela dava 6456 e o real e 7099." + NL)
        fp.write("; colunas: nivel, exp_para_subir, medido" + NL + NL)
        for n in niveis:
            v = medidos.get(n)
            med = "True" if v is not None else antigos.get(n, {}).get("medido", "False")
            if v is None:
                v = antigos.get(n, {}).get("exp_para_subir")
            if v is None:
                continue
            fp.write("[%d]" % n + NL)
            fp.write("exp_para_subir = %s" % v + NL)
            fp.write("medido = %s" % med + NL + NL)
    print()
    print("gravado: Level.ini com %d niveis, %d medidos" % (len(niveis), len(medidos)))
    return 0


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