# -*- coding: utf-8 -*-
"""Extrai os pontos de nascimento do CLIENT, dos arquivos .npc de cada mapa.

    python tools/importar_spawn_client.py                    mostra
    python tools/importar_spawn_client.py --mapa berneo      so um mapa
    python tools/importar_spawn_client.py --mapa berneo --aplicar

Ate 25/08 o spawn so vinha de captura: alguem tinha que ver o monstro pra ele existir pra
nos, e por isso o NpcSpawn.ini tinha 98 pontos de mob. O client tem 9104, em 191 arquivos
.npc espalhados pelas pastas de mapa, cobrindo 635 especies -- inclusive mapas onde ninguem
nunca entrou.

FORMATO DO REGISTRO (medido em 25/08):

    <nome do modelo, com prefixo de tamanho>  ex "M_AntWorker._OB"
    <nome da instancia, idem>                 ex "M_AntWorker_30"
    [ 0: 4]  npc_id            LE u32
    [ 4: 8]  x                 float, LOCAL ao bloco
    [ 8:12]  ALTURA            float   <- cuidado: a ordem e (x, altura, y), nao (x, y, z)
    [12:16]  y                 float, LOCAL ao bloco
    [20:24]  rotacao           float, radianos (0, 1.57, 3.14...)
    [28:32]  quantidade        LE u32  quantos nascem neste ponto (0 = um so, caso dos NPCs)
    [32:36]  raio              float   raio de perambulacao (10, 50, 100, 300)
    [36:40]  destino           LE u32  mapa de destino, so em objeto de teleporte

COORDENADA DE MUNDO. Os arquivos sao uma grade: berneo0, berneo1, berneo64, berneo65...
com 64 colunas e blocos de 5120 unidades.

    x_mundo = (bloco % 64) * 5120 + x_local
    y_mundo = (bloco // 64) * 5120 + y_local

Validado contra o fio: o Domingo sai em (8892, 6712) e a captura do oficial mostra
(8891, 6711) -- UM ponto de diferenca. Nas 13 aranhas medidas o erro mediano e 53 unidades,
que e o monstro tendo andado (o raio de perambulacao delas e 300).

ID DE ENTIDADE. E o INDICE de enumeracao dentro do mapa, contando as entradas na ordem
crescente de bloco. Conferido em 8 NPCs de Berneo cujo id foi medido no fio:

    Oshirus 18, Panteao 203, Lucas 204, Jasao 320, Mike 322, Domingo 370,
    Caixa de correio 391, Salibent 407

Oito de oito, exatos. Isso importa mais do que parece: o client DESENHA essas entidades a
partir destes arquivos, e mandar 04 02 com id proprio nao substitui o que ele desenhou --
cria um segundo por cima. Foi o que aconteceu em 25/08, com dois Domingos na tela.

UM MAPA POR VEZ. Os 9104 pontos sao de 45 mapas diferentes e a enumeracao recomeca em cada
um. Despejar todos num mundo so nao faz sentido, e por isso o --mapa e obrigatorio pra
gravar.
"""
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

MAPAS = r"C:\Games\Karos\ResUnpak\Map"
BLOCO = 5120.0
COLUNAS = 64
TIPO_NPC = 1
CR = chr(13) + chr(10)
BARRA = chr(92)
# offsets relativos ao npc_id
OFF_X, OFF_ALT, OFF_Y, OFF_ROT, OFF_QTD, OFF_RAIO, OFF_DEST = 4, 8, 12, 20, 28, 32, 36


def escapar(v):
    if v is None:
        return None
    s = str(v).replace(BARRA, BARRA + BARRA)
    return s.replace(chr(9), BARRA + "t").replace(chr(13), "").replace(chr(10), BARRA + "n")


def arquivos_por_mapa():
    """{nome do mapa: [caminhos em ordem crescente de bloco]}"""
    saida = collections.defaultdict(list)
    for cam in glob.glob(os.path.join(MAPAS, "**", "*.npc"), recursive=True):
        base = os.path.basename(cam)[:-4]
        m = re.match(r"(.+?)(\d+)$", base)
        if not m:
            continue
        saida[m.group(1)].append((int(m.group(2)), cam))
    for k in saida:
        saida[k].sort()
    return saida


def ler_mapa(nome, blocos, catalogo):
    """Pontos de um mapa, ja em coordenada de mundo e com o id de entidade."""
    pontos = []
    indice = 0
    for bloco, cam in blocos:
        d = open(cam, "rb").read()
        o = 0
        while o < len(d) - 40:
            v = struct.unpack_from("<I", d, o)[0]
            if v in catalogo:
                x = struct.unpack_from("<f", d, o + OFF_X)[0]
                alt = struct.unpack_from("<f", d, o + OFF_ALT)[0]
                y = struct.unpack_from("<f", d, o + OFF_Y)[0]
                # o filtro e o que separa um registro de verdade de um u32 que por acaso
                # cai num id: coordenada tem que estar DENTRO do bloco
                if 0 <= x <= BLOCO * 1.2 and 0 <= y <= BLOCO * 1.2 and 0 < alt < 5000:
                    indice += 1
                    pontos.append({
                        "entidade_id": indice,
                        "npc_id": v,
                        "tipo": "npc" if catalogo[v].get("tipo") == TIPO_NPC else "mob",
                        "x": round((bloco % COLUNAS) * BLOCO + x, 3),
                        "y": round((bloco // COLUNAS) * BLOCO + y, 3),
                        "z": round(alt, 3),
                        "rotacao": round(struct.unpack_from("<f", d, o + OFF_ROT)[0], 4),
                        "quantidade": struct.unpack_from("<I", d, o + OFF_QTD)[0],
                        "raio": round(struct.unpack_from("<f", d, o + OFF_RAIO)[0], 1),
                        "destino": struct.unpack_from("<I", d, o + OFF_DEST)[0] or None,
                        "bloco": bloco,
                    })
                    o += 4
                    continue
            o += 1
    return pontos


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    mapa = None
    if "--mapa" in sys.argv:
        mapa = sys.argv[sys.argv.index("--mapa") + 1]

    D.carregar()
    catalogo = {int(r["npc_id"]): r for r in D.tabela("Npc")}
    por_mapa = arquivos_por_mapa()

    if mapa is None:
        print("%d mapas, %d arquivos .npc" % (len(por_mapa), sum(len(v) for v in por_mapa.values())))
        print()
        print("%-22s %-8s %-8s %-8s %s" % ("mapa", "blocos", "pontos", "mobs", "npcs"))
        print("-" * 60)
        tot = 0
        for nome in sorted(por_mapa):
            p = ler_mapa(nome, por_mapa[nome], catalogo)
            nm = sum(1 for x in p if x["tipo"] == "mob")
            print("%-22s %-8d %-8d %-8d %d" % (nome, len(por_mapa[nome]), len(p), nm, len(p) - nm))
            tot += len(p)
        print("-" * 60)
        print("%-22s %-8s %d" % ("TOTAL", "", tot))
        print()
        print("escolha um com --mapa NOME. Nao da pra juntar todos: sao mundos diferentes e")
        print("a enumeracao do id de entidade recomeca em cada um.")
        return 0

    if mapa not in por_mapa:
        print("mapa %r nao existe. Ha: %s" % (mapa, ", ".join(sorted(por_mapa))))
        return 1

    pontos = ler_mapa(mapa, por_mapa[mapa], catalogo)
    mobs = [p for p in pontos if p["tipo"] == "mob"]
    npcs = [p for p in pontos if p["tipo"] == "npc"]
    total_bichos = sum(max(1, p["quantidade"]) for p in mobs)
    print("mapa %s: %d blocos, %d pontos (%d mob, %d npc)"
          % (mapa, len(por_mapa[mapa]), len(pontos), len(mobs), len(npcs)))
    print("monstros que nascem de fato: %d (a quantidade de cada ponto somada)" % total_bichos)
    print()
    esp = collections.Counter(p["npc_id"] for p in mobs)
    print("especies de monstro mais comuns:")
    for nid, n in esp.most_common(8):
        print("   %-10s %-30s %d ponto(s)" % (nid, str(catalogo[nid].get("nome"))[:30], n))

    # Confere contra o NpcSpawn.ini atual, por POSICAO -- nao por npc_id.
    #
    # A versao anterior desta comparacao indexava por npc_id sozinho, o que colapsa todos os
    # pontos da MESMA especie numa unica entrada (25 especies de mob + ~44 npc_id de NPC =
    # 69 chaves pra 518 pontos). Cada ponto acabava comparado contra um ponto ARBITRARIO da
    # mesma especie, e o resultado -- "69 iguais, 449 diferentes" -- nao dizia nada sobre os
    # dados: media quantos pontos calharam de ser o ultimo da especie no dict.
    #
    # npc_id + posicao arredondada identifica o ponto de verdade, porque a extracao e
    # deterministica a partir dos mesmos .npc do client.
    if D.existe("NpcSpawn"):
        atual = {}
        for r in D.tabela("NpcSpawn"):
            if not r.get("entidade_id"):
                continue
            chave = (int(r["npc_id"]), round(float(r["x"]), 1), round(float(r["y"]), 1))
            atual[chave] = int(r["entidade_id"])
        bate = fora = sem_correspondente = 0
        for p in pontos:
            chave = (p["npc_id"], round(p["x"], 1), round(p["y"], 1))
            m = atual.get(chave)
            if m is None:
                sem_correspondente += 1
            elif m == p["entidade_id"]:
                bate += 1
            else:
                fora += 1
        print()
        print("conferencia com o NpcSpawn.ini atual, por posicao:")
        print("   id de entidade igual: %d | diferente: %d | sem correspondente: %d"
              % (bate, fora, sem_correspondente))

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

    cols = ("npc_id", "entidade_id", "tipo", "x", "y", "z", "rotacao",
            "quantidade", "raio", "destino", "bloco")
    destino = D.caminho("NpcSpawn", "Npcs")
    with open(destino, "w", encoding="utf-8", newline=CR) as fp:
        w = fp.write
        w("; NpcSpawn -- pontos de nascimento, extraidos do CLIENT (mapa %s)." % mapa + CR)
        w("; colunas: spawn_id, " + ", ".join(cols) + CR)
        w(";" + CR)
        w("; Vem dos arquivos .npc das pastas de mapa, nao de captura. Antes disso o spawn so" + CR)
        w("; existia pra monstro que alguem tivesse visto -- eram 98 pontos." + CR)
        w(";" + CR)
        w("; entidade_id e o INDICE de enumeracao dentro do mapa, na ordem crescente de bloco." + CR)
        w("; Conferido em 8 NPCs de Berneo cujo id foi medido no fio, e bateram os 8." + CR)
        w("; Isso e o que impede duplicar: o client DESENHA essas entidades a partir destes" + CR)
        w("; mesmos arquivos, e um 04 02 com id proprio cria um segundo por cima em vez de" + CR)
        w("; substituir -- foi o caso dos dois Domingos em 25/08." + CR)
        w(";" + CR)
        w("; quantidade: quantos monstros nascem no ponto. Zero quer dizer UM (e o valor dos" + CR)
        w("; NPCs). Uma entrada de Aranha pequena traz 10, com raio 300." + CR)
        w("; raio      : ate onde ele perambula a partir daqui." + CR)
        w("; destino   : mapa de destino, so em objeto de teleporte (a Pedra de Retorno traz" + CR)
        w(";             190023, que e a Pedra do retorno a Lupinel)." + CR)
        w(CR)
        for i, p in enumerate(pontos, 1):
            w("[%d]" % i + CR)
            w("; %s" % (catalogo[p["npc_id"]].get("nome") or "?") + CR)
            for c in cols:
                v = escapar(p.get(c))
                if v not in (None, ""):
                    w("%s = %s" % (c, v) + CR)
            w(CR)
    print()
    print("gravado: %s (%s bytes, %d pontos)"
          % (destino, "{:,}".format(os.path.getsize(destino)), len(pontos)))
    return 0


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