"""TPC/texture format detection and auto read/write dispatch (TPC, TGA, DDS, BMP)."""

from __future__ import annotations

import os
import struct

from typing import TYPE_CHECKING

from pykotor.common.stream import BinaryReader
from pykotor.resource.formats.tpc.io_bmp import TPCBMPWriter
from pykotor.resource.formats.tpc.io_dds import TPCDDSReader, TPCDDSWriter
from pykotor.resource.formats.tpc.io_tga import TPCTGAReader, TPCTGAWriter
from pykotor.resource.formats.tpc.io_tpc import TPCBinaryReader, TPCBinaryWriter
from pykotor.resource.formats.tpc.tpc_data import TPC
from pykotor.resource.type import ResourceType
from pykotor.tools.path import CaseAwarePath

if TYPE_CHECKING:
    from pykotor.resource.formats.tpc.tpc_data import TPC
    from pykotor.resource.type import SOURCE_TYPES, TARGET_TYPES


def detect_tpc(
    source: SOURCE_TYPES,
    offset: int = 0,
) -> ResourceType:
    """Returns what format the TPC data is believed to be in.

    This function performs a basic check and does not guarantee accuracy of the result or integrity of the data.

    Args:
    ----
        source: Source of the TPC data.
        offset: Offset into the source data.

    Raises:
    ------
        FileNotFoundError: If the file could not be found.
        IsADirectoryError: If the specified path is a directory (Unix-like systems only).
        PermissionError: If the file could not be accessed.

    Returns:
    -------
        The format of the TPC data.
    """

    def do_check(sample: bytes) -> ResourceType:
        if sample.startswith(b"DDS "):
            return ResourceType.DDS

        if len(sample) >= 20:
            # BioWare DDS header heuristic: width/height/bpp/datasize (uint32 LE)
            width, height, bpp, data_size = struct.unpack_from("<IIII", sample, 0)
            if 0 < width < 0x8000 and 0 < height < 0x8000 and bpp in (3, 4):
                expected = (width * height) // 2 if bpp == 3 else width * height
                if data_size == expected:
                    return ResourceType.DDS

        if len(sample) < 100:  # noqa: PLR2004
            return ResourceType.TGA

        for i in range(15, min(len(sample), 100)):
            if sample[i] != 0:
                return ResourceType.TGA
        return ResourceType.TPC

    if isinstance(source, (str, os.PathLike)):
        suffix = CaseAwarePath(source).suffix.lower()
        if suffix == ".dds":
            return ResourceType.DDS

    file_format: ResourceType = ResourceType.INVALID
    try:
        if isinstance(source, (str, os.PathLike)):
            with BinaryReader.from_file(source, offset) as reader:
                file_format = do_check(bytes(reader.read_bytes(128)))
        elif isinstance(source, (bytes, bytearray)):
            file_format = do_check(bytes(source[:128]))
        elif isinstance(source, BinaryReader):
            file_format = do_check(bytes(source.read_bytes(128)))
            source.skip(-128)
    except (FileNotFoundError, PermissionError, IsADirectoryError):
        raise
    except OSError:
        file_format = ResourceType.INVALID

    return file_format


def read_tpc(
    source: SOURCE_TYPES,
    offset: int = 0,
    size: int | None = None,
    txi_source: SOURCE_TYPES | None = None,
) -> TPC:
    """Returns an TPC instance from the source.

    The file format (TPC or TGA) is automatically determined before parsing the data.

    Args:
    ----
        source: The source of the data.
        offset: The byte offset of the file inside the data.
        size: Number of bytes to allowed to read from the stream. If not specified, uses the whole stream.
        txi_source: An optional source to the TXI data to use. If this is a filepath, it *must* exist on disk.

    Raises:
    ------
        FileNotFoundError: If the file could not be found.
        IsADirectoryError: If the specified path is a directory (Unix-like systems only).
        PermissionError: If the file could not be accessed.
        ValueError: If the file was corrupted or the format could not be determined.

    Returns:
    -------
        An TPC instance.
    """
    file_format: ResourceType = detect_tpc(source, offset)

    loaded_tpc: TPC
    if file_format == ResourceType.TPC:
        loaded_tpc = TPCBinaryReader(source, offset, size or 0).load()
    elif file_format == ResourceType.TGA:
        loaded_tpc = TPCTGAReader(source, offset, size or 0).load()
    elif file_format == ResourceType.DDS:
        loaded_tpc = TPCDDSReader(source, offset, size or 0).load()
    else:
        msg = "Failed to determine the format of the TPC/TGA file."
        raise ValueError(msg)
    if txi_source is None and isinstance(source, (os.PathLike, str)):
        txi_path = CaseAwarePath(source).with_suffix(".txi")
        if not txi_path.is_file():
            return loaded_tpc
        txi_source = txi_path
    elif isinstance(txi_source, (os.PathLike, str)):
        txi_path = CaseAwarePath(txi_source).with_suffix(".txi")
        if not txi_path.is_file():
            return loaded_tpc
        txi_source = txi_path

    if txi_source is None:
        return loaded_tpc
    with BinaryReader.from_auto(txi_source) as f:
        loaded_tpc.txi = f.read_all().decode(encoding="ascii", errors="ignore")
    return loaded_tpc


def write_tpc(
    tpc: TPC,
    target: TARGET_TYPES,
    file_format: ResourceType = ResourceType.TPC,
):
    """Writes the TPC data to the target location with the specified format (TPC, TGA or BMP).

    Args:
    ----
        tpc: The TPC file being written.
        target: The location to write the data to.
        file_format: The file format.

    Raises:
    ------
        IsADirectoryError: If the specified path is a directory (Unix-like systems only).
        PermissionError: If the file could not be written to the specified destination.
        ValueError: If the specified format was unsupported.
    """
    if file_format == ResourceType.TGA:
        TPCTGAWriter(tpc, target).write()
    elif file_format == ResourceType.BMP:
        TPCBMPWriter(tpc, target).write()
    elif file_format == ResourceType.DDS:
        TPCDDSWriter(tpc, target).write()
    elif file_format == ResourceType.TPC:
        TPCBinaryWriter(tpc, target).write()
    else:
        msg = "Unsupported format specified; use TPC, TGA, DDS or BMP."
        raise ValueError(msg)


def bytes_tpc(
    tpc: TPC,
    file_format: ResourceType = ResourceType.TPC,
) -> bytes:
    """Returns the TPC data in the specified format (TPC, TGA, DDS or BMP) as a bytes object.

    This is a convenience method that wraps the write_tpc() method.

    Args:
    ----
        tpc: The target TPC object.
        file_format: The file format.

    Raises:
    ------
        ValueError: If the specified format was unsupported.

    Returns:
    -------
        The TPC data.
    """
    data = bytearray()
    write_tpc(tpc, data, file_format)
    return bytes(data)
