# -*- coding: utf-8 -*-
r"""Tira TUDO o que uma captura tem, numa passagem so, e diz o que cada accao provocou.

    python tools/extrair_tudo.py <captura.log> [mais.log ...]
    python tools/extrair_tudo.py --todas
    python tools/extrair_tudo.py <captura.log> --csv saida.csv

POR QUE EXISTE. Ate 28/08 mediu-se uma coisa de cada vez: uma tarde para a regeneracao,
outra para o passo do monstro, outra para as opcoes de item. Cada uma custou uma gravacao ao
jogador e horas a decifrar, e o resultado de uma nao ajudava a seguinte. Ele resumiu-o
melhor do que eu: "uma coisa puxa a outra... tinha que pegar os detalhes tudo, se nao vamos
ficar uma eternidade".

A IDEIA CENTRAL, e a unica coisa aqui que e mesmo nova: em vez de decifrar um pacote para
depois perceber o que ele faz, **mede-se o que MUDOU no personagem logo a seguir a cada
accao**. O estado do personagem esta todo no 08 01 e no 08 02, que chegam a toda a hora;
basta guardar uma linha do tempo desse estado e, para cada accao, tirar a diferenca entre o
antes e o depois. Assim:

    usou o item X   ->  MP +40, e mais nada          =>  o item X e uma pocao de mana
    usou a skill Y  ->  MP -18, alvo perdeu 240 HP   =>  a skill Y custa 18 e da 240
    matou o mob Z   ->  EXP +24, reputacao +8        =>  o abate de Z vale isto

Nao e preciso saber o formato do pacote para nenhuma destas respostas. E funciona tambem
para os 82 opcodes que ainda nao deciframos: a seccao final passa cada um deles pelo mesmo
teste e diz o que ele mexe.

O QUE SAI:
    1. sessao          quanto tempo, que personagem, que niveis
    2. estado          a linha do tempo de HP/MP/EXP/nivel/reputacao
    3. combate         golpes dados e levados, dano, cadencia, falhas
    4. monstros        movimento por estado, com giro e arranque
    5. abates          o que cada morte rendeu -- EXP, reputacao, contador54
    6. skills          quais foram usadas, por quem, e o que custaram
    7. itens           apanhados, movidos, usados, e o efeito de cada um
    8. regeneracao     pelos DOIS caminhos (08 01 e 08 06) -- ver a nota la em baixo
    9. desconhecidos   cada opcode por decifrar, e o que ele mexe no personagem

AVISO SOBRE MISTURAR POPULACOES. Este ficheiro conta as amostras em todo o lado, e isso e de
proposito. Tres vezes em 28/08 uma medicao deu um numero que nao existia em caso nenhum, por
juntar coisas diferentes: a perseguicao com o passeio (21 pacotes contra 1739, e a media deu
passeio), o mob com o pet (velocidades de 30 e de 100), a regeneracao lida no pacote errado.
Onde houver duas populacoes, separa-se e mostram-se as duas. Uma linha com n pequeno e um
aviso, nao um resultado.
"""
import collections
import glob
import math
import os
import re
import statistics
import struct
import sys

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(RAIZ, "server"))
try:
    import data_files as D
except Exception:                                                    # noqa: BLE001
    D = None

CAB = re.compile(r"\[([\d:.]+)\] (SEND|RECV) sock=")
HEX = re.compile(r"(?:[0-9A-Fa-f]{2} )+\s*")

# Janela depois de uma accao dentro da qual se procura o efeito dela. Curta de proposito:
# com 5 s apanhava-se a regeneracao e o dano do mob por cima do efeito que se queria medir.
JANELA_EFEITO = 1.5

# (offset, formato) do item_id em cada pacote de item. Medido, nao suposto -- ver o
# comentario no sitio onde e usado.
OFF_ITEM = {
    "06 03": (14, ">I"),      # apanhar
    "06 04": (28, "<I"),      # item no chao
    "06 06": (13, "<I"),      # mover
    "06 00": (13, ">I"),      # equipar
    "06 01": (13, ">I"),      # desequipar
}

NOMES = {
    "02 00": "login", "02 01": "lista de personagens", "02 05": "ficha do personagem",
    "03 00": "?", "03 02": "?", "03 06": "?", "03 0a": "?", "03 0b": "?",
    "04 01": "movimento", "04 02": "aparicao", "04 03": "desaparicao", "04 04": "?",
    "05 00": "inicio de golpe", "05 01": "golpe", "05 02": "dano",
    "05 03": "USO DE SKILL", "05 04": "ressurreicao", "05 05": "lista de skills",
    "05 09": "?", "05 0b": "?", "05 0c": "?",
    "06 00": "equipar", "06 01": "desequipar", "06 03": "item apanhado",
    "06 04": "item no chao", "06 05": "inventario", "06 06": "item movido",
    "08 00": "estado do heroi", "08 01": "barra do heroi", "08 02": "atributos",
    "08 06": "barra de entidade", "09 03": "progresso de missao",
    "09 04": "missoes activas", "0b 00": "keepalive",
}


def dia_da_captura(caminho):
    """A data da captura: do nome (karos_AAAAMMDD_HHMM.log) ou da data do ficheiro."""
    m = re.search(r"(20[0-9]{6})", os.path.basename(caminho))
    if m:
        s = m.group(1)
        return "%s-%s-%s" % (s[:4], s[4:6], s[6:8])
    import datetime
    return datetime.date.fromtimestamp(os.path.getmtime(caminho)).isoformat()


DIAS_SEMANA = ("segunda", "terca", "quarta", "quinta", "sexta", "sabado", "domingo")


def evento_no_dia(dia, sistema="exp"):
    """(multiplicador, fonte) do evento desse dia para esse sistema, ou None se nao souber.

    O calendario e SEMANAL: a chave e o dia da semana, nao a data. Ver
    Data/Common/EventosOficial.ini para a historia e para as regras."""
    if D is None or not dia:
        return None
    try:
        import datetime
        ds = DIAS_SEMANA[datetime.date.fromisoformat(dia).weekday()]
        linhas = D.tabela("EventosOficial")
        for r in linhas:
            if str(r.get("dia_semana")) == ds and str(r.get("sistema")) == sistema:
                return float(r.get("multiplicador") or 1.0), str(r.get("fonte") or "")
        # o calendario e conhecido; este dia nao tem evento NESTE sistema
        return 1.0, "%s: sem evento de %s" % (ds, sistema)
    except Exception:                                                # noqa: BLE001
        return None


def seg(t):
    h, m, s = t.split(":")
    return int(h) * 3600 + int(m) * 60 + float(s)


def pacotes(caminho):
    """(tempo, sentido, corpo) de cada pacote da captura."""
    atual, blocos = None, []
    with open(caminho, encoding="latin-1", errors="replace") as f:
        for l in f:
            m = CAB.match(l)
            if m:
                atual = [m.group(1), m.group(2), bytearray()]
                blocos.append(atual)
                continue
            if atual is not None and HEX.fullmatch(l):
                try:
                    atual[2].extend(bytes.fromhex(l.strip()))
                except ValueError:
                    pass
    for t, d, b in blocos:
        pos = 0
        while pos + 4 <= len(b):
            (n,) = struct.unpack_from(">I", b, pos)
            if n == 0 or n > 65535 or pos + 4 + n > len(b):
                break
            yield seg(t), d, bytes(b[pos + 4:pos + 4 + n])
            pos += 4 + n


def p50(v):
    return statistics.median(v) if v else 0.0


# ---------------------------------------------------------------------------
# passagem unica
# ---------------------------------------------------------------------------
def ler(caminho):
    d = {
        "t0": None, "t1": None, "eu": None,
        "op": collections.Counter(), "tam": collections.defaultdict(collections.Counter),
        "estado": [],          # (t, hp, hp_max, mp, mp_max, exp, limiar, nivel, rep, c54)
        "vida_barra": [],      # (t, hp) do 08 06 do proprio -- o outro caminho da vida
        "mov": collections.defaultdict(list),   # ent -> (t, estado, ax, ay, bx, by)
        "dano": [],            # (t, alvo)
        "golpe_mob": [],       # (t, atacante)
        "skill": [],           # (t, lancador, skill_id)
        "hp_ent": collections.defaultdict(list),   # ent -> (t, hp)
        "item": [],            # (t, opcode, item_id)
        "bruto": collections.defaultdict(list),    # opcode -> [(t, corpo)] amostras
        # QUE BICHO E CADA ENTIDADE. O 04 02 traz o npc_id em [70:74], e o Data/Mob.ini
        # tem o nivel e a EXP base de cada um. Sem este cruzamento a ferramenta media a EXP
        # por abate mas nao sabia de QUE monstro -- e a penalidade por diferenca de nivel,
        # que e o que interessa, ficava impossivel de separar. Foi o jogador que apontou:
        # "vc tem todos os mobs e level deles".
        "npc": {},
        "dia": dia_da_captura(caminho),
    }
    for t, sentido, c in pacotes(caminho):
        if d["t0"] is None:
            d["t0"] = t
        d["t1"] = t
        if sentido != "RECV" or len(c) < 2:
            continue
        k = "%02x %02x" % (c[0], c[1])
        d["op"][k] += 1
        d["tam"][k][len(c)] += 1
        if len(d["bruto"][k]) < 300:
            d["bruto"][k].append((t, c))

        if k == "08 01" and len(c) >= 58:
            if d["eu"] is None:
                d["eu"] = struct.unpack_from(">I", c, 7)[0]
            d["estado"].append((
                t,
                struct.unpack_from("<I", c, 15)[0], struct.unpack_from("<I", c, 19)[0],
                struct.unpack_from("<I", c, 23)[0], struct.unpack_from("<I", c, 27)[0],
                struct.unpack_from("<Q", c, 69)[0] if len(c) >= 77 else 0,
                struct.unpack_from("<Q", c, 77)[0] if len(c) >= 85 else 0,
                c[43],
                struct.unpack_from("<I", c, 45)[0],
                struct.unpack_from(">I", c, 54)[0]))
        elif k == "08 06" and len(c) >= 19:
            ent = struct.unpack_from(">I", c, 7)[0]
            hp = struct.unpack_from(">I", c, 15)[0]
            d["hp_ent"][ent].append((t, hp))
            if d["eu"] is not None and ent == d["eu"]:
                d["vida_barra"].append((t, hp))
        elif k == "04 02" and len(c) >= 74:
            d["npc"][struct.unpack_from("<I", c, 8)[0]] = struct.unpack_from("<I", c, 70)[0]
        elif k == "04 01" and len(c) == 42:
            d["mov"][struct.unpack_from(">I", c, 8)[0]].append(
                (t, c[7],
                 struct.unpack_from("<f", c, 12)[0], struct.unpack_from("<f", c, 16)[0],
                 struct.unpack_from("<f", c, 24)[0], struct.unpack_from("<f", c, 28)[0]))
        elif k == "05 02" and len(c) >= 11:
            d["dano"].append((t, struct.unpack_from(">I", c, 7)[0]))
        elif k == "05 00" and len(c) >= 11:
            d["golpe_mob"].append((t, struct.unpack_from(">I", c, 7)[0]))
        elif k == "05 03" and len(c) >= 15:
            d["skill"].append((t, struct.unpack_from(">I", c, 7)[0],
                               struct.unpack_from(">I", c, 11)[0]))
        # OFFSET DO item_id POR OPCODE, medido contra o proprio catalogo em 28/08: para cada
        # posicao possivel contou-se quantos valores eram itens que existem em Item.ini, e
        # ficaram os que acertam em 200 de 200. Adivinhar um offset unico dava numeros como
        # 4294967040, que e o que aparecia antes.
        elif k in OFF_ITEM and len(c) >= 20:
            off, fmt = OFF_ITEM[k]
            if len(c) >= off + 4:
                d["item"].append((t, k, struct.unpack_from(fmt, c, off)[0]))
    return d


# ---------------------------------------------------------------------------
# o motor: o que mudou no personagem depois de uma accao
# ---------------------------------------------------------------------------
CAMPOS = ("hp", "hp_max", "mp", "mp_max", "exp", "limiar", "nivel", "reputacao", "c54")


def estado_em(estado, t):
    """A amostra de estado imediatamente antes de t."""
    ant = None
    for x in estado:
        if x[0] > t:
            break
        ant = x
    return ant


def efeito(estado, t, janela=JANELA_EFEITO):
    """{campo: variacao} entre o estado antes de t e o ultimo dentro da janela."""
    a = estado_em(estado, t)
    if a is None:
        return {}
    depois = [x for x in estado if t <= x[0] <= t + janela]
    if not depois:
        return {}
    b = depois[-1]
    saida = {}
    for i, nome in enumerate(CAMPOS, start=1):
        if b[i] != a[i]:
            saida[nome] = b[i] - a[i]
    return saida


def resumir_efeitos(efeitos):
    """De uma lista de dicionarios de efeito, a mediana de cada campo e quantas vezes."""
    por = collections.defaultdict(list)
    for e in efeitos:
        for k, v in e.items():
            por[k].append(v)
    return {k: (p50(v), len(v)) for k, v in sorted(por.items())}


def fmt_efeito(res, n):
    if not res:
        return "sem efeito medido"
    return ", ".join("%s %+g (%d/%d)" % (k, v, q, n) for k, (v, q) in res.items())


# ---------------------------------------------------------------------------
# seccoes
# ---------------------------------------------------------------------------
def sec_sessao(d, out):
    out("SESSAO")
    ev = evento_no_dia(d.get("dia", ""))
    out("   dia %s%s" % (d.get("dia"),
        ("   EVENTO DE EXP x%g -- medicoes de EXP nao valem" % ev[0])
        if ev and ev[0] != 1.0 else ""))
    # A CAPTURA PODE PASSAR DA MEIA-NOITE. O relogio do hook e hora do dia, portanto
    # 23:59 -> 00:01 da uma duracao de menos 24 horas. Foi o "-671,9 minutos" da primeira
    # corrida desta ferramenta.
    dur = (d["t1"] or 0) - (d["t0"] or 0)
    if dur < 0:
        dur += 24 * 3600
    out("   %.1f minutos, %d pacotes, %d opcodes distintos"
        % (dur / 60, sum(d["op"].values()), len(d["op"])))
    if d["estado"]:
        niveis = sorted({x[7] for x in d["estado"]})
        out("   personagem %s, niveis %s, HP maximo %s"
            % (d["eu"], niveis, sorted({x[2] for x in d["estado"]})))
    else:
        out("   sem 08 01 -- nao da para medir efeito nenhum nesta captura")


def sec_combate(d, out):
    out("")
    out("COMBATE")
    out("   golpes de monstro (05 00): %d   pacotes de dano (05 02): %d"
        % (len(d["golpe_mob"]), len(d["dano"])))
    por = collections.defaultdict(list)
    for t, at in d["golpe_mob"]:
        por[at].append(t)
    cad = []
    for ts in por.values():
        ts.sort()
        cad += [b - a for a, b in zip(ts, ts[1:]) if 0.5 < b - a < 15]
    if cad:
        out("   cadencia do golpe de monstro: mediana %.2f s (%d amostras)" % (p50(cad), len(cad)))
    # dano levado pelo jogador: descidas de HP
    perdas = [a[1] - b[1] for a, b in zip(d["estado"], d["estado"][1:])
              if b[1] < a[1] and a[2] == b[2]]
    if perdas:
        out("   dano levado por golpe: mediana %d, min %d, max %d (%d golpes)"
            % (p50(perdas), min(perdas), max(perdas), len(perdas)))


def sec_monstros(d, out):
    out("")
    out("MONSTROS  (movimento, por estado do byte [7])")
    por_estado = collections.defaultdict(lambda: {"av": [], "dt": [], "decl": [], "giro": []})
    for ent, seq in d["mov"].items():
        seq.sort()
        for i in range(len(seq) - 1):
            e = seq[i][1]
            dt = seq[i + 1][0] - seq[i][0]
            if not 0.05 < dt < 3.0:
                continue
            g = por_estado[e]
            g["av"].append(math.hypot(seq[i + 1][2] - seq[i][2], seq[i + 1][3] - seq[i][3]))
            g["dt"].append(dt)
            g["decl"].append(math.hypot(seq[i][4] - seq[i][2], seq[i][5] - seq[i][3]))
            # SO COM DIRECCAO. Num pacote parado a origem e o destino sao o mesmo ponto e
            # o atan2 devolve lixo -- media 87 graus de "viragem" em quem esta quieto.
            d0 = math.hypot(seq[i][4] - seq[i][2], seq[i][5] - seq[i][3])
            d1 = math.hypot(seq[i + 1][4] - seq[i + 1][2], seq[i + 1][5] - seq[i + 1][3])
            if d0 > 0.01 and d1 > 0.01:
                a = math.atan2(seq[i][5] - seq[i][3], seq[i][4] - seq[i][2])
                b = math.atan2(seq[i + 1][5] - seq[i + 1][3], seq[i + 1][4] - seq[i + 1][2])
                g["giro"].append(math.degrees(abs((b - a + math.pi) % (2 * math.pi) - math.pi)))
    ROT = {0: "andar", 1: "correr", 2: "parado"}
    for e in sorted(por_estado):
        g = por_estado[e]
        if len(g["av"]) < 10:
            continue
        vel = [a / t for a, t in zip(g["av"], g["dt"]) if t > 0]
        out("   estado %d (%-6s) %5d passos  avanco %.2f  decl %.2f  dt %.3f s  %.1f u/s"
            % (e, ROT.get(e, "?"), len(g["av"]), p50(g["av"]), p50(g["decl"]),
               p50(g["dt"]), p50(vel)))
        if g["giro"]:
            out("        vira %.1f graus (mediana), %d%% em linha recta (%d amostras)"
                % (p50(g["giro"]),
                   100 * sum(1 for x in g["giro"] if x < 1) // len(g["giro"]), len(g["giro"])))


def sec_abates(d, out):
    out("")
    out("ABATES  (o que cada morte rendeu, por monstro e por diferenca de nivel)")
    mobs = {}
    if D is not None:
        try:
            D.carregar()
            mobs = D.indice("Mob", "mob_id")
        except Exception:                                            # noqa: BLE001
            pass
    mortes = []
    for ent, serie in d["hp_ent"].items():
        if ent == d["eu"]:
            continue
        serie.sort()
        for (t0, h0), (t1, h1) in zip(serie, serie[1:]):
            if h0 > 0 and h1 == 0:
                mortes.append((t1, d["npc"].get(ent)))
    if not mortes:
        out("   nenhuma morte de monstro nesta captura")
        return
    # SO OS ABATES LIMPOS: um de cada vez, sem outro a morrer na mesma janela. Com duas
    # mortes juntas a EXP de uma entra no efeito da outra e a curva sai torta.
    mortes.sort()
    limpas = [m for i, m in enumerate(mortes)
              if (i == 0 or m[0] - mortes[i - 1][0] > 4.0)
              and (i + 1 == len(mortes) or mortes[i + 1][0] - m[0] > 4.0)]
    out("   %d mortes, %d isoladas (sem outra a menos de 4 s)" % (len(mortes), len(limpas)))

    por_mob = collections.defaultdict(list)
    por_dif = collections.defaultdict(list)
    niveis_de = collections.defaultdict(list)
    for t, npc in limpas:
        est = estado_em(d["estado"], t)
        ef = efeito(d["estado"], t, 3.0)
        if not ef or "exp" not in ef or ef["exp"] <= 0:
            continue
        ficha = mobs.get(npc) if npc else None
        nome = str(ficha.get("nome")) if ficha else ("npc %s" % npc)
        nivel_mob = int(ficha.get("nivel") or 0) if ficha else 0
        por_mob[(npc, nome, nivel_mob)].append((ef["exp"], ef.get("reputacao", 0),
                                                ef.get("c54", 0)))
        niveis_de[npc].append(est[7] if est else 0)
        if nivel_mob and est:
            por_dif[est[7] - nivel_mob].append((ef["exp"], nivel_mob))
    if por_mob:
        out("")
        out("   %-9s %-26s %-5s %5s %8s %7s %7s" %
            ("npc_id", "monstro", "nivel", "n", "EXP", "reput.", "c54"))
        for (npc, nome, niv), v in sorted(por_mob.items(), key=lambda x: -len(x[1]))[:14]:
            out("   %-9s %-26s %-5s %5d %8.0f %7.0f %7.0f"
                % (npc, nome[:26], niv or "?", len(v), p50([x[0] for x in v]),
                   p50([x[1] for x in v]), p50([x[2] for x in v])))
    # PENALIDADE, NORMALIZADA POR MONSTRO.
    #
    # A EXP de um abate depende de DUAS coisas ao mesmo tempo: de que monstro e, e de quanto
    # o jogador esta acima dele. Juntar os abates so pela diferenca mistura as duas -- na
    # primeira versao disto, a linha "-18" tinha Espadachins de nivel 26 a valer 94 e a
    # linha "-9" tinha Caes brancos do MESMO nivel 26 a valer 622, e a leitura obvia
    # ("a penalidade cresce") era falsa: sao monstros diferentes com bases diferentes.
    #
    # A separacao certa e por (monstro, diferenca): para cada monstro toma-se a EXP dele sem
    # penalidade -- a maior observada, que e a de quando o jogador ainda nao estava acima --
    # e divide-se. O que fica e a FRACCAO, que ja e comparavel entre monstros e pode ser
    # juntada. E a mesma disciplina de sempre: normalizar antes de juntar populacoes.
    ev = evento_no_dia(d.get("dia", ""))
    if ev and ev[0] != 1.0:
        out("")
        out("   MEDICAO DE EXP RECUSADA: %s tinha evento de EXP x%g." % (d.get("dia"), ev[0]))
        out("   %s" % ev[1][:70])
        out("   Nao se divide pelo multiplicador: nao esta medido se ele SOMA ou MULTIPLICA")
        out("   com os outros bonus activos. Meca-se noutro dia.")
        return
    if ev is None and d.get("dia"):
        out("")
        out("   AVISO: sem registo de eventos para %s. Se houvesse um evento de EXP a" % d["dia"])
        out("   correr, os numeros abaixo estao multiplicados e nao se ve por onde.")
    if por_mob:
        # A BASE E A MEDIANA DOS ABATES SEM PENALIDADE, nao o maximo.
        #
        # Com o maximo, um unico abate contaminado -- duas mortes na mesma janela, ou uma
        # recompensa de missao pelo meio -- vira a base de todos os outros. Foi o que deu na
        # primeira versao: o Espadachim Aguia mostrava fraccao 0,18 em 24 abates, que era so
        # um outlier a servir de referencia.
        #
        # "Sem penalidade" e diferenca <= 0, ou seja o jogador ao nivel do monstro ou abaixo.
        # Se um monstro nunca foi morto nessas condicoes, ele fica de fora em vez de arranjar
        # uma base a partir do que ha.
        base_de = {}
        sem_base = 0
        for (npc, _nome, niv), v in por_mob.items():
            if not niv:
                continue
            iguais = [x[0] for x, dif in
                      ((x, est_niv - niv) for x, est_niv in zip(v, niveis_de[npc]))
                      if dif <= 0]
            if len(iguais) >= 2:
                base_de[npc] = p50(iguais)
            else:
                sem_base += 1
        fraccoes = collections.defaultdict(list)
        for t, npc in limpas:
            est = estado_em(d["estado"], t)
            ef = efeito(d["estado"], t, 3.0)
            if not (ef and ef.get("exp", 0) > 0 and est and npc in base_de):
                continue
            ficha = mobs.get(npc)
            nivel_mob = int(ficha.get("nivel") or 0) if ficha else 0
            if not nivel_mob or base_de[npc] <= 0:
                continue
            fraccoes[est[7] - nivel_mob].append((ef["exp"] / base_de[npc], npc))
        if base_de:
            out("")
            out("   EXP BASE MEDIDA (mediana dos abates sem penalidade) -- para o Mob.ini,")
            out("   onde hoje so 3 dos 926 monstros a tem preenchida:")
            for (npc, nome, niv), v in sorted(por_mob.items(), key=lambda x: -len(x[1])):
                if npc in base_de:
                    out("      %-9s nivel %-4s exp_base %6.0f   (%d abates)"
                        % (npc, niv or "?", base_de[npc], len(v)))
            if sem_base:
                out("      (%d monstros sem abate ao nivel ou abaixo -- sem base)" % sem_base)
        if fraccoes:
            out("")
            out("   PENALIDADE por diferenca de nivel, cada abate dividido pela EXP base")
            out("   do PROPRIO monstro (1,00 = sem penalidade):")
            out("      %-6s %5s %8s   %s" % ("dif", "n", "fraccao", "monstros"))
            for dif in sorted(fraccoes):
                v = fraccoes[dif]
                if len(v) < 3:
                    continue
                quantos = len({npc for _f, npc in v})
                out("      %+-6d %5d %8.2f   %d monstro(s) distinto(s)"
                    % (dif, len(v), p50([f for f, _n in v]), quantos))
            poucos = sum(1 for v in fraccoes.values() if len(v) < 3)
            if poucos:
                out("      (%d diferencas com menos de 3 abates, omitidas)" % poucos)


def sec_skills(d, out):
    out("")
    out("SKILLS  (05 03 -- quem lancou o que, e quanto custou)")
    if not d["skill"]:
        out("   nenhuma skill lancada")
        return
    nomes = {}
    if D is not None:
        try:
            D.carregar()
            nomes = {r["skill_id"]: r.get("descricao") for r in D.tabela("Skill")}
        except Exception:                                            # noqa: BLE001
            pass
    por = collections.defaultdict(list)
    for t, quem, sid in d["skill"]:
        por[(quem == d["eu"], sid)].append(t)
    for (minha, sid), ts in sorted(por.items(), key=lambda x: -len(x[1]))[:14]:
        efs = [efeito(d["estado"], t) for t in ts] if minha else []
        efs = [e for e in efs if e]
        res = fmt_efeito(resumir_efeitos(efs), len(efs)) if efs else ""
        out("   %-8d x%-5d %-6s %s" % (sid, len(ts), "MINHA" if minha else "de outro",
                                       str(nomes.get(sid, ""))[:40]))
        if res:
            out("        %s" % res)


def sec_itens(d, out):
    out("")
    out("ITENS  (o que cada um provocou no personagem)")
    if not d["item"]:
        out("   nenhum")
        return
    por = collections.defaultdict(list)
    for t, k, iid in d["item"]:
        por[(k, iid)].append(t)
    cat = {}
    if D is not None:
        try:
            D.carregar()
            cat = D.indice("Item", "item_id")
        except Exception:                                            # noqa: BLE001
            pass
    for (k, iid), ts in sorted(por.items(), key=lambda x: -len(x[1]))[:16]:
        efs = [e for e in (efeito(d["estado"], t) for t in ts) if e]
        nome = str((cat.get(iid) or {}).get("nome", ""))[:34] if cat else ""
        out("   %s %-11d x%-4d %-34s %s"
            % (k, iid, len(ts), nome, fmt_efeito(resumir_efeitos(efs), len(efs))))


def sec_regeneracao(d, out):
    out("")
    out("REGENERACAO  (pelos DOIS caminhos)")
    # ver analisar_captura.seccao_regeneracao: ignorar o 08 06 custou tres analises erradas
    serie = [(t, hp, "08 01") for t, hp, *_r in
             ((x[0], x[1]) for x in d["estado"])] + \
            [(t, hp, "08 06") for t, hp in d["vida_barra"]]
    serie.sort()
    subidas = [(t, hp - a[1], f) for (t, hp, f), a in zip(serie[1:], serie) if hp > a[1]]
    if not subidas:
        out("   nenhuma subida de HP")
        return
    out("   %d subidas: %s" % (len(subidas),
                               dict(collections.Counter(f for _t, _d, f in subidas))))
    passos = collections.Counter(p for _t, p, _f in subidas)
    out("   passos: %s" % ", ".join("+%d x%d" % (k, v) for k, v in passos.most_common(5)))
    dom = passos.most_common(1)[0][0]
    ts = [t for t, p, _f in subidas if p == dom]
    gaps = [b - a for a, b in zip(ts, ts[1:]) if 0.5 < b - a < 30]
    if gaps:
        out("   intervalo do passo dominante: %.2f s (%d amostras)" % (p50(gaps), len(gaps)))
    # MP
    subm = [(b[0], b[3] - a[3]) for a, b in zip(d["estado"], d["estado"][1:])
            if b[3] > a[3] and b[4] == a[4]]
    if subm:
        pm = collections.Counter(p for _t, p in subm)
        out("   MP: %d subidas, passos %s" % (len(subm), pm.most_common(4)))
    else:
        out("   MP: nenhuma subida")


def sec_desconhecidos(d, out):
    out("")
    out("OPCODES POR DECIFRAR  (e o que cada um mexe no personagem)")
    desc = [(k, v) for k, v in d["op"].most_common() if NOMES.get(k, "?") == "?"]
    if not desc:
        out("   nenhum")
        return
    for k, n in desc[:18]:
        amostras = d["bruto"].get(k, [])
        efs = [e for e in (efeito(d["estado"], t) for t, _c in amostras[:120]) if e]
        tam = ", ".join(str(x) for x, _ in d["tam"][k].most_common(2))
        res = fmt_efeito(resumir_efeitos(efs), len(efs)) if efs else "nada muda a seguir"
        out("   %s x%-6d tam %-12s %s" % (k, n, tam, res))


def main() -> int:
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if "--todas" in sys.argv:
        args = sorted(glob.glob(r"C:\Users\Administrator\Desktop\CapturaKaros\*.log"))
    if not args:
        print(__doc__.strip().splitlines()[2])
        return 1
    for cam in args:
        if not os.path.exists(cam):
            print("nao existe: %s" % cam)
            continue
        print("=" * 78)
        print(os.path.basename(cam))
        print("=" * 78)
        d = ler(cam)
        out = print
        for f in (sec_sessao, sec_combate, sec_monstros, sec_abates,
                  sec_skills, sec_itens, sec_regeneracao, sec_desconhecidos):
            try:
                f(d, out)
            except Exception as e:                                   # noqa: BLE001
                out("")
                out("   [%s falhou: %s: %s]" % (f.__name__, type(e).__name__, str(e)[:100]))
        print()
    return 0


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