# -*- coding: utf-8 -*-
"""Ha formula para a quantidade de Carat? Depende do nivel do mob? Ha penalidade?

    python tools/medir_carat.py

Mede nas capturas do oficial, cruzando tres coisas que ja se sabem ler do fio:

    o nivel do JOGADOR      byte [43] do 08 01 (o medidor do heroi)
    o nivel do MONSTRO      do npc_id da morte, pelo Data/Monsters/Mob.ini
    o CARAT largado         item 999999 no 06 04, quantidade em [42:46]

A ligacao saco->monstro e a mesma do medir_mobdrop.py: janela de tempo e raio a volta do
corpo. Quem quiser mudar os limites muda la, para nao haver duas versoes da mesma regra.

O QUE SE PROCURA, POR ESTA ORDEM

    1. Carat medio por NIVEL DO MOB           ha curva?
    2. Carat medio por DIFERENCA DE NIVEL     ha penalidade, como na EXP?
    3. as duas juntas                         qual das duas manda

A pergunta 2 e a que interessa mais, porque explicaria as faixas absurdamente largas que a
medicao por monstro deu (o Cao branco entre 11 e 120 Carat). Se houver penalidade, o mesmo
monstro rende cada vez menos a medida que o jogador sobe -- e uma sessao inteira de captura,
onde o jogador subiu varios niveis, mistura tudo isso numa faixa unica.
"""
import collections
import glob
import math
import os
import struct
import sys

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(RAIZ, "tools", "analise"))
sys.path.insert(0, os.path.join(RAIZ, "server"))
from ler_hook import blocos                                         # noqa: E402
import data_files as D                                              # noqa: E402

DESKTOP = os.path.join(os.path.expanduser("~"), "Desktop", "CapturaKaros")
CARAT = 999999
JANELA = 1.5
RAIO = 80.0
OFF_NIVEL_JOGADOR = 43            # byte, dentro do 08 01


def eventos(caminho):
    for t, d, _s, dados in blocos(caminho):
        i, n = 0, len(dados)
        while i + 4 <= n:
            tam = int.from_bytes(dados[i:i + 4], "big")
            if tam <= 0 or tam > 65535 or i + 4 + tam > n:
                break
            c = dados[i + 4:i + 4 + tam]
            if len(c) >= 2:
                yield t, d, c[:2], c
            i += 4 + tam


def varrer(caminho, amostras):
    npc_de, pos_de, vivo = {}, {}, {}
    mortes = []
    nivel_jog = None
    meu_id = None
    pendentes = []          # (t, qtd, x, y, nivel, dono_do_saco, meu_id)

    for t, direcao, op, c in eventos(caminho):
        if direcao == "SEND":
            # O ID DO PROPRIO JOGADOR so se sabe pelo que o CLIENT ENVIA. O 04 01 que CHEGA
            # traz o movimento de toda a gente que esta perto, e ler o id dai dava o do
            # ultimo desconhecido que passou -- foi o que esvaziou a amostra a primeira vez.
            if op == bytes([4, 1]) and len(c) >= 12:
                meu_id = struct.unpack_from(">I", c, 8)[0]
            continue
        if op == bytes([8, 1]) and len(c) > OFF_NIVEL_JOGADOR:
            n = c[OFF_NIVEL_JOGADOR]
            if 1 <= n <= 200:
                nivel_jog = n
        elif op == bytes([4, 2]) and len(c) >= 74:
            ent = struct.unpack_from("<I", c, 8)[0]
            npc_de[ent] = struct.unpack_from("<I", c, 70)[0]
            pos_de[ent] = struct.unpack_from("<3f", c, 12)[:2]
            vivo[ent] = True
        elif op == bytes([3, 0x0B]) and len(c) >= 22:
            ent = struct.unpack_from(">I", c, 6)[0]
            if ent in npc_de:
                pos_de[ent] = struct.unpack_from("<3f", c, 10)[:2]
        elif op == bytes([8, 6]) and len(c) >= 23:
            ent = struct.unpack_from(">I", c, 7)[0]
            hp = struct.unpack_from(">I", c, 15)[0]
            if hp == 0 and vivo.get(ent) and ent in npc_de and ent in pos_de:
                vivo[ent] = False
                mortes.append((t, npc_de[ent], pos_de[ent][0], pos_de[ent][1]))
            elif hp > 0:
                vivo[ent] = True
        elif op == bytes([6, 4]) and len(c) == 187:
            if struct.unpack_from("<I", c, 28)[0] != CARAT:
                continue
            p = struct.unpack_from("<3f", c, 173)
            pendentes.append((t, struct.unpack_from("<I", c, 42)[0], p[0], p[1], nivel_jog,
                              struct.unpack_from(">I", c, 6)[0], meu_id))

    for ts, qtd, sx, sy, njog, dono, meu in pendentes:
        if njog is None:
            continue
        # SO OS SACOS DO PROPRIO JOGADOR. Um saco reservado a outra pessoa traz a
        # quantidade calculada para o NIVEL DELA, e misturar isso e medir a penalidade de
        # um desconhecido. Foi o que fez a mesma Aranha nivel 1 aparecer com 30 Carat numa
        # captura e 2 noutra.
        if dono != 0xFFFFFFFF and meu is not None and dono != meu:
            continue
        cand = set(npc for tm, npc, mx, my in mortes
                   if 0 <= ts - tm <= JANELA and math.hypot(sx - mx, sy - my) <= RAIO)
        if len(cand) != 1:
            continue
        amostras.append((cand.pop(), njog, qtd, os.path.basename(caminho)))


def media(v):
    return sum(v) / float(len(v)) if v else 0.0


def main() -> int:
    D.carregar()
    mobs = dict((r["mob_id"], r) for r in D.tabela("Mob"))
    alvos = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not alvos:
        alvos = sorted(glob.glob(os.path.join(DESKTOP, "*.log")))

    amostras = []
    for a in alvos:
        varrer(a, amostras)

    # so amostras com nivel de monstro conhecido
    dados = []
    for npc, njog, qtd, _cap in amostras:
        m = mobs.get(npc)
        nm = m.get("nivel") if m else None
        if nm:
            dados.append((int(nm), int(njog), int(qtd)))
    print("sacos de Carat ligados a um monstro de nivel conhecido: %d" % len(dados))
    if not dados:
        print("nada para medir")
        return 0
    nj = sorted(set(d[1] for d in dados))
    print("niveis do jogador nas capturas: %s" % nj)
    print()

    # ---------------------------------------------------------------- 1. por nivel do mob
    print("1. CARAT POR NIVEL DO MONSTRO")
    print("   %-8s %-8s %-10s %-10s %s" % ("nivel", "amostras", "media", "min-max", "media/nivel"))
    por_nm = collections.defaultdict(list)
    for nm, _njog, q in dados:
        por_nm[nm].append(q)
    for nm in sorted(por_nm):
        v = por_nm[nm]
        print("   %-8d %-8d %-10.1f %-10s %.2f"
              % (nm, len(v), media(v), "%d-%d" % (min(v), max(v)), media(v) / nm))
    print()

    # ------------------------------------------------------- 2. por diferenca de nivel
    print("2. CARAT POR DIFERENCA DE NIVEL (nivel do mob menos nivel do jogador)")
    print("   %-8s %-8s %-10s %s" % ("dif", "amostras", "media", "min-max"))
    por_dif = collections.defaultdict(list)
    for nm, njog, q in dados:
        por_dif[nm - njog].append(q)
    for d in sorted(por_dif):
        v = por_dif[d]
        print("   %-8d %-8d %-10.1f %s" % (d, len(v), media(v), "%d-%d" % (min(v), max(v))))
    print()

    # ---------------------------------------- 3. a MESMA ESPECIE, vista de niveis diferentes
    #
    # E o unico teste que separa as duas hipoteses. Agrupar por NIVEL do mob nao chega:
    # nivel 1 junta varias especies, e especies diferentes tem base diferente. Fixando a
    # ESPECIE, a base fica constante e o que sobra e so o efeito do nivel do jogador.
    print("3. A MESMA ESPECIE, VISTA DE NIVEIS DIFERENTES DO JOGADOR")
    print("   Fixa a especie e portanto fixa a base. O que variar aqui e penalidade.")
    print()
    por_esp = collections.defaultdict(lambda: collections.defaultdict(list))
    for npc, njog, qtd, _cap in amostras:
        m = mobs.get(npc)
        if m and m.get("nivel"):
            por_esp[npc][njog].append(qtd)
    for npc in sorted(por_esp, key=lambda k: -sum(len(v) for v in por_esp[k].values())):
        js = sorted(por_esp[npc])
        if len(js) < 3:
            continue
        m = mobs[npc]
        print("   %s (npc %d, nivel %s), %d abates"
              % (str(m.get("nome"))[:26], npc, m.get("nivel"),
                 sum(len(v) for v in por_esp[npc].values())))
        print("      %-10s %s" % ("jogador", "  ".join("%4d" % j for j in js)))
        print("      %-10s %s" % ("dif",
              "  ".join("%4d" % (j - int(m.get("nivel"))) for j in js)))
        print("      %-10s %s" % ("carat",
              "  ".join("%4.0f" % media(por_esp[npc][j]) for j in js)))
        print("      %-10s %s" % ("n",
              "  ".join("%4d" % len(por_esp[npc][j]) for j in js)))
        print()

    # ------------------------------------------------- 4. a mesma especie, POR CAPTURA
    #
    # Necessario antes de tirar formula nenhuma. As especies discordavam entre si sobre
    # onde a penalidade comeca (o Javali plano ate +3, a Aguia a cair em +2), e a explicacao
    # mais provavel nao e a formula: e um EVENTO de Carat num dos dias. O Data/Common/
    # EventosOficial.ini existe exactamente porque isto ja aconteceu com a EXP.
    print("4. A MESMA ESPECIE, SEPARADA POR CAPTURA")
    print("   Se duas capturas derem valores diferentes para a MESMA especie e o MESMO nivel")
    print("   do jogador, entao ha alguma coisa fora da formula -- um evento, por exemplo.")
    print()
    por_cap = collections.defaultdict(lambda: collections.defaultdict(list))
    for npc, njog, qtd, cap in amostras:
        por_cap[npc][(cap, njog)].append(qtd)
    for npc in sorted(por_cap, key=lambda k: -sum(len(v) for v in por_cap[k].values()))[:6]:
        m = mobs.get(npc) or {}
        print("   %s (nivel %s)" % (str(m.get("nome"))[:30], m.get("nivel")))
        for (cap, njog) in sorted(por_cap[npc]):
            v = por_cap[npc][(cap, njog)]
            if len(v) < 3:
                continue
            print("      %-30s jogador %-4d n=%-5d media %6.1f   min-max %d-%d"
                  % (cap[6:20], njog, len(v), media(v), min(v), max(v)))
        print()
    return 0


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