from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any


DEFAULT_TEXT_LAYER = {"x": 0.0, "y": 0.0, "scale": 0.3, "spacing": 0.02}
DEFAULT_TEXT_COLOR = [1.0, 1.0, 1.0, 1.0]
SURFACE_KEYS = {"leftDoor", "rightDoor", "hood"}


@dataclass(frozen=True)
class RenderedNumberplate:
    output_path: Path
    surface: str
    number: str
    background_path: Path
    font_path: Path | None
    text_layer: dict[str, float]
    text_color: list[float]
    rendered_text: bool


def coerce_text_layer(value: Any, fallback: dict[str, float] | None = None) -> dict[str, float]:
    source = value if isinstance(value, dict) else {}
    base = fallback if isinstance(fallback, dict) else DEFAULT_TEXT_LAYER

    def numeric(key: str, lo: float, hi: float) -> float:
        try:
            raw = source.get(key, base.get(key, DEFAULT_TEXT_LAYER[key]))
            number = float(raw)
        except Exception:
            number = float(DEFAULT_TEXT_LAYER[key])
        return max(lo, min(hi, number))

    return {
        "x": numeric("x", -1.0, 1.0),
        "y": numeric("y", -1.0, 1.0),
        "scale": numeric("scale", 0.05, 1.0),
        "spacing": numeric("spacing", -0.1, 0.5),
    }


def coerce_text_color(value: Any, fallback: list[float] | None = None) -> list[float]:
    source = value if isinstance(value, (list, tuple)) else fallback or DEFAULT_TEXT_COLOR
    result: list[float] = []
    for index in range(4):
        try:
            channel = float(source[index])
        except Exception:
            channel = float(DEFAULT_TEXT_COLOR[index])
        result.append(max(0.0, min(1.0, channel)))
    return result


def clean_number(value: Any, fallback: str = "250") -> str:
    text = re.sub(r"\D+", "", str(value or "")).strip()[:3]
    return text or re.sub(r"\D+", "", fallback)[:3] or "250"


def linear_channel_to_srgb(value: float) -> int:
    channel = max(0.0, min(1.0, float(value)))
    if channel <= 0.0031308:
        srgb = channel * 12.92
    else:
        srgb = 1.055 * (channel ** (1.0 / 2.4)) - 0.055
    return max(0, min(255, int(round(srgb * 255.0))))


def _alpha_runs(counts: list[int], threshold: int) -> list[dict[str, int]]:
    runs: list[dict[str, int]] = []
    start: int | None = None
    for index, count in enumerate(counts):
        if count > threshold:
            if start is None:
                start = index
        elif start is not None:
            runs.append({"start": start, "end": index - 1, "size": index - start})
            start = None
    if start is not None:
        runs.append({"start": start, "end": len(counts) - 1, "size": len(counts) - start})
    return runs


def fallback_glyph_boxes(width: int = 1024, height: int = 512) -> list[dict[str, int]]:
    cell_width = width / 8.0
    cell_height = height / 2.0
    boxes: list[dict[str, int]] = []
    for index in range(10):
        row = 0 if index < 8 else 1
        column = index if index < 8 else index - 8
        boxes.append(
            {
                "x": int(round(column * cell_width)),
                "y": int(round(row * cell_height)),
                "w": int(round(cell_width)),
                "h": int(round(cell_height)),
            }
        )
    return boxes


def detect_glyph_boxes(image: Any) -> list[dict[str, int]]:
    width, height = image.size
    source = image.convert("RGBA")
    alpha = source.getchannel("A").load()
    row_counts: list[int] = []
    for y in range(height):
        count = 0
        for x in range(width):
            if alpha[x, y] > 16:
                count += 1
        row_counts.append(count)
    rows = [
        row
        for row in _alpha_runs(row_counts, max(4, round(width * 0.006)))
        if row["size"] > height * 0.08
    ][:2]
    if len(rows) < 2:
        return fallback_glyph_boxes(width, height)
    boxes: list[dict[str, int]] = []
    for row_index, row in enumerate(rows):
        col_counts: list[int] = []
        for x in range(width):
            count = 0
            for y in range(row["start"], row["end"] + 1):
                if alpha[x, y] > 16:
                    count += 1
            col_counts.append(count)
        expected = 8 if row_index == 0 else 2
        cols = [
            col
            for col in _alpha_runs(col_counts, max(3, round(row["size"] * 0.025)))
            if col["size"] > width * 0.03
        ][:expected]
        boxes.extend(
            {"x": col["start"], "y": row["start"], "w": col["end"] - col["start"] + 1, "h": row["end"] - row["start"] + 1}
            for col in cols
        )
    return boxes if len(boxes) == 10 else fallback_glyph_boxes(width, height)


def render_numberplate_png(
    background_path: str | Path,
    output_path: str | Path,
    *,
    font_path: str | Path | None = None,
    number: str = "250",
    text_layer: dict[str, Any] | None = None,
    text_color: list[float] | tuple[float, ...] | None = None,
    surface: str = "",
) -> RenderedNumberplate:
    try:
        from PIL import Image
    except Exception as exc:
        raise RuntimeError("Pillow is required to render baked numberplate textures") from exc

    background = Path(background_path)
    output = Path(output_path)
    font = Path(font_path) if font_path else None
    layer = coerce_text_layer(text_layer)
    color = coerce_text_color(text_color)
    digits = clean_number(number)

    with Image.open(background) as background_image:
        canvas = background_image.convert("RGBA")
    rendered_text = False

    if font and font.exists() and digits:
        with Image.open(font) as font_image:
            atlas = font_image.convert("RGBA")
        boxes = detect_glyph_boxes(atlas)
        glyphs = [boxes[int(digit)] for digit in digits if digit.isdigit()]
        if len(glyphs) == len(digits):
            max_glyph_height = max((glyph["h"] for glyph in glyphs), default=1)
            target_height = canvas.height * layer["scale"]
            scale = target_height / max(1, max_glyph_height)
            spacing = canvas.height * layer["spacing"]
            total_width = sum(glyph["w"] * scale for glyph in glyphs) + spacing * max(0, len(glyphs) - 1)
            x = canvas.width * (0.5 + layer["x"]) - total_width * 0.5
            center_y = canvas.height * (0.53 + layer["y"])
            tint = (
                linear_channel_to_srgb(color[0]),
                linear_channel_to_srgb(color[1]),
                linear_channel_to_srgb(color[2]),
                max(0, min(255, int(round(color[3] * 255.0)))),
            )
            resample = getattr(getattr(Image, "Resampling", Image), "LANCZOS", Image.BICUBIC)
            for glyph in glyphs:
                crop = atlas.crop((glyph["x"], glyph["y"], glyph["x"] + glyph["w"], glyph["y"] + glyph["h"]))
                target_width = max(1, int(round(glyph["w"] * scale)))
                target_height_px = max(1, int(round(glyph["h"] * scale)))
                glyph_image = crop.resize((target_width, target_height_px), resample)
                tinted = Image.new("RGBA", glyph_image.size, tint)
                tinted.putalpha(glyph_image.getchannel("A"))
                canvas.alpha_composite(tinted, (int(round(x)), int(round(center_y - target_height_px * 0.5))))
                x += glyph["w"] * scale + spacing
            rendered_text = True

    output.parent.mkdir(parents=True, exist_ok=True)
    canvas.save(output, format="PNG")
    return RenderedNumberplate(
        output_path=output,
        surface=surface,
        number=digits,
        background_path=background,
        font_path=font,
        text_layer=layer,
        text_color=color,
        rendered_text=rendered_text,
    )
