# -*- coding: utf-8 -*-
"""Troca o nome do jogo no TEXTO das tabelas .lod do client.

    python tools/renomear_jogo.py --ver          so lista o que mudaria
    python tools/renomear_jogo.py --aplicar      grava (guarda .orig_backup)

A troca e Karos -> Axion, preservando a caixa (KAROS -> AXION, karos -> axion). As duas
palavras tem CINCO letras, e e isso que torna o patch viavel: as strings do .lod tem
prefixo de tamanho, entao mexer no comprimento exigiria remontar a tabela inteira.

A REGRA QUE IMPORTA: nao trocar dentro de nome de arquivo. Das ~1770 ocorrencias de
"Karos" nas tabelas, a grande maioria e caminho de textura -- KarosUI_Action.dds,
karos_pet_icon01.dds, KarosGuild_Mark_frame.dds. Renomear essas nao muda nada na tela e
faz o client procurar um arquivo que nao existe. Entao cada ocorrencia e olhada dentro da
string em que vive, e a string e pulada se parecer um recurso.

A re-cifragem sai byte-diferente do original (o byte delimitador que segue um zero e
arbitrario e o decifrador o descarta), mas o PLAINTEXT bate exatamente -- conferido por
round-trip nas sete tabelas antes de aplicar qualquer troca.
"""
import os
import re
import shutil
import sys

sys.path.insert(0, r"D:\RoshEmulator\server")
import lod_crypto  # noqa: E402

BASE = r"C:\Games\Karos\ResUnpak\Interface\IE_Lod"
IDIOMA = "_pt"
DE, PARA = "karos", "axion"
EXTENSOES = (".dds", ".tga", ".png", ".ogg", ".bmp", ".jpg", ".pak", ".lod", ".exe")

# tabelas com texto que o jogador le; as demais so tem nome de recurso
# itemBase e itemAttr ficam DE FORA de proposito: sao tabelas de numeros, e a sequencia
# "karos" aparece nelas por acaso, no meio de bytes binarios. Uma "troca" ali corromperia
# um item, nao renomearia nada.
TABELAS = ["string", "quest", "npcDialogue", "eventDiary", "launcherString",
           "helpSystem", "EventInform", "npcShop", "skillBase", "petBase"]


def imprimivel(b):
    return 32 <= b < 127 or b >= 0xA0


def contexto(buf, pos):
    """A string em que a ocorrencia vive: bytes imprimiveis contiguos ao redor."""
    ini = pos
    while ini > 0 and imprimivel(buf[ini - 1]):
        ini -= 1
    fim = pos
    while fim < len(buf) and imprimivel(buf[fim]):
        fim += 1
    return ini, fim, bytes(buf[ini:fim])


def eh_recurso(s: bytes) -> bool:
    baixo = s.lower()
    return any(e.encode() in baixo for e in EXTENSOES)


def parece_texto(s: bytes) -> bool:
    """Frase de verdade, e nao uma sequencia de bytes que por acaso deu "karos".

    O teste e ter espaco: todo texto que o jogador le nessas tabelas e uma frase ou um
    nome composto. Foi o que separou 'Bolo de Aniversario Karos' (real) de
    'ï¿½karos5I_it' (lixo binario do meio do itemBase).
    """
    return b" " in s.strip()


def com_caixa(orig: bytes) -> bytes:
    """PARA na mesma caixa que a ocorrencia original."""
    if orig.isupper():
        return PARA.upper().encode()
    if orig[:1].isupper():
        return PARA.capitalize().encode()
    return PARA.encode()


def analisar(plaintext: bytearray):
    """Devolve (trocas, pulados). Trocas e uma lista de (pos, novo, contexto)."""
    trocas, pulados = [], []
    for m in re.finditer(DE.encode(), bytes(plaintext), re.IGNORECASE):
        pos = m.start()
        ini, fim, ctx = contexto(plaintext, pos)
        if eh_recurso(ctx) or not parece_texto(ctx):
            pulados.append(ctx)
            continue
        trocas.append((pos, com_caixa(m.group()), ctx))
    return trocas, pulados


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    if not aplicar and "--ver" not in sys.argv:
        print(__doc__)
        return 2

    total_t = total_p = 0
    for nome in TABELAS:
        cam = os.path.join(BASE, nome + ".lod" + IDIOMA)
        if not os.path.exists(cam):
            continue
        plaintext = bytearray(lod_crypto.decrypt_lod_stream(open(cam, "rb").read()))
        trocas, pulados = analisar(plaintext)
        total_t += len(trocas)
        total_p += len(pulados)
        if not trocas and not pulados:
            continue
        print("%-16s %3d trocas, %4d pulados (nome de arquivo)"
              % (nome, len(trocas), len(pulados)))
        vistos = set()
        for pos, novo, ctx in trocas:
            t = ctx.decode("latin-1")[:88]
            if t not in vistos:
                vistos.add(t)
                print("      %r" % t)

        if aplicar and trocas:
            bkp = cam + ".orig_backup"
            if not os.path.exists(bkp):
                shutil.copy2(cam, bkp)

            def patch(buf, _trocas=trocas):
                for pos, novo, _ in _trocas:
                    buf[pos:pos + len(novo)] = novo

            lod_crypto.patch_lod_file(cam, cam + ".novo", patch)
            # confere ANTES de substituir: o plaintext do novo tem que ter zero
            # ocorrencias fora de nome de arquivo, e o mesmo tamanho
            novo_plain = lod_crypto.decrypt_lod_stream(open(cam + ".novo", "rb").read())
            if len(novo_plain) != len(plaintext):
                print("      ABORTADO: tamanho do plaintext mudou")
                os.remove(cam + ".novo")
                return 1
            resto, _ = analisar(bytearray(novo_plain))
            if resto:
                print("      ABORTADO: sobraram %d ocorrencias" % len(resto))
                os.remove(cam + ".novo")
                return 1
            os.replace(cam + ".novo", cam)
            print("      gravado (backup em %s)" % os.path.basename(bkp))

    print()
    print("TOTAL: %d trocas de texto, %d ocorrencias preservadas por serem nome de arquivo"
          % (total_t, total_p))
    if not aplicar:
        print("(nada foi gravado -- rode com --aplicar)")
    return 0


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