"""Icon handler for Resource Hacker.

This module provides functionality to handle icon resources including
ICO file parsing, creation, and conversion between different formats.
"""

from __future__ import annotations

import logging
import struct
from dataclasses import dataclass
from pathlib import Path

from PIL import Image

# Handle ImageQt import
try:
    from PIL import ImageQt
except ImportError:
    ImageQt = None

logger = logging.getLogger(__name__)

# ICO file format constants
ICO_HEADER_SIZE = 6
ICO_DIR_ENTRY_SIZE = 16
ICO_TYPE_ICON = 1
ICO_TYPE_CURSOR = 2


@dataclass
class IconEntry:
    """Represents a single icon entry in an ICO file."""

    width: int
    height: int
    color_count: int
    planes: int
    bit_count: int
    size: int
    offset: int


@dataclass
class ICOFile:
    """Represents an ICO file structure."""

    entries: list[IconEntry]
    image_data: list[bytes]

    def __post_init__(self) -> None:
        """Initialize default empty lists."""
        if self.entries is None:
            self.entries = []
        if self.image_data is None:
            self.image_data = []

    def add_entry(self, entry: IconEntry, data: bytes) -> None:
        """Add an icon entry with its image data."""
        self.entries.append(entry)
        self.image_data.append(data)

    def get_largest_icon(self) -> tuple[IconEntry, bytes] | None:
        """Get the largest icon entry and its data."""
        if not self.entries:
            return None

        # Find entry with largest dimensions
        largest_idx = 0
        largest_size = self.entries[0].width * self.entries[0].height

        for i, entry in enumerate(self.entries[1:], 1):
            size = entry.width * entry.height
            if size > largest_size:
                largest_size = size
                largest_idx = i

        return self.entries[largest_idx], self.image_data[largest_idx]

    def get_all_sizes(self) -> list[tuple[int, int]]:
        """Get all icon sizes."""
        return [(entry.width, entry.height) for entry in self.entries]


class IconHandler:
    """Handler for icon resource operations."""

    @staticmethod
    def parse_ico_file(ico_path: Path) -> ICOFile | None:
        """Parse an ICO file and extract its structure.

        Args:
            ico_path: Path to the ICO file

        Returns
        -------
            ICOFile object if parsing successful, None otherwise
        """
        try:
            with open(ico_path, "rb") as f:
                # Read ICO header
                header_data = f.read(ICO_HEADER_SIZE)
                if len(header_data) < ICO_HEADER_SIZE:
                    logger.error("Invalid ICO file: too short")
                    return None

                reserved, ico_type, count = struct.unpack("<HHH", header_data)

                if reserved != 0:
                    logger.error("Invalid ICO file: reserved field not zero")
                    return None

                if ico_type != ICO_TYPE_ICON:
                    logger.error(f"Invalid ICO type: {ico_type}")
                    return None

                if count == 0:
                    logger.error("ICO file contains no icons")
                    return None

                ico_file = ICOFile()

                # Read directory entries
                for i in range(count):
                    entry_data = f.read(ICO_DIR_ENTRY_SIZE)
                    if len(entry_data) < ICO_DIR_ENTRY_SIZE:
                        logger.error(f"Invalid ICO file: incomplete entry {i}")
                        return None

                    (
                        width,
                        height,
                        color_count,
                        reserved,
                        planes,
                        bit_count,
                        size,
                        offset,
                    ) = struct.unpack("<BBBBHHII", entry_data)

                    # Convert width/height (0 means 256)
                    display_width = width if width > 0 else 256
                    display_height = height if height > 0 else 256

                    entry = IconEntry(
                        width=display_width,
                        height=display_height,
                        color_count=color_count,
                        planes=planes,
                        bit_count=bit_count,
                        size=size,
                        offset=offset,
                    )

                    # Read image data
                    current_pos = f.tell()
                    f.seek(offset)
                    image_data = f.read(size)
                    f.seek(current_pos)

                    ico_file.add_entry(entry, image_data)

                logger.info(f"Parsed ICO file with {count} icons")
                return ico_file

        except Exception as e:
            logger.error(f"Failed to parse ICO file {ico_path}: {e}")
            return None

    @staticmethod
    def create_ico_from_images(image_paths: list[Path], output_path: Path) -> bool:
        """Create an ICO file from multiple image files.

        Args:
            image_paths: List of image file paths
            output_path: Output ICO file path

        Returns
        -------
            bool: True if creation successful, False otherwise
        """
        try:
            images = []

            # Load and validate images
            for img_path in image_paths:
                try:
                    img = Image.open(img_path)
                    # Convert to RGBA if necessary
                    if img.mode != "RGBA":
                        img = img.convert("RGBA")
                    images.append(img)
                except Exception as e:
                    logger.error(f"Failed to load image {img_path}: {e}")
                    return False

            if not images:
                logger.error("No valid images provided")
                return False

            # Create ICO file
            return IconHandler._create_ico(images, output_path)

        except Exception as e:
            logger.error(f"Failed to create ICO file: {e}")
            return False

    @staticmethod
    def _create_ico(images: list[Image.Image], output_path: Path) -> bool:
        """Create ICO file from PIL images."""
        try:
            output_path.parent.mkdir(parents=True, exist_ok=True)

            with open(output_path, "wb") as f:
                # Write ICO header
                header = struct.pack("<HHH", 0, ICO_TYPE_ICON, len(images))
                f.write(header)

                # Calculate offsets
                dir_entry_offset = ICO_HEADER_SIZE
                image_data_offset = dir_entry_offset + (len(images) * ICO_DIR_ENTRY_SIZE)

                # Write directory entries and collect image data
                image_data_list = []

                for _i, img in enumerate(images):
                    # Resize image if necessary (ICO supports up to 256x256)
                    if img.width > 256 or img.height > 256:
                        img = img.resize((256, 256), Image.Resampling.LANCZOS)

                    # Convert to ICO format (BMP with alpha channel)
                    ico_data = IconHandler._image_to_ico_format(img)

                    # Write directory entry
                    width = img.width if img.width < 256 else 0
                    height = img.height if img.height < 256 else 0

                    dir_entry = struct.pack(
                        "<BBBBHHII",
                        width,  # bWidth
                        height,  # bHeight
                        0,  # bColorCount
                        0,  # bReserved
                        1,  # wPlanes
                        32,  # wBitCount
                        len(ico_data),  # dwBytesInRes
                        image_data_offset,
                    )  # dwImageOffset

                    f.write(dir_entry)
                    image_data_list.append(ico_data)
                    image_data_offset += len(ico_data)

                # Write image data
                for ico_data in image_data_list:
                    f.write(ico_data)

            logger.info(f"Created ICO file: {output_path}")
            return True

        except Exception as e:
            logger.error(f"Failed to write ICO file: {e}")
            return False

    @staticmethod
    def _image_to_ico_format(img: Image.Image) -> bytes:
        """Convert PIL image to ICO format (BMP with alpha channel)."""
        # ICO uses a special BMP format with alpha channel
        # This is a simplified implementation

        # Create BMP header for ICO
        width, height = img.size

        # ICO BMP format: height is doubled (contains XOR and AND masks)
        bmp_height = height * 2

        # Calculate row size (must be multiple of 4 bytes)
        row_size = ((width * 32 + 31) // 32) * 4
        image_size = row_size * bmp_height
        header_size = 40  # BITMAPINFOHEADER size

        # Create BMP header
        bmp_header = bytearray()
        bmp_header.extend(struct.pack("<I", header_size))  # biSize
        bmp_header.extend(struct.pack("<i", width))  # biWidth
        bmp_header.extend(struct.pack("<i", height * 2))  # biHeight (doubled)
        bmp_header.extend(struct.pack("<H", 1))  # biPlanes
        bmp_header.extend(struct.pack("<H", 32))  # biBitCount
        bmp_header.extend(struct.pack("<I", 0))  # biCompression (BI_RGB)
        bmp_header.extend(struct.pack("<I", image_size))  # biSizeImage
        bmp_header.extend(struct.pack("<i", 0))  # biXPelsPerMeter
        bmp_header.extend(struct.pack("<i", 0))  # biYPelsPerMeter
        bmp_header.extend(struct.pack("<I", 0))  # biClrUsed
        bmp_header.extend(struct.pack("<I", 0))  # biClrImportant

        # Convert image data
        rgba_data = img.tobytes()

        # Create XOR mask (actual image data)
        xor_mask = bytearray()
        for y in range(height - 1, -1, -1):  # BMP is bottom-up
            row_start = y * width * 4
            row_end = row_start + width * 4
            row_data = rgba_data[row_start:row_end]

            # Convert RGBA to BGRA (Windows BMP format)
            for i in range(0, len(row_data), 4):
                b, g, r, a = row_data[i : i + 4]
                xor_mask.extend([b, g, r, a])

            # Pad row to 4-byte boundary
            padding = (4 - (len(xor_mask) % 4)) % 4
            xor_mask.extend([0] * padding)

        # Create AND mask (all zeros for full opacity)
        and_mask_row_size = ((width + 31) // 32) * 4
        and_mask = bytearray(and_mask_row_size * height)

        # Combine header, XOR mask, and AND mask
        ico_data = bmp_header + xor_mask + and_mask
        return bytes(ico_data)

    @staticmethod
    def extract_icon_from_resource(resource_data: bytes) -> Image.Image | None:
        """Extract icon image from resource data.

        Args:
            resource_data: Raw icon resource data

        Returns
        -------
            PIL Image if extraction successful, None otherwise
        """
        try:
            logger.debug(f"Attempting to extract icon from {len(resource_data)} bytes of data")

            # Method 1: Try to parse as ICO format first
            if len(resource_data) >= ICO_HEADER_SIZE:
                try:
                    reserved, ico_type, count = struct.unpack("<HHH", resource_data[:6])
                    logger.debug(f"ICO header: reserved={reserved}, type={ico_type}, count={count}")

                    if reserved == 0 and ico_type == ICO_TYPE_ICON and count > 0:
                        # This is an ICO file, extract the largest icon
                        ico_file = IconHandler.parse_ico_from_bytes(resource_data)
                        if ico_file:
                            largest_entry = ico_file.get_largest_icon()
                            if largest_entry:
                                _, image_data = largest_entry
                                image = IconHandler._ico_data_to_image(image_data)
                                if image:
                                    logger.debug(f"Successfully extracted ICO image: {image.size}")
                                    return image
                except struct.error as e:
                    logger.debug(f"ICO header parsing failed: {e}")
                    pass

            # Method 2: Try to parse as raw BMP/PNG/JPEG data
            logger.debug("Trying direct image format parsing")
            image = IconHandler._direct_image_parse(resource_data)
            if image:
                logger.debug(f"Successfully parsed as direct image: {image.size}")
                return image

            # Method 3: Try to parse as raw BMP data with header reconstruction
            logger.debug("Trying BMP data parsing")
            image = IconHandler._bmp_data_to_image(resource_data)
            if image:
                logger.debug(f"Successfully parsed as BMP: {image.size}")
                return image

            logger.warning(f"All icon extraction methods failed for {len(resource_data)} bytes")
            return None

        except Exception as e:
            logger.error(f"Failed to extract icon from resource: {e}")
            return None

    @staticmethod
    def parse_ico_from_bytes(data: bytes) -> ICOFile | None:
        """Parse ICO data from bytes."""
        try:
            # Similar to parse_ico_file but from bytes
            if len(data) < ICO_HEADER_SIZE:
                return None

            reserved, ico_type, count = struct.unpack("<HHH", data[:6])

            if reserved != 0 or ico_type != ICO_TYPE_ICON or count == 0:
                return None

            ico_file = ICOFile()
            offset = ICO_HEADER_SIZE

            for _i in range(count):
                if offset + ICO_DIR_ENTRY_SIZE > len(data):
                    return None

                entry_data = data[offset : offset + ICO_DIR_ENTRY_SIZE]
                (
                    width,
                    height,
                    color_count,
                    reserved,
                    planes,
                    bit_count,
                    size,
                    image_offset,
                ) = struct.unpack("<BBBBHHII", entry_data)

                display_width = width if width > 0 else 256
                display_height = height if height > 0 else 256

                if image_offset + size > len(data):
                    return None

                image_data = data[image_offset : image_offset + size]

                entry = IconEntry(
                    width=display_width,
                    height=display_height,
                    color_count=color_count,
                    planes=planes,
                    bit_count=bit_count,
                    size=size,
                    offset=image_offset,
                )

                ico_file.add_entry(entry, image_data)
                offset += ICO_DIR_ENTRY_SIZE

            return ico_file

        except Exception:
            return None

    @staticmethod
    def _ico_data_to_image(data: bytes) -> Image.Image | None:
        """Convert ICO image data to PIL Image."""
        try:
            # This is a simplified implementation
            # Full ICO parsing is complex due to BMP format variations
            return Image.open(data)
        except Exception:
            return None

    @staticmethod
    def _direct_image_parse(data: bytes) -> Image.Image | None:
        """Try to parse data as common image formats directly."""
        try:
            logger.debug(f"Attempting direct image parse of {len(data)} bytes")

            # Validate input data
            if not data or len(data) == 0:
                logger.debug("Empty data provided")
                return None

            # Try common image formats
            formats_to_try = ["ICO", "PNG", "JPEG", "BMP", "GIF"]

            for format_name in formats_to_try:
                try:
                    # Create a BytesIO object to simulate file reading
                    from io import BytesIO

                    image_stream = BytesIO(data)

                    # Open and verify the image
                    image = Image.open(image_stream)

                    # Verify the image is valid (this can cause truncation errors)
                    try:
                        image.verify()
                    except Exception as verify_error:
                        logger.debug(f"Verification failed for {format_name}: {verify_error}")
                        continue

                    # Reopen for actual use (verify() consumes the stream)
                    image_stream.seek(0)
                    image = Image.open(image_stream)

                    # Additional validation
                    if image.size[0] <= 0 or image.size[1] <= 0:
                        logger.debug(f"Invalid image dimensions for {format_name}: {image.size}")
                        continue

                    logger.debug(f"Successfully parsed as {format_name}: {image.size} {image.mode}")
                    return image
                except Exception as format_error:
                    logger.debug(f"Failed to parse as {format_name}: {format_error}")
                    continue

            logger.debug("All format attempts failed")
            return None
        except Exception as e:
            logger.error(f"Direct image parsing failed: {e}")
            return None

    @staticmethod
    def _bmp_data_to_image(data: bytes) -> Image.Image | None:
        """Convert BMP data to PIL Image."""
        try:
            return Image.open(data)
        except Exception:
            return None
