#!/usr/bin/env python3
from __future__ import annotations

import json
import random
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple

from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics

try:
    import cv2
except ImportError as exc:
    raise SystemExit(
        "OpenCV ist erforderlich. Installiere es mit: pip install opencv-contrib-python"
    ) from exc


# =========================
# Header / Parameter
# =========================

PAGE_FORMAT = "A0"            # z.B. A0, A1, A2, A3, A4
ORIENTATION = "portrait"      # "portrait" oder "landscape"

NUM_ARUCOS = 60
ARUCO_SIZE_MM = 25.0
ARUCO_START_ID = 46          # erster Marker aus DICT_4X4_250
SEED = 223                    # Zufalls-Seed für reproduzierbare Verteilung

PAGE_BORDER_MARGIN_MM = 50.0  # Abstand aller Marker vom Seitenrand

FORBIDDEN_RECT_W_MM = 204.0
FORBIDDEN_RECT_H_MM = 1000.0
FORBIDDEN_RECT_MARGIN_MM = 30.0   # keine ArUcos innerhalb dieses Abstands

LINE_WIDTH_MM = 1.0           # Linienstärke des Rechtecks
TEXT_FONT = "Times-Roman"
TEXT_SIZE_PT = 8
TEXT_GAP_MM = 4.0

OUTPUT_BASENAME = f"A0_{NUM_ARUCOS}Arucos_{int(ARUCO_SIZE_MM)}mm_Seet{SEED}"


# =========================
# DIN-Formate
# =========================

DIN_SIZES_MM = {
    "A0": (841.0, 1189.0),
    "A1": (594.0, 841.0),
    "A2": (420.0, 594.0),
    "A3": (297.0, 420.0),
    "A4": (210.0, 297.0),
}


@dataclass(frozen=True)
class RectMM:
    x: float
    y: float
    w: float
    h: float

    def intersects(self, other: "RectMM") -> bool:
        return not (
            self.x + self.w <= other.x
            or other.x + other.w <= self.x
            or self.y + self.h <= other.y
            or other.y + other.h <= self.y
        )


def mm_to_pt(value_mm: float) -> float:
    return value_mm * mm


def get_page_size_mm(page_format: str, orientation: str) -> Tuple[float, float]:
    if page_format not in DIN_SIZES_MM:
        raise ValueError(f"Unbekanntes Format: {page_format}. Unterstützt: {sorted(DIN_SIZES_MM)}")

    w_mm, h_mm = DIN_SIZES_MM[page_format]
    if orientation.lower() == "portrait":
        return w_mm, h_mm
    if orientation.lower() == "landscape":
        return h_mm, w_mm
    raise ValueError("ORIENTATION muss 'portrait' oder 'landscape' sein.")


def centered_rect(page_w_mm: float, page_h_mm: float, rect_w_mm: float, rect_h_mm: float) -> RectMM:
    return RectMM(
        x=(page_w_mm - rect_w_mm) / 2.0,
        y=(page_h_mm - rect_h_mm) / 2.0,
        w=rect_w_mm,
        h=rect_h_mm,
    )


def expand_rect(rect: RectMM, margin_mm: float) -> RectMM:
    return RectMM(
        x=rect.x - margin_mm,
        y=rect.y - margin_mm,
        w=rect.w + 2.0 * margin_mm,
        h=rect.h + 2.0 * margin_mm,
    )


def get_aruco_dictionary():
    # DICT_4X4_250 hat IDs 0..249
    return cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)


def marker_module_pattern(marker_id: int) -> List[List[int]]:
    """
    Liefert ein 6x6-Raster (inkl. schwarzem Rand).
    1 = schwarz, 0 = weiss
    """
    aruco_dict = get_aruco_dictionary()
    img = cv2.aruco.generateImageMarker(aruco_dict, marker_id, 600)  # nur zum Abtasten
    modules = 6
    cell = img.shape[0] // modules

    pattern: List[List[int]] = []
    for r in range(modules):
        row: List[int] = []
        for c in range(modules):
            cy = int((r + 0.5) * cell)
            cx = int((c + 0.5) * cell)
            pixel = int(img[cy, cx])  # 0 = schwarz, 255 = weiss
            row.append(1 if pixel < 128 else 0)
        pattern.append(row)
    return pattern


def draw_aruco_vector(
    c: canvas.Canvas,
    x_mm: float,
    y_mm: float,
    size_mm: float,
    marker_id: int,
    page_h_mm: float,
) -> None:
    """
    Zeichnet den Marker als Vektor-Rechtecke.
    x_mm, y_mm = linke obere Ecke in mm.
    """
    pattern = marker_module_pattern(marker_id)
    modules = len(pattern)
    cell_mm = size_mm / modules

    for r in range(modules):
        for col in range(modules):
            if pattern[r][col] == 1:
                cell_x_mm = x_mm + col * cell_mm
                #cell_y_mm = y_mm + (modules - 1 - r) * cell_mm
                cell_y_mm = y_mm + r * cell_mm
                c.rect(
                    mm_to_pt(cell_x_mm),
                    mm_to_pt(page_h_mm - cell_y_mm - cell_mm),
                    mm_to_pt(cell_mm),
                    mm_to_pt(cell_mm),
                    stroke=0,
                    fill=1,
                )


def draw_aruco_label(
    c: canvas.Canvas,
    x_mm: float,
    y_mm: float,
    size_mm: float,
    page_h_mm: float,
    marker_id: int,
) -> None:
    c.setFont(TEXT_FONT, TEXT_SIZE_PT)
    text = str(marker_id)
    text_width_pt = pdfmetrics.stringWidth(text, TEXT_FONT, TEXT_SIZE_PT)
    text_x_pt = mm_to_pt(x_mm + size_mm / 2.0) - text_width_pt / 2.0
    font = pdfmetrics.getFont(TEXT_FONT)
    ascent_mm = (font.face.ascent / 1000.0) * TEXT_SIZE_PT * 0.352777777777778
    text_baseline_y_mm = y_mm + size_mm + TEXT_GAP_MM + ascent_mm
    c.drawString(text_x_pt, mm_to_pt(page_h_mm - text_baseline_y_mm), text)


def place_markers(
    page_w_mm: float,
    page_h_mm: float,
    num_markers: int,
    marker_size_mm: float,
    border_margin_mm: float,
    forbidden_area: RectMM,
    forbidden_margin_mm: float,
    seed: int,
) -> List[dict]:
    rng = random.Random(seed)
    placed: List[RectMM] = []
    result: List[dict] = []

    allowed_x_min = border_margin_mm
    allowed_y_min = border_margin_mm
    allowed_x_max = page_w_mm - border_margin_mm - marker_size_mm
    allowed_y_max = page_h_mm - border_margin_mm - marker_size_mm

    if allowed_x_max < allowed_x_min or allowed_y_max < allowed_y_min:
        raise RuntimeError("Das Poster ist zu klein für Randabstand und Markergrösse.")

    excluded = expand_rect(forbidden_area, forbidden_margin_mm)

    attempts = 0
    max_attempts = 200000

    while len(result) < num_markers and attempts < max_attempts:
        attempts += 1
        x = rng.uniform(allowed_x_min, allowed_x_max)
        y = rng.uniform(allowed_y_min, allowed_y_max)
        candidate = RectMM(x=x, y=y, w=marker_size_mm, h=marker_size_mm)

        if any(candidate.intersects(other) for other in placed):
            continue
        if candidate.intersects(excluded):
            continue

        placed.append(candidate)
        result.append(
            {
                "id": ARUCO_START_ID + len(result),
                "x_mm": round(x, 2),
                "y_mm": round(y, 2),
                "size_mm": marker_size_mm,
            }
        )

    if len(result) < num_markers:
        raise RuntimeError(
            f"Nicht alle Marker konnten platziert werden: {len(result)} von {num_markers} "
            f"(nach {attempts} Versuchen)."
        )

    return result


def main() -> None:
    page_w_mm, page_h_mm = get_page_size_mm(PAGE_FORMAT, ORIENTATION)

    if ARUCO_START_ID < 0 or ARUCO_START_ID + NUM_ARUCOS > 250:
        raise ValueError("ARUCO_START_ID + NUM_ARUCOS muss in den Bereich 0..249 von DICT_4X4_250 fallen.")

    forbidden_rect = centered_rect(page_w_mm, page_h_mm, FORBIDDEN_RECT_W_MM, FORBIDDEN_RECT_H_MM)

    # Neues lokales Koordinatensystem
    origin_x_mm = forbidden_rect.x + forbidden_rect.w - 90.0
    origin_y_mm = forbidden_rect.y

    placements = place_markers(
        page_w_mm=page_w_mm,
        page_h_mm=page_h_mm,
        num_markers=NUM_ARUCOS,
        marker_size_mm=ARUCO_SIZE_MM,
        border_margin_mm=PAGE_BORDER_MARGIN_MM,
        forbidden_area=forbidden_rect,
        forbidden_margin_mm=FORBIDDEN_RECT_MARGIN_MM,
        seed=SEED,
    )

    pdf_path = Path(f"{OUTPUT_BASENAME}.pdf")
    json_path = Path(f"{OUTPUT_BASENAME}.json")

    c = canvas.Canvas(str(pdf_path), pagesize=(mm_to_pt(page_w_mm), mm_to_pt(page_h_mm)))
    c.setTitle(pdf_path.stem)
    c.setAuthor("OpenAI")
    c.setSubject("A0 poster with random ArUco placement")

    # Weisser Hintergrund
    c.setFillColorRGB(1, 1, 1)
    c.rect(0, 0, mm_to_pt(page_w_mm), mm_to_pt(page_h_mm), stroke=0, fill=1)

    # Rechteck mit 1 mm schwarzer Linie
    c.setStrokeColorRGB(0, 0, 0)
    c.setLineWidth(mm_to_pt(LINE_WIDTH_MM))
    c.rect(
        mm_to_pt(forbidden_rect.x),
        mm_to_pt(page_h_mm - forbidden_rect.y - forbidden_rect.h),
        mm_to_pt(forbidden_rect.w),
        mm_to_pt(forbidden_rect.h),
        stroke=1,
        fill=0,
    )

    # Koordinatensystem

    ARROW_LEN_MM = 50.0

    # X-Achse (rot, nach unten)
    c.setStrokeColorRGB(1, 0, 0)
    c.setLineWidth(mm_to_pt(1.0))

    c.line(
        mm_to_pt(origin_x_mm),
        mm_to_pt(page_h_mm - origin_y_mm),
        mm_to_pt(origin_x_mm),
        mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)),
    )

    # Pfeilspitze
    c.line(
        mm_to_pt(origin_x_mm),
        mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)),
        mm_to_pt(origin_x_mm - 4),
        mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM - 4)),
    )
    c.line(
        mm_to_pt(origin_x_mm),
        mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM)),
        mm_to_pt(origin_x_mm + 4),
        mm_to_pt(page_h_mm - (origin_y_mm + ARROW_LEN_MM - 4)),
    )

    # Y-Achse (grün, nach rechts)
    c.setStrokeColorRGB(0, 0.7, 0)

    c.line(
        mm_to_pt(origin_x_mm),
        mm_to_pt(page_h_mm - origin_y_mm),
        mm_to_pt(origin_x_mm + ARROW_LEN_MM),
        mm_to_pt(page_h_mm - origin_y_mm),
    )

    # Pfeilspitze
    c.line(
        mm_to_pt(origin_x_mm + ARROW_LEN_MM),
        mm_to_pt(page_h_mm - origin_y_mm),
        mm_to_pt(origin_x_mm + ARROW_LEN_MM - 4),
        mm_to_pt(page_h_mm - origin_y_mm - 4),
    )
    c.line(
        mm_to_pt(origin_x_mm + ARROW_LEN_MM),
        mm_to_pt(page_h_mm - origin_y_mm),
        mm_to_pt(origin_x_mm + ARROW_LEN_MM - 4),
        mm_to_pt(page_h_mm - origin_y_mm + 4),
    )

    c.setStrokeColorRGB(0, 0, 0)

    # ArUcos zeichnen
    c.setFillColorRGB(0, 0, 0)
    for item in placements:
        draw_aruco_vector(
            c=c,
            x_mm=item["x_mm"],
            y_mm=item["y_mm"],
            size_mm=item["size_mm"],
            marker_id=item["id"],
            page_h_mm=page_h_mm,
        )
        draw_aruco_label(
            c=c,
            x_mm=item["x_mm"],
            y_mm=item["y_mm"],
            size_mm=item["size_mm"],
            page_h_mm=page_h_mm,
            marker_id=item["id"],
        )

    c.showPage()
    c.save()

    # JSON mit Positionen
    with json_path.open("w", encoding="utf-8") as f:
        meta = {
            "page_format": PAGE_FORMAT,
            "orientation": ORIENTATION,
            "page_size_mm": {"width": page_w_mm, "height": page_h_mm},
            "seed": SEED,
            "num_arucos": NUM_ARUCOS,
            "aruco_size_mm": ARUCO_SIZE_MM,
            "aruco_dictionary": "DICT_4X4_250",
            "aruco_start_id": ARUCO_START_ID,
            "page_border_margin_mm": PAGE_BORDER_MARGIN_MM,
            "forbidden_rectangle_mm": {
                "x": round(forbidden_rect.x, 2),
                "y": round(forbidden_rect.y, 2),
                "w": forbidden_rect.w,
                "h": forbidden_rect.h,
            },
            "forbidden_rectangle_margin_mm": FORBIDDEN_RECT_MARGIN_MM,
        }

        f.write(json.dumps(meta, indent=2, ensure_ascii=False)[:-2])
        f.write(',\n  "placements": [\n')

        for index, p in enumerate(placements):
            item = {
                "id": p["id"],
                "position": [
                    round((p["y_mm"] + ARUCO_SIZE_MM / 2) - origin_y_mm, 2),
                    -1*round(origin_x_mm - (p["x_mm"] + ARUCO_SIZE_MM / 2), 2),
                    -27.3,
                ],
                "normal": [0, 0, 1],
                "spin": 90,
            }
            line = json.dumps(item, ensure_ascii=False)
            if index < len(placements) - 1:
                line += ","
            f.write(f"    {line}\n")

        f.write("  ]\n}\n")

    print(f"PDF geschrieben:  {pdf_path.resolve()}")
    print(f"JSON geschrieben:  {json_path.resolve()}")


if __name__ == "__main__":
    main()