# -*- coding: utf-8 -*-
"""Extrai o comportamento dos monstros de uma captura do hook.

Serve para comparar o nosso servidor com o oficial no mesmo pe: as duas capturas passam
pelo mesmo codigo, e o que sair diferente e diferenca de comportamento, nao de metodo.

O QUE MEDE, por fase:

  passeio     estado 0 -- ritmo, passo, velocidade real e declarada
  perseguicao estado 1 a APROXIMAR-SE do jogador
  retorno     estado 1 a AFASTAR-SE do jogador (o bicho a voltar para o poste)
  parado      estado 2 -- quanto tempo fica quieto de cada vez
  combate     05 00 do mob -- cadencia dos golpes e para onde ele se vira
  dano        o que o servidor manda quando o JOGADOR acerta

A separacao entre perseguir e voltar e feita pela distancia ao jogador, nao por adivinhar:
dentro de uma corrida continua de estado 1, se a distancia ao jogador encolhe ele persegue;
se cresce, vai-se embora.
"""
import collections
import math
import os
import struct
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ler_hook import pacotes                                        # noqa: E402

BURACO = 2.0        # dois pacotes separados por mais que isto sao corridas diferentes
MOB_MINIMO = 1000   # npc_id abaixo disto e jogador ou NPC de cidade

OP_MOVER = bytes([4, 1])
OP_APARECER = bytes([4, 2])
OP_GOLPE = bytes([5, 0])
OP_DANO = bytes([5, 2])
OP_ATAQUE_CLIENT = bytes([5, 1])


def _p(v, q):
    """Percentil q de v, sem depender de biblioteca."""
    if not v:
        return 0.0
    v = sorted(v)
    return v[min(len(v) - 1, int(q * len(v)))]


def rumo(dx, dy):
    """A convencao do jogo: (3*pi/2 - atan2(dy, dx)) mod 2*pi. Zero aponta a SUL."""
    return (1.5 * math.pi - math.atan2(dy, dx)) % (2 * math.pi)


def carregar(caminho):
    """Eventos ja separados por tipo."""
    mov = collections.defaultdict(list)
    npc_de = {}
    golpes, danos, jogador = [], [], []
    for t, op, c in pacotes(caminho, direcao='RECV'):
        if op == OP_APARECER and len(c) >= 74:
            npc_de[struct.unpack_from('<I', c, 8)[0]] = struct.unpack_from('<I', c, 70)[0]
        elif op == OP_MOVER and len(c) >= 42:
            e = struct.unpack_from('>I', c, 8)[0]
            ox, oy, _z = struct.unpack_from('<3f', c, 12)
            dx, dy, _z2 = struct.unpack_from('<3f', c, 24)
            mov[e].append((t, c[7], ox, oy, dx, dy, struct.unpack_from('<f', c, 36)[0]))
        elif op == OP_GOLPE and len(c) >= 55:
            golpes.append((t, struct.unpack_from('>I', c, 7)[0],
                           struct.unpack_from('>I', c, 33)[0],
                           struct.unpack_from('<3f', c, 16)[:2],
                           struct.unpack_from('<3f', c, 37)[:2],
                           struct.unpack_from('<f', c, 28)[0]))
        elif op == OP_DANO and len(c) == 51:
            danos.append((t, struct.unpack_from('>I', c, 7)[0],
                          struct.unpack_from('<H', c, 22)[0],
                          struct.unpack_from('>I', c, 24)[0] & 0xFF,
                          struct.unpack_from('<f', c, 47)[0]))
    # a posicao do jogador vem do que o CLIENT manda
    for t, op, c in pacotes(caminho, direcao='SEND'):
        if op == OP_MOVER and len(c) >= 24:
            x, y, _z = struct.unpack_from('<3f', c, 12)
            jogador.append((t, x, y))
        elif op == OP_ATAQUE_CLIENT and len(c) >= 28:
            x, y, _z = struct.unpack_from('<3f', c, 16)
            jogador.append((t, x, y))
    jogador.sort()
    return {'mov': mov, 'npc': npc_de, 'golpes': golpes,
            'danos': danos, 'jogador': jogador}


def _pos_jogador(jog, t):
    """Onde o jogador estava no instante t, pela amostra mais proxima."""
    if not jog:
        return None
    lo, hi = 0, len(jog) - 1
    while lo < hi:
        meio = (lo + hi) // 2
        if jog[meio][0] < t:
            lo = meio + 1
        else:
            hi = meio
    cands = [jog[i] for i in (lo - 1, lo) if 0 <= i < len(jog)]
    melhor = min(cands, key=lambda s: abs(s[0] - t))
    return (melhor[1], melhor[2]) if abs(melhor[0] - t) < 5.0 else None


def corridas(ev, estado):
    """Sequencias continuas de um estado, por entidade. So monstros."""
    saida = []
    for e, lst in ev['mov'].items():
        if ev['npc'].get(e, 0) <= MOB_MINIMO:
            continue
        # NAO ORDENAR. A ordem do ficheiro E a ordem do fio.
        #
        # Estava aqui `sorted(lst)` sem chave. Varios 04 01 partilham o mesmo carimbo de
        # milissegundo -- o hook carimba a chamada WSARecv, nao o pacote, e uma leitura traz
        # 2 ou 3 pacotes de uma vez -- e o sorted, ao empatar no tempo, passava a comparar o
        # elemento seguinte da tupla, que e a coordenada. Resultado: pacotes trocados de
        # ordem, e 7,3 por cento dos passos de perseguicao a andar PARA TRAS (167 de 2278;
        # na ordem do ficheiro sao 0 de 2369).
        #
        # No histograma agregado o efeito parecia pequeno, e foi por isso que sobreviveu:
        # 21,33 passava de 30 para 35 por cento. Mas destroi tudo o que depende da SEQUENCIA
        # -- rumo, giro, solavanco, arranque -- e foi com estes numeros que se concluiu em
        # 28/08 que o passo de perseguicao era 18,67 e se mudou a constante do servidor. O
        # passo verdadeiro em regime e 21,33; o 18,67 e so o segundo passo da sequencia de
        # entrada (2,667 -> 18,667 -> 21,333 -> 21,333 ...).
        #
        # Se for mesmo preciso ordenar, tem de ser `sorted(lst, key=lambda r: r[0])`, que e
        # estavel e nao desempata pela posicao.
        i = 0
        while i < len(lst):
            if lst[i][1] != estado:
                i += 1
                continue
            j = i
            while (j + 1 < len(lst) and lst[j + 1][1] == estado
                   and lst[j + 1][0] - lst[j][0] < BURACO):
                j += 1
            if j > i:
                saida.append((e, lst[i:j + 1]))
            i = j + 1
    return saida


def _fase(ev, est):
    """Metricas de um estado de movimento."""
    jog = ev['jogador']
    dts, passos, decls, vels, duracoes = [], [], [], [], []
    velocidades = []
    giros, solavancos, arranques = [], [], []
    aprox, afasta, alcance = [], [], []
    for _e, seq in corridas(ev, est):
        duracoes.append(seq[-1][0] - seq[0][0])
        # VIRAR E ACELERAR -- as duas coisas que o jogador viu e que nenhuma metrica media.
        #
        # "fica sempre virando de um lado pro outro" e "da um arranque depois diminui a
        # velocidade", 28/08. As duas sao mensuraveis e nenhuma aparecia aqui: media-se a
        # velocidade e o passo, que sao MEDIAS, e uma media esconde exactamente o tremor e o
        # solavanco. Duas trajectorias com a mesma velocidade mediana podem ser uma linha
        # recta e um ziguezague.
        rumos_seq = []
        for s in seq:
            dx, dy = s[4] - s[2], s[5] - s[3]
            rumos_seq.append(rumo(dx, dy) if math.hypot(dx, dy) > 0.01 else None)
        for a, b in zip(rumos_seq, rumos_seq[1:]):
            if a is None or b is None:
                continue
            giros.append(math.degrees(abs((b - a + math.pi) % (2 * math.pi) - math.pi)))
        vs_local = []
        for k in range(1, len(seq)):
            dt = seq[k][0] - seq[k - 1][0]
            if 0 < dt < BURACO:
                vs_local.append(math.hypot(seq[k][2] - seq[k - 1][2],
                                           seq[k][3] - seq[k - 1][3]) / dt)
        # solavanco: quanto a velocidade salta de um passo para o seguinte, em proporcao
        for a, b in zip(vs_local, vs_local[1:]):
            if a > 0.5:
                solavancos.append(abs(b - a) / a)
        if len(vs_local) >= 4:
            # arranque: os dois primeiros passos contra o resto do trecho
            ini = sum(vs_local[:2]) / 2.0
            resto = sum(vs_local[2:]) / len(vs_local[2:])
            if resto > 0.5:
                arranques.append(ini / resto)

        for k in range(1, len(seq)):
            dt = seq[k][0] - seq[k - 1][0]
            if 0 < dt < BURACO:
                d = math.hypot(seq[k][2] - seq[k - 1][2], seq[k][3] - seq[k - 1][3])
                dts.append(dt)
                passos.append(d)
                # A VELOCIDADE E MEDIDA PASSO A PASSO, e so depois se tira a mediana.
                #
                # Dividir a mediana do passo pela mediana do intervalo parece a mesma coisa e
                # nao e: o passo da perseguicao no oficial e bimodal (5,33 e 16,00, conforme o
                # pacote venha depressa ou devagar) e as duas medianas saltam entre modos de
                # forma independente. Numa captura isso deu 5,33/0,135 = 39,5 u/s e noutra
                # 16,00/0,241 = 66,4 -- a mesma populacao a dar respostas com 68 por cento de
                # diferenca, so por causa da conta. A mediana das velocidades nao tem esse
                # problema porque cada amostra ja e uma razao coerente consigo mesma.
                velocidades.append(d / dt)
        for s in seq:
            decls.append(math.hypot(s[4] - s[2], s[5] - s[3]))
            vels.append(s[6])
        if est == 1:
            a = _pos_jogador(jog, seq[0][0])
            b = _pos_jogador(jog, seq[-1][0])
            if a and b:
                d0 = math.hypot(seq[0][2] - a[0], seq[0][3] - a[1])
                d1 = math.hypot(seq[-1][2] - b[0], seq[-1][3] - b[1])
                (aprox if d1 < d0 else afasta).append(seq[-1][0] - seq[0][0])
                if d1 < d0:
                    alcance.append(math.hypot(seq[-1][2] - seq[0][2],
                                              seq[-1][3] - seq[0][3]))
    med_dt = _p(dts, .5)
    saida = {
        'pacotes': len(decls), 'corridas': len(duracoes),
        'dt': med_dt, 'passo': _p(passos, .5),
        'velocidade': _p(velocidades, .5),
        'vel_p25': _p(velocidades, .25), 'vel_p75': _p(velocidades, .75),
        'dist_decl': _p(decls, .5), 'vel_decl': _p(vels, .5),
        'duracao': _p(duracoes, .5), 'duracao_max': max(duracoes) if duracoes else 0.0,
        # quanto o monstro VIRA entre pacotes seguidos
        'giro': _p(giros, .5), 'giro_p90': _p(giros, .9),
        'giro_recto_pc': (100.0 * sum(1 for g in giros if g < 1.0) / len(giros)
                          if giros else 0.0),
        # quanto a velocidade SALTA de um passo para o seguinte (0 = constante)
        'solavanco': _p(solavancos, .5),
        # velocidade dos dois primeiros passos a dividir pela do resto (1 = sem arranque)
        'arranque': _p(arranques, .5),
    }
    if est == 1:
        saida.update({
            'a_perseguir': len(aprox), 'a_voltar': len(afasta),
            'dur_perseguindo': _p(aprox, .5),
            'dur_perseguindo_max': max(aprox) if aprox else 0.0,
            'distancia_percorrida': _p(alcance, .5),
        })
    return saida


def medir(ev):
    """Todas as metricas de uma captura, num dicionario."""
    m = {'passeio': _fase(ev, 0), 'perseguicao': _fase(ev, 1), 'parado': _fase(ev, 2)}

    porpar = collections.defaultdict(list)
    rots = []
    for t, at, alvo, pa, pal, rot in ev['golpes']:
        if ev['npc'].get(at, 0) <= MOB_MINIMO:
            continue
        porpar[(at, alvo)].append(t)
        if math.hypot(pal[0] - pa[0], pal[1] - pa[1]) > 0.5:
            esperado = rumo(pal[0] - pa[0], pal[1] - pa[1])
            d = abs((esperado - rot + math.pi) % (2 * math.pi) - math.pi)
            rots.append(math.degrees(d))
    cad, sessoes = [], []
    for _k, ts in porpar.items():
        ts.sort()
        ini = ant = ts[0]
        for t in ts[1:]:
            if t - ant > 8.0:
                if ant > ini:
                    sessoes.append(ant - ini)
                ini = t
            else:
                cad.append(t - ant)
            ant = t
        if ant > ini:
            sessoes.append(ant - ini)
    m['combate'] = {
        'golpes': sum(len(v) for v in porpar.values()),
        'cadencia': _p(cad, .5),
        'sessao': _p(sessoes, .5), 'sessao_max': max(sessoes) if sessoes else 0.0,
        'erro_rumo': _p(rots, .5), 'rumo_amostras': len(rots),
    }

    rot_dano = [d[4] for d in ev['danos']]
    n = len(rot_dano) or 1
    m['dano'] = {
        'pacotes': len(rot_dano),
        'rot_distintos': len({round(r, 4) for r in rot_dano}),
        'rot_zeros_pc': 100.0 * sum(1 for r in rot_dano if abs(r) < 1e-6) / n,
        'rot_na_faixa_pc': 100.0 * sum(1 for r in rot_dano if 0 <= r <= 6.2832) / n,
    }
    return m
