## Copyright (C) 2026  Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Palette: active-color picker + a small gradient swatch widget.

The picker (`Palette`) is just a wrapped `Gtk.ColorButton` that emits
`color-changed` and remembers the previous active color. The previous
color is surfaced visually by the gradient tool button, not in the palette
itself — see `GradientSwatch` below, used by `editor.py`.
"""

from __future__ import annotations

from enum import Enum

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gdk  # NOQA: E402
from gi.repository import GObject  # NOQA: E402
from gi.repository import Gtk  # NOQA: E402

from solaar.i18n import _  # NOQA: E402

from ._icons import attach_themed_icon  # NOQA: E402

_UNSET_ICON_NAME = "solaar-tool-palette-off-symbolic"


class GtkSignal(Enum):
    DRAW = "draw"
    COLOR_SET = "color-set"
    TOGGLED = "toggled"


def _rgb_to_int(rgba: Gdk.RGBA) -> int:
    r = max(0, min(255, int(round(rgba.red * 255))))
    g = max(0, min(255, int(round(rgba.green * 255))))
    b = max(0, min(255, int(round(rgba.blue * 255))))
    return (r << 16) | (g << 8) | b


def _int_to_rgba(c: int) -> Gdk.RGBA:
    rgba = Gdk.RGBA()
    if c is None or c < 0:
        rgba.red = rgba.green = rgba.blue = 0.5
        rgba.alpha = 1.0
        return rgba
    rgba.red = ((c >> 16) & 0xFF) / 255.0
    rgba.green = ((c >> 8) & 0xFF) / 255.0
    rgba.blue = (c & 0xFF) / 255.0
    rgba.alpha = 1.0
    return rgba


# Sentinel for "no change" / unset paint. Matches special_keys.COLORSPLUS["No change"].
UNSET_COLOR = -1


class Palette(Gtk.Box):
    __gsignals__ = {
        "color-changed": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
    }

    def __init__(self, active: int = 0xFF0000, previous: int = 0xFF0000) -> None:
        super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        # _color/_last_color are always real RGB values; the unset toggle is
        # a separate channel so the gradient swatch (which mirrors these) is
        # unaffected by switching to "no change" paint mode.
        self._color: int = int(active)
        self._last_color: int = int(previous)
        self._unset_mode: bool = False

        self._color_btn = Gtk.ColorButton()
        self._color_btn.set_use_alpha(False)
        self._color_btn.set_rgba(_int_to_rgba(self._color))
        self._color_btn.set_tooltip_text(_("Active color"))
        self._color_btn.connect(GtkSignal.COLOR_SET.value, self._on_color_set)
        self.pack_start(self._color_btn, False, False, 0)

        self._unset_btn = Gtk.ToggleButton()
        self._unset_btn.set_tooltip_text(_("Paint as 'no change' — clears the cell to the zone base color"))
        unset_label = _("Unset")
        if attach_themed_icon(self._unset_btn, _UNSET_ICON_NAME) is not None:
            self._unset_btn.get_accessible().set_name(unset_label)
        else:
            self._unset_btn.set_label(unset_label)
        self._unset_btn.connect(GtkSignal.TOGGLED.value, self._on_unset_toggled)
        self.pack_start(self._unset_btn, False, False, 0)

    def shutdown(self) -> None:
        # attach_themed_icon connects to the button's own style-updated
        # signal; GTK disconnects it automatically when the button is
        # destroyed, so there is nothing to clean up here.
        pass

    def _on_color_set(self, btn: Gtk.ColorButton) -> None:
        c = _rgb_to_int(btn.get_rgba())
        unset_was_on = self._unset_mode
        if c == self._color and not unset_was_on:
            return
        if c != self._color:
            self._last_color = self._color
            self._color = c
        if unset_was_on:
            self._unset_mode = False
            self._unset_btn.set_active(False)
        self.emit("color-changed", self.get_color())

    def _on_unset_toggled(self, btn: Gtk.ToggleButton) -> None:
        new_state = bool(btn.get_active())
        if new_state == self._unset_mode:
            return
        self._unset_mode = new_state
        self.emit("color-changed", self.get_color())

    def get_color(self) -> int:
        return UNSET_COLOR if self._unset_mode else self._color

    def get_picker_color(self) -> int:
        """The most recent real RGB pick — independent of the unset toggle.
        Use this for visuals that should always reflect actual colors (e.g.
        the gradient swatch).
        """
        return self._color

    def get_last_color(self) -> int:
        return self._last_color

    def is_unset(self) -> bool:
        return self._unset_mode


class GradientSwatch(Gtk.DrawingArea):
    """Small icon: diagonal gradient from `previous` (bottom-left) to `active` (top-right).

    Used as the visual on the gradient tool button so the user can see at a
    glance which two colors the next gradient stroke will fade between.
    """

    SIZE = 22

    def __init__(self) -> None:
        super().__init__()
        self.set_size_request(self.SIZE, self.SIZE)
        self._active: int = 0xFF0000
        self._previous: int = 0xFF0000
        self.connect(GtkSignal.DRAW.value, self._on_draw)
        # Re-render when the GTK theme changes, so the rounded-square
        # outline (drawn in the theme foreground color) stays in sync
        # with the tool icons next to it.
        self.connect("style-updated", lambda w: w.queue_draw())

    def update(self, active: int, previous: int) -> None:
        self._active = int(active)
        self._previous = int(previous)
        self.queue_draw()

    def get_active(self) -> int:
        return self._active

    def get_previous(self) -> int:
        return self._previous

    def get_colors(self) -> tuple[int, int]:
        """Return (active, previous) — the colors the gradient tool will paint with."""
        return (self._active, self._previous)

    @staticmethod
    def _rounded_rect_path(cr, x: float, y: float, w: float, h: float, r: float) -> None:
        cr.new_sub_path()
        cr.arc(x + w - r, y + r, r, -1.5708, 0)
        cr.arc(x + w - r, y + h - r, r, 0, 1.5708)
        cr.arc(x + r, y + h - r, r, 1.5708, 3.1416)
        cr.arc(x + r, y + r, r, 3.1416, 4.7124)
        cr.close_path()

    def _on_draw(self, _w, cr) -> None:
        import cairo  # local: keeps the module light when GradientSwatch isn't built

        def rgb(c: int) -> tuple[float, float, float]:
            if c is None or c < 0:
                return (0.5, 0.5, 0.5)
            return (((c >> 16) & 0xFF) / 255.0, ((c >> 8) & 0xFF) / 255.0, (c & 0xFF) / 255.0)

        # Render in Tabler "square" coordinates (24x24 viewBox, rounded
        # rect from (3,3) to (21,21), corner radius 2, stroke 2) and let
        # cairo scale to the swatch's pixel size. Matches the outline
        # style of the tool icons exactly.
        cr.save()
        cr.scale(self.SIZE / 24.0, self.SIZE / 24.0)

        # Build the rounded-square path once, clip+fill the gradient
        # inside it, then re-build and stroke the outline in the theme
        # foreground color.
        self._rounded_rect_path(cr, 3, 3, 18, 18, 2)
        cr.save()
        cr.clip()
        # Top-left (previous, gradient start) → bottom-right (active, end).
        # Matches the directional behavior of dragging the line tool TL → BR.
        # Endpoints are shifted inward by the arc inset (corner radius * (1
        # - 1/sqrt(2)), ~0.586 for r=2) so t=0 lands on the actual visible
        # TL corner pixel of the rounded rect — without this, the rendered
        # corners sample at t≈0.033/0.967 and the displayed colors are
        # ~8 RGB units short of the true endpoint colors.
        inset = 2 * (1 - 1 / (2**0.5))
        pat = cairo.LinearGradient(3 + inset, 3 + inset, 21 - inset, 21 - inset)
        pat.add_color_stop_rgb(0.0, *rgb(self._previous))
        pat.add_color_stop_rgb(1.0, *rgb(self._active))
        cr.set_source(pat)
        cr.rectangle(3, 3, 18, 18)
        cr.fill()
        cr.restore()  # drop clip

        self._rounded_rect_path(cr, 3, 3, 18, 18, 2)
        fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL)
        cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha)
        cr.set_line_width(2)
        cr.set_line_join(cairo.LINE_JOIN_ROUND)
        cr.set_line_cap(cairo.LINE_CAP_ROUND)
        cr.stroke()
        cr.restore()
