# -*- coding: utf-8 -*-
"""Extrai o spawn de monstros de VARIAS capturas do oficial e grava em Data/NpcSpawn.ini.

    python tools/importar_spawn_data.py                      usa todas as capturas grandes
    python tools/importar_spawn_data.py cap1.log cap2.log    usa as que voce escolher
    python tools/importar_spawn_data.py --aplicar            grava

Substitui o importar_spawn.py, que escrevia em dbo.npc_spawn -- tabela que saiu do banco em
24/08, quando o conteudo migrou para Data/. Duas diferencas alem do destino:

  - Junta VARIAS capturas em vez de uma. Cada sessao no oficial ve um pedaco do mapa; somando
    todas, a cobertura fica bem maior que a de qualquer uma sozinha.
  - Agrupa entre capturas tambem, entao o mesmo mob avistado em duas sessoes nao vira dois
    pontos.

Cada CMoveAppear (04 02) que o servidor oficial manda descreve um monstro que entrou no
campo de visao:

    [ 8:12] entity_id da sessao   -- descartavel, muda a cada login
    [12:24] posicao x, y, z       -- 3 floats LE, o que interessa
    [28:32] HP atual   [32:36] HP maximo
    [70:74] npc_id                -- casa com Data/Npc.ini

O mesmo monstro reaparece muitas vezes e andando, por isso o agrupamento por raio: dois
avistamentos do mesmo npc_id a menos de RAIO de distancia contam como UM ponto.
"""
import glob
import math
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

RAIO = 300.0          # distancia abaixo da qual dois avistamentos sao o mesmo ponto
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("NpcSpawn", "Npcs")


def frames(caminho):
    atual, blocos = None, []
    for linha in open(caminho, encoding="latin-1", errors="replace"):
        m = re.match(r"\[[\d:.]+\] (SEND|RECV) sock=", linha)
        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*", linha):
            atual[1].extend(bytes.fromhex(linha.strip()))
    for direcao, dados in blocos:
        if direcao != "RECV":
            continue
        pos = 0
        while pos + 4 <= len(dados):
            (n,) = struct.unpack_from(">I", dados, pos)
            if n == 0 or n > 16384 or pos + 4 + n > len(dados):
                break
            corpo = bytes(dados[pos + 4:pos + 4 + n])
            if len(corpo) >= 74 and corpo[0] == 4 and corpo[1] == 2:
                yield corpo
            pos += 4 + n


def avistamentos(caminhos, so_mob):
    for cam in caminhos:
        vistos = 0
        for c in frames(cam):
            npc_id = struct.unpack_from("<I", c, 70)[0]
            if npc_id not in so_mob:
                continue
            x, y, z = struct.unpack_from("<3f", c, 12)
            hp = struct.unpack_from("<I", c, 32)[0]
            if not (0 < abs(x) < 1e6 and 0 < abs(y) < 1e6):
                continue
            vistos += 1
            yield {"npc_id": npc_id, "x": x, "y": y, "z": z,
                   "hp": hp if 0 < hp < 1000000 else None,
                   "origem": os.path.basename(cam)}
        print("   %-46s %d avistamentos" % (os.path.basename(cam), vistos))


def agrupar(pontos):
    grupos = []
    for p in pontos:
        for g in grupos:
            if g["npc_id"] != p["npc_id"]:
                continue
            if math.hypot(g["x"] - p["x"], g["y"] - p["y"]) < RAIO:
                g["n"] += 1
                if p["hp"] and not g["hp"]:
                    g["hp"] = p["hp"]
                break
        else:
            grupos.append(dict(p, n=1))
    return grupos


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    escolhidas = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not escolhidas:
        escolhidas = sorted(glob.glob(r"C:\RoshHook\traffic*.log"),
                            key=os.path.getsize, reverse=True)[:6]
    D.carregar()
    npcs = D.indice("Npc", "npc_id")
    # so o que e monstro: no Karos, npc e mob dividem a tabela. O que da pra separar sem
    # adivinhar e o HP -- npc de cidade vem sempre com 1450 fixo e nao se move.
    so_mob = set(npcs)

    print("capturas usadas:")
    pontos = list(avistamentos(escolhidas, so_mob))
    print()
    grupos = agrupar(pontos)
    print("%d avistamentos -> %d pontos de spawn" % (len(pontos), len(grupos)))
    print()
    por_especie = {}
    for g in grupos:
        por_especie.setdefault(g["npc_id"], []).append(g)
    print("%-10s %-36s %-7s %s" % ("npc_id", "nome", "pontos", "hp"))
    print("-" * 68)
    for npc_id, gs in sorted(por_especie.items(), key=lambda kv: -len(kv[1])):
        ficha = npcs.get(npc_id) or {}
        hp = next((g["hp"] for g in gs if g["hp"]), None)
        print("%-10d %-36s %-7d %s" % (npc_id, (ficha.get("nome") or "?")[:35], len(gs), hp or "?"))

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

    # NPC de cidade e monstro dividem a tabela de npc no Karos. O que separa sem adivinhar
    # e o HP: todo NPC parado vem com 1450 CRAVADO na captura -- Lucas, Asuka, a Caixa de
    # correio, todos. Monstro nunca tem esse valor. O servidor precisa da distincao porque
    # NPC fica parado e nao renasce; monstro anda, morre e volta.
    HP_DE_NPC = 1450
    cols = ("spawn_id", "npc_id", "tipo", "x", "y", "z", "hp_max", "avistado", "origem")
    with open(DESTINO, "w", encoding="utf-8", newline=NL) as fp:
        fp.write("; NpcSpawn -- pontos de nascimento de monstro, extraidos de captura do oficial" + NL)
        fp.write("; Gerado por tools/importar_spawn_data.py a partir de %d capturas." % len(escolhidas) + NL)
        fp.write("; colunas: " + ", ".join(cols) + NL + NL)
        for i, g in enumerate(grupos, 1):
            fp.write("[%d]" % i + NL)
            fp.write("npc_id = %d" % g["npc_id"] + NL)
            fp.write("tipo = %s" % ("npc" if g["hp"] == HP_DE_NPC else "mob") + NL)
            fp.write("x = %.3f" % g["x"] + NL)
            fp.write("y = %.3f" % g["y"] + NL)
            fp.write("z = %.3f" % g["z"] + NL)
            if g["hp"]:
                fp.write("hp_max = %d" % g["hp"] + NL)
            fp.write("avistado = %d" % g["n"] + NL)
            fp.write("origem = %s" % g["origem"] + NL + NL)
    print()
    nmob = sum(1 for g in grupos if g["hp"] != HP_DE_NPC)
    print("gravado: %s (%s bytes, %d pontos -- %d monstros e %d npcs)"
          % (os.path.basename(DESTINO), "{:,}".format(os.path.getsize(DESTINO)),
             len(grupos), nmob, len(grupos) - nmob))
    return 0


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