# -*- coding: utf-8 -*-
r"""Tempo de renascimento por especie, MEDIDO nas capturas do oficial.

    python tools/medir_respawn.py              mostra
    python tools/medir_respawn.py --aplicar    grava Data/Monsters/MobRespawn.ini

COMO SE MEDE. Do `08 06` que poe o HP a zero ate ao `04 02` seguinte da MESMA entidade.

O METODO ANTERIOR ERA OUTRO E NAO DAVA NUMERO. Ate 30/08 este ficheiro emparelhava por
GEOMETRIA -- dois nascimentos seguidos em cima do mesmo poste, com ids de entidade
diferentes, na premissa de que "quem renasce recebe um id novo". Achou 16 renascimentos em
37 capturas, todos de uma especie, com p25 108 s e p75 708 s. Espalhamento desses nao e uma
constante mal medida: e sinal de que os pares nao eram renascimentos.

Por isso o servidor ficou com `SEGUNDOS_RESPAWN = 15.0` para toda a fauna, tirado a mao de
duas mortes da captura de 26/08.

O emparelhamento pelo HP zero da 4.316 renascimentos e a premissa antiga cai: o id de
entidade E o mesmo antes e depois da morte.

A TENSAO DA DISTRIBUICAO E A PROVA. Nao ha como um emparelhamento errado produzir p25,
mediana e p75 iguais ate a decima em 2.175 mortes de aranha. Um par ao acaso espalha; estes
nao espalham:

    Tirano sanguinario    6,0 s   164 mortes      Espadachim aguia    70,0 s   761
    Aranha pequena        7,0 s  2175 mortes      Guardiao Estripador 77,0 s    43
    Urso coruja           9,0 s    95 mortes      Cao branco          85,0 s   282

A FADA NAO APARECE, e nao e falha da medicao: em 4.316 renascimentos ela tem ZERO. O
jogador explicou porque -- *"Fadas nao tem lugar fixo, e aleatoria"* e *"o respaw dela e
mais de hr, pq dropa itens pra fazer asa"*. Nao e um poste que renasce, e outra mecanica, e
nao se inventa aqui uma linha para ela.
"""
import argparse
import collections
import glob
import os
import re
import struct
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

CR = chr(13) + chr(10)
TB = chr(9)

FONTES = (
    r"C:\RoshHook\traffic*.log",
    r"C:\Users\Administrator\Desktop\CapturaKaros\*.log",
)
"""As duas pastas do oficial. A segunda esteve de fora de varias ferramentas ate 30/08 e e
a maior -- so o karos_20260827_0838.log tem 108 MB."""

JANELA_MAX = 7200.0
"""Acima de duas horas nao e renascimento: e o mesmo id reaproveitado noutra sessao."""

MIN_AMOSTRAS = 3
"""Poucas chegam porque a distribuicao e cravada -- ver o cabecalho."""

ESPALHAMENTO_SUSPEITO = 2.0
"""p75-p25 acima disto quer dizer que a mediana nao resume a especie."""


def fontes():
    vistos, saida = set(), []
    for padrao in FONTES:
        for c in sorted(glob.glob(padrao)):
            k = os.path.abspath(c).lower()
            if k not in vistos:
                vistos.add(k)
                saida.append(c)
    return saida


def frames_de(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), m.group(2), bytearray()]
            blocos.append(atual)
            continue
        if atual is not None and re.fullmatch(r"(?:[0-9A-Fa-f]{2} )+\s*", l):
            atual[2].extend(bytes.fromhex(l.strip()))
    for t, d, b in blocos:
        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
            yield t, d, bytes(b[pos + 4:pos + 4 + n])
            pos += 4 + n


def segundos(t):
    h, m, s = t.split(":")
    return int(h) * 3600 + int(m) * 60 + float(s)


def coletar():
    """{npc_id: [segundos entre a morte e o renascimento]}"""
    por_especie = collections.defaultdict(list)
    for cam in fontes():
        quem, morto_em = {}, {}
        for t, d, c in frames_de(cam):
            if d != "RECV":
                continue
            agora = segundos(t)
            if c[:2] == bytes([4, 2]) and len(c) >= 74:
                eid = struct.unpack_from("<I", c, 8)[0]
                quem[eid] = struct.unpack_from("<I", c, 70)[0]
                t0 = morto_em.pop(eid, None)
                if t0 is not None and 0.5 < agora - t0 < JANELA_MAX:
                    por_especie[quem[eid]].append(agora - t0)
            elif c[:2] == bytes([8, 6]) and len(c) >= 19:
                eid = struct.unpack_from(">I", c, 7)[0]
                if struct.unpack_from(">I", c, 15)[0] == 0 and eid in quem:
                    # `setdefault`: o oficial repete o 08 06 com HP zero varias vezes, e o
                    # que conta e a PRIMEIRA -- o instante da morte.
                    morto_em.setdefault(eid, agora)
    return por_especie


def main() -> int:
    ap = argparse.ArgumentParser(description="Tempo de renascimento por especie.")
    ap.add_argument("--aplicar", action="store_true")
    args = ap.parse_args()

    D.carregar()
    nomes = {}
    for r in D.tabela("Mob"):
        try:
            nomes[int(r["mob_id"])] = r.get("nome") or ""
        except (ValueError, TypeError):
            pass

    por_especie = coletar()
    linhas = []
    for nid, v in por_especie.items():
        if len(v) < MIN_AMOSTRAS:
            continue
        s = sorted(v)
        # O ESPALHAMENTO ENTRA NA TABELA. Uma linha com p75-p25 grande nao e uma constante
        # mal medida -- e uma especie cujo renascimento depende de outra coisa, e quem ler
        # depois tem de conseguir distinguir os dois casos sem voltar a captura.
        espalha = s[int(len(s) * 0.75)] - s[int(len(s) * 0.25)]
        linhas.append((nid, s[len(s) // 2], len(v), espalha, nomes.get(nid, "")))
    linhas.sort(key=lambda x: x[1])

    print("%-10s %-26s %-9s %-8s %s" % ("mob_id", "nome", "segundos", "mortes", "espalhamento"))
    print("-" * 74)
    for nid, med, n, esp, nome in linhas:
        marca = "   <- nao e constante" if esp > ESPALHAMENTO_SUSPEITO else ""
        print("%-10d %-26s %-9.1f %-8d %.1f%s" % (nid, nome[:26], med, n, esp, marca))
    print()
    print("especies com linha: %d   renascimentos lidos: %d"
          % (len(linhas), sum(len(v) for v in por_especie.values())))

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

    destino = os.path.join(RAIZ, "server", "Data", "Monsters", "MobRespawn.ini")
    with open(destino, "w", encoding="utf-8", newline="") as f:
        w = f.write
        w("; MobRespawn -- segundos entre a morte e o renascimento, por especie." + CR)
        w("; MEDIDO no oficial: do 08 06 com HP zero ao 04 02 seguinte da mesma entidade." + CR)
        w("; Gerado por tools/medir_respawn.py. Ver o cabecalho de la." + CR)
        w(";" + CR)
        w("; espalhamento e p75-p25. Perto de zero quer dizer constante do servidor deles;" + CR)
        w("; acima de 2 s a especie depende de outra coisa e a mediana nao a resume." + CR)
        w("; Quem nao estiver aqui usa entity_sim.SEGUNDOS_RESPAWN." + CR)
        w("//mob_id" + TB + "segundos" + TB + "mortes" + TB + "espalhamento" + TB + "nome" + CR)
        for nid, med, n, esp, nome in linhas:
            w("%d%s%.1f%s%d%s%.1f%s%s%s" % (nid, TB, med, TB, n, TB, esp, TB, nome, CR))
    print()
    print("gravado: %s (%d linhas)" % (destino, len(linhas)))
    return 0


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