# -*- coding: utf-8 -*-
r"""Extrai as "Caracteristicas especiais" da caixa de texto de cada item.

    python tools/importar_bonus_de_item.py             mostra
    python tools/importar_bonus_de_item.py --aplicar   grava Data/Items/ItemBonus.ini

A CADEIA, medida em 28/08 a partir das "[Asas de anjo (24 horas)]" (item 999900058), cuja
caixa de texto mostra tres linhas -- Saude +1004,00, Mana +328,00 e Poder de ataque de
habilidade +3%:

    itemAttr, 5 ranhuras no fim do registo  ->  30000300   (uma skill passiva)
    skillLevel, registo dessa skill, +264   ->  5 ids de skillData:
                                                30003101, 30003201, 30108003
    skillData, um registo por efeito        ->  (tipo, valor fixo, valor percentual)
                                                46 fixo 1004 | 47 fixo 328 | 108 pct 3
    FUN_006076cb(tipo) no AMo.exe           ->  id de string  ->  "Saude", "Mana",
                                                "Poder de ataque de habilidade"

Os tres valores batem ao centimo com o que o jogador ve no ecra.

POR QUE ISTO FALTAVA NO SERVIDOR. O `bruto` do item na nossa base vem todo a zero depois do
byte 20 -- o servidor nao manda opcao nenhuma -- e mesmo assim a caixa de texto mostra as
tres linhas. Quem as desenha e o CLIENT, a partir destas tabelas. O que faltava era o
servidor somar os mesmos numeros na ficha; sem isso a caixa promete e o combate ignora.

RESOLVE A DUVIDA DE 27/08. docs/opcoes-de-item.md deixou em aberto se o caso do switch era
`grupo % 100` (48 de 53 grupos) ou o INDICE da linha (98 de 321), com a observacao do jogador
-- uma asa com "poder de habilidade" -- a apontar para o indice, ja que o caso 108 e "Poder
de ataque de habilidade" e o resto nunca chega a 100. Era falsa oposicao: o switch tem 98
casos e vai ate 136, e serve DOIS consumidores. No uniqueOptionType o argumento e
`grupo % 100`, que so produz 0..99; no skillData e a coluna `tipo`, que chega a 136. A asa do
jogador vinha deste segundo caminho. As duas leituras estavam certas, em tabelas diferentes.

O PASSO FIXO NAO SERVE PARA ANDAR NESTES FICHEIROS. Lendo o skillData de 59 em 59 bytes a
partir do cabecalho, so 24% dos ids saem crescentes: o passo vale no inicio e parte-se a
meio. Aqui os registos localizam-se pelo ENCHIMENTO de 15 bytes com que todos terminam, e
assim saem 4086 registos com os ids 100% crescentes. O cabecalho declara 4146, portanto 60
ficam de fora -- os que nao terminam com esse enchimento. Nao se inventou nada para eles.
"""
import collections
import io
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, "server"))
sys.path.insert(0, os.path.join(RAIZ, "tools"))
import lod_crypto                                                    # noqa: E402
import data_files as D                                               # noqa: E402
from itembase import atributos, BASE                                 # noqa: E402

NL = chr(10)
TAB = chr(9)

# O enchimento com que termina todo registo do skillData. Substitui o passo fixo.
ENCHIMENTO = bytes.fromhex("010000003001000000300100000030")
ANTES_DO_ENCHIMENTO = 44        # bytes do registo antes dele
OFF_FIXO = 24                   # valor absoluto, double
OFF_PCT = 32                    # valor percentual, double
OFF_RANHURAS = 264              # onde o skillLevel guarda os 5 ids de skillData
N_RANHURAS = 5

# Caso do switch de FUN_006076cb -> nome que o client mostra na caixa de texto.
# Lido do AMo.exe pelo Ghidra em headless e cruzado com o string.lod. Reproduzir:
#     analyzeHeadless <dir> KarosOnline -process AMo.exe -noanalysis
#         -scriptPath <dir> -postScript KarosOpcaoValor.java
NOMES = {
    1: "Forca",
    2: "Vitalidade",
    3: "Destreza",
    4: "Sabedoria",
    5: "Inteligencia",
    6: "Forca de Ataque Fisico",
    7: "A forca atual de um ataque fisico",
    8: "Poder magico. ataques",
    9: "O poder atual do Ataque Magico",
    10: "Classificacao de ataque",
    11: "Chance de Critico",
    12: "Multiplicador de critico",
    13: "Velocidade de ataque",
    14: "Velocidade de invocacao",
    15: "Alcance de habilidade",
    16: "Classificacao de protecao",
    17: "Protecao fisica",
    18: "Protecao fisica atual",
    19: "Magico. protecao",
    20: "Protecao magica atual",
    21: "Chance de evasao",
    22: "Chance de bloqueio",
    23: "Absorcao de dano",
    24: "Discricao",
    25: "Raiva",
    27: "Velocidade de movimento",
    28: "Saude",
    29: "Mana",
    30: "Regeneracao de HP",
    31: "Regeneracao do MP",
    32: "absorcao de HP",
    33: "Absorcao de MP",
    34: "Efeito negativo",
    35: "Efeito negativo",
    36: "Medo",
    46: "Saude",
    47: "Mana",
    48: "Acao da aura",
    49: "Forca de Ataque Fisico PK",
    50: "O poder do Ataque Magico PK",
    51: "Classificacao de ataque PK",
    52: "Chance de Creta PK",
    53: "Coef. Creta PK",
    54: "Classificacao de protecao PK",
    55: "Protecao fisica PK",
    56: "Protecao magica PK",
    57: "Coef. evasao PK",
    58: "Oculos fletta",
    59: "Experiencia",
    60: "Experiencia",
    62: "Experiencia",
    63: "Saude chamada",
    64: "Reduz o tempo de recarga",
    65: "Chance de um item cair",
    71: "Forca de ataque",
    72: "Aumentar a experiencia do animal de estimacao",
    73: "Forca de Ataque Fisico",
    74: "Poder magico. ataques",
    80: "Cupom",
    81: "Forca",
    82: "Experiencia",
    83: "Experiencia adicional",
    84: "A experiencia de fletta",
    85: "Impossibilidade de movimento",
    86: "Impossibilidade de ataque",
    87: "Impossibilidade de Utilizacao do objeto",
    96: "Inverter danos",
    97: "Reduzir o poder de Creta",
    98: "Reduzir o dano critico",
    99: "Roubo de saude",
    100: "Vitalidade",
    101: "Inteligencia",
    102: "Sabedoria",
    103: "Caracteristicas do MV",
    105: "Absorcao de MP",
    108: "Poder de ataque de habilidade",
    113: "Ataque fixo",
    114: "Dano de ataque fixo aumentado",
    115: "Para ao atacar",
    116: "Dano aumentado na guerra de coalizao",
    117: "Dano reduzido na guerra de coalizao",
    118: "Aumento do PR",
    119: "Aumento do or",
    121: "Ignorando a protecao",
    122: "Recuperacao de dano",
    123: "Transforme mana em poder magico",
    124: "Protecao contra condicoes indesejadas",
    125: "Deteccao de invisibilidade",
    126: "Dano extra proporcional a vida",
    127: "Remocao de ganho",
    128: "Eficacia do tratamento",
    129: "Resistencia ao dano",
    131: "Aumenta a obtencao de quilates em",
    132: "Aumenta a chance de encantar",
    133: "Aumenta o numero de pontos de Arena obtidos em",
    134: "Reduz a quantidade de experiencia perdida na morte",
    135: "Aumenta o reputation points de",
    136: "Aumenta todas as caracteristicas de",
}


def _abrir(nome, pular_zero=False):
    for suf in ("_pt", "_en", ""):
        cam = os.path.join(BASE, nome + ".lod" + suf)
        if os.path.exists(cam):
            return lod_crypto.decrypt_lod_stream(open(cam, "rb").read(),
                                                 pular_zero=pular_zero)
    raise SystemExit("nao achei " + nome)


def efeitos_do_skilldata():
    """id de skillData -> (tipo, valor fixo, valor percentual)."""
    d = _abrir("skillData")
    saida, p = {}, 0
    while True:
        p = d.find(ENCHIMENTO, p)
        if p < 0:
            break
        i = p - ANTES_DO_ENCHIMENTO
        if i >= 0:
            sid, tipo = struct.unpack_from("<2I", d, i)
            saida[sid] = (tipo,
                          struct.unpack_from("<d", d, i + OFF_FIXO)[0],
                          struct.unpack_from("<d", d, i + OFF_PCT)[0])
        p += 1
    return saida


def buscador_de_ranhuras(sl, dados):
    """Devolve a funcao skill -> lista de ids de skillData.

    O skillLevel tambem nao anda a passo fixo, entao procura-se o id e confirma-se o achado
    por duas condicoes: o nivel em +4 tem de ser pequeno, e ao menos uma das 5 ranhuras tem
    de existir no skillData. Sem a segunda, qualquer sitio onde o numero aparecesse por
    acaso passava por registo.
    """
    cache = {}

    def procurar(skill):
        if skill in cache:
            return cache[skill]
        alvo = struct.pack("<I", skill)
        achado, p = None, 0
        while True:
            p = sl.find(alvo, p)
            if p < 0:
                break
            if p + OFF_RANHURAS + 4 * N_RANHURAS <= len(sl):
                nivel = struct.unpack_from("<I", sl, p + 4)[0]
                if 1 <= nivel <= 30:
                    r = struct.unpack_from("<%dI" % N_RANHURAS, sl, p + OFF_RANHURAS)
                    if any(x in dados for x in r):
                        achado = [x for x in r if x in dados]
                        break
            p += 1
        cache[skill] = achado
        return achado

    return procurar


def recolher():
    dados = efeitos_do_skilldata()
    procurar = buscador_de_ranhuras(_abrir("skillLevel"), dados)
    linhas = []
    sem_nome = collections.Counter()
    nao_resolvem = collections.Counter()
    for item_id, refino, _estrela, vals, _s in atributos():
        for ranhura in vals[8:13]:
            # As 5 ranhuras estao no fim do registo do itemAttr, onde o parser le doubles.
            # Sao inteiros: volta-se aos bits crus. Um id de skill lido como double da um
            # denormal (1,48e-316 no caso da asa) -- foi esse numero absurdo que denunciou
            # que a coluna nao era numero nenhum, e sim um id.
            skill = struct.unpack("<Q", struct.pack("<d", ranhura))[0]
            if not 0 < skill < 2 ** 32:
                continue
            ids = procurar(skill)
            if not ids:
                nao_resolvem[skill] += 1
                continue
            for sid in ids:
                tipo, fixo, pct = dados[sid]
                if not fixo and not pct:
                    continue
                if tipo not in NOMES:
                    sem_nome[tipo] += 1
                valor, unidade = (fixo, "abs") if fixo else (pct, "%")
                linhas.append((item_id, refino, skill, sid, tipo,
                               NOMES.get(tipo, "?"), valor, unidade))
    return linhas, dados, sem_nome, nao_resolvem


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    linhas, dados, sem_nome, nao_resolvem = recolher()
    itens = {l[0] for l in linhas}
    print("skillData        : %d efeitos" % len(dados))
    print("linhas de bonus  : %d, em %d itens" % (len(linhas), len(itens)))
    print("skills sem saida : %d valores distintos (%d ocorrencias)"
          % (len(nao_resolvem), sum(nao_resolvem.values())))
    if sem_nome:
        print("tipos sem nome   : %s" % sorted(sem_nome))

    print()
    print("conferencia -- Asas de anjo (24 horas), item 999900058:")
    for l in [x for x in linhas if x[0] == 999900058]:
        print("   %-34s +%.2f%s" % (l[5], l[6], "%" if l[7] == "%" else ""))

    if not aplicar:
        print()
        print("nada gravado. rode com --aplicar")
        return 0

    destino = D.caminho("ItemBonus", "Items")
    colunas = ("item_id", "refino", "skill_id", "efeito_id", "tipo",
               "nome", "valor", "unidade")
    corpo = [(str(a), str(b), str(c), str(d), str(e), f, "%.2f" % g, h)
             for a, b, c, d, e, f, g, h in sorted(linhas)]
    larg = []
    for i, c in enumerate(colunas):
        maior = len(c) + (2 if i == 0 else 0)
        for l in corpo:
            maior = max(maior, len(l[i]))
        larg.append(maior + 2)

    def escrever(vals, prefixo=""):
        fora = []
        for i, v in enumerate(vals):
            v = (prefixo if i == 0 else "") + v
            fora.append(v if i == len(vals) - 1 else v.ljust(max(0, larg[i] - 1)) + TAB)
        return "".join(fora).rstrip()

    with io.open(destino, "w", encoding="utf-8", newline="") as f:
        w = f.write
        w("; ItemBonus -- as \"Caracteristicas especiais\" da caixa de texto de cada item." + NL)
        w(";" + NL)
        w("; A cadeia: itemAttr da a skill passiva do item, skillLevel da os 5 efeitos dela," + NL)
        w("; skillData da (tipo, valor). Ver tools/importar_bonus_de_item.py." + NL)
        w(";" + NL)
        w("; unidade abs = valor absoluto, somado ao atributo." + NL)
        w("; unidade %   = percentual sobre o atributo." + NL)
        w(";" + NL)
        w("; colunas: " + ", ".join(colunas) + NL)
        w(NL)
        w(escrever(colunas, "//") + NL)
        for l in corpo:
            w(escrever(list(l)) + NL)
    print()
    print("gravado: %s  (%d linhas)" % (destino, len(corpo)))
    return 0


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