# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Color Palette

16 named retro colors for creature customization.
Handles hex parsing, RGB tuples, and ANSI truecolor escape codes.
"""

from typing import Optional

# ── Named palette ────────────────────────────────────────────────────

PALETTE: dict[str, tuple[int, int, int]] = {
    # Warm
    "crimson": (220, 20, 60),
    "ember": (255, 94, 19),
    "gold": (255, 215, 0),
    "amber": (255, 191, 0),
    # Cool
    "emerald": (46, 139, 87),
    "jade": (0, 168, 107),
    "sapphire": (15, 82, 186),
    "cobalt": (0, 71, 171),
    "violet": (138, 43, 226),
    "lavender": (180, 130, 220),
    # Neutral
    "silver": (192, 192, 192),
    "ivory": (255, 255, 240),
    "slate": (112, 128, 144),
    "obsidian": (48, 48, 48),
    # Special
    "rose": (255, 102, 153),
    "teal": (0, 180, 180),
}

PALETTE_ORDER = [
    "crimson",
    "ember",
    "gold",
    "amber",
    "emerald",
    "jade",
    "sapphire",
    "cobalt",
    "violet",
    "lavender",
    "slate",
    "obsidian",
    "silver",
    "ivory",
    "rose",
    "teal",
]


# ── Hex parsing ──────────────────────────────────────────────────────


def hex_to_rgb(hex_str: str) -> Optional[tuple[int, int, int]]:
    """Parse '#RRGGBB' or '#RGB' to (R, G, B). Returns None on invalid input."""
    h = hex_str.lstrip("#")
    if len(h) == 3:
        h = h[0] * 2 + h[1] * 2 + h[2] * 2
    if len(h) != 6:
        return None
    try:
        return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
    except ValueError:
        return None


def rgb_to_hex(r: int, g: int, b: int) -> str:
    """Convert (R, G, B) to '#RRGGBB'."""
    return f"#{r:02X}{g:02X}{b:02X}"


# ── Color resolution ────────────────────────────────────────────────


def resolve_color(color_input: str) -> Optional[tuple[int, int, int]]:
    """Resolve a named color or hex code to an RGB tuple."""
    name = color_input.strip().lower()
    if name in PALETTE:
        return PALETTE[name]
    return hex_to_rgb(color_input)


# Build reverse lookup: hex -> palette name (populated lazily)
_HEX_TO_NAME: dict[str, str] = {}


def hex_to_palette_name(hex_str: str) -> str:
    """Reverse-map a hex color to its palette name, or '' if not a palette color."""
    if not _HEX_TO_NAME:
        for pname, (r, g, b) in PALETTE.items():
            _HEX_TO_NAME[f"#{r:02X}{g:02X}{b:02X}"] = pname
    return _HEX_TO_NAME.get(hex_str.upper().strip(), "")


# ── ANSI escape codes ───────────────────────────────────────────────


def ansi_fg(r: int, g: int, b: int) -> str:
    """Truecolor foreground escape sequence."""
    return f"\033[38;2;{r};{g};{b}m"


def ansi_reset() -> str:
    """Reset all ANSI attributes."""
    return "\033[0m"


# ── Brightness adjustment ───────────────────────────────────────────


def adjust_brightness(rgb: tuple[int, int, int], factor: float) -> tuple[int, int, int]:
    """Adjust brightness by factor. >1.0 = lighter, <1.0 = darker."""
    r, g, b = rgb
    return (
        min(255, max(0, int(r * factor))),
        min(255, max(0, int(g * factor))),
        min(255, max(0, int(b * factor))),
    )


def compute_highlight(body_rgb: tuple[int, int, int]) -> tuple[int, int, int]:
    """Lighter variant of body color for highlights."""
    return adjust_brightness(body_rgb, 1.3)


def compute_shadow(body_rgb: tuple[int, int, int]) -> tuple[int, int, int]:
    """Darker variant of body color for shadows."""
    return adjust_brightness(body_rgb, 0.7)


# ── Color map builder ───────────────────────────────────────────────


def build_color_map(
    color_body: str, color_eyes: str, color_accent: str
) -> dict[str, tuple[int, int, int]]:
    """Build the full sprite color mapping from creature colors.

    Returns a dict mapping role codes to RGB tuples:
      B = body, E = eyes, A = accent, H = highlight, S = shadow, # = outline
    """
    body = resolve_color(color_body) or (74, 144, 217)
    eyes = resolve_color(color_eyes) or (255, 215, 0)
    accent = resolve_color(color_accent) or (255, 107, 107)

    return {
        "B": body,
        "E": eyes,
        "A": accent,
        "H": compute_highlight(body),
        "S": compute_shadow(body),
        "#": (30, 30, 30),
    }
