import base64
from typing import Any, Callable

from lib.archive import FArchiveReader, FArchiveWriter


def custom_version_reader(reader: FArchiveReader):
    return (reader.guid(), reader.i32())


def custom_version_writer(writer: FArchiveWriter, value: tuple[str, int]):
    writer.guid(value[0])
    writer.i32(value[1])


class GvasHeader:
    magic: int
    save_game_version: int
    package_file_version_ue4: int
    package_file_version_ue5: int
    engine_version_major: int
    engine_version_minor: int
    engine_version_patch: int
    engine_version_changelist: int
    engine_version_branch: str
    custom_version_format: int
    custom_versions: list[tuple[str, int]]
    save_game_class_name: str

    @staticmethod
    def read(reader: FArchiveReader) -> "GvasHeader":
        header = GvasHeader()
        # FileTypeTag
        header.magic = reader.i32()
        if header.magic != 0x53415647:
            raise Exception("invalid magic")
        # SaveGameFileVersion
        header.save_game_version = reader.i32()
        if header.save_game_version != 3:
            raise Exception(
                f"expected save game version 3, got {header.save_game_version}"
            )
        # PackageFileUEVersion
        header.package_file_version_ue4 = reader.i32()
        header.package_file_version_ue5 = reader.i32()
        # SavedEngineVersion
        header.engine_version_major = reader.u16()
        header.engine_version_minor = reader.u16()
        header.engine_version_patch = reader.u16()
        header.engine_version_changelist = reader.u32()
        header.engine_version_branch = reader.fstring()
        # CustomVersionFormat
        header.custom_version_format = reader.i32()
        if header.custom_version_format != 3:
            raise Exception(
                f"expected custom version format 3, got {header.custom_version_format}"
            )
        # CustomVersions
        header.custom_versions = reader.tarray(custom_version_reader)
        header.save_game_class_name = reader.fstring()
        return header

    @staticmethod
    def load(dict: dict[str, Any]) -> "GvasHeader":
        header = GvasHeader()
        header.magic = dict["magic"]
        header.save_game_version = dict["save_game_version"]
        header.package_file_version_ue4 = dict["package_file_version_ue4"]
        header.package_file_version_ue5 = dict["package_file_version_ue5"]
        header.engine_version_major = dict["engine_version_major"]
        header.engine_version_minor = dict["engine_version_minor"]
        header.engine_version_patch = dict["engine_version_patch"]
        header.engine_version_changelist = dict["engine_version_changelist"]
        header.engine_version_branch = dict["engine_version_branch"]
        header.custom_version_format = dict["custom_version_format"]
        header.custom_versions = dict["custom_versions"]
        header.save_game_class_name = dict["save_game_class_name"]
        return header

    def dump(self) -> dict[str, Any]:
        return {
            "magic": self.magic,
            "save_game_version": self.save_game_version,
            "package_file_version_ue4": self.package_file_version_ue4,
            "package_file_version_ue5": self.package_file_version_ue5,
            "engine_version_major": self.engine_version_major,
            "engine_version_minor": self.engine_version_minor,
            "engine_version_patch": self.engine_version_patch,
            "engine_version_changelist": self.engine_version_changelist,
            "engine_version_branch": self.engine_version_branch,
            "custom_version_format": self.custom_version_format,
            "custom_versions": self.custom_versions,
            "save_game_class_name": self.save_game_class_name,
        }

    def write(self, writer: FArchiveWriter):
        writer.i32(self.magic)
        writer.i32(self.save_game_version)
        writer.i32(self.package_file_version_ue4)
        writer.i32(self.package_file_version_ue5)
        writer.u16(self.engine_version_major)
        writer.u16(self.engine_version_minor)
        writer.u16(self.engine_version_patch)
        writer.u32(self.engine_version_changelist)
        writer.fstring(self.engine_version_branch)
        writer.i32(self.custom_version_format)
        writer.tarray(custom_version_writer, self.custom_versions)
        writer.fstring(self.save_game_class_name)


class GvasFile:
    header: GvasHeader
    properties: dict[str, Any]
    trailer: bytes

    @staticmethod
    def read(
        data: bytes,
        type_hints: dict[str, str] = {},
        custom_properties: dict[str, tuple[Callable, Callable]] = {},
    ) -> "GvasFile":
        gvas_file = GvasFile()
        reader = FArchiveReader(data, type_hints, custom_properties)
        gvas_file.header = GvasHeader.read(reader)
        gvas_file.properties = reader.properties_until_end()
        gvas_file.trailer = reader.read_to_end()
        if gvas_file.trailer != b"\x00\x00\x00\x00":
            print(
                f"{len(gvas_file.trailer)} bytes of trailer data, file may not have fully parsed"
            )
        return gvas_file

    @staticmethod
    def load(dict: dict[str, Any]) -> "GvasFile":
        gvas_file = GvasFile()
        gvas_file.header = GvasHeader.load(dict["header"])
        gvas_file.properties = dict["properties"]
        gvas_file.trailer = base64.b64decode(dict["trailer"])
        return gvas_file

    def dump(self) -> dict[str, Any]:
        return {
            "header": self.header.dump(),
            "properties": self.properties,
            "trailer": base64.b64encode(self.trailer).decode("utf-8"),
        }

    def write(
        self, custom_properties: dict[str, tuple[Callable, Callable]] = {}
    ) -> bytes:
        writer = FArchiveWriter(custom_properties)
        self.header.write(writer)
        writer.properties(self.properties)
        writer.write(self.trailer)
        return writer.bytes()
