# -*- coding: utf-8 -*-
r"""Mede o ALCANCE DE ATAQUE de cada especie de monstro, nas capturas do oficial.

    python tools/medir_alcance.py  ->  Data/Monsters/MobAlcance.ini

O pacote 05 00 (golpe do monstro) traz a posicao do atacante e a do alvo. A distancia entre
elas no instante do golpe E o alcance, e nao ha que deduzir nada.

PORQUE EXISTE. A coluna alcance_ataque do Mob.ini estava preenchida em 5 de 926 monstros; os
outros 921 usavam o valor por omissao de 30 para tudo. Medido em 5.151 golpes:

    Aranha pequena          mediana 17,0    p90 21,0
    Filhote de urso feroz   mediana 18,4
    Cao branco              mediana 25,3
    Espadachim Aguia        mediana 28,1
    todos juntos            mediana 27,7    p90 29,6

Uma aranha pequena parava a 30 do jogador e batia-lhe de la, o que o jogador viu de imediato:
"o mob quando ataca ele fica longe de mim". O tecto parece ser 30 -- nenhum p90 passa de 29,9
-- e o que varia e quanto ABAIXO disso cada bicho fica.

Usa-se a MEDIANA e nao o maximo: o maximo apanha o golpe dado no instante em que o jogador ja
se afastava, e isso alarga o alcance de todos.
"""
import collections
import math
import os
import statistics
import sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "analise"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "server"))
import metricas as M                                                # noqa: E402

MINIMO_AMOSTRAS = 8


def main() -> int:
    nomes = {}
    try:
        import data_files as D
        D.carregar()
        nomes = {r["mob_id"]: r.get("nome") for r in D.tabela("Mob") if r.get("mob_id")}
    except Exception as e:                                          # noqa: BLE001
        print("(sem nomes: %s)" % str(e)[:60])

    alvos = list(sys.argv[1:])
    if not alvos:
        base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "captures")
        alvos = [os.path.join(base, f) for f in sorted(os.listdir(base))
                 if f.startswith("oficial") and f.endswith(".log")]
        d2 = os.path.join(os.environ.get("USERPROFILE", ""), "Desktop", "CapturaKaros")
        if os.path.isdir(d2):
            alvos += [os.path.join(d2, f) for f in sorted(os.listdir(d2))
                      if f.endswith(".log") and "2209" not in f]

    por = collections.defaultdict(list)
    cadencia = collections.defaultdict(list)
    pares = collections.defaultdict(list)
    for cam in alvos:
        try:
            ev = M.carregar(cam)
        except Exception:                                           # noqa: BLE001
            continue
        for _t, at, _alvo, pa, pal, _rot in ev["golpes"]:
            npc = ev["npc"].get(at, 0)
            if npc <= M.MOB_MINIMO:
                continue
            d = math.hypot(pal[0] - pa[0], pal[1] - pa[1])
            if 0 < d < 500:
                por[npc].append(d)
            pares[(cam, at, _alvo, npc)].append(_t)

    # CADENCIA entre golpes do mesmo mob no mesmo alvo. Medida: mediana global 2,98 s,
    # e por especie entre 2,39 e 4,19. O servidor tinha 4,21 para todas -- o topo da
    # distribuicao, ou seja o mob mais lento a bater de todo o jogo.
    for (_c, _at, _al, npc), v in pares.items():
        v.sort()
        for a, b in zip(v, v[1:]):
            if 0.2 < b - a < 15:
                cadencia[npc].append(b - a)

    # QUANTAS AMOSTRAS PARA A CADENCIA -- e porque sao mais do que para o alcance.
    #
    # Sao duas medicoes com feitios diferentes. O alcance e uma distancia observada: cada golpe
    # da uma amostra honesta e oito chegam. A cadencia e um intervalo, e um intervalo mente
    # sempre PARA CIMA: se o mob perde o alvo entre dois golpes, aquele intervalo sai 2x ou 3x
    # a recarga real. A distribuicao tem sempre cauda de multiplos, e com amostra pequena a
    # mediana cai dentro dela.
    #
    # Ve-se nos numeros. Javali-de-sabre, n=17, mediana 6,13 -- mas o histograma tem dois
    # montes, 4,5-6,0 e 8,0-10,0, e o segundo e o dobro do primeiro. Com 17 amostras nao ha
    # como os separar. Especies com n acima de 100 sao estaveis (Cao branco 2,98 em 1179;
    # Tirano sanguinario 4,20 em 818) porque o monte base domina.
    #
    # Tentei filtrar a cauda -- semente no p25, cortar acima de 1,6x. Corrigia o Javali
    # (6,13 -> 5,62) e ESTRAGAVA o Peekskin (2,87 -> 1,01): ha especies com rajada de dois
    # golpes a ~1 s e o filtro colapsava para a rajada. Trocava um enviesamento por outro.
    #
    # Fica o honesto: abaixo do limite nao se afirma nada por especie e usa-se a mediana
    # global, com 3402 amostras. A coluna cad_n diz quais sao medidas e quais herdadas.
    MIN_CADENCIA = 60
    _tudo = sorted(x for lst in cadencia.values() for x in lst)
    CAD_GLOBAL = _tudo[len(_tudo) // 2] if _tudo else 0.0
    print("cadencia global: %.2f s em %d intervalos" % (CAD_GLOBAL, len(_tudo)))

    linhas = []
    for npc, v in sorted(por.items()):
        if len(v) < MINIMO_AMOSTRAS:
            continue
        # O ALCANCE E O TECTO, NAO A DISTANCIA TIPICA. Este numero ja esteve no p90, depois
        # no p25 e depois na mediana, e as tres vezes por raciocinio errado. O histograma dos
        # 5.172 golpes do oficial resolve a questao de vez:
        #
        #      22 | ##########  446        28 | ############################  2185
        #      24 | ##########  493        30 |  4
        #      26 | ####################  890        32 |  1
        #
        # Ha uma PAREDE em 30. O balde 28-30 tem 2.185 golpes e o seguinte tem 4. E o p95 de
        # todas as especies com amostra decente encosta la: 29,8  30,0  29,7  29,9  29,8  30,0.
        # Nao ha especie curta e especie comprida -- ha um alcance so, 30, e o que varia e a
        # que distancia cada bicho CALHA estar quando a recarga vence. A mediana mede isso
        # ultimo, que nao e o alcance de nada.
        #
        # PORQUE E QUE A MEDIANA PARECIA CERTA. Havia um atalho no entity_sim que fingia a
        # distancia -- qualquer mob a menos de alcance+21,33 era tratado como se ja estivesse
        # em alcance. Com ele ligado, pôr 30 aqui dava 51 de alcance efectivo e via-se logo
        # ("o mob quando ataca ele fica longe de mim"). Baixei este numero para compensar
        # aquele, em vez de tirar o atalho. Dois erros a anularem-se, e mal.
        #
        # Tirado o atalho (29/08), o alcance curto ficou a descoberto de imediato: "so bate se
        # eu bater nele, mas eu me mover ele vem ate mim e nao bate". Com 27 e o jogador a
        # mexer-se, a distancia no servidor quase nunca desce abaixo disso.
        #
        # p95 e nao o maximo: o maximo apanha a cauda de golpes a 71, 110, 189 unidades, que
        # sao o mob a bater no instante em que o alvo saltou para longe, nao alcance.
        s = sorted(v)
        alc = min(30.0, s[min(len(s) - 1, int(len(s) * 0.95))])
        cad = sorted(cadencia.get(npc) or [])
        medida = len(cad) >= MIN_CADENCIA
        c50 = cad[len(cad) // 2] if medida else CAD_GLOBAL
        linhas.append((npc, alc, len(v), c50, len(cad) if medida else 0,
                       nomes.get(npc) or ""))
    print("%d especies com %d ou mais golpes medidos" % (len(linhas), MINIMO_AMOSTRAS))

    destino = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                           "..", "server", "Data", "Monsters", "MobAlcance.ini")
    nl = chr(10)
    with open(destino, "w", encoding="utf-8", newline="") as f:
        f.write("; MobAlcance -- alcance de ataque por especie, MEDIDO no oficial." + nl)
        f.write("; Gerado por tools/medir_alcance.py. Ver o cabecalho dessa ferramenta." + nl)
        f.write("; O que nao estiver aqui usa o valor por omissao de dados_de_agressao." + nl)
        f.write("; A cadencia so e por especie com %d ou mais intervalos; abaixo disso"
                % MIN_CADENCIA + " e a mediana global e cad_n vem a zero." + nl)
        f.write("//mob_id\talcance\tamostras\tcadencia\tcad_n\tnome" + nl)
        for npc, alc, n, cad, cadn, nome in linhas:
            f.write("%d\t%.1f\t%d\t%.2f\t%d\t%s%s"
                    % (npc, alc, n, cad, cadn, nome, nl))
    print("escrito em %s" % os.path.normpath(destino))
    return 0


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