"""Visualisation des couleurs utilisées dans l'inventaire filtré."""

from pathlib import Path
from typing import Dict, Iterable, List, Tuple

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D

from lib.filesystem import ensure_parent_dir
from lib.color_sort import lab_sort_key, sort_hex_colors_lab
from lib.rebrickable.color_ignores import is_ignored_color_rgb, is_ignored_part_category
from lib.rebrickable.parts_inventory import normalize_boolean
from lib.rebrickable.stats import read_rows


def sort_colors_perceptually(colors: Iterable[dict]) -> List[dict]:
    """Trie les couleurs via l'espace Lab (teinte perçue, chroma, luminosité)."""
    ordered_hex = sort_hex_colors_lab(color["color_rgb"] for color in colors)
    index_map = {hex_value: index for index, hex_value in enumerate(ordered_hex)}
    return sorted(colors, key=lambda color: index_map[color["color_rgb"]])


def load_part_categories(parts_catalog_path: Path) -> Dict[str, str]:
    """Indexe les catégories par part_num."""
    categories: Dict[str, str] = {}
    with parts_catalog_path.open() as catalog_file:
        import csv

        reader = csv.DictReader(catalog_file)
        for row in reader:
            categories[row["part_num"]] = row["part_cat_id"]
    return categories


def load_used_colors(
    parts_path: Path,
    colors_path: Path,
    parts_catalog_path: Path,
    minifig_only: bool = False,
) -> List[dict]:
    """Charge les couleurs utilisées (hors rechanges) et leurs quantités totales.

    Si minifig_only est vrai, ne conserve que les pièces marquées is_minifig_part=true.
    Sinon, exclut les pièces de minifig.
    """
    rows = read_rows(parts_path)
    categories = load_part_categories(parts_catalog_path)
    colors_lookup = {(row["rgb"], normalize_boolean(row["is_trans"])): row["name"] for row in read_rows(colors_path)}
    totals: Dict[Tuple[str, str], int] = {}
    for row in rows:
        if is_ignored_color_rgb(row["color_rgb"]):
            continue
        if is_ignored_part_category(categories[row["part_num"]]):
            continue
        if minifig_only and row.get("is_minifig_part") != "true":
            continue
        if not minifig_only and row.get("is_minifig_part") == "true":
            continue
        key = (row["color_rgb"], row["is_translucent"])
        totals[key] = totals.get(key, 0) + int(row["quantity_in_set"])
    used_colors = []
    for (color_rgb, is_translucent), quantity in totals.items():
        used_colors.append(
            {
                "color_rgb": color_rgb,
                "is_translucent": is_translucent,
                "name": colors_lookup.get((color_rgb, is_translucent), color_rgb),
                "quantity": quantity,
            }
        )
    return sort_colors_perceptually(used_colors)


def build_hex_positions(count: int, columns: int = 9, spacing: float = 1.1) -> List[Tuple[float, float]]:
    """Construit des positions hexagonales pour une mise en page aérée."""
    positions: List[Tuple[float, float]] = []
    rows = (count + columns - 1) // columns
    vertical_gap = spacing * 0.85
    for row in range(rows):
        offset = 0.0 if row % 2 == 0 else spacing / 2
        for col in range(columns):
            index = row * columns + col
            if index >= count:
                return positions
            x = col * spacing + offset
            y = -row * vertical_gap
            positions.append((x, y))
    return positions


def build_background(width: float, height: float, resolution: int = 600) -> np.ndarray:
    """Génère un fond dégradé pour mettre en valeur les couleurs translucides."""
    x = np.linspace(-1.0, 1.0, resolution)
    y = np.linspace(-1.0, 1.0, resolution)
    xv, yv = np.meshgrid(x, y)
    radial = np.sqrt(xv**2 + yv**2)
    diagonal = (xv + yv) / 2
    layer = 0.35 + 0.35 * (1 - radial) + 0.2 * diagonal
    layer = np.clip(layer, 0.05, 0.95)
    background = np.dstack((layer * 0.9, layer * 0.92, layer))
    return background


def plot_colors_grid(
    parts_path: Path,
    colors_path: Path,
    parts_catalog_path: Path,
    destination_path: Path,
    minifig_only: bool = False,
) -> None:
    """Dessine une grille artistique des couleurs utilisées."""
    colors = load_used_colors(parts_path, colors_path, parts_catalog_path, minifig_only=minifig_only)
    positions = build_hex_positions(len(colors))
    x_values = [x for x, _ in positions]
    y_values = [y for _, y in positions]
    width = max(x_values) - min(x_values) + 1.5
    height = max(y_values) - min(y_values) + 1.5

    fig, ax = plt.subplots(figsize=(10, 10), facecolor="#0b0c10")
    background = build_background(width, height)
    ax.imshow(
        background,
        extent=[min(x_values) - 0.75, min(x_values) - 0.75 + width, min(y_values) - 0.75, min(y_values) - 0.75 + height],
        origin="lower",
        zorder=0,
    )

    max_quantity = max(color["quantity"] for color in colors)
    min_marker = 720
    max_marker = 1600

    for (x, y), color in zip(positions, colors):
        is_translucent = color["is_translucent"] == "true"
        alpha = 0.65 if is_translucent else 1.0
        edge = "#f7f7f7" if is_translucent else "#0d0d0d"
        size = min_marker + (max_marker - min_marker) * (color["quantity"] / max_quantity)
        if is_translucent:
            ax.scatter(
                x,
                y,
                s=size * 1.25,
                c="#ffffff",
                alpha=0.18,
                edgecolors="none",
                linewidths=0,
                zorder=2,
            )
        ax.scatter(
            x,
            y,
            s=size,
            c=f"#{color['color_rgb']}",
            alpha=alpha,
            edgecolors=edge,
            linewidths=1.1,
            zorder=3,
        )

    legend_handles = [
        Line2D([0], [0], marker="o", color="none", markerfacecolor="#cccccc", markeredgecolor="#0d0d0d", markersize=10, label="Opaque"),
        Line2D(
            [0],
            [0],
            marker="o",
            color="none",
            markerfacecolor="#cccccc",
            markeredgecolor="#f7f7f7",
            markersize=10,
            alpha=0.65,
            label="Translucide",
        ),
    ]
    legend_y = 1.06 if not minifig_only else 1.08
    ax.legend(
        handles=legend_handles,
        loc="upper center",
        bbox_to_anchor=(0.5, legend_y),
        ncol=2,
        frameon=False,
        labelcolor="#f0f0f0",
    )

    title_prefix = "Palette des couleurs utilisées (rechanges incluses)"
    if minifig_only:
        title_prefix = "Palette des couleurs de minifigs (rechanges incluses)"
    ax.set_title(title_prefix, fontsize=14, color="#f0f0f0", pad=28)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_xlim(min(x_values) - 1.0, max(x_values) + 1.0)
    ax.set_ylim(min(y_values) - 1.0, max(y_values) + 1.0)
    for spine in ax.spines.values():
        spine.set_visible(False)

    ensure_parent_dir(destination_path)
    fig.tight_layout()
    fig.savefig(destination_path, dpi=200)
    plt.close(fig)
