"""IFO (module info) generic: GFF-based module metadata and entry points."""

from __future__ import annotations

from typing import TYPE_CHECKING

from pykotor.common.language import LocalizedString
from pykotor.common.misc import Game, ResRef
from pykotor.resource.formats.gff import GFF, GFFContent, GFFList, read_gff, write_gff
from pykotor.resource.formats.gff.gff_auto import bytes_gff
from pykotor.resource.type import ResourceType
from utility.common.geometry import Vector2, Vector3

if TYPE_CHECKING:
    from pykotor.resource.type import SOURCE_TYPES, TARGET_TYPES


class IFO:
    """Stores module information.

    IFO files are GFF-based format files that store module information including
    entry points, script hooks, area lists, and module metadata. IFO files define
    the starting area, entry position/direction, module scripts, and expansion pack
    requirements for a game module.

    References (REVA):
    ------------------
        LoadModuleStart @ (K1: 0x004c9050, TSL: 0x0072aaa0) - main IFO GFF parser; reads all Mod_* fields.
        MainLoop (caller) @ (K1: 0x004babb0, TSL: 0x007b6bb0). Fields: Mod_IsSaveGame (BYTE), Mod_ID (VOID),
        Mod_Creator_ID (INT), Mod_Version (DWORD), Mod_Name/Mod_Description (CExoLocString), Mod_Tag (CExoString),
        Mod_Entry_Area (CResRef), Mod_Entry_X/Y/Z, Mod_Entry_Dir_X/Y (FLOAT), Mod_On* (CResRef), Mod_Area_list (list),
        time/deprecated fields. Same structure and defaults in both games.
    """

    BINARY_TYPE = ResourceType.IFO

    def __init__(self):
        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:15
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:100 (id: Uint8Array = new Uint8Array(16)) (id: Uint8Array = new Uint8Array(16)
        # Module ID: 16-byte unique identifier generated by toolset (32 bytes in save games)
        self.mod_id: bytes = b""

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:20
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:105 (name: CExoLocString)
        # Localized module name displayed in-game
        self.mod_name: LocalizedString = LocalizedString.from_invalid()

        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:243 (areaName from Mod_Area_list[0].Area_Name)
        # First area name from Mod_Area_list (KotOR modules typically have one area)
        self.area_name: ResRef = ResRef.from_blank()

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:25
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:270 (entryArea = Mod_Entry_Area)
        # ResRef of entry area where player spawns
        self.resref: ResRef = ResRef.from_blank()

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:29-30
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:271-272 (entryDirectionX/Y)
        # Entry direction angle (computed from Mod_Entry_Dir_X and Mod_Entry_Dir_Y)
        self.entry_direction: float = 0.0

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:26-28
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:273-275 (entryX/Y/Z)
        # Entry position coordinates where player spawns
        self.entry_position: Vector3 = Vector3.from_null()

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:21
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:110 (tag: string)
        # Module tag identifier
        self.tag: str = ""

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:18
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:115 (voId: string)
        # Voice over folder name (for localized voice files)
        self.vo_id: str = ""

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:40-54
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:59 (scriptResRefs Map)
        # Module script hooks (ResRefs executed on various events)
        self.on_acquire_item: ResRef = ResRef.from_blank()
        self.on_activate_item: ResRef = ResRef.from_blank()
        self.on_enter: ResRef = ResRef.from_blank()
        self.on_leave: ResRef = ResRef.from_blank()
        self.on_heartbeat: ResRef = ResRef.from_blank()
        self.on_load: ResRef = ResRef.from_blank()
        self.on_start: ResRef = ResRef.from_blank()
        self.on_player_death: ResRef = ResRef.from_blank()
        self.on_player_dying: ResRef = ResRef.from_blank()
        self.on_player_levelup: ResRef = ResRef.from_blank()
        self.on_player_respawn: ResRef = ResRef.from_blank()
        self.on_unacquire_item: ResRef = ResRef.from_blank()
        self.on_user_defined: ResRef = ResRef.from_blank()

        # Deprecated fields (not used by KotOR engine, from NWN):

        # https://github.com/th3w1zard1/Kotor.NET/tree/master/Kotor.NET/Resources/KotorIFO/IFO.cs:16-17,19,22-24,31-39,54-55
        # https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/Module.ts:160-181,267-268 (various deprecated fields)
        self.creator_id: int = 0
        self.version: int = 0
        self.expansion_id: int = 0
        self.hak: str = ""
        self.description: LocalizedString = LocalizedString.from_invalid()
        self.dawn_hour: int = 0
        self.dusk_hour: int = 0
        self.time_scale: int = 0
        self.start_month: int = 0
        self.start_day: int = 0
        self.start_hour: int = 0
        self.start_year: int = 0
        self.xp_scale: int = 0
        self.on_player_rest: ResRef = ResRef.from_blank()
        self.start_movie: ResRef = ResRef.from_blank()


def construct_ifo(
    gff: GFF,
) -> IFO:
    """Construct IFO from GFF.

    Defaults when field missing (REVA): all Mod_* fields optional; engine uses same defaults below.

    Reference functions (5 K1, 5 TSL):
    K1: (1) LoadModuleStart @ 0x004c9050 (reads all Mod_*), (2) MainLoop @ 0x004babb0 (caller),
    (3) CExoString ctor @ 0x005b3190 (string fields), (4) GetTopLevelStruct/ReadField* GFF path,
    (5) FreeChunk @ 0x004070a0 (GFF cleanup). TSL: (1) LoadModuleStart @ 0x0072aaa0,
    (2) MainLoop @ 0x007b6bb0 (caller), (3)-(5) same semantics. Entry dir: when both zero engine uses dir_x=1.0 dir_y=0.0.
    """
    ifo = IFO()

    root = gff.root
    # Mod_ID VOID 0x20: default empty. K1 LoadModuleStart @ 0x004c9050 ReadFieldVOID; TSL @ 0x0072aaa0. Omit OK.
    ifo.mod_id = root.acquire("Mod_ID", b"")
    # Mod_VO_ID CExoString: default "". K1 0x004c9050, TSL 0x0072aaa0. Omit OK.
    ifo.vo_id = root.acquire("Mod_VO_ID", "")
    # Mod_Name CExoLocString: default empty. K1/TSL LoadModuleStart ReadFieldCExoLocString. Omit OK.
    ifo.mod_name = root.acquire("Mod_Name", LocalizedString.from_invalid())
    # Mod_Tag CExoString: default "". K1/TSL ReadFieldCExoString; ref K1 swkotor.exe:0x00745c3c. Omit OK.
    ifo.tag = root.acquire("Mod_Tag", "")
    # Mod_Entry_Area CResRef: default blank. K1/TSL ReadFieldCResRef. Omit OK.
    ifo.resref = root.acquire("Mod_Entry_Area", ResRef.from_blank())
    # Mod_Entry_X/Y/Z FLOAT: default 0.0. K1 0x004c9050, TSL 0x0072aaa0 ReadFieldFLOAT. Omit OK.
    ifo.entry_position.x = root.acquire("Mod_Entry_X", 0.0)
    ifo.entry_position.y = root.acquire("Mod_Entry_Y", 0.0)
    ifo.entry_position.z = root.acquire("Mod_Entry_Z", 0.0)
    # Mod_On* script hooks CResRef: default blank. K1/TSL LoadModuleStart ReadFieldCResRef. Omit OK.
    ifo.on_heartbeat = root.acquire("Mod_OnHeartbeat", ResRef.from_blank())
    ifo.on_load = root.acquire("Mod_OnModLoad", ResRef.from_blank())
    ifo.on_start = root.acquire("Mod_OnModStart", ResRef.from_blank())
    ifo.on_enter = root.acquire("Mod_OnClientEntr", ResRef.from_blank())
    ifo.on_leave = root.acquire("Mod_OnClientLeav", ResRef.from_blank())
    ifo.on_activate_item = root.acquire("Mod_OnActvtItem", ResRef.from_blank())
    ifo.on_acquire_item = root.acquire("Mod_OnAcquirItem", ResRef.from_blank())
    ifo.on_user_defined = root.acquire("Mod_OnUsrDefined", ResRef.from_blank())
    ifo.on_unacquire_item = root.acquire("Mod_OnUnAqreItem", ResRef.from_blank())
    ifo.on_player_death = root.acquire("Mod_OnPlrDeath", ResRef.from_blank())
    ifo.on_player_dying = root.acquire("Mod_OnPlrDying", ResRef.from_blank())
    ifo.on_player_levelup = root.acquire("Mod_OnPlrLvlUp", ResRef.from_blank())
    ifo.on_player_respawn = root.acquire("Mod_OnSpawnBtnDn", ResRef.from_blank())
    # Expansion_Pack, Mod_Hak, Mod_Description, Mod_OnPlrRest, time: 0/""/empty. K1 0x004c9050, TSL 0x0072aaa0. Omit OK.
    ifo.expansion_id = root.acquire("Expansion_Pack", 0)
    ifo.hak = root.acquire("Mod_Hak", "")
    ifo.description = root.acquire("Mod_Description", LocalizedString.from_invalid())
    ifo.on_player_rest = root.acquire("Mod_OnPlrRest", ResRef.from_blank())
    ifo.dawn_hour = root.acquire("Mod_DawnHour", 0)
    ifo.dusk_hour = root.acquire("Mod_DuskHour", 0)
    ifo.time_scale = root.acquire("Mod_MinPerHour", 0)
    ifo.start_month = root.acquire("Mod_StartMonth", 0)
    ifo.start_day = root.acquire("Mod_StartDay", 0)
    ifo.start_hour = root.acquire("Mod_StartHour", 0)
    ifo.start_year = root.acquire("Mod_StartYear", 0)
    ifo.xp_scale = root.acquire("Mod_XPScale", 0)
    ifo.start_movie = root.acquire("Mod_StartMovie", ResRef.from_blank())
    # Mod_Creator_ID INT 0, Mod_Version DWORD 0. K1/TSL LoadModuleStart. Omit OK.
    ifo.creator_id = root.acquire("Mod_Creator_ID", 0)
    ifo.version = root.acquire("Mod_Version", 0)

    # Mod_Entry_Dir_X/Y FLOAT 0.0 when missing; engine fallback dir_x=1.0 dir_y=0.0. K1 0x004c9050, TSL 0x0072aaa0. Omit OK.
    dir_x = root.acquire("Mod_Entry_Dir_X", 0.0)
    dir_y = root.acquire("Mod_Entry_Dir_Y", 0.0)
    ifo.entry_direction = Vector2(dir_x, dir_y).angle()

    # Mod_Area_list: first struct Area_Name CResRef default blank. K1/TSL LoadModuleStart GetList Mod_Area_list. Omit OK.
    area_list = root.acquire("Mod_Area_list", GFFList())
    if area_list and len(area_list) > 0:
        first_area = area_list.at(0)
        if first_area:
            ifo.area_name = first_area.acquire("Area_Name", ResRef.from_blank())

    return ifo


def dismantle_ifo(
    ifo: IFO,
    game: Game = Game.K2,
    *,
    use_deprecated: bool = True,
) -> GFF:
    """Build IFO GFF from IFO. Write values match engine read defaults (K1 0x004c9050, TSL 0x0072aaa0).

    Reference functions: same as construct_ifo (LoadModuleStart/MainLoop K1 0x004c9050/0x004babb0,
    TSL 0x0072aaa0/0x007b6bb0). Mod_IsSaveGame BYTE 0 when omitted; Mod_Area_list at least one struct.
    """
    gff = GFF(GFFContent.IFO)
    root = gff.root

    # Mod_ID, Mod_VO_ID, Mod_Name, Mod_Tag: engine default empty/"". K1 0x004c9050, TSL 0x0072aaa0. Mod_IsSaveGame BYTE 0.
    root.set_binary("Mod_ID", ifo.mod_id)
    root.set_string("Mod_VO_ID", ifo.vo_id)
    root.set_locstring("Mod_Name", ifo.mod_name)
    root.set_string("Mod_Tag", ifo.tag)
    root.set_uint8("Mod_IsSaveGame", 0)
    root.set_resref("Mod_Entry_Area", ifo.resref)
    root.set_single("Mod_Entry_X", ifo.entry_position.x)
    root.set_single("Mod_Entry_Y", ifo.entry_position.y)
    root.set_single("Mod_Entry_Z", ifo.entry_position.z)
    root.set_resref("Mod_OnHeartbeat", ifo.on_heartbeat)
    root.set_resref("Mod_OnModLoad", ifo.on_load)
    root.set_resref("Mod_OnModStart", ifo.on_start)
    root.set_resref("Mod_OnClientEntr", ifo.on_enter)
    root.set_resref("Mod_OnClientLeav", ifo.on_leave)
    root.set_resref("Mod_OnActvtItem", ifo.on_activate_item)
    root.set_resref("Mod_OnAcquirItem", ifo.on_acquire_item)
    root.set_resref("Mod_OnUsrDefined", ifo.on_user_defined)
    root.set_resref("Mod_OnUnAqreItem", ifo.on_unacquire_item)
    root.set_resref("Mod_OnPlrDeath", ifo.on_player_death)
    root.set_resref("Mod_OnPlrDying", ifo.on_player_dying)
    root.set_resref("Mod_OnPlrLvlUp", ifo.on_player_levelup)
    root.set_resref("Mod_OnSpawnBtnDn", ifo.on_player_respawn)

    # Mod_Entry_Dir_X/Y: engine default 0.0; fallback 1.0,0.0. K1 LoadModuleStart 0x004c9050, TSL 0x0072aaa0.
    entry_direction = Vector2.from_angle(ifo.entry_direction)
    root.set_single("Mod_Entry_Dir_X", entry_direction.x)
    root.set_single("Mod_Entry_Dir_Y", entry_direction.y)

    # Mod_Area_list: at least one struct with Area_Name. K1/TSL LoadModuleStart GetList; we write one entry.
    root.set_list("Mod_Area_list", GFFList()).add(6).set_resref("Area_Name", ifo.area_name)

    # Deprecated/time/creator/version/lists: engine defaults 0/""/empty. K1/TSL LoadModuleStart; optional when use_deprecated.
    if use_deprecated:
        root.set_uint16("Expansion_Pack", ifo.expansion_id)
        root.set_string("Mod_Hak", ifo.hak)
        root.set_locstring("Mod_Description", ifo.description)
        root.set_resref("Mod_OnPlrRest", ifo.on_player_rest)
        root.set_uint8("Mod_DawnHour", ifo.dawn_hour)
        root.set_uint8("Mod_DuskHour", ifo.dusk_hour)
        root.set_uint8("Mod_MinPerHour", ifo.time_scale)
        root.set_uint8("Mod_StartMonth", ifo.start_month)
        root.set_uint8("Mod_StartDay", ifo.start_day)
        root.set_uint8("Mod_StartHour", ifo.start_hour)
        root.set_uint32("Mod_StartYear", ifo.start_year)
        root.set_uint8("Mod_XPScale", ifo.xp_scale)
        root.set_resref("Mod_StartMovie", ifo.start_movie)
        root.set_int32("Mod_Creator_ID", ifo.creator_id)
        root.set_uint32("Mod_Version", ifo.version)
        root.set_list("Mod_GVar_List", GFFList())
        root.set_list("Mod_Expan_List", GFFList())
        root.set_list("Mod_CutSceneList", GFFList())

    return gff


def read_ifo(
    source: SOURCE_TYPES,
    offset: int = 0,
    size: int | None = None,
) -> IFO:
    gff = read_gff(source, offset, size)
    return construct_ifo(gff)


def write_ifo(
    ifo: IFO,
    target: TARGET_TYPES,
    game: Game = Game.K2,
    file_format: ResourceType = ResourceType.GFF,
    *,
    use_deprecated: bool = True,
):
    gff = dismantle_ifo(ifo, game, use_deprecated=use_deprecated)
    write_gff(gff, target, file_format)


def bytes_ifo(
    ifo: IFO,
    game: Game = Game.K2,
    file_format: ResourceType = ResourceType.GFF,
    *,
    use_deprecated: bool = True,
) -> bytes:
    gff = dismantle_ifo(ifo, game, use_deprecated=use_deprecated)
    return bytes_gff(gff, file_format)
