"""Binary GFF (Generic File Format) read/write: V3.2 and common field types."""

from __future__ import annotations

import struct

from typing import TYPE_CHECKING, Any

from pykotor.common.misc import ResRef
from pykotor.common.stream import BinaryWriter
from pykotor.resource.formats.gff import GFF, GFFContent, GFFFieldType, GFFList, GFFStruct
from pykotor.resource.type import ResourceReader, ResourceWriter, autoclose

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

_COMPLEX_FIELD: set[GFFFieldType] = {
    GFFFieldType.UInt64,
    GFFFieldType.Int64,
    GFFFieldType.Double,
    GFFFieldType.String,
    GFFFieldType.ResRef,
    GFFFieldType.LocalizedString,
    GFFFieldType.Binary,
    GFFFieldType.Vector3,
    GFFFieldType.Vector4,
}


class GFFBinaryReader(ResourceReader):
    """Binary GFF file reader.

    Reads binary GFF (Generic File Format) files used throughout KotOR for structured data storage.
    Supports GFF V3.2 format. Note: V3.3, V4.0, and V4.1 are not currently supported.

    References:
    ----------
        Based on swkotor.exe GFF structure:
        - CResGFF::CreateGFFFile @ 0x00411260 - Creates new GFF file with file_type and version
          * Sets file_type from 4-character string (e.g., "UTI ", "DLG ", "ARE ")
          * Sets file_version from GFFVersion string "V3.2" @ 0x0073e2c8
          * Creates root struct with AddStruct(this, 0xffffffff)
        - CResGFF::WriteGFFFile @ 0x00413030 - Writes GFF data to file
          * Opens file with "wb" mode
          * Calls Pack() to prepare data
          * Calls WriteGFFData() to write binary format
        - CResGFF::WriteGFFData @ 0x004113d0 - Writes GFF header and data sections
          * Writes 0x38 byte header
          * Writes structs (12 bytes each)
          * Writes fields (12 bytes each)
          * Writes labels (16 bytes each)
          * Writes field_data, field_indices, list_indices
        - GFFVersion string "V3.2" @ 0x0073e2c8 - Hardcoded GFF version identifier
        - "gff" string @ 0x0074dd00 - GFF format identifier

    Missing Features:
    ----------------
        - GFF V3.3, V4.0, V4.1 support (xoreos-tools supports these, KotOR likely does not)
        - StrRef field type (reone supports this at gffreader.cpp:141-142, 199-204)
    """

    def __init__(
        self,
        source: SOURCE_TYPES,
        offset: int = 0,
        size: int = 0,
    ):
        super().__init__(source, offset, size)
        self._gff: GFF | None = None

        self._labels: list[str] = []
        self._field_data_offset: int = 0
        self._field_indices_offset: int = 0
        self._list_indices_offset: int = 0
        self._struct_offset: int = 0
        self._field_offset: int = 0

    @autoclose
    def load(self, *, auto_close: bool = True) -> GFF:  # noqa: FBT001, FBT002, ARG002
        self._gff = GFF()
        file_type = self._reader.read_string(4)
        file_version = self._reader.read_string(4)

        if not any(x for x in GFFContent if x.value == file_type):
            msg = "Not a valid binary GFF file."
            raise ValueError(msg)

        # REVERSE ENGINEERING FINDINGS (Based on swkotor.exe GFF structure):
        # The KOTOR engine's CResGFF::CreateGFFFile function does NOT accept a version parameter.
        # Instead, it uses a hardcoded global variable GFFVersion (0x0073e2c8) containing "V3.2".
        # The function copies this hardcoded version to the file header using:
        # - CExoString::CExoString(&local_14, GFFVersion_ptr);  // Copy "V3.2" string
        # - CExoString::operator[] to extract individual bytes [3],[2],[1],[0]
        # - *(uint *)this->header->file_version = CONCAT31(CONCAT21(CONCAT11(cVar1,cVar2),cVar3),cVar4);
        # This indicates little-endian storage of the hardcoded "V3.2" version as 4 bytes.
        # The engine appears hardcoded to only create V3.2 GFF files.
        #
        # NOTE: xoreos-tools supports V3.2, V3.3, V4.0, V4.1 (probably for other games/engines)

        if file_version != "V3.2":
            msg = "The GFF version of the file is unsupported."
            raise ValueError(msg)

        self._gff.content = GFFContent(file_type)

        # Read GFF header offsets and counts
        self._struct_offset = self._reader.read_uint32()
        self._reader.read_uint32()  # struct count
        self._field_offset = self._reader.read_uint32()
        self._reader.read_uint32()  # field count

        label_offset: int = self._reader.read_uint32()
        label_count: int = self._reader.read_uint32()

        self._field_data_offset = self._reader.read_uint32()
        self._reader.read_uint32()  # field data count
        self._field_indices_offset = self._reader.read_uint32()
        self._reader.read_uint32()  # field indices count
        self._list_indices_offset = self._reader.read_uint32()
        self._reader.read_uint32()  # list indices count

        # Read label array (16-byte null-terminated strings)
        self._labels.clear()
        self._reader.seek(label_offset)
        self._labels.extend(self._reader.read_string(16) for _ in range(label_count))
        self._load_struct(self._gff.root, 0)

        return self._gff

    def _load_struct(
        self,
        gff_struct: GFFStruct,
        struct_index: int,
    ):
        # Read struct header (12 bytes: struct_id, data/offset, field_count)
        self._reader.seek(self._struct_offset + struct_index * 12)
        struct_id, data, field_count = (
            self._reader.read_int32(),
            self._reader.read_uint32(),
            self._reader.read_uint32(),
        )

        gff_struct.struct_id = struct_id

        # Handle empty structs (field_count == 0), single field (field_count == 1), or multiple fields
        if field_count == 1:
            self._load_field(gff_struct, data)
        elif field_count > 1:
            # Read field indices array - batch read for efficiency
            self._reader.seek(self._field_indices_offset + data)
            if field_count > 0:
                indices_data = self._reader.read_bytes(field_count * 4)
                indices = list(struct.unpack(f"<{field_count}I", indices_data))
            else:
                indices = []
            # Optimize: batch read all field headers at once to reduce seeks
            self._load_fields_batch(gff_struct, indices)

    def _load_fields_batch(
        self,
        gff_struct: GFFStruct,
        field_indices: list[int],
    ):
        """Optimized batch loading of multiple fields to reduce seeks.

        Reads all field headers in one batch operation, then processes each field.
        This reduces seeks from N (one per field) to 1 (one batch read).
        """
        if not field_indices:
            return

        # Calculate the range of field offsets we need to read
        min_index = min(field_indices)
        max_index = max(field_indices)

        # Read all field headers in one batch (12 bytes per field header)
        batch_start = self._field_offset + min_index * 12
        batch_size = (max_index - min_index + 1) * 12
        self._reader.seek(batch_start)
        batch_data = self._reader.read_bytes(batch_size)

        # Optimize: process fields directly without intermediate list
        # Extract labels list reference once for faster access
        labels = self._labels
        for field_index in field_indices:
            # Calculate offset within batch
            offset_in_batch = (field_index - min_index) * 12
            # Parse field header: field_type (4), label_id (4), data/offset (4)
            # Use struct.unpack_from for better performance (avoids slice allocation)
            field_type_id, label_id, data_or_offset = struct.unpack_from("<III", batch_data, offset_in_batch)
            label = labels[label_id]
            self._load_field_value_by_id(gff_struct, field_type_id, label, data_or_offset)

    def _load_field(
        self,
        gff_struct: GFFStruct,
        field_index: int,
    ):
        # Read field header (12 bytes: field_type, label_index, data/offset)
        self._reader.seek(self._field_offset + field_index * 12)
        field_type_id = self._reader.read_uint32()
        label_id = self._reader.read_uint32()
        data_or_offset = self._reader.read_uint32()

        label = self._labels[label_id]
        # Optimize: use integer comparisons instead of enum lookups in hot path
        self._load_field_value_by_id(gff_struct, field_type_id, label, data_or_offset)

    def _load_field_value_by_id(
        self,
        gff_struct: GFFStruct,
        field_type_id: int,
        label: str,
        data_or_offset: int,
    ):
        """Load a field value using integer field type ID (optimized hot path).

        Uses integer comparisons instead of enum lookups for better performance.
        """
        # Handle complex fields (stored in field data section) vs simple fields (inline)
        # Use integer comparisons: UInt64=6, Int64=7, Double=9, String=10, ResRef=11,
        # LocalizedString=12, Binary=13, Vector4=16, Vector3=17
        if field_type_id in (6, 7, 9, 10, 11, 12, 13, 16, 17):  # _COMPLEX_FIELD values
            offset = data_or_offset  # relative to field data
            self._reader.seek(self._field_data_offset + offset)
            if field_type_id == 6:  # GFFFieldType.UInt64
                gff_struct.set_uint64(label, self._reader.read_uint64())
            elif field_type_id == 7:  # GFFFieldType.Int64
                gff_struct.set_int64(label, self._reader.read_int64())
            elif field_type_id == 9:  # GFFFieldType.Double
                gff_struct.set_double(label, self._reader.read_double())
            elif field_type_id == 10:  # GFFFieldType.String
                length = self._reader.read_uint32()
                gff_struct.set_string(label, self._reader.read_string(length))
            elif field_type_id == 11:  # GFFFieldType.ResRef
                length = self._reader.read_uint8()
                resref = ResRef(self._reader.read_string(length).strip())
                gff_struct.set_resref(label, resref)
            elif field_type_id == 12:  # GFFFieldType.LocalizedString
                # NOTE: reone warns if count > 1, but PyKotor reads all substrings
                gff_struct.set_locstring(label, self._reader.read_locstring())
            elif field_type_id == 13:  # GFFFieldType.Binary
                length = self._reader.read_uint32()
                gff_struct.set_binary(label, self._reader.read_bytes(length))
            elif field_type_id == 17:  # GFFFieldType.Vector3
                gff_struct.set_vector3(label, self._reader.read_vector3())
            elif field_type_id == 16:  # GFFFieldType.Vector4
                gff_struct.set_vector4(label, self._reader.read_vector4())
        elif field_type_id == 14:  # GFFFieldType.Struct
            struct_index = data_or_offset
            new_struct = GFFStruct()
            self._load_struct(new_struct, struct_index)
            gff_struct.set_struct(label, new_struct)
        elif field_type_id == 15:  # GFFFieldType.List
            self._load_list(gff_struct, label, data_or_offset)
        elif field_type_id == 0:  # GFFFieldType.UInt8
            # Inline: data is in the first byte of data_or_offset (little-endian)
            gff_struct.set_uint8(label, data_or_offset & 0xFF)
        elif field_type_id == 1:  # GFFFieldType.Int8
            # Inline: data is in the first byte, interpret as signed (Python int, not unsigned 32-bit)
            byte_value = data_or_offset & 0xFF
            int8_value = struct.unpack("<b", struct.pack("<B", byte_value))[0]
            gff_struct.set_int8(label, int8_value)
        elif field_type_id == 2:  # GFFFieldType.UInt16
            # Inline: data is in the first 2 bytes of data_or_offset (little-endian)
            gff_struct.set_uint16(label, data_or_offset & 0xFFFF)
        elif field_type_id == 3:  # GFFFieldType.Int16
            # Inline: data is in the first 2 bytes, interpret as signed (Python int, not unsigned 32-bit)
            word_value = data_or_offset & 0xFFFF
            int16_value = struct.unpack("<h", struct.pack("<H", word_value))[0]
            gff_struct.set_int16(label, int16_value)
        elif field_type_id == 4:  # GFFFieldType.UInt32
            # Inline: data is the full 4 bytes
            gff_struct.set_uint32(label, data_or_offset)
        elif field_type_id == 5:  # GFFFieldType.Int32
            # Inline: data is the full 4 bytes interpreted as signed
            # Use ctypes for faster conversion (avoids struct pack/unpack overhead)
            int32_value = struct.unpack("<i", struct.pack("<I", data_or_offset))[0]
            gff_struct.set_int32(label, int32_value)
        elif field_type_id == 8:  # GFFFieldType.Single
            # Inline: data is the full 4 bytes interpreted as float
            # Use struct.unpack directly on the 4-byte value
            float_value = struct.unpack("<f", struct.pack("<I", data_or_offset))[0]
            gff_struct.set_single(label, float_value)
        # NOTE: StrRef field type not supported (reone supports at gffreader.cpp:141-142, 199-204)

    def _load_list(self, gff_struct: GFFStruct, label: str, offset: int | None = None):
        if offset is None:
            offset = self._reader.read_uint32()  # relative to list indices
        self._reader.seek(self._list_indices_offset + offset)
        value = GFFList()
        count = self._reader.read_uint32()

        # Optimize: batch read all list indices at once instead of one-by-one
        if count > 0:
            indices_data = self._reader.read_bytes(count * 4)
            list_indices = list(struct.unpack(f"<{count}I", indices_data))
        else:
            list_indices = []

        for struct_index in list_indices:
            value.add(0)
            child: GFFStruct | None = value.at(len(value) - 1)
            if child is not None:
                self._load_struct(child, struct_index)
        gff_struct.set_list(label, value)


class GFFBinaryWriter(ResourceWriter):
    """Binary GFF file writer.

    Writes binary GFF (Generic File Format) files.

    Supports GFF V3.2 format.

    NOTE: V3.3, V4.0, V4.1 are NOT currently supported.

    Derived Implementations:
    ----------
        https://github.com/seedhartha/reone/tree/master/src/libs/resource/format/gffwriter.cpp:271 (header offset calculation)
        https://github.com/seedhartha/reone/tree/master/src/libs/resource/format/gffwriter.cpp:294-317 (struct/field/label array writing)
    """

    def __init__(
        self,
        gff: GFF,
        target: TARGET_TYPES,
    ):
        super().__init__(target)
        self._gff: GFF = gff

        self._struct_writer: BinaryWriter = BinaryWriter.to_bytearray()
        self._field_writer: BinaryWriter = BinaryWriter.to_bytearray()
        self._field_data_writer: BinaryWriter = BinaryWriter.to_bytearray()
        self._field_indices_writer: BinaryWriter = BinaryWriter.to_bytearray()
        self._list_indices_writer: BinaryWriter = BinaryWriter.to_bytearray()

        self._labels: list[str] = []
        self._label_to_index: dict[str, int] = {}  # O(1) label lookup when writing

        self._struct_count: int = 0
        self._field_count: int = 0

    @autoclose
    def write(self, *, auto_close: bool = True):  # noqa: FBT001, FBT002, ARG002  # pyright: ignore[reportUnusedParameters]
        self._build_struct(self._gff.root)

        # Header offset is 0x38 (56 bytes) - GFF signature (8) + offsets/counts (48)
        struct_offset = 56
        struct_count = self._struct_writer.size() // 12
        field_offset = struct_offset + self._struct_writer.size()
        field_count = self._field_writer.size() // 12
        label_offset = field_offset + self._field_writer.size()
        label_count = len(self._labels)
        field_data_offset = label_offset + len(self._labels) * 16
        field_data_count = self._field_data_writer.size()
        field_indices_offset = field_data_offset + self._field_data_writer.size()
        field_indices_count = self._field_indices_writer.size()
        list_indices_offset = field_indices_offset + self._field_indices_writer.size()
        list_indices_count = self._list_indices_writer.size()

        self._writer.write_string(self._gff.content.value)
        self._writer.write_string("V3.2")
        self._writer.write_uint32(struct_offset)
        self._writer.write_uint32(struct_count)
        self._writer.write_uint32(field_offset)
        self._writer.write_uint32(field_count)
        self._writer.write_uint32(label_offset)
        self._writer.write_uint32(label_count)
        self._writer.write_uint32(field_data_offset)
        self._writer.write_uint32(field_data_count)
        self._writer.write_uint32(field_indices_offset)
        self._writer.write_uint32(field_indices_count)
        self._writer.write_uint32(list_indices_offset)
        self._writer.write_uint32(list_indices_count)

        self._writer.write_bytes(self._struct_writer.data())
        self._writer.write_bytes(self._field_writer.data())
        for label in self._labels:
            self._writer.write_string(label, string_length=16)
        self._writer.write_bytes(self._field_data_writer.data())
        self._writer.write_bytes(self._field_indices_writer.data())
        self._writer.write_bytes(self._list_indices_writer.data())

    def _build_struct(
        self,
        gff_struct: GFFStruct,
    ):
        self._struct_count += 1
        struct_id = gff_struct.struct_id
        field_count = len(gff_struct)

        self._struct_writer.write_uint32(struct_id, max_neg1=True)

        # Handle empty structs (0xFFFFFFFF), single field (inline), or multiple fields (indices array)
        if field_count == 0:
            self._struct_writer.write_uint32(0xFFFFFFFF)
            self._struct_writer.write_uint32(0)
        elif field_count == 1:
            self._struct_writer.write_uint32(self._field_count)
            self._struct_writer.write_uint32(field_count)

            for label, field_type, value in gff_struct:
                self._build_field(label, value, field_type)
        elif field_count > 1:
            self._write_large_struct(field_count, gff_struct)

    def _write_large_struct(self, field_count: int, gff_struct: GFFStruct):
        self._struct_writer.write_uint32(self._field_indices_writer.size())
        self._struct_writer.write_uint32(field_count)

        self._field_indices_writer.end()
        pos = self._field_indices_writer.position()
        self._field_indices_writer.write_bytes(b"\x00\x00\x00\x00" * field_count)

        for i, (label, field_type, value) in enumerate(gff_struct):
            self._field_indices_writer.seek(pos + i * 4)
            self._field_indices_writer.write_uint32(self._field_count)
            self._build_field(label, value, field_type)

    def _build_list(
        self,
        gff_list: GFFList,
    ):
        self._list_indices_writer.end()
        self._list_indices_writer.write_uint32(len(gff_list))
        pos = self._list_indices_writer.position()
        self._list_indices_writer.write_bytes(b"\x00\x00\x00\x00" * len(gff_list))
        for i, gff_struct in enumerate(gff_list):
            self._list_indices_writer.seek(pos + i * 4)
            self._list_indices_writer.write_uint32(self._struct_count)
            self._build_struct(gff_struct)

    def _build_field(
        self,
        label: str,
        value: Any,
        field_type: GFFFieldType,
    ):
        self._field_count += 1
        field_type_id = field_type.value
        label_index = self._label_index(label)

        self._field_writer.write_uint32(field_type_id)
        self._field_writer.write_uint32(label_index)

        if field_type in _COMPLEX_FIELD:
            self._field_writer.write_uint32(self._field_data_writer.size())

            self._field_data_writer.end()
            if field_type == GFFFieldType.UInt64:
                self._field_data_writer.write_uint64(value)
            elif field_type == GFFFieldType.Int64:
                self._field_data_writer.write_int64(value)
            elif field_type == GFFFieldType.Double:
                self._field_data_writer.write_double(value)
            elif field_type == GFFFieldType.String:
                self._field_data_writer.write_string(value, prefix_length=4)
            elif field_type == GFFFieldType.ResRef:
                self._field_data_writer.write_string(str(value), prefix_length=1)
            elif field_type == GFFFieldType.LocalizedString:
                self._field_data_writer.write_locstring(value)
            elif field_type == GFFFieldType.Binary:
                self._field_data_writer.write_uint32(len(value))
                self._field_data_writer.write_bytes(value)
            elif field_type == GFFFieldType.Vector4:
                self._field_data_writer.write_vector4(value)
            elif field_type == GFFFieldType.Vector3:
                self._field_data_writer.write_vector3(value)
        elif field_type == GFFFieldType.Struct:
            self._field_writer.write_uint32(self._struct_count)
            self._build_struct(value)
        elif field_type == GFFFieldType.List:
            self._field_writer.write_uint32(self._list_indices_writer.size())
            self._build_list(value)
        elif field_type == GFFFieldType.UInt8:
            self._field_writer.write_uint32(value, max_neg1=True)
        elif field_type == GFFFieldType.Int8:
            self._field_writer.write_int32(value)
        elif field_type == GFFFieldType.UInt16:
            self._field_writer.write_uint32(value, max_neg1=True)
        elif field_type == GFFFieldType.Int16:
            self._field_writer.write_int32(value)
        elif field_type == GFFFieldType.UInt32:
            self._field_writer.write_uint32(value, max_neg1=True)
        elif field_type == GFFFieldType.Int32:
            self._field_writer.write_int32(value)
        elif field_type == GFFFieldType.Single:
            self._field_writer.write_single(value)
        else:
            msg = f"Unknown field type '{field_type}'"
            raise ValueError(msg)

    def _label_index(
        self,
        label: str,
    ) -> int:
        idx = self._label_to_index.get(label)
        if idx is not None:
            return idx
        self._labels.append(label)
        new_idx = len(self._labels) - 1
        self._label_to_index[label] = new_idx
        return new_idx
