#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Style loader for figrecipe.

Loads style configuration from YAML file and provides centralized access
to all style parameters.

Usage:
    from figrecipe.styles import load_style, get_style, STYLE

    # Load default style
    style = load_style()
    fig, ax = ps.subplots(**style.to_subplots_kwargs())

    # Access individual style parameters
    line_width = STYLE.lines.trace_mm
"""

__all__ = [
    "load_style",
    "load_preset",
    "unload_style",
    "get_style",
    "reload_style",
    "list_presets",
    "STYLE",
    "to_subplots_kwargs",
]

from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from ruamel.yaml import YAML

# Path to presets directory
_PRESETS_DIR = Path(__file__).parent / "presets"

# Path to default style file (for backwards compatibility)
_DEFAULT_STYLE_PATH = Path(__file__).parent / "FIGRECIPE_STYLE.yaml"

# Preset aliases (for branding - FigRecipe is part of SciTeX ecosystem)
_PRESET_ALIASES = {
    "FIGRECIPE": "SCITEX",
}

# Global style cache
_STYLE_CACHE: Optional["DotDict"] = None
_CURRENT_STYLE_NAME: Optional[str] = None


def list_presets() -> List[str]:
    """List available style presets.

    Returns
    -------
    list of str
        Names of available presets.
        Use `dark=True` parameter for dark variants.

    Examples
    --------
    >>> fr.list_presets()
    ['MATPLOTLIB', 'SCITEX']

    >>> fr.load_style("SCITEX")           # Scientific publication style
    >>> fr.load_style("SCITEX", dark=True)  # Dark variant
    >>> fr.load_style("MATPLOTLIB")       # Vanilla matplotlib
    """
    # Show only user-facing presets (not internal file names)
    return ["MATPLOTLIB", "SCITEX"]


class DotDict(dict):
    """Dictionary with dot-notation access to nested keys.

    Examples
    --------
    >>> d = DotDict({"axes": {"width_mm": 40}})
    >>> d.axes.width_mm
    40
    """

    def __getattr__(self, key: str) -> Any:
        # Handle special methods first
        if key == "to_subplots_kwargs":
            return lambda: to_subplots_kwargs(self)
        try:
            value = self[key]
            if isinstance(value, dict) and not isinstance(value, DotDict):
                value = DotDict(value)
                self[key] = value
            return value
        except KeyError:
            raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")

    def __setattr__(self, key: str, value: Any) -> None:
        self[key] = value

    def __delattr__(self, key: str) -> None:
        try:
            del self[key]
        except KeyError:
            raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")

    def get(self, key: str, default: Any = None) -> Any:
        """Get value with default, supporting nested keys with dots."""
        if "." in key:
            parts = key.split(".")
            value = self
            for part in parts:
                if isinstance(value, dict) and part in value:
                    value = value[part]
                else:
                    return default
            return value
        return super().get(key, default)


def _deep_merge(base: Dict, override: Dict) -> Dict:
    """Deep merge two dictionaries, with override taking precedence."""
    result = base.copy()
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = _deep_merge(result[key], value)
        else:
            result[key] = value
    return result


def _load_yaml(path: Union[str, Path]) -> Dict:
    """Load YAML file and return as dictionary."""
    yaml = YAML()
    yaml.preserve_quotes = True
    with open(path, "r") as f:
        return dict(yaml.load(f))


def reload_style(style: Optional[Union[str, Path]] = None) -> DotDict:
    """Reload style from YAML file (clears cache).

    Parameters
    ----------
    style : str or Path, optional
        Style preset name (e.g., "SCIENTIFIC") or path to YAML file.
        If None, uses default SCIENTIFIC preset.

    Returns
    -------
    DotDict
        Style configuration as DotDict for dot-access
    """
    global _STYLE_CACHE, _CURRENT_STYLE_NAME
    _STYLE_CACHE = None
    _CURRENT_STYLE_NAME = None
    return load_style(style)


def _apply_dark_theme(style_dict: Dict) -> Dict:
    """Apply dark theme transformation to a style dictionary.

    Changes only UI elements (background, text, spines, ticks) -
    NOT the data/figure colors for scientific integrity.

    Parameters
    ----------
    style_dict : dict
        Original style dictionary

    Returns
    -------
    dict
        Style dictionary with dark theme applied
    """
    import copy

    result = copy.deepcopy(style_dict)

    # Monaco/VS Code dark theme colors (from scitex-cloud UIUX.md)
    dark_colors = {
        "figure_bg": "#1e1e1e",  # VS Code main background
        "axes_bg": "#1e1e1e",  # Same as figure background
        "legend_bg": "#1e1e1e",  # Same as figure background
        "text": "#d4d4d4",  # VS Code default text
        "spine": "#3c3c3c",  # Subtle border color
        "tick": "#d4d4d4",  # Match text
        "grid": "#3a3a3a",  # Subtle grid
    }

    # Update theme section
    if "theme" not in result:
        result["theme"] = {}
    result["theme"]["mode"] = "dark"
    result["theme"]["dark"] = dark_colors

    # Update output to not be transparent (dark bg needs to show)
    if "output" in result:
        result["output"]["transparent"] = False

    return result


def load_preset(name: str, dark: bool = False) -> Optional[DotDict]:
    """Load a style preset without affecting global cache.

    This is useful for GUI editors that need to switch themes
    without affecting the global state.

    Parameters
    ----------
    name : str
        Preset name (e.g., "SCITEX", "MATPLOTLIB")
    dark : bool, optional
        If True, apply dark theme transformation

    Returns
    -------
    DotDict or None
        Style configuration as DotDict, or None if not found
    """
    # Resolve aliases
    resolved_name = _PRESET_ALIASES.get(name.upper(), name.upper())

    # Find the preset file
    style_path = _PRESETS_DIR / f"{resolved_name}.yaml"

    if not style_path.exists():
        return None

    style_dict = _load_yaml(style_path)
    style_dict["_name"] = name.upper()

    # Apply dark theme if requested
    if dark:
        style_dict = _apply_dark_theme(style_dict)

    return DotDict(style_dict)


def unload_style() -> None:
    """Unload the current style and reset to matplotlib defaults.

    After calling this, subsequent `subplots()` calls will use vanilla
    matplotlib behavior without FigRecipe styling.

    Examples
    --------
    >>> import figrecipe as fr
    >>> fr.load_style("SCITEX")  # Apply scientific style
    >>> fig, ax = fr.subplots()  # Styled
    >>> fr.unload_style()        # Reset to matplotlib defaults
    >>> fig, ax = fr.subplots()  # Vanilla matplotlib
    """
    global _STYLE_CACHE, _CURRENT_STYLE_NAME
    _STYLE_CACHE = None
    _CURRENT_STYLE_NAME = None

    # Reset matplotlib rcParams to defaults
    import matplotlib as mpl

    mpl.rcParams.update(mpl.rcParamsDefault)


def load_style(
    style: Optional[Union[str, Path, bool]] = "SCITEX", dark: bool = False
) -> Optional[DotDict]:
    """Load style configuration from preset or YAML file.

    Parameters
    ----------
    style : str, Path, bool, or None
        One of:
        - "SCITEX" / "FIGRECIPE": Scientific publication style (default)
        - "MATPLOTLIB": Vanilla matplotlib defaults
        - Path to custom YAML file: "/path/to/my_style.yaml"
        - None or False: Unload style (reset to matplotlib defaults)
    dark : bool, optional
        If True, apply dark theme transformation (default: False)

    Returns
    -------
    DotDict or None
        Style configuration as DotDict for dot-access.
        Returns None if style is unloaded.

    Examples
    --------
    >>> import figrecipe as fr

    >>> # Load scientific style (default)
    >>> style = fr.load_style()
    >>> style = fr.load_style("SCITEX")  # explicit

    >>> # Load dark variant
    >>> fr.load_style("SCITEX_DARK")
    >>> fr.load_style("SCITEX", dark=True)  # equivalent

    >>> # Reset to vanilla matplotlib
    >>> fr.load_style(None)    # unload
    >>> fr.load_style(False)   # unload
    >>> fr.load_style("MATPLOTLIB")  # explicit vanilla

    >>> # Load custom YAML file
    >>> fr.load_style("/path/to/my_style.yaml")

    >>> # Access style values
    >>> style = fr.load_style("SCITEX")
    >>> style.fonts.axis_label_pt
    7
    """
    global _STYLE_CACHE, _CURRENT_STYLE_NAME

    # Handle None or False as unload
    if style is None or style is False:
        unload_style()
        return None

    # Handle _DARK suffix in style name
    apply_dark = dark
    base_style = style
    if isinstance(style, str) and style.upper().endswith("_DARK"):
        apply_dark = True
        base_style = style[:-5]  # Remove "_DARK" suffix

    # Build cache key
    cache_key = f"{base_style}{'_DARK' if apply_dark else ''}"

    # Use cache if available and same style requested
    if _STYLE_CACHE is not None and cache_key == _CURRENT_STYLE_NAME:
        return _STYLE_CACHE

    # Resolve aliases (e.g., SCITEX -> FIGRECIPE)
    if isinstance(base_style, str):
        resolved_style = _PRESET_ALIASES.get(base_style.upper(), base_style)
    else:
        resolved_style = base_style

    # Determine the style path
    if isinstance(resolved_style, Path) or (
        isinstance(resolved_style, str)
        and (
            "/" in resolved_style
            or "\\" in resolved_style
            or resolved_style.endswith(".yaml")
        )
    ):
        # Explicit file path
        style_path = Path(resolved_style)
        style_name = str(resolved_style)
    else:
        # Preset name (e.g., "FIGRECIPE", "MATPLOTLIB")
        style_path = _PRESETS_DIR / f"{resolved_style.upper()}.yaml"
        style_name = resolved_style.upper()

    # Check if file exists
    if not style_path.exists():
        available = list_presets()
        raise FileNotFoundError(
            f"Style not found: {style}\n"
            f"Available presets: {available}\n"
            f"Or provide a path to a custom YAML file."
        )

    style_dict = _load_yaml(style_path)

    # Apply dark theme if requested
    if apply_dark:
        style_dict = _apply_dark_theme(style_dict)
        style_name = f"{style_name}_DARK"

    # Convert to DotDict for convenient access
    result = DotDict(style_dict)

    # Cache the style
    _STYLE_CACHE = result
    _CURRENT_STYLE_NAME = style_name

    return result


def get_style() -> DotDict:
    """Get the current loaded style (loads default if not yet loaded).

    Returns
    -------
    DotDict
        Current style configuration
    """
    global _STYLE_CACHE
    if _STYLE_CACHE is None:
        return load_style()
    return _STYLE_CACHE


def to_subplots_kwargs(style: Optional[DotDict] = None) -> Dict[str, Any]:
    """Convert style DotDict to kwargs for ps.subplots().

    Uses YAML-compatible flattened key names as the single source of truth.
    For example, YAML `fonts.axis_label_pt` becomes `fonts_axis_label_pt`.

    Parameters
    ----------
    style : DotDict, optional
        Style configuration. If None, uses current loaded style.

    Returns
    -------
    dict
        Keyword arguments for ps.subplots() with YAML-compatible flattened keys.

    Examples
    --------
    >>> style = load_style()
    >>> kwargs = to_subplots_kwargs(style)
    >>> fig, ax = ps.subplots(**kwargs)
    """
    if style is None:
        style = get_style()

    # YAML-compatible flattened keys (single source of truth)
    result = {
        # Axes (axes.* in YAML)
        "axes_width_mm": style.axes.width_mm,
        "axes_height_mm": style.axes.height_mm,
        "axes_thickness_mm": style.axes.thickness_mm,
        # Margins (margins.* in YAML)
        "margins_left_mm": style.margins.left_mm,
        "margins_right_mm": style.margins.right_mm,
        "margins_bottom_mm": style.margins.bottom_mm,
        "margins_top_mm": style.margins.top_mm,
        # Spacing (spacing.* in YAML)
        "spacing_horizontal_mm": style.spacing.horizontal_mm,
        "spacing_vertical_mm": style.spacing.vertical_mm,
        # Ticks (ticks.* in YAML)
        "ticks_length_mm": style.ticks.length_mm,
        "ticks_thickness_mm": style.ticks.thickness_mm,
        "ticks_direction": style.ticks.get("direction", "out"),
        "ticks_n_ticks": style.ticks.n_ticks,
        # Lines (lines.* in YAML)
        "lines_trace_mm": style.lines.trace_mm,
        "lines_errorbar_mm": style.lines.get("errorbar_mm", 0.2),
        "lines_errorbar_cap_mm": style.lines.get("errorbar_cap_mm", 0.8),
        # Markers (markers.* in YAML)
        "markers_size_mm": style.markers.size_mm,
        "markers_scatter_mm": style.markers.get("scatter_mm", style.markers.size_mm),
        "markers_flier_mm": style.markers.get("flier_mm", style.markers.size_mm),
        "markers_edge_width_mm": style.markers.get("edge_width_mm"),
        # Boxplot (boxplot.* in YAML)
        "boxplot_line_mm": style.get("boxplot", {}).get("line_mm", 0.2),
        "boxplot_whisker_mm": style.get("boxplot", {}).get("whisker_mm", 0.2),
        "boxplot_cap_mm": style.get("boxplot", {}).get("cap_mm", 0.2),
        "boxplot_median_mm": style.get("boxplot", {}).get("median_mm", 0.2),
        "boxplot_median_color": style.get("boxplot", {}).get("median_color", "black"),
        "boxplot_flier_edge_mm": style.get("boxplot", {}).get("flier_edge_mm", 0.2),
        # Violinplot (violinplot.* in YAML)
        "violinplot_line_mm": style.get("violinplot", {}).get("line_mm", 0.2),
        "violinplot_inner": style.get("violinplot", {}).get("inner", "box"),
        "violinplot_box_width_mm": style.get("violinplot", {}).get("box_width_mm", 1.5),
        "violinplot_whisker_mm": style.get("violinplot", {}).get("whisker_mm", 0.2),
        "violinplot_median_mm": style.get("violinplot", {}).get("median_mm", 0.8),
        # Barplot (barplot.* in YAML)
        "barplot_edge_mm": style.get("barplot", {}).get("edge_mm", 0.2),
        # Histogram (histogram.* in YAML)
        "histogram_edge_mm": style.get("histogram", {}).get("edge_mm", 0.2),
        # Pie chart (pie.* in YAML)
        "pie_text_pt": style.get("pie", {}).get("text_pt", 6),
        "pie_show_axes": style.get("pie", {}).get("show_axes", False),
        # Imshow (imshow.* in YAML)
        "imshow_show_axes": style.get("imshow", {}).get("show_axes", False),
        "imshow_show_labels": style.get("imshow", {}).get("show_labels", False),
        # Fonts (fonts.* in YAML)
        "fonts_family": style.fonts.family,
        "fonts_axis_label_pt": style.fonts.axis_label_pt,
        "fonts_tick_label_pt": style.fonts.tick_label_pt,
        "fonts_title_pt": style.fonts.title_pt,
        "fonts_suptitle_pt": style.fonts.suptitle_pt,
        "fonts_legend_pt": style.fonts.legend_pt,
        "fonts_annotation_pt": style.fonts.get("annotation_pt", 6),
        # Padding (padding.* in YAML)
        "padding_label_pt": style.padding.label_pt,
        "padding_tick_pt": style.padding.tick_pt,
        "padding_title_pt": style.padding.title_pt,
        # Output (output.* in YAML)
        "output_dpi": style.output.dpi,
        "output_transparent": style.output.get("transparent", True),
        "output_format": style.output.get("format", "pdf"),
        # Theme (theme.* in YAML)
        "theme_mode": style.theme.mode,
    }

    # Add theme colors from preset if available
    theme_mode = style.theme.mode
    if "theme" in style and theme_mode in style.theme:
        result["theme_colors"] = dict(style.theme[theme_mode])

    # Add color palette if available
    if "colors" in style and "palette" in style.colors:
        result["color_palette"] = list(style.colors.palette)

    # Add behavior settings (behavior.* in YAML)
    if "behavior" in style:
        behavior = style.behavior
        if hasattr(behavior, "hide_top_spine"):
            result["behavior_hide_top_spine"] = behavior.hide_top_spine
        if hasattr(behavior, "hide_right_spine"):
            result["behavior_hide_right_spine"] = behavior.hide_right_spine
        if hasattr(behavior, "grid"):
            result["behavior_grid"] = behavior.grid
        if hasattr(behavior, "auto_scale_axes"):
            result["behavior_auto_scale_axes"] = behavior.auto_scale_axes
        if hasattr(behavior, "constrained_layout"):
            result["behavior_constrained_layout"] = behavior.constrained_layout

    # Legacy key aliases for backwards compatibility
    # (These allow existing code using old keys to still work)
    result["margin_left_mm"] = result["margins_left_mm"]
    result["margin_right_mm"] = result["margins_right_mm"]
    result["margin_bottom_mm"] = result["margins_bottom_mm"]
    result["margin_top_mm"] = result["margins_top_mm"]
    result["space_w_mm"] = result["spacing_horizontal_mm"]
    result["space_h_mm"] = result["spacing_vertical_mm"]
    result["tick_length_mm"] = result["ticks_length_mm"]
    result["tick_thickness_mm"] = result["ticks_thickness_mm"]
    result["n_ticks"] = result["ticks_n_ticks"]
    result["trace_thickness_mm"] = result["lines_trace_mm"]
    result["marker_size_mm"] = result["markers_size_mm"]
    result["font_family"] = result["fonts_family"]
    result["axis_font_size_pt"] = result["fonts_axis_label_pt"]
    result["tick_font_size_pt"] = result["fonts_tick_label_pt"]
    result["title_font_size_pt"] = result["fonts_title_pt"]
    result["suptitle_font_size_pt"] = result["fonts_suptitle_pt"]
    result["legend_font_size_pt"] = result["fonts_legend_pt"]
    result["label_pad_pt"] = result["padding_label_pt"]
    result["tick_pad_pt"] = result["padding_tick_pt"]
    result["title_pad_pt"] = result["padding_title_pt"]
    result["dpi"] = result["output_dpi"]
    result["theme"] = result["theme_mode"]
    result["hide_top_spine"] = result.get("behavior_hide_top_spine", True)
    result["hide_right_spine"] = result.get("behavior_hide_right_spine", True)
    result["grid"] = result.get("behavior_grid", False)
    result["auto_scale_axes"] = result.get("behavior_auto_scale_axes", True)
    result["constrained_layout"] = result.get("behavior_constrained_layout", False)

    return result


# Lazy-loaded global STYLE object
class _StyleProxy:
    """Proxy object that loads style on first access."""

    def __getattr__(self, name: str) -> Any:
        return getattr(get_style(), name)

    def __repr__(self) -> str:
        return repr(get_style())

    def to_subplots_kwargs(self) -> Dict[str, Any]:
        """Convert style to subplots kwargs."""
        return to_subplots_kwargs()


STYLE = _StyleProxy()


if __name__ == "__main__":
    # Test loading
    print("Loading default style...")
    style = load_style()
    print(f"  axes.width_mm: {style.axes.width_mm}")
    print(f"  fonts.axis_label_pt: {style.fonts.axis_label_pt}")
    print(f"  lines.trace_mm: {style.lines.trace_mm}")

    print("\nConverting to subplots kwargs...")
    kwargs = to_subplots_kwargs()
    for k, v in list(kwargs.items())[:5]:
        print(f"  {k}: {v}")

    print("\nUsing STYLE proxy...")
    print(f"  STYLE.fonts.family: {STYLE.fonts.family}")
