# theme.py
"""
Theming support for manual_slop GUI.

Palettes
--------
Each palette is a dict mapping semantic names to (R,G,B) or (R,G,B,A) tuples.
The names correspond to dpg theme colour / style constants.

Font handling
-------------
Call apply_font(path, size) to load a TTF and bind it as the global default.
Call set_scale(factor) to set the global font scale (DPI scaling).

Usage
-----
    import theme
    theme.apply("10x")          # apply a named palette
    theme.apply_font("C:/Windows/Fonts/CascadiaCode.ttf", 15)
    theme.set_scale(1.25)
"""

import dearpygui.dearpygui as dpg
from pathlib import Path
from typing import Any

# ------------------------------------------------------------------ palettes

# Colour key names match the DPG mvThemeCol_* constants (string lookup below).
# Only keys that differ from DPG defaults need to be listed.

_PALETTES: dict[str, dict[str, Any]] = {
 "DPG Default": {},  # empty = reset to DPG built-in defaults
 "10x Dark": {
  # Window / frame chrome
  "WindowBg":             ( 34,  32,  28),
  "ChildBg":              ( 30,  28,  24),
  "PopupBg":              ( 35,  30,  20),
  "Border":               ( 60,  55,  50),
  "BorderShadow":         (  0,   0,   0,   0),
  "FrameBg":              ( 45,  42,  38),
  "FrameBgHovered":       ( 60,  56,  50),
  "FrameBgActive":        ( 75,  70,  62),
  # Title bars
  "TitleBg":              ( 40,  35,  25),
  "TitleBgActive":        ( 60,  45,  15),
  "TitleBgCollapsed":     ( 30,  27,  20),
  # Menu bar
  "MenuBarBg":            ( 35,  30,  20),
  # Scrollbar
  "ScrollbarBg":          ( 30,  28,  24),
  "ScrollbarGrab":        ( 80,  78,  72),
  "ScrollbarGrabHovered": (100, 100,  92),
  "ScrollbarGrabActive":  (120, 118, 110),
  # Check marks / radio buttons
  "CheckMark":            (194, 164,  74),
  # Sliders
  "SliderGrab":           (126,  78,  14),
  "SliderGrabActive":     (194, 140,  30),
  # Buttons
  "Button":               ( 83,  76,  60),
  "ButtonHovered":        (126,  78,  14),
  "ButtonActive":         (115,  90,  70),
  # Headers (collapsing headers, selectables, listbox items)
  "Header":               ( 83,  76,  60),
  "HeaderHovered":        (126,  78,  14),
  "HeaderActive":         (115,  90,  70),
  # Separator
  "Separator":            ( 70,  65,  55),
  "SeparatorHovered":     (126,  78,  14),
  "SeparatorActive":      (194, 164,  74),
  # Resize grip
  "ResizeGrip":           ( 60,  55,  44),
  "ResizeGripHovered":    (126,  78,  14),
  "ResizeGripActive":     (194, 164,  74),
  # Tab bar
  "Tab":                  ( 83,  83,  70),
  "TabHovered":           (126,  77,  25),
  "TabActive":            (126,  77,  25),
  "TabUnfocused":         ( 60,  58,  50),
  "TabUnfocusedActive":   ( 90,  80,  55),
  # Docking
  "DockingPreview":       (126,  78,  14, 180),
  "DockingEmptyBg":       ( 20,  20,  20),
  # Text
  "Text":                 (200, 200, 200),
  "TextDisabled":         (130, 130, 120),
  # Input text cursor / selection
  "TextSelectedBg":       ( 59,  86, 142, 180),
  # Plot / table lines
  "TableHeaderBg":        ( 55,  50,  38),
  "TableBorderStrong":    ( 70,  65,  55),
  "TableBorderLight":     ( 50,  47,  42),
  "TableRowBg":           (  0,   0,   0,   0),
  "TableRowBgAlt":        ( 40,  38,  34,  40),
  # Misc
  "NavHighlight":         (126,  78,  14),
  "NavWindowingHighlight":(194, 164,  74, 180),
  "NavWindowingDimBg":    ( 20,  20,  20,  80),
  "ModalWindowDimBg":     ( 10,  10,  10, 100),
 },
 "Nord Dark": {
  "WindowBg":             ( 36,  41,  49),
  "ChildBg":              ( 30,  34,  42),
  "PopupBg":              ( 36,  41,  49),
  "Border":               ( 59,  66,  82),
  "BorderShadow":         (  0,   0,   0,   0),
  "FrameBg":              ( 46,  52,  64),
  "FrameBgHovered":       ( 59,  66,  82),
  "FrameBgActive":        ( 67,  76,  94),
  "TitleBg":              ( 36,  41,  49),
  "TitleBgActive":        ( 59,  66,  82),
  "TitleBgCollapsed":     ( 30,  34,  42),
  "MenuBarBg":            ( 46,  52,  64),
  "ScrollbarBg":          ( 30,  34,  42),
  "ScrollbarGrab":        ( 76,  86, 106),
  "ScrollbarGrabHovered": ( 94, 129, 172),
  "ScrollbarGrabActive":  (129, 161, 193),
  "CheckMark":            (136, 192, 208),
  "SliderGrab":           ( 94, 129, 172),
  "SliderGrabActive":     (129, 161, 193),
  "Button":               ( 59,  66,  82),
  "ButtonHovered":        ( 94, 129, 172),
  "ButtonActive":         (129, 161, 193),
  "Header":               ( 59,  66,  82),
  "HeaderHovered":        ( 94, 129, 172),
  "HeaderActive":         (129, 161, 193),
  "Separator":            ( 59,  66,  82),
  "SeparatorHovered":     ( 94, 129, 172),
  "SeparatorActive":      (136, 192, 208),
  "ResizeGrip":           ( 59,  66,  82),
  "ResizeGripHovered":    ( 94, 129, 172),
  "ResizeGripActive":     (136, 192, 208),
  "Tab":                  ( 46,  52,  64),
  "TabHovered":           ( 94, 129, 172),
  "TabActive":            ( 76,  86, 106),
  "TabUnfocused":         ( 36,  41,  49),
  "TabUnfocusedActive":   ( 59,  66,  82),
  "DockingPreview":       ( 94, 129, 172, 180),
  "DockingEmptyBg":       ( 20,  22,  28),
  "Text":                 (216, 222, 233),
  "TextDisabled":         (116, 128, 150),
  "TextSelectedBg":       ( 94, 129, 172, 180),
  "TableHeaderBg":        ( 59,  66,  82),
  "TableBorderStrong":    ( 76,  86, 106),
  "TableBorderLight":     ( 59,  66,  82),
  "TableRowBg":           (  0,   0,   0,   0),
  "TableRowBgAlt":        ( 46,  52,  64,  40),
  "NavHighlight":         (136, 192, 208),
  "ModalWindowDimBg":     ( 10,  12,  16, 100),
 },
 "Monokai": {
  "WindowBg":             ( 39,  40,  34),
  "ChildBg":              ( 34,  35,  29),
  "PopupBg":              ( 39,  40,  34),
  "Border":               ( 60,  61,  52),
  "BorderShadow":         (  0,   0,   0,   0),
  "FrameBg":              ( 50,  51,  44),
  "FrameBgHovered":       ( 65,  67,  56),
  "FrameBgActive":        ( 80,  82,  68),
  "TitleBg":              ( 39,  40,  34),
  "TitleBgActive":        ( 73,  72,  62),
  "TitleBgCollapsed":     ( 30,  31,  26),
  "MenuBarBg":            ( 50,  51,  44),
  "ScrollbarBg":          ( 34,  35,  29),
  "ScrollbarGrab":        ( 80,  80,  72),
  "ScrollbarGrabHovered": (102, 217,  39),
  "ScrollbarGrabActive":  (166, 226,  46),
  "CheckMark":            (166, 226,  46),
  "SliderGrab":           (102, 217,  39),
  "SliderGrabActive":     (166, 226,  46),
  "Button":               ( 73,  72,  62),
  "ButtonHovered":        (249, 38,  114),
  "ButtonActive":         (198,  30,   92),
  "Header":               ( 73,  72,  62),
  "HeaderHovered":        (249,  38, 114),
  "HeaderActive":         (198,  30,  92),
  "Separator":            ( 60,  61,  52),
  "SeparatorHovered":     (249,  38, 114),
  "SeparatorActive":      (166, 226,  46),
  "ResizeGrip":           ( 73,  72,  62),
  "ResizeGripHovered":    (249,  38, 114),
  "ResizeGripActive":     (166, 226,  46),
  "Tab":                  ( 73,  72,  62),
  "TabHovered":           (249,  38, 114),
  "TabActive":            (249,  38, 114),
  "TabUnfocused":         ( 50,  51,  44),
  "TabUnfocusedActive":   ( 90,  88,  76),
  "DockingPreview":       (249,  38, 114, 180),
  "DockingEmptyBg":       ( 20,  20,  18),
  "Text":                 (248, 248, 242),
  "TextDisabled":         (117, 113, 94),
  "TextSelectedBg":       (249,  38, 114, 150),
  "TableHeaderBg":        ( 60,  61,  52),
  "TableBorderStrong":    ( 73,  72,  62),
  "TableBorderLight":     ( 55,  56,  48),
  "TableRowBg":           (  0,   0,   0,   0),
  "TableRowBgAlt":        ( 50,  51,  44,  40),
  "NavHighlight":         (166, 226,  46),
  "ModalWindowDimBg":     ( 10,  10,   8, 100),
 },
}

PALETTE_NAMES: list[str] = list(_PALETTES.keys())

# ------------------------------------------------------------------ colour key -> mvThemeCol_* mapping

# Maps our friendly name -> dpg constant name
_COL_MAP: dict[str, str] = {
 "Text":                     "mvThemeCol_Text",
 "TextDisabled":             "mvThemeCol_TextDisabled",
 "WindowBg":                 "mvThemeCol_WindowBg",
 "ChildBg":                  "mvThemeCol_ChildBg",
 "PopupBg":                  "mvThemeCol_PopupBg",
 "Border":                   "mvThemeCol_Border",
 "BorderShadow":             "mvThemeCol_BorderShadow",
 "FrameBg":                  "mvThemeCol_FrameBg",
 "FrameBgHovered":           "mvThemeCol_FrameBgHovered",
 "FrameBgActive":            "mvThemeCol_FrameBgActive",
 "TitleBg":                  "mvThemeCol_TitleBg",
 "TitleBgActive":            "mvThemeCol_TitleBgActive",
 "TitleBgCollapsed":         "mvThemeCol_TitleBgCollapsed",
 "MenuBarBg":                "mvThemeCol_MenuBarBg",
 "ScrollbarBg":              "mvThemeCol_ScrollbarBg",
 "ScrollbarGrab":            "mvThemeCol_ScrollbarGrab",
 "ScrollbarGrabHovered":     "mvThemeCol_ScrollbarGrabHovered",
 "ScrollbarGrabActive":      "mvThemeCol_ScrollbarGrabActive",
 "CheckMark":                "mvThemeCol_CheckMark",
 "SliderGrab":               "mvThemeCol_SliderGrab",
 "SliderGrabActive":         "mvThemeCol_SliderGrabActive",
 "Button":                   "mvThemeCol_Button",
 "ButtonHovered":            "mvThemeCol_ButtonHovered",
 "ButtonActive":             "mvThemeCol_ButtonActive",
 "Header":                   "mvThemeCol_Header",
 "HeaderHovered":            "mvThemeCol_HeaderHovered",
 "HeaderActive":             "mvThemeCol_HeaderActive",
 "Separator":                "mvThemeCol_Separator",
 "SeparatorHovered":         "mvThemeCol_SeparatorHovered",
 "SeparatorActive":          "mvThemeCol_SeparatorActive",
 "ResizeGrip":               "mvThemeCol_ResizeGrip",
 "ResizeGripHovered":        "mvThemeCol_ResizeGripHovered",
 "ResizeGripActive":         "mvThemeCol_ResizeGripActive",
 "Tab":                      "mvThemeCol_Tab",
 "TabHovered":               "mvThemeCol_TabHovered",
 "TabActive":                "mvThemeCol_TabActive",
 "TabUnfocused":             "mvThemeCol_TabUnfocused",
 "TabUnfocusedActive":       "mvThemeCol_TabUnfocusedActive",
 "DockingPreview":           "mvThemeCol_DockingPreview",
 "DockingEmptyBg":           "mvThemeCol_DockingEmptyBg",
 "TextSelectedBg":           "mvThemeCol_TextSelectedBg",
 "TableHeaderBg":            "mvThemeCol_TableHeaderBg",
 "TableBorderStrong":        "mvThemeCol_TableBorderStrong",
 "TableBorderLight":         "mvThemeCol_TableBorderLight",
 "TableRowBg":               "mvThemeCol_TableRowBg",
 "TableRowBgAlt":            "mvThemeCol_TableRowBgAlt",
 "NavHighlight":             "mvThemeCol_NavHighlight",
 "NavWindowingHighlight":    "mvThemeCol_NavWindowingHighlight",
 "NavWindowingDimBg":        "mvThemeCol_NavWindowingDimBg",
 "ModalWindowDimBg":         "mvThemeCol_ModalWindowDimBg",
}

# ------------------------------------------------------------------ state

_current_theme_tag: str | None  = None
_current_font_tag: str | None   = None
_font_registry_tag: str | None  = None
_current_palette:  str          = "DPG Default"
_current_font_path: str         = ""
_current_font_size: float       = 14.0
_current_scale:    float        = 1.0
_shader_config: dict[str, Any]  = {
 "crt": False,
 "bloom": False,
 "bg": "none",
 "custom_window_frame": False,
}

# ------------------------------------------------------------------ public API

def get_palette_names() -> list[str]:
 return list(_PALETTES.keys())

def get_current_palette() -> str:
 return _current_palette

def get_current_font_path() -> str:
 return _current_font_path

def get_current_font_size() -> float:
 return _current_font_size

def get_current_scale() -> float:
 return _current_scale

def get_shader_config(key: str) -> Any:
 """Get a specific shader configuration value."""
 return _shader_config.get(key)

def get_window_frame_config() -> bool:
 """Get the window frame configuration."""
 return _shader_config.get("custom_window_frame", False)

def get_palette_colours(name: str) -> dict[str, Any]:
 """Return a copy of the colour dict for the named palette."""
 return dict(_PALETTES.get(name, {}))

def apply(palette_name: str, overrides: dict[str, Any] | None = None) -> None:
 """
    Build a global DPG theme from the named palette plus optional per-colour
    overrides, and bind it as the default theme.

    overrides: {colour_key: (R,G,B) or (R,G,B,A)} — merged on top of palette.
    """
 global _current_theme_tag, _current_palette
 _current_palette = palette_name
 colours = dict(_PALETTES.get(palette_name, {}))
 if overrides:
  colours.update(overrides)
  # Delete the old theme if one exists
 if _current_theme_tag is not None:
  try:
   dpg.delete_item(_current_theme_tag)
  except Exception:
   pass
  _current_theme_tag = None
 if palette_name == "DPG Default" and not overrides:
 # Bind an empty theme to reset to DPG defaults
  with dpg.theme() as t:
   with dpg.theme_component(dpg.mvAll):
    pass
  dpg.bind_theme(t)
  _current_theme_tag = t
  return
 with dpg.theme() as t:
  with dpg.theme_component(dpg.mvAll):
   for name, colour in colours.items():
    const_name = _COL_MAP.get(name)
    if const_name is None:
     continue
    const = getattr(dpg, const_name, None)
    if const is None:
     continue
     # Ensure 4-tuple
    if len(colour) == 3:
     colour = (*colour, 255)
    dpg.add_theme_color(const, colour)
 dpg.bind_theme(t)
 _current_theme_tag = t

def apply_font(font_path: str, size: float = 14.0) -> None:
 """
    Load the TTF at font_path at the given point size and bind it globally.
    Safe to call multiple times.  Uses a single persistent font_registry; only
    the font *item* tag is tracked.  Passing an empty path or a missing file
    resets to the DPG built-in font.
    """
 global _current_font_tag, _current_font_path, _current_font_size, _font_registry_tag
 _current_font_path = font_path
 _current_font_size = size
 if not font_path or not Path(font_path).exists():
 # Reset to default built-in font
  dpg.bind_font(0)
  _current_font_tag = None
  return
  # Create the registry once
 if _font_registry_tag is None or not dpg.does_item_exist(_font_registry_tag):
  with dpg.font_registry() as reg:
   _font_registry_tag = reg
   # Delete previous custom font item only (not the registry)
 if _current_font_tag is not None:
  try:
   dpg.delete_item(_current_font_tag)
  except Exception:
   pass
  _current_font_tag = None
 font = dpg.add_font(font_path, size, parent=_font_registry_tag)
 _current_font_tag = font
 dpg.bind_font(font)

def set_scale(factor: float) -> None:
 """Set the global Dear PyGui font/UI scale factor."""
 global _current_scale
 _current_scale = factor
 dpg.set_global_font_scale(factor)

def save_to_config(config: dict[str, Any]) -> None:
 """Persist theme settings into the config dict under [theme]."""
 config.setdefault("theme", {})
 config["theme"]["palette"]   = _current_palette
 config["theme"]["font_path"] = _current_font_path
 config["theme"]["font_size"] = _current_font_size
 config["theme"]["scale"]     = _current_scale

def load_from_config(config: dict[str, Any]) -> None:
 """Read [theme] from config and apply everything."""
 t = config.get("theme", {})
 palette   = t.get("palette",   "DPG Default")
 font_path = t.get("font_path", "")
 font_size = float(t.get("font_size", 14.0))
 scale     = float(t.get("scale",     1.0))
 apply(palette)
 if font_path:
  apply_font(font_path, font_size)
 set_scale(scale)
 global _shader_config
 _shader_config["crt"] = t.get("shader_crt", False)
 _shader_config["bloom"] = t.get("shader_bloom", False)
 _shader_config["bg"] = t.get("shader_bg", "none")
 _shader_config["custom_window_frame"] = t.get("custom_window_frame", False)

