# -*- coding: utf-8 -*-
"""Extrai o spawn de monstros de uma captura do oficial e grava em dbo.npc_spawn.

    python tools/importar_spawn.py C:/RoshHook/traffic.log

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

    [ 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 dbo.npc / npcInfo.lod

O mesmo monstro reaparece muitas vezes na captura (a cada vez que entra na tela), e andando.
Por isso os pontos sao agrupados: dois avistamentos do mesmo npc_id a menos de RAIO de
distancia contam como UM ponto de spawn. Sem isso 361 avistamentos de Aranha pequena
virariam 361 aranhas empilhadas.
"""
import math
import os
import re
import struct
import sys

import pyodbc

CONEXAO = ("DRIVER={ODBC Driver 18 for SQL Server};SERVER=localhost;"
           "DATABASE=Rosh;Trusted_Connection=yes;TrustServerCertificate=yes")
RAIO = 60.0        # dois avistamentos do mesmo npc dentro disto sao o mesmo ponto

TABELA = """
IF OBJECT_ID('dbo.npc_spawn') IS NOT NULL DROP TABLE dbo.npc_spawn;
CREATE TABLE dbo.npc_spawn (
    spawn_id   INT IDENTITY(1,1) PRIMARY KEY,
    npc_id     BIGINT NOT NULL,
    x          FLOAT  NOT NULL,
    y          FLOAT  NOT NULL,
    z          FLOAT  NOT NULL,
    hp_max     INT    NULL,
    avistado   INT    NULL,
    origem     NVARCHAR(120) NULL
);
"""


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


def avistamentos(cam, validos):
    for direcao, f in frames(cam):
        if direcao != "RECV" or f[:2] != bytes([4, 2]) or len(f) < 74:
            continue
        npc_id = struct.unpack_from("<I", f, 70)[0]
        if npc_id not in validos:
            continue          # 04 02 de JOGADOR nao tem npc_id valido aqui
        x, y, z = struct.unpack_from("<3f", f, 12)
        hp_max = struct.unpack_from("<I", f, 32)[0]
        yield npc_id, x, y, z, hp_max


def agrupar(pontos):
    """Junta avistamentos proximos do mesmo npc num unico ponto (media corrida)."""
    grupos = []
    for npc_id, x, y, z, hp in pontos:
        for g in grupos:
            if g["npc_id"] == npc_id and math.hypot(g["x"] - x, g["y"] - y) <= RAIO:
                n = g["n"]
                g["x"] = (g["x"] * n + x) / (n + 1)
                g["y"] = (g["y"] * n + y) / (n + 1)
                g["z"] = (g["z"] * n + z) / (n + 1)
                g["n"] = n + 1
                if hp:
                    g["hp"] = max(g["hp"] or 0, hp)
                break
        else:
            grupos.append({"npc_id": npc_id, "x": x, "y": y, "z": z, "hp": hp, "n": 1})
    return grupos


def main() -> int:
    if len(sys.argv) < 2:
        print(__doc__)
        return 2
    # Trava --aplicar, igual aos outros importadores. Sem ela, rodar "so pra ver o que
    # tem na captura" GRAVAVA: em 24/08 isso devolveu 62 aranhas a um mundo que estava
    # vazio de proposito, pra medir no oficial.
    aplicar = "--aplicar" in sys.argv
    cam = next(a for a in sys.argv[1:] if not a.startswith("--"))
    if not os.path.exists(cam):
        print(f"nao achei a captura: {cam}")
        return 1

    con = pyodbc.connect(CONEXAO, autocommit=False)
    cur = con.cursor()
    cur.execute("SELECT npc_id, nome FROM dbo.npc")
    nomes = {l[0]: l[1] for l in cur.fetchall()}
    if not nomes:
        print("dbo.npc esta vazia -- rode antes: python tools/importar_npcs.py")
        return 1

    pontos = list(avistamentos(cam, nomes))
    if not pontos:
        print("nenhum 04 02 de monstro nesta captura "
              "(so SEND? o hook precisa pegar RECV tambem)")
        return 1
    grupos = agrupar(pontos)
    especies = {g["npc_id"] for g in grupos}
    print(f"{len(pontos)} avistamentos -> {len(grupos)} pontos de spawn, "
          f"{len(especies)} especies")

    if not aplicar:
        for g in grupos[:8]:
            print("   npc %-10s (%.0f, %.0f)" % (g["npc_id"], g["x"], g["y"]))
        print()
        print("nada gravado. rode com --aplicar")
        return 0

    for parte in TABELA.split(";"):
        if parte.strip():
            cur.execute(parte)
    cur.fast_executemany = True
    cur.executemany(
        "INSERT INTO dbo.npc_spawn (npc_id, x, y, z, hp_max, avistado, origem) "
        "VALUES (?,?,?,?,?,?,?)",
        [(g["npc_id"], g["x"], g["y"], g["z"], g["hp"] or None, g["n"],
          os.path.basename(cam)) for g in grupos])
    con.commit()

    print()
    print("especies encontradas:")
    porta = {}
    for g in grupos:
        porta.setdefault(g["npc_id"], []).append(g)
    for npc_id, gs in sorted(porta.items(), key=lambda kv: -len(kv[1])):
        hp = next((g["hp"] for g in gs if g["hp"]), None)
        print("  %-10d %-34s pontos=%-4d hp=%-6s nivel~%d" %
              (npc_id, (nomes.get(npc_id) or "?")[:34], len(gs), hp or "?",
               npc_id % 100))
    con.close()
    return 0


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