# -*- coding: utf-8 -*-
r"""Poe o brilho de refino de uma arma a ir ate +18, como o Arena faz.

    python tools/refino18_modelo.py SMF_SET_01_STICK            mostra
    python tools/refino18_modelo.py SMF_SET_01_STICK --aplicar  grava (com backup .bak13)
    python tools/refino18_modelo.py --todos --aplicar           todos os modelos

O BRILHO ESTA NO MODELO DA ARMA, nao no executavel nem numa tabela .lod -- ver
docs/brilho-de-refino.md. Cada `Character/Weapon/*._OB` traz duas listas paralelas:

    efeitos: [valor float 4][len 4][nome]         13 entradas
    slots:   [marcador 4][len 4][nome 16][40]     13 entradas, "Weapon_Enchant01".."13"

OS SLOTS NAO SAO OS NIVEIS -- foi o erro que fiz a primeira vez, quando acrescentei
"Weapon_Enchant14".."18" a lista. Sao 13 posicoes fixas; quem diz o nivel e o CONTADOR
(o u32 imediatamente antes da lista) e os EFEITOS que la vao. Medido nos 309 modelos do
Arena que tem brilho ate +18: todos tem 13 slots, contador 18, e efeitos do 06 ao 18.

    nosso:  13 efeitos, HAM_LANCE_11 .. _53      ->  niveis  1..13
    arena:  13 efeitos, Enchant_Stick_06 .. _18  ->  niveis  6..18

Entao o que se faz aqui e so trocar os 13 nomes por
Enchant_<familia>_06._EF ate _18._EF. As sete familias existem todas neste client desde
Dez/2023 (200 ficheiros orfaos que nenhum modelo referia) -- Bow, Dagger, FireArms, Shield,
Stick, Sword_1H e Sword_2H, cada uma com os 13 niveis.

O preco: os refinos 1 a 5 deixam de ter brilho, como no Arena.
"""
import argparse
import os
import re
import shutil
import struct
import sys

WEAPON = r"C:\Games\Axion\ResUnpak\Character\Weapon"
EFEITOS = r"C:\Games\Axion\ResUnpak\Character\Effect"
NIVEIS = list(range(6, 19))          # 13 niveis, 6 a 18 -- a gama do Arena
# Nem todos os modelos tem 13 efeitos: 8 tem 10 e 3 tem 12. Para esses usam-se os ULTIMOS
# n niveis ate ao 18, que mantem o topo no sitio certo -- e o topo que se ve.
CONTADOR_NOVO = 18

# Sufixo do nome do modelo -> familia do efeito. Medido nos 309 modelos do Arena.
FAMILIA = {
    "SWORD": "Sword_1H", "DUALSWORD": "Sword_1H", "BLADE": "Sword_2H",
    "STICK": "Stick", "WAND": "Stick", "STAFF": "Stick", "LANCE": "Stick", "HILL": "Stick",
    "BOW": "Bow", "CANNON": "FireArms", "HANDGUN": "FireArms", "RIFLE": "FireArms",
    "DAGGER": "Dagger", "SHELD": "Shield", "SHIELD": "Shield",
}


def familia_de(nome_modelo):
    """Procura no nome QUALQUER dos sufixos conhecidos, do mais longo para o mais curto.

    Nao basta olhar para o fim: 40 modelos acabam em digito (STICK1, LANCE01) e outros tem
    o tipo no meio (SMF_WEAPON_STAFF_01). E a ordem por comprimento importa, senao
    DUALSWORD casa com SWORD e leva a familia errada.
    """
    alto = nome_modelo.upper()
    for chave in sorted(FAMILIA, key=len, reverse=True):
        if re.search(r"(?:^|_)%s(?:\d*)(?:_|$)" % chave, alto):
            return FAMILIA[chave]
    return None


def listas(d):
    """(off_contador, ini, n) da lista de efeitos, ou None."""
    m = re.search(rb"[A-Za-z0-9_]{4,40}\._EF", d)
    if not m:
        return None
    ini = m.start() - 8                       # valor(4) + len(4) antes do nome
    return ini - 4, ini, struct.unpack_from("<I", d, ini - 4)[0]


def entradas(d, ini, n):
    out, o = [], ini
    for _ in range(n):
        val = d[o:o + 4]
        ln = struct.unpack_from("<I", d, o + 4)[0]
        out.append((val, d[o + 4 + 4:o + 8 + ln]))
        o = o + 8 + ln
    return out, o


def slots(d):
    """(off_contador, ini, n) da lista de Weapon_EnchantNN. Entrada = marcador+len+nome+40."""
    m = re.search(rb"Weapon_Enchant01", d)
    if not m:
        return None
    ini = m.start() - 8
    return ini - 4, ini, struct.unpack_from("<I", d, ini - 4)[0]


def ler_slots(d, ini, n):
    out, o = [], ini
    for _ in range(n):
        if o + 8 > len(d):
            return out, o
        ln = struct.unpack_from("<I", d, o + 4)[0]
        if not (1 <= ln <= 60) or o + 8 + ln + 40 > len(d):
            return out, o
        out.append((d[o:o + 4], d[o + 8:o + 8 + ln], d[o + 8 + ln:o + 8 + ln + 40]))
        o = o + 8 + ln + 40
    return out, o


def coerente(dados):
    """Rele o ficheiro inteiro: as duas listas tem de fechar antes do que vem a seguir.

    ESTE TESTE FALTAVA e custou dois travamentos ao jogador em 01/09.
    """
    try:
        r = listas(dados)
        if not r:
            return False, "sem lista de efeitos"
        _oc, ini, n = r
        o = ini
        for _ in range(n):
            ln = struct.unpack_from("<I", dados, o + 4)[0]
            if not (1 <= ln <= 60) or o + 8 + ln > len(dados):
                return False, "efeitos nao fecham"
            o += 8 + ln
        fim_ef = o
        rs = slots(dados)
        if not rs:
            return False, "sem lista de slots"
        _os, inis, ns = rs
        if fim_ef > inis:
            return False, "efeitos invadem os slots"
        sl, fim_sl = ler_slots(dados, inis, ns)
        if len(sl) != ns:
            return False, "slots nao fecham (%d de %d)" % (len(sl), ns)
        if fim_sl > len(dados):
            return False, "slots passam do fim"
        return True, "ok"
    except Exception as e:                                          # noqa: BLE001
        return False, str(e)[:60]


def converter(cam, aplicar, verboso=True, forcar=None):
    nome = os.path.basename(cam)[:-4]
    d = open(cam, "rb").read()
    if not re.search(rb"Weapon_Enchant01", d):
        return None
    r = listas(d)
    if not r:
        return None
    off_c, ini, n = r
    if n == CONTADOR_NOVO:
        return ("ja", nome, 0)
    if not 1 <= n <= 13:
        return ("contador estranho (%d)" % n, nome, 0)
    fam = forcar or familia_de(nome)
    if fam is None:
        return ("sem familia", nome, 0)
    ents, fim = entradas(d, ini, n)
    # SALVAGUARDA. O contador TEM de bater com o numero de entradas que se conseguem ler,
    # e a leitura tem de fechar num sitio plausivel. Sem isto, em 01/09, ficaram 11 modelos
    # com contador 18 e so 10 ou 12 entradas -- e o client passou a TRAVAR ANTES DA TELA DE
    # PERSONAGEM, porque carrega os modelos todos no arranque e um mal formado leva o resto
    # a frente. Converter so o que fecha ao byte.
    if len(ents) != n or fim > len(d):
        return ("nao fecha (%d entradas para contador %d)" % (len(ents), n), nome, 0)
    # QUALQUER NUMERO DE ENTRADAS SERVE, desde que se saia com 18. Onze modelos tem 10 ou
    # 12 efeitos em vez de 13; ficarem com menos de 18 e o que faz o +18 nao brilhar e o
    # +13 mostrar o efeito do 18 -- o client usa slot = refino.
    if n > 18:
        return ("mais de 18 efeitos (%d)" % n, nome, 0)
    # slot N -> nivel N. So ha efeitos do 6 ao 18, entao os niveis 1..5 repetem o do 6.
    nomes = ["Enchant_%s_%02d._EF" % (fam, min(18, max(6, k))) for k in range(1, 19)]
    faltam = [x for x in nomes if not os.path.exists(os.path.join(EFEITOS, x))]
    if faltam:
        return ("faltam efeitos: %s" % faltam[0], nome, 0)

    # DEZOITO ENTRADAS A SERIO, nas duas listas. O client usa slot = refino (1..13 aqui),
    # ao contrario do Arena, que usa slot = refino-5 e por isso lhe bastam 13 entradas para
    # os niveis 6..18. Com 13 entradas ca, um item +15 procura o slot 15, nao o encontra e
    # nao desenha nada -- foi o que o jogador viu em 01/09.
    #
    # E O CONTADOR TEM DE ACOMPANHAR: ele conta as ENTRADAS. Poe-lo a 18 deixando 13, como
    # fiz primeiro, faz o leitor ir buscar cinco que nao existem -- o client travava a
    # caminho da tela de personagem. Aqui acrescentam-se as entradas E os contadores juntos.
    rs = slots(d)
    if not rs:
        return ("sem lista de slots", nome, 0)
    off_cs, ini_sl, n_sl = rs
    sl, fim_sl = ler_slots(d, ini_sl, n_sl)
    if len(sl) != n_sl or not 1 <= n_sl <= 18:
        return ("slots inesperados (%d/%d)" % (len(sl), n_sl), nome, 0)

    # efeitos: os 13 que la estao viram os niveis 1..13; os 5 novos sao 14..18
    faltam = [x for x in set(nomes) if not os.path.exists(os.path.join(EFEITOS, x))]
    if faltam:
        return ("faltam efeitos: %s" % faltam[0], nome, 0)
    bloco = b""
    for i, novo_nome in enumerate(nomes):
        val = ents[i][0] if i < len(ents) else ents[-1][0]
        bloco += val + struct.pack("<I", len(novo_nome)) + novo_nome.encode("latin-1")

    bloco_sl = b""
    for i in range(18):
        if i < n_sl:
            marc, nm, dados = sl[i]
        else:
            _m, _n, dados = sl[-1]
            marc = struct.pack("<HH", i + 1, 1)
            nm = ("Weapon_Enchant%02d" % (i + 1)).encode("latin-1")
            dados = bytearray(dados)
            struct.pack_into("<I", dados, 0, i)
            dados = bytes(dados)
        bloco_sl += marc + struct.pack("<I", len(nm)) + nm + dados

    saida = bytearray(d)
    saida[ini_sl:fim_sl] = bloco_sl
    saida[ini:fim] = bloco
    desloc = len(bloco) - (fim - ini)
    struct.pack_into("<I", saida, off_cs + desloc, 18)
    struct.pack_into("<I", saida, off_c, 18)

    ok, porque = coerente(bytes(saida))
    if not ok:
        return ("RECUSADO, nao rele: %s" % porque, nome, 0)

    if verboso:
        print("%s  familia=%s  %d -> %d bytes" % (nome, fam, len(d), len(saida)))
        print("   %s -> %s" % (ents[0][1].decode(), nomes[0]))
        print("   %s -> %s" % (ents[-1][1].decode(), nomes[-1]))
    if aplicar:
        bak = cam + ".bak13"
        if not os.path.exists(bak):
            shutil.copy2(cam, bak)
        open(cam, "wb").write(bytes(saida))
    return ("ok", nome, len(saida) - len(d))


def main() -> int:
    ap = argparse.ArgumentParser(description="Brilho de refino ate +18 nos modelos de arma.")
    ap.add_argument("modelo", nargs="?", help="nome sem extensao, ex: SMF_SET_01_STICK")
    ap.add_argument("--todos", action="store_true")
    ap.add_argument("--aplicar", action="store_true")
    ap.add_argument("--familia", help="forca a familia do efeito, para isolar problemas")
    args = ap.parse_args()

    if args.todos:
        import collections
        cont = collections.Counter()
        for f in sorted(os.listdir(WEAPON)):
            if not f.lower().endswith("._ob"):
                continue
            r = converter(os.path.join(WEAPON, f), args.aplicar, verboso=False)
            if r:
                cont[r[0]] += 1
        for k, v in cont.most_common():
            print("   %-28s %d modelos" % (k, v))
    elif args.modelo:
        r = converter(os.path.join(WEAPON, args.modelo + "._OB"), args.aplicar,
                      forcar=args.familia)
        if r is None:
            print("modelo sem lista de encantamento (ou inexistente)")
            return 1
        print("resultado: %s" % r[0])
    else:
        ap.error("de um modelo ou --todos")
    if not args.aplicar:
        print("\nnada gravado. junte --aplicar")
    return 0


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