import dataclasses
from typing import Tuple, Dict, List

import numpy as np
from PIL import Image


@dataclasses.dataclass
class DeviceSpec:
    model: str
    size: Tuple[int, int]
    mirror: bool = False
    rotation: bool = False
    second_color: bool = True
    tft: bool = False
    compression: bool = False
    max_voltage: float = 2.9
    min_voltage: float = 2.2

ModelId = int

DEVICE_SPECS: Dict[ModelId, DeviceSpec] = {
    0x0B: DeviceSpec(
        model="EPD 2.1\" BWR",
        size=(250, 122)
    ),
    0x33: DeviceSpec(
        model="EPD 2.9\" BWR",
        size=(296, 128),
        mirror=True,
        max_voltage=3.0
    ),
    0x4B: DeviceSpec(
        model="EPD 4.2\" BWR",
        size=(400, 300),
        rotation=True,
        max_voltage=3.0
    ),
    0x2B: DeviceSpec(
        model="EPD 7.5\" BWR",
        size=(800, 480),
        mirror=True,
        rotation=True,
        compression=True,
        max_voltage=3.0
    ),
    0xA0: DeviceSpec(
        model="TFT 2.1\" BW",
        size=(250, 132),
        second_color=False,
        tft=True
    )
}

def convert_to_gicisky_bytes(img: Image.Image, model: ModelId, lum_threshold: int = 128, red_threshold: int = 170) -> bytes:
    """
    Process image according to Gicisky device specifications.
    Usage with image.optimize is recommended.

    Args:
        img: PIL Image to process
        model: Device model identifier
        lum_threshold: Luminance threshold for black/white conversion
        red_threshold: Threshold for red color detection

    Returns:
        bytes: Processed image data in Gicisky format
    """
    # Get device specifications
    specs = DEVICE_SPECS.get(model)
    if not specs:
        raise ValueError("Unknown model")

    width, height = specs.size

    # Resize image to device dimensions
    img = img.convert("RGB").resize((width, height), Image.Resampling.LANCZOS)

    # Apply TFT transformation if enabled
    if specs.tft:
        # Resize to half width and double height
        img = img.resize((width // 2, height * 2), Image.Resampling.LANCZOS)
        width, height = img.size

    # Apply mirroring if enabled
    if specs.mirror:
        img = img.transpose(Image.FLIP_TOP_BOTTOM)

    # Convert to numpy array
    arr = np.array(img)

    # Process pixels - column-major order
    byte_data, red_byte_data = [], []
    current_byte, current_red_byte = 0, 0
    bit_position = 7

    for x in range(width):
        for y in range(height):
            r, g, b = arr[y, x]  # Note: numpy uses [y, x] indexing
            luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b

            # Apply thresholding based on compression setting
            if specs.compression:
                # When compression is enabled, dark pixels set bits
                if luminance < lum_threshold:
                    current_byte |= (1 << bit_position)
            else:
                # When compression is disabled, light pixels set bits
                if luminance > lum_threshold:
                    current_byte |= (1 << bit_position)

            # Red color detection
            if r > red_threshold and g < red_threshold:
                current_red_byte |= (1 << bit_position)

            bit_position -= 1
            if bit_position < 0:
                byte_data.append(current_byte)
                red_byte_data.append(current_red_byte)
                current_byte, current_red_byte = 0, 0
                bit_position = 7

    # Handle remaining bits
    if bit_position != 7:
        byte_data.append(current_byte)
        red_byte_data.append(current_red_byte)

    # Apply compression if enabled
    if specs.compression:
        byte_data_compressed = _apply_compression(byte_data, red_byte_data, width, height, specs.second_color)
    else:
        # Simple concatenation when compression is disabled
        byte_data_compressed = byte_data[:]
        if specs.second_color:
            byte_data_compressed.extend(red_byte_data)

    return bytes(byte_data_compressed)


def _apply_compression(byte_data: List[int], red_byte_data: List[int],
                       width: int, height: int, second_color: bool) -> List[int]:
    """
    Apply compression algorithm

    Args:
        byte_data: Black/white pixel data
        red_byte_data: Red pixel data
        width: Image width
        height: Image height
        second_color: Whether to include second color (red) data

    Returns:
        List[int]: Compressed byte data
    """
    byte_data_compressed = [0x00, 0x00, 0x00, 0x00]  # Header
    byte_per_line = height // 8
    current_pos = 0

    # Process black/white data
    for i in range(width):
        # Add line header
        byte_data_compressed.extend([
            0x75,
            byte_per_line + 7,
            byte_per_line,
            0x00,
            0x00,
            0x00,
            0x00
        ])

        # Add pixel data for this line
        for b in range(byte_per_line):
            if current_pos < len(byte_data):
                byte_data_compressed.append(byte_data[current_pos])
            else:
                byte_data_compressed.append(0x00)  # Padding
            current_pos += 1

    # Process red data if enabled
    if second_color:
        current_pos = 0
        for i in range(width):
            # Add line header
            byte_data_compressed.extend([
                0x75,
                byte_per_line + 7,
                byte_per_line,
                0x00,
                0x00,
                0x00,
                0x00
            ])

            # Add pixel data for this line
            for b in range(byte_per_line):
                if current_pos < len(red_byte_data):
                    byte_data_compressed.append(red_byte_data[current_pos])
                else:
                    byte_data_compressed.append(0x00)  # Padding
                current_pos += 1

    # Update header with total length
    total_length = len(byte_data_compressed)
    byte_data_compressed[0] = total_length & 0xff
    byte_data_compressed[1] = (total_length >> 8) & 0xff
    byte_data_compressed[2] = (total_length >> 16) & 0xff
    byte_data_compressed[3] = (total_length >> 24) & 0xff

    return byte_data_compressed
