# -*- coding: utf-8 -*-
r"""Acrescenta os refinos 14 a 18 em falta ao itemAttr do client, sem trocar o ficheiro.

    python tools/mesclar_itemattr.py             mostra
    python tools/mesclar_itemattr.py --aplicar   grava no client

PORQUE E PRECISO. O servidor ja manda o refino certo no 06 00 (medido: byte 17 = 0x12 para
o +18 da bengala), mas o client procura a linha (item, refino) nesta tabela para montar o
item; nao a encontrando, nao desenha nada -- nem o numero, nem os atributos.

PORQUE NAO SE COPIAM AS LINHAS DO ARENA. Os valores base sao outros: o item 1001 no +13
vale 16.12/22.42 aqui e 14.00/19.00 la. Copiar dava atributos errados em 2247 itens. O que
se traz do Arena e so a CURVA, aplicada aos nossos numeros:

  refinos 14 e 15 -> interpolacao linear entre o nosso +13 e o nosso +16, que ja existem
                     (o ficheiro de 2019 tem os 862 itens do +16, mas nao os degraus)
  refinos 17 e 18 -> nosso +16 vezes a razao arena[n]/arena[16]
  sem +16 nosso   -> nosso +13 vezes a razao arena[n]/arena[13]

O +16 tambem entra nesta lista: o ficheiro de 2019 so o tem para 862 dos 2247 itens que tem
+13, e sem a linha o client nao desenha esse degrau. As linhas que ja existem sao mantidas
como estao -- nunca se reescreve um valor que o jogo ja tinha.

Formato do registo, conferido byte a byte contra o Arena:
    item(u32) refino(u32) grau(u32) + 13 doubles -> 12 + 13*8 = 116 bytes exactos,
    seguidos de duas strings com prefixo u32 de comprimento

QUAIS DOS 13 SOBEM COM O REFINO, medido no Arena sobre os itens que tem +13 e +18:
    12, 20   ataque fisico min/max      994 de 999 itens (99%)
    28, 36   ataque magico min/max      217 de 223 (97%)
    60       proteccao                 1577 de 1614 (98%)   botas do novato: 6 -> 16 -> 32
    68                                   769 de 806 (95%)
    84                                   728 de 731 (100%)
    44       velocidade de ataque         0 de 973 (0%)     1.10 na espada, 0.847 na bengala
    52       alcance                      0 de 1007 (0%)
   108                                    0 de 75 (0%)
Interpolam-se os 13, nao uma escolha a dedo: os que nao variam ficam constantes por
construccao, porque tem o mesmo valor no +13 e no +16 (e razao 1.0 no Arena). A primeira
versao, de 01/09, so mexia em 12/20/28/36 e deixava a proteccao presa no valor do +13 --
as botas +18 mostravam "Proteccao: 11" em vez de subirem.
O grau (offset 8) muda de 3 em 3 refinos e e o que troca o modelo do item; o Arena poe 0
nos degraus novos, e e o que se faz aqui.
"""
import argparse
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, "tools"))
import lod_crypto as L                                              # noqa: E402

NOSSO = r"C:\Games\Axion\ResUnpak\Interface\IE_Lod\itemAttr.lod_pt"
ARENA = r"C:\Games\Arena\ResUnpak\Interface\IE_Lod\itemAttr.lod_en"
DEST = os.path.dirname(NOSSO)
TAM_NUM = 116
OFF_GRAU = 8
OFF_VALORES = 12
N_VALORES = 13          # 13 doubles, offsets 12 a 108
NOVOS = (14, 15, 16, 17, 18)


def ler(cam, xor=0):
    """[(item, refino, grau, [4 doubles], bytes_do_registo_inteiro)] na ordem do ficheiro."""
    d = L.decrypt_lod_stream(open(cam, "rb").read(), pular_zero=False, xor=xor)
    ver, tot = struct.unpack_from("<II", d, 0)
    pos, out = 8, []
    for _ in range(tot):
        ini = pos
        iid, ref, grau = struct.unpack_from("<3I", d, pos)
        vals = list(struct.unpack_from("<%dd" % N_VALORES, d, pos + OFF_VALORES))
        pos += TAM_NUM
        for _s in range(2):
            (t,) = struct.unpack_from("<I", d, pos)
            pos += 4 + t
        out.append((iid, ref, grau, vals, bytes(d[ini:pos])))
    return ver, out


def monta(molde, refino, vals):
    """Copia o registo do +13 e troca refino, grau e os 4 doubles. Strings vao intactas."""
    b = bytearray(molde)
    struct.pack_into("<I", b, 4, refino)
    struct.pack_into("<I", b, OFF_GRAU, 0)
    struct.pack_into("<%dd" % N_VALORES, b, OFF_VALORES, *vals)
    return bytes(b)


def main() -> int:
    ap = argparse.ArgumentParser(description="Refinos 14/15/17/18 no itemAttr do client.")
    ap.add_argument("--aplicar", action="store_true")
    args = ap.parse_args()

    ver, ln = ler(NOSSO)
    _va, la = ler(ARENA, L.XOR_ARENA)
    print("nosso: %d linhas (versao %d) | arena: %d linhas" % (len(ln), ver, len(la)))

    nosso = {(r[0], r[1]): r for r in ln}
    arena = {(r[0], r[1]): r for r in la}
    ja = set(k for k in nosso)

    novos_por_item = {}
    via = {"interpolado": 0, "razao13": 0, "sem_base": 0}
    for iid, ref, _g, vals, molde in ln:
        if ref != 13:
            continue
        r16 = nosso.get((iid, 16))
        a13, a16 = arena.get((iid, 13)), arena.get((iid, 16))
        lista = []
        for n in NOVOS:
            if (iid, n) in ja:
                continue
            an = arena.get((iid, n))
            if r16 is not None:
                if n < 16:                                    # 14 e 15: entre 13 e 16
                    f = (n - 13) / 3.0
                    v = [vals[i] + (r16[3][i] - vals[i]) * f for i in range(N_VALORES)]
                else:                                         # 17 e 18: curva do arena
                    if an and a16:
                        v = [r16[3][i] * (an[3][i] / a16[3][i]) if a16[3][i] else r16[3][i]
                             for i in range(N_VALORES)]
                    else:                                     # sem arena: passo do 13->16
                        p = [(r16[3][i] - vals[i]) / 3.0 for i in range(N_VALORES)]
                        v = [r16[3][i] + p[i] * (n - 16) for i in range(N_VALORES)]
                via["interpolado"] += 1
            elif an and a13:
                v = [vals[i] * (an[3][i] / a13[3][i]) if a13[3][i] else vals[i]
                     for i in range(N_VALORES)]
                via["razao13"] += 1
            else:
                via["sem_base"] += 1
                continue
            lista.append(monta(molde, n, [round(x, 2) for x in v]))
        if lista:
            novos_por_item[iid] = lista

    total = sum(len(v) for v in novos_por_item.values())
    print("itens com +13: %d | linhas novas: %d" % (sum(1 for r in ln if r[1] == 13), total))
    print("   por interpolacao 13-16: %d | por razao do arena sobre o +13: %d | sem base: %d"
          % (via["interpolado"], via["razao13"], via["sem_base"]))

    # amostra
    for alvo in (43201, 16001, 5001):
        if alvo in novos_por_item:
            print("\n   item %d:" % alvo)
            for r in (13, 14, 15, 16, 17, 18):
                b = nosso.get((alvo, r))
                if b:
                    print("     +%-3d %s   (ja existia)" % (r, " ".join("%7.2f" % x for x in b[3])))
                    continue
                for reg in novos_por_item[alvo]:
                    if struct.unpack_from("<I", reg, 4)[0] == r:
                        v = struct.unpack_from("<%dd" % N_VALORES, reg, OFF_VALORES)
                        print("     +%-3d %s   NOVO" % (r, " ".join("%7.2f" % x for x in v)))

    # reconstroi: ordem original intacta, novos logo a seguir a ultima linha do item
    corpo = bytearray(struct.pack("<II", ver, len(ln) + total))
    for i, (iid, _r, _g, _v, reg) in enumerate(ln):
        corpo += reg
        ultimo = (i + 1 == len(ln)) or (ln[i + 1][0] != iid)
        if ultimo and iid in novos_por_item:
            for b in novos_por_item[iid]:
                corpo += b
    print("\ncorpo novo: %d linhas, %d bytes" % (len(ln) + total, len(corpo)))

    if not args.aplicar:
        print("\nnada gravado. corra com --aplicar")
        return 0

    cru = open(NOSSO, "rb").read()
    prefixo, tabela, chave = L._extract_header(cru)
    cifrado = prefixo + L.encrypt_lod_body(bytes(corpo), tabela, chave, pular_zero=False)
    n = 0
    for f in os.listdir(DEST):
        if f.lower().startswith("itemattr.lod_"):
            open(os.path.join(DEST, f), "wb").write(cifrado)
            n += 1
    print("gravado em %d ficheiros, %d bytes cada" % (n, len(cifrado)))

    _v2, rel = ler(os.path.join(DEST, "itemAttr.lod_pt"))
    refs = sorted(set(r[1] for r in rel))
    print("releitura: %d linhas, refinos %s" % (len(rel), refs))
    return 0


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