"""Enhanced Metafile (EMF) blob builder used for vector fallbacks."""

from __future__ import annotations

import struct
from dataclasses import dataclass
from enum import IntEnum
from typing import Dict, Iterable, List, Optional, Sequence, Tuple

from tokenmoulds.design.units import EMU_PER_INCH


@dataclass(frozen=True)
class DashPattern:
    """Normalised dash pattern expressed in EMUs."""

    pattern: Tuple[float, ...]
    offset: float = 0.0

    def is_solid(self) -> bool:
        return not self.pattern or all(value <= 0 for value in self.pattern)


class EMFRecordType(IntEnum):
    """EMF record identifiers used when serialising records."""

    EMR_HEADER = 1
    EMR_POLYBEZIER = 2
    EMR_POLYGON = 3
    EMR_POLYLINE = 4
    EMR_POLYBEZIERTO = 5
    EMR_POLYLINETO = 6
    EMR_POLYPOLYGON = 8
    EMR_POLYPOLYLINE = 7
    EMR_SETPOLYFILLMODE = 19
    EMR_SETWORLDTRANSFORM = 35
    EMR_SAVEDC = 33
    EMR_RESTOREDC = 34
    EMR_INTERSECTCLIPRECT = 30
    EMR_BEGINPATH = 59
    EMR_ENDPATH = 60
    EMR_CLOSEFIGURE = 61
    EMR_FILLPATH = 62
    EMR_STROKEPATH = 64
    EMR_SELECTOBJECT = 37
    EMR_CREATEPEN = 38
    EMR_CREATEBRUSHINDIRECT = 39
    EMR_DELETEOBJECT = 40
    EMR_RECTANGLE = 43
    EMR_FILLRGN = 71
    EMR_CREATEDIBPATTERNBRUSHPT = 94
    EMR_STRETCHDIBITS = 65
    EMR_EOF = 14


class EMFBrushStyle(IntEnum):
    """Brush styles supported by the builder."""

    BS_SOLID = 0
    BS_NULL = 1
    BS_HATCHED = 2
    BS_PATTERN = 3
    BS_INDEXED = 4
    BS_DIBPATTERN = 5
    BS_DIBPATTERNPT = 6
    BS_PATTERN8X8 = 7
    BS_DIBPATTERN8X8 = 8
    BS_MONOPATTERN = 9


class EMFHatchStyle(IntEnum):
    """Hatch styles understood by PowerPoint."""

    HS_HORIZONTAL = 0
    HS_VERTICAL = 1
    HS_FDIAGONAL = 2
    HS_BDIAGONAL = 3
    HS_CROSS = 4
    HS_DIAGCROSS = 5


@dataclass(frozen=True)
class BrushSpec:
    color: int


@dataclass(frozen=True)
class PenSpec:
    color: int
    width_px: int
    line_cap: int = 0
    line_join: int = 0
    pen_style: int = 0


NULL_BRUSH_HANDLE = 0x80000005
NULL_PEN_HANDLE = 0x80000008
HMM_PER_EMU = 2540 / EMU_PER_INCH  # hundredth of millimetres per EMU


def apply_dash_pattern(
    points: Sequence[Tuple[float, float]],
    pattern: DashPattern | None,
) -> List[List[Tuple[float, float]]]:
    """Break a polyline into dash segments."""

    if not points or len(points) < 2:
        return []
    if pattern is None or pattern.is_solid():
        return [list(points)]

    segments: List[List[Tuple[float, float]]] = []
    pattern_values = [abs(value) for value in pattern.pattern if value > 0]
    if not pattern_values:
        return [list(points)]

    pattern_length = sum(pattern_values)
    dash_index = 0
    dash_remaining = pattern_values[dash_index]
    offset = pattern.offset % pattern_length
    while offset > 0:
        if offset < dash_remaining:
            dash_remaining -= offset
            offset = 0
        else:
            offset -= dash_remaining
            dash_index = (dash_index + 1) % len(pattern_values)
            dash_remaining = pattern_values[dash_index]

    drawing = bool(dash_index % 2 == 0)
    current_segment: List[Tuple[float, float]] = []

    for start, end in zip(points[:-1], points[1:]):
        sx, sy = start
        ex, ey = end
        dx = ex - sx
        dy = ey - sy
        segment_length = (dx * dx + dy * dy) ** 0.5
        if segment_length == 0:
            continue

        consumed = 0.0
        while consumed < segment_length:
            step = min(dash_remaining, segment_length - consumed)
            ix = sx + dx * ((consumed + step) / segment_length)
            iy = sy + dy * ((consumed + step) / segment_length)

            if drawing:
                if not current_segment:
                    current_segment.append(
                        (
                            sx + dx * (consumed / segment_length),
                            sy + dy * (consumed / segment_length),
                        )
                    )
                current_segment.append((ix, iy))

            consumed += step
            dash_remaining -= step

            if dash_remaining <= 1e-6:
                if drawing and current_segment:
                    segments.append(current_segment)
                    current_segment = []

                dash_index = (dash_index + 1) % len(pattern_values)
                dash_remaining = pattern_values[dash_index]
                drawing = dash_index % 2 == 0

        sx, sy = ex, ey

    if drawing and current_segment:
        segments.append(current_segment)

    return [segment for segment in segments if len(segment) > 1]


class EMFBlob:
    """In-memory EMF builder that emits valid header + record streams."""

    def __init__(self, width_emu: int, height_emu: int, *, dpi: int = 96) -> None:
        if width_emu <= 0 or height_emu <= 0:
            raise ValueError("width_emu and height_emu must be positive integers")
        self.width_emu = int(width_emu)
        self.height_emu = int(height_emu)
        self.dpi = dpi
        self._records: list[bytes] = []
        self._handles: list[int] = []
        self._next_handle = 1
        self._brush_cache: Dict[BrushSpec, int] = {}
        self._pen_cache: Dict[PenSpec, int] = {}
        self._init_header()

    @staticmethod
    def _align4(value: int) -> int:
        return (value + 3) & ~3

    @staticmethod
    def _pad4(data: bytes) -> bytes:
        padding = (-len(data)) % 4
        if padding:
            data += b"\x00" * padding
        return data

    # ------------------------------------------------------------------
    # Public helpers
    # ------------------------------------------------------------------

    def create_solid_brush(self, rgb: int) -> int:
        """Create a solid brush; returns the EMF handle."""

        return self._create_brush(style=EMFBrushStyle.BS_SOLID, color=rgb, hatch=0)

    def create_null_brush(self) -> None:
        """Select the stock NULL brush."""

        self._append_record(
            EMFRecordType.EMR_SELECTOBJECT, struct.pack("<I", NULL_BRUSH_HANDLE)
        )

    def create_pen(
        self,
        rgb: int,
        width_px: int = 1,
        *,
        line_cap: int = 0,
        line_join: int = 0,
        pen_style: int = 0,
    ) -> int:
        """Create a cosmetic pen."""

        handle = self._allocate_handle()
        width_px = max(1, int(width_px))
        style = (
            (pen_style & 0x0000000F)
            | (line_cap & 0x00000F00)
            | (line_join & 0x0000F000)
        )
        payload = struct.pack("<I", handle) + struct.pack(
            "<IiiI", style, width_px, 0, rgb
        )
        self._append_record(EMFRecordType.EMR_CREATEPEN, payload)
        return handle

    def get_solid_brush(self, rgb: int) -> int:
        """Return a cached solid brush handle."""

        spec = BrushSpec(rgb)
        handle = self._brush_cache.get(spec)
        if handle is None:
            handle = self.create_solid_brush(rgb)
            self._brush_cache[spec] = handle
        return handle

    def create_dib_pattern_brush(self, bmp_bytes: bytes, *, usage: int = 0) -> int:
        """Create a DIB pattern brush from a BMP payload."""

        if not bmp_bytes.startswith(b"BM"):
            raise ValueError("bmp_bytes must be a little-endian BMP payload")

        data_index = struct.unpack_from("<I", bmp_bytes, 10)[0]
        header_size = struct.unpack_from("<I", bmp_bytes, 14)[0]
        bmi = bmp_bytes[14 : 14 + header_size]
        bits = bmp_bytes[data_index:]

        bmi_padded = self._pad4(bmi)
        bits_padded = self._pad4(bits)

        handle = self._allocate_handle()
        off_bmi = 8 + 6 * 4  # header + payload fields
        off_bits = off_bmi + len(bmi_padded)
        payload = struct.pack(
            "<IIIIII",
            handle,
            usage,
            off_bmi,
            len(bmi),
            off_bits,
            len(bits),
        )
        payload += bmi_padded + bits_padded
        self._append_record(EMFRecordType.EMR_CREATEDIBPATTERNBRUSHPT, payload)
        return handle

    def get_pen(
        self,
        rgb: int,
        width_px: int = 1,
        *,
        line_cap: int = 0,
        line_join: int = 0,
        pen_style: int = 0,
    ) -> int:
        """Return a cached pen handle."""

        spec = PenSpec(rgb, max(1, int(width_px)), line_cap, line_join, pen_style)
        handle = self._pen_cache.get(spec)
        if handle is None:
            handle = self.create_pen(
                rgb,
                spec.width_px,
                line_cap=spec.line_cap,
                line_join=spec.line_join,
                pen_style=spec.pen_style,
            )
            self._pen_cache[spec] = handle
        return handle

    def stroke_polyline(
        self,
        points: Sequence[Tuple[int, int]],
        *,
        pen_handle: Optional[int] = None,
        pen_color: Optional[int] = None,
        pen_width_px: int = 1,
        dash_pattern: DashPattern | None = None,
        line_cap: int = 0,
        line_join: int = 0,
        pen_style: int = 0,
    ) -> None:
        """Stroke a polyline, optionally applying a dash pattern."""

        if len(points) < 2:
            return
        if pen_handle is None:
            if pen_color is None:
                raise ValueError(
                    "pen_color is required when pen_handle is not supplied"
                )
            pen_handle = self.get_pen(
                pen_color,
                pen_width_px,
                line_cap=line_cap,
                line_join=line_join,
                pen_style=pen_style,
            )

        segments = [list(points)]
        if dash_pattern is not None and not dash_pattern.is_solid():
            float_points = [(float(x), float(y)) for x, y in points]
            segments = [
                [(int(round(px)), int(round(py))) for px, py in segment]
                for segment in apply_dash_pattern(float_points, dash_pattern)
            ]
            segments = [segment for segment in segments if len(segment) >= 2]

        if not segments:
            return

        self._select_brush(None)
        self._select_pen(pen_handle)
        for segment in segments:
            payload = _points_payload(self._ensure_int_points(segment))
            self._append_record(EMFRecordType.EMR_POLYLINE, payload)

    def select_object(self, handle: int | None) -> None:
        """Select an EMF object into the device context."""

        if handle is None:
            return
        self._append_record(EMFRecordType.EMR_SELECTOBJECT, struct.pack("<I", handle))

    def fill_rectangle(
        self, left: int, top: int, width: int, height: int, brush_handle: int | None
    ) -> None:
        """Fill rectangle bounds using the currently selected brush."""

        if brush_handle is None:
            self.create_null_brush()
        else:
            self.select_object(brush_handle)
        rect = struct.pack("<4l", left, top, left + width, top + height)
        self._append_record(EMFRecordType.EMR_RECTANGLE, rect)

    def finalize(self) -> bytes:
        """Serialise header + records into a single EMF byte stream."""

        self._append_record(EMFRecordType.EMR_EOF, struct.pack("<III", 0, 0, 0))
        total_size = sum(len(record) for record in self._records)
        record_count = len(self._records)

        header = bytearray(self._records[0])
        struct.pack_into("<I", header, 48, total_size)
        struct.pack_into("<I", header, 52, record_count)
        struct.pack_into("<H", header, 56, len(self._handles))
        self._records[0] = bytes(header)

        return b"".join(self._records)

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _create_brush(self, *, style: EMFBrushStyle, color: int, hatch: int) -> int:
        handle = self._allocate_handle()
        payload = struct.pack("<I", handle) + struct.pack(
            "<III", int(style), color, hatch
        )
        self._append_record(EMFRecordType.EMR_CREATEBRUSHINDIRECT, payload)
        return handle

    def _allocate_handle(self) -> int:
        handle = self._next_handle
        self._handles.append(handle)
        self._next_handle += 1
        return handle

    def _append_record(self, record_type: EMFRecordType, payload: bytes) -> None:
        size = 8 + len(payload)
        self._records.append(struct.pack("<II", int(record_type), size) + payload)

    def _select_pen(self, handle: int | None) -> None:
        if handle is None:
            self._append_record(
                EMFRecordType.EMR_SELECTOBJECT, struct.pack("<I", NULL_PEN_HANDLE)
            )
        else:
            self.select_object(handle)

    def _select_brush(self, handle: int | None) -> None:
        if handle is None:
            self._append_record(
                EMFRecordType.EMR_SELECTOBJECT, struct.pack("<I", NULL_BRUSH_HANDLE)
            )
        else:
            self.select_object(handle)

    @staticmethod
    def _ensure_int_points(
        points: Iterable[Tuple[int | float, int | float]],
    ) -> List[Tuple[int, int]]:
        return [(int(round(x)), int(round(y))) for x, y in points]

    def _init_header(self) -> None:
        width_hmm = int(round(self.width_emu * HMM_PER_EMU))
        height_hmm = int(round(self.height_emu * HMM_PER_EMU))
        width_px = max(1, int(round(self.width_emu / EMU_PER_INCH * self.dpi)))
        height_px = max(1, int(round(self.height_emu / EMU_PER_INCH * self.dpi)))
        width_mm = max(1, int(round(width_hmm / 100)))
        height_mm = max(1, int(round(height_hmm / 100)))

        header_size = 108
        payload = bytearray(header_size)
        struct.pack_into("<I", payload, 0, int(EMFRecordType.EMR_HEADER))
        struct.pack_into("<I", payload, 4, header_size)
        # rclBounds: bounding rectangle in device units (pixels)
        struct.pack_into("<4l", payload, 8, 0, 0, width_px, height_px)
        # rclFrame: bounding rectangle in 0.01mm
        struct.pack_into("<4l", payload, 24, 0, 0, width_hmm, height_hmm)
        struct.pack_into("<I", payload, 40, 0x464D4520)  # " EMF" signature
        struct.pack_into("<I", payload, 44, 0x00010000)  # version
        struct.pack_into("<I", payload, 48, 0)  # nBytes placeholder
        struct.pack_into("<I", payload, 52, 0)  # nRecords placeholder
        struct.pack_into("<H", payload, 56, 0)  # nHandles placeholder
        struct.pack_into("<H", payload, 58, 0)  # reserved
        struct.pack_into("<I", payload, 60, 0)  # nDescription
        struct.pack_into("<I", payload, 64, 0)  # offDescription
        struct.pack_into("<I", payload, 68, 0)  # nPalEntries
        # szlDevice: device size in pixels
        struct.pack_into("<II", payload, 72, width_px, height_px)
        # szlMillimeters: device size in millimetres
        struct.pack_into("<II", payload, 80, width_mm, height_mm)
        struct.pack_into("<I", payload, 88, 0)  # cbPixelFormat
        struct.pack_into("<I", payload, 92, 0)  # offPixelFormat
        struct.pack_into("<I", payload, 96, 0)  # bOpenGL
        # szlMicrometers: device size in micrometers
        struct.pack_into(
            "<II", payload, 100, max(1, width_hmm * 10), max(1, height_hmm * 10)
        )
        self._records.append(bytes(payload))


def _points_payload(points: Sequence[Tuple[int, int]]) -> bytes:
    """Build the bounds + count + point-array payload for polygon/polyline EMF records."""
    min_x, min_y, max_x, max_y = _bounds(points)
    bounds = struct.pack("<4l", min_x, min_y, max_x, max_y)
    count = len(points)
    data = b"".join(struct.pack("<ii", x, y) for x, y in points)
    return bounds + struct.pack("<I", count) + data


def _bounds(points: Sequence[Tuple[int, int]]) -> Tuple[int, int, int, int]:
    xs = [x for x, _ in points]
    ys = [y for _, y in points]
    return min(xs), min(ys), max(xs), max(ys)


__all__ = [
    "DashPattern",
    "apply_dash_pattern",
    "EMFBlob",
    "EMFRecordType",
    "EMFBrushStyle",
    "EMFHatchStyle",
    "NULL_BRUSH_HANDLE",
    "NULL_PEN_HANDLE",
]
