"""DXF output generator — AC1015+ DXF from PostProcessResult using ezdxf."""

from __future__ import annotations

import io
from typing import Literal

import ezdxf

from pipeline.postprocess import PathInfo, PostProcessResult

# DXF $INSUNITS codes per the HEADER variable specification
_INSUNITS_MAP: dict[str, int] = {
    "inches": 1,
    "mm": 4,
}

# $MEASUREMENT: 0 = Imperial (inches), 1 = Metric (mm)
_MEASUREMENT_MAP: dict[str, int] = {
    "inches": 0,
    "mm": 1,
}


def _add_path_to_msp(
    msp: ezdxf.layouts.BaseLayout,
    path: PathInfo,
    layer: str = "0",
    scale_factor: float = 1.0,
) -> None:
    """Add a single PathInfo as an LWPOLYLINE entity to the modelspace.

    Closed paths get the LWPOLYLINE close flag set.
    Islands are placed on a separate "ISLANDS" layer for downstream CAM tools.

    When *scale_factor* is not 1.0, all coordinates are multiplied by it
    to convert from pixel space to real-world units.
    """
    coords = path.simplified_coords
    if len(coords) < 2:
        return

    target_layer = "ISLANDS" if path.is_island else layer

    # LWPOLYLINE uses (x, y[, start_width, end_width, bulge]) tuples
    points = [(x * scale_factor, y * scale_factor) for x, y in coords]

    # Remove duplicate close point if the polyline close flag handles it
    if path.is_closed and len(points) > 1 and points[0] == points[-1]:
        points = points[:-1]

    msp.add_lwpolyline(
        points,
        dxfattribs={"layer": target_layer},
        close=path.is_closed,
    )


def generate_dxf(
    result: PostProcessResult,
    *,
    units: Literal["inches", "mm"] | None = None,
    scale_factor: float = 1.0,
    layer_map: dict[int, str] | None = None,
) -> bytes:
    """Generate an AC1015 (AutoCAD R2000) DXF file from post-processed path data.

    Creates LWPOLYLINE entities from simplified coordinate data. Islands (holes)
    are placed on an "ISLANDS" layer; outer contours on the default "0" layer.

    Args:
        result: PostProcessResult from the post-processing pipeline.
        units: Target unit system — ``'inches'`` or ``'mm'``.  When set, the
            ``$INSUNITS`` and ``$MEASUREMENT`` DXF header variables are written
            so downstream tools (Inkscape, LightBurn, AutoCAD) interpret
            coordinate values in the correct unit.
        scale_factor: Multiplier applied to every coordinate.  Use this to
            convert from pixel space to real-world units (e.g. 1/96 for
            96 PPI → inches, 25.4/96 for 96 PPI → mm).
        layer_map: Optional mapping of path index → DXF layer name.  Paths
            whose index is absent fall back to the default layer logic
            (``"ISLANDS"`` for islands, ``"0"`` for outer contours).

    Returns:
        DXF file content as bytes.
    """
    doc = ezdxf.new(dxfversion="R2000")  # AC1015

    # --- Unit metadata in DXF header ---
    if units is not None:
        doc.header["$INSUNITS"] = _INSUNITS_MAP[units]
        doc.header["$MEASUREMENT"] = _MEASUREMENT_MAP[units]

    msp = doc.modelspace()

    # Create ISLANDS layer for hole/island paths
    doc.layers.add("ISLANDS", color=1)  # color 1 = red in AutoCAD

    # Create any custom layers requested by layer_map
    if layer_map:
        for layer_name in set(layer_map.values()):
            if layer_name not in ("0", "ISLANDS"):
                doc.layers.add(layer_name)

    for idx, path in enumerate(result.paths):
        # Determine the base layer: explicit layer_map entry, or default logic
        if layer_map and idx in layer_map:
            base_layer = layer_map[idx]
        else:
            base_layer = "0"
        _add_path_to_msp(msp, path, layer=base_layer, scale_factor=scale_factor)

    # Write to string buffer, then encode to bytes
    stream = io.StringIO()
    doc.write(stream)
    return stream.getvalue().encode("utf-8")
