# -*- coding: utf-8 -*-
"""Mede o movimento do NOSSO mob a partir do espiao do fio, ao vivo.

    python tools/medir_ao_vivo.py [minutos]     (por omissao os ultimos 10)

Nao precisa de gravador no lado do jogador: o servidor ja escreve tudo o que envia em
captures/espiao_fio.log. Formato de cada linha: "<epoch> <nome> <hex do payload>".

Mede as MESMAS grandezas que o tools/juntar_mob.py tira das capturas do oficial, para os
numeros poderem ser postos lado a lado. Separa perseguir de voltar pelo sinal da variacao da
distancia ao jogador -- sem isso misturam-se duas populacoes opostas, que e o erro que mais
custou neste projecto.
"""
import collections
import io
import math
import os
import statistics
import struct
import time
import sys

CAM = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "captures", "espiao_fio.log")
NOME_ESTADO = {0: "andar", 1: "correr", 2: "parado", 3: "est.3", 4: "est.4", 5: "est.5"}


def ler(minutos=10.0, caminho=CAM):
    """Ultimos N minutos: movimento por entidade e posicoes do jogador."""
    tamanho = os.path.getsize(caminho)
    # LE A MAIS DE PROPOSITO e filtra pelo tempo a seguir. A estimativa por bytes serve so
    # para nao varrer 67 MB; ela erra por um factor grande conforme o que esta a acontecer no
    # mundo, e em 28/08 uma janela pedida de 0,6 min trouxe varios minutos de dados -- de
    # ANTES de uma recarga -- e fez parecer que a alteracao nao tinha pegado.
    salto = max(0, tamanho - int(max(2.0, minutos) * 60 * 900_000))
    limite = None
    mov = collections.defaultdict(list)
    jog = []
    t_ini = None
    with io.open(caminho, encoding="utf-8", errors="ignore") as f:
        if salto:
            f.seek(salto)
            f.readline()
        for ln in f:
            partes = ln.split(" ", 2)
            if len(partes) < 3:
                continue
            try:
                t = float(partes[0])
            except ValueError:
                continue
            if partes[1] == "JOGADOR":
                # linha de posicao do jogador, escrita pelo laco do mundo uma vez por segundo
                campos = partes[2].rsplit(" ", 3)
                if len(campos) == 4:
                    try:
                        jog.append((t, float(campos[1]), float(campos[2])))
                    except ValueError:
                        pass
                continue
            try:
                b = bytes.fromhex(partes[2].replace(" ", ""))
            except ValueError:
                continue
            if len(b) < 40 or b[0] != 4 or b[1] != 1:
                continue
            if t_ini is None:
                t_ini = t
            e = int.from_bytes(b[8:12], "big")
            ox, oy = struct.unpack_from("<2f", b, 12)
            dx, dy = struct.unpack_from("<2f", b, 24)
            mov[e].append((t, b[7], ox, oy, dx, dy))
    # AGORA SIM a janela pedida: corta pelo carimbo do ultimo pacote para tras
    if mov:
        fim = max(v[-1][0] for v in mov.values())
        limite = fim - minutos * 60.0
        for e in list(mov):
            mov[e] = [r for r in mov[e] if r[0] >= limite]
            if not mov[e]:
                del mov[e]
        t_ini = min((v[0][0] for v in mov.values()), default=None)
        jog = [r for r in jog if r[0] >= limite]
    return mov, jog, t_ini


def _ids_de_jogador():
    """Os entity_id dos personagens, do banco. Vazio se o banco nao responder."""
    try:
        import os as _os
        import sys as _sys
        _sys.path.insert(0, _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
                                          "..", "server"))
        import db as _db
        con = _db._conectar()
        if con is None:
            return set()
        cur = con.cursor()
        cur.execute("SELECT entity_id FROM dbo.personagem")
        return {int(r[0]) for r in cur.fetchall()}
    except Exception:                                                 # noqa: BLE001
        return set()


def medir(minutos=10.0):
    mov, jog, t_ini = ler(minutos)
    if not mov:
        print("sem pacotes de movimento nos ultimos %g minutos" % minutos)
        return 1
    # o JOGADOR e a entidade cujo movimento vem do proprio cliente; aqui usa-se a heuristica
    # do metricas.py: ids baixos sao jogadores/NPC de cidade, mob e acima de 1000
    fim = max(v[-1][0] for v in mov.values())
    print("janela REAL: %.2f min (ate %s),  %d entidades,  %d pacotes 04 01"
          % ((fim - t_ini) / 60.0, time.strftime("%H:%M:%S", time.localtime(fim)),
             len(mov), sum(len(v) for v in mov.values())))
    print()

    # QUEM E O JOGADOR. Sem isto nao se separa perseguir de voltar, e o estado 1 junta as
    # duas: a caca anda 18,67 por passo e o regresso ao poste 42,66. Medi-las juntas da 30,67,
    # que e um numero que nao descreve nenhuma das duas. E o mesmo erro de populacoes que ja
    # custou varias conclusoes erradas neste projecto.
    jogadores = _ids_de_jogador()
    jog.sort()

    def _perto_do_jogador(t):
        if not jog:
            return None
        lo, hi = 0, len(jog) - 1
        while lo < hi:
            m = (lo + hi) // 2
            if jog[m][0] < t:
                lo = m + 1
            else:
                hi = m
        cand = min((jog[k] for k in (lo - 1, lo, lo + 1) if 0 <= k < len(jog)),
                   key=lambda r: abs(r[0] - t))
        return cand if abs(cand[0] - t) < 2.0 else None

    por_estado = collections.defaultdict(lambda: {"dt": [], "passo": [], "decl": [], "giro": []})
    for _e, seq in mov.items():
        if _e in jogadores:
            continue
        seq.sort()
        rumos = []
        for t, est, ox, oy, dx, dy in seq:
            vx, vy = dx - ox, dy - oy
            rumos.append((est, math.atan2(vy, vx) if math.hypot(vx, vy) > 0.01 else None))
        for k in range(1, len(seq)):
            (ta, ea, oxa, oya, dxa, dya) = seq[k - 1]
            (tb, eb, oxb, oyb, _dxb, _dyb) = seq[k]
            if ea != eb:
                continue
            dt = tb - ta
            if not (0 < dt < 2.0):
                continue
            chave = ea
            if ea == 1:
                ja, jb = _perto_do_jogador(ta), _perto_do_jogador(tb)
                if ja and jb:
                    da = math.hypot(oxa - ja[1], oya - ja[2])
                    db_ = math.hypot(oxb - jb[1], oyb - jb[2])
                    chave = "perseguir" if db_ < da - 1.0 else (
                        "voltar" if db_ > da + 1.0 else "de lado")
                else:
                    chave = "correr (sem jogador)"
            g = por_estado[chave]
            g["dt"].append(dt)
            g["passo"].append(math.hypot(oxb - oxa, oyb - oya))
            g["decl"].append(math.hypot(dxa - oxa, dya - oya))
            ra, rb = rumos[k - 1][1], rumos[k][1]
            if ra is not None and rb is not None:
                g["giro"].append(math.degrees(abs((rb - ra + math.pi) % (2 * math.pi) - math.pi)))

    print("%-8s %6s %9s %9s %9s %8s %9s %9s"
          % ("estado", "n", "intervalo", "passo", "declarado", "razao", "vel u/s", "recto %"))
    print("-" * 74)
    for est in sorted(por_estado, key=str):
        g = por_estado[est]
        if len(g["dt"]) < 5:
            continue
        dt = statistics.median(g["dt"])
        pa = statistics.median(g["passo"])
        de = statistics.median(g["decl"])
        razoes = [a / b for a, b in zip(g["passo"], g["decl"]) if b > 0.01]
        recto = 100.0 * sum(1 for x in g["giro"] if x < 1.0) / max(1, len(g["giro"]))
        print("%-8s %6d %9.3f %9.2f %9.2f %8.2f %9.1f %9.0f"
              % (NOME_ESTADO.get(est, str(est)), len(g["dt"]), dt, pa, de,
                 statistics.median(razoes) if razoes else 0.0, pa / dt if dt else 0.0, recto))
    print()
    for est in sorted(por_estado, key=str):
        g = por_estado[est]
        if len(g["giro"]) < 10:
            continue
        c = collections.Counter(round(x / 5) * 5 for x in g["giro"])
        n = len(g["giro"])
        print("  virada em %-7s: %s" % (NOME_ESTADO.get(est, str(est)),
              "  ".join("%d gr x%.0f%%" % (k, 100.0 * v / n) for k, v in c.most_common(6))))
    return 0


if __name__ == "__main__":
    raise SystemExit(medir(float(sys.argv[1]) if len(sys.argv) > 1 else 10.0))
