#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - Lite DPI
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK4
# Licence: GPLv2
#--------------------------------------------------------------------------------------------------------

import gi
gi.require_version('Gtk', '4.0')
gi.require_version("Pango", "1.0")
gi.require_version("PangoCairo", "1.0")
from gi.repository import Gtk, Gdk, GLib, Pango, PangoCairo

import gettext as _gt, locale as _loc
TEXTDOMAIN = "lite-dpi"
try:
    _loc.setlocale(_loc.LC_ALL, "")
except _loc.Error:
    pass
_gt.bindtextdomain(TEXTDOMAIN, "/usr/share/locale")
_gt.textdomain(TEXTDOMAIN)
_ = _gt.translation(TEXTDOMAIN, "/usr/share/locale", fallback=True).gettext

import subprocess
import shutil


# ===========================================================================
# CANONICAL Linux Lite house-style dialog — copy this block VERBATIM into each
# GTK4 Lite app (just below imports). Reference: lite-autologin.
#
# REQUIRES these imports in the host app:
#   gi.require_version("Pango", "1.0"); gi.require_version("PangoCairo", "1.0")
#   from gi.repository import Gtk, Gdk, GLib, Pango, PangoCairo
# and a gettext `_` callable (every Lite GTK4 app already has one).
#
# USAGE:
#   LiteDialog.result(parent, "success", _("Title"), _("Subtitle"))
#   LiteDialog.result(parent, "error",   _("Title"), _("Subtitle"))
#   LiteDialog.confirm(parent, _("Remove X?"), _("This cannot be undone."),
#                      confirm_label=_("Remove"), destructive=True,
#                      on_confirm=do_it)
# RULES (do not change): glyph colour is PangoCairo (never a coloured GtkLabel);
# NO dim text anywhere; mode-aware palette.
# ===========================================================================

class LiteDialog(Gtk.Window):
    _GLYPH = {"success": "✓", "error": "✕", "warning": "⚠",
              "question": "?", "info": "i"}
    _css_loaded = False

    def __init__(self, parent, kind="info", title="", subtitle="", buttons=None):
        super().__init__(modal=True, resizable=False)
        self.set_title(parent.get_title() if parent is not None else "")
        if parent is not None:
            self.set_transient_for(parent)
        self.set_default_size(440, -1)
        LiteDialog._ensure_css()

        self.set_titlebar(Gtk.HeaderBar())

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
                      margin_top=28, margin_bottom=24, margin_start=32, margin_end=32)
        self.set_child(box)

        # Glyph drawn with PangoCairo in the kind colour (a DrawingArea, not a
        # GtkLabel): GtkLabel drops markup/CSS foreground under the LL theme's
        # provider priority, but Cairo painting can't be overridden by CSS.
        box.append(self._make_glyph(kind))

        t = Gtk.Label(label=title)
        t.add_css_class("lite-dlg-title")
        t.set_halign(Gtk.Align.CENTER)
        t.set_justify(Gtk.Justification.CENTER)
        t.set_wrap(True)
        box.append(t)

        if subtitle:
            s = Gtk.Label(label=subtitle)
            s.set_halign(Gtk.Align.CENTER)
            s.set_justify(Gtk.Justification.CENTER)
            s.set_wrap(True)
            box.append(s)

        actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                          margin_top=10)
        actions.set_halign(Gtk.Align.CENTER)
        actions.add_css_class("lite-dlg-actions")
        box.append(actions)

        focus_btn = None
        for spec in (buttons or [{"label": _("Close"), "primary": True}]):
            b = Gtk.Button(label=spec.get("label", _("OK")))
            b.add_css_class("lite-dlg-btn")
            if spec.get("primary"):
                b.add_css_class("suggested-action")
            if spec.get("destructive"):
                b.add_css_class("destructive-action")
            b.connect("clicked", self._on_click, spec.get("callback"))
            actions.append(b)
            if spec.get("primary"):
                focus_btn = b
        if focus_btn is not None:
            focus_btn.grab_focus()

    def _on_click(self, _btn, callback):
        self.close()
        if callback is not None:
            callback()

    @staticmethod
    def _is_dark():
        s = Gtk.Settings.get_default()
        if s is None:
            return False
        try:
            if s.get_property("gtk-application-prefer-dark-theme"):
                return True
            return "dark" in (s.get_property("gtk-theme-name") or "").lower()
        except Exception:
            return False

    @classmethod
    def _palette(cls):
        if cls._is_dark():
            return {"success": "#57e389", "error": "#ff7b72", "warning": "#f9c84e",
                    "question": "#78aeed", "info": "#78aeed"}
        return {"success": "#26a269", "error": "#e01b24", "warning": "#e8870a",
                "question": "#3584e4", "info": "#3584e4"}

    @classmethod
    def _make_glyph(cls, kind):
        char = cls._GLYPH.get(kind, "")
        hexc = cls._palette().get(kind, "#000000")
        r = int(hexc[1:3], 16) / 255.0
        g = int(hexc[3:5], 16) / 255.0
        b = int(hexc[5:7], 16) / 255.0
        area = Gtk.DrawingArea()
        area.set_content_width(72)
        area.set_content_height(60)
        area.set_halign(Gtk.Align.CENTER)

        def _draw(_area, cr, w, h):
            layout = PangoCairo.create_layout(cr)
            layout.set_markup('<span weight="bold" size="40000">%s</span>' % char)
            tw, th = layout.get_pixel_size()
            cr.set_source_rgb(r, g, b)
            cr.move_to((w - tw) / 2.0, (h - th) / 2.0)
            PangoCairo.show_layout(cr, layout)

        area.set_draw_func(_draw)
        return area

    @classmethod
    def _ensure_css(cls):
        if cls._css_loaded:
            return
        provider = Gtk.CssProvider()
        provider.load_from_data(b"""
        .lite-dlg-title { font-size: 19px; font-weight: bold; }
        .lite-dlg-actions button {
            border-radius: 18px;
            padding-top: 7px; padding-bottom: 7px;
            padding-left: 22px; padding-right: 22px;
            min-width: 86px;
        }
        """)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(), provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
        cls._css_loaded = True

    @classmethod
    def result(cls, parent, kind, title, subtitle="", button=None, on_close=None):
        cls(parent, kind=kind, title=title, subtitle=subtitle,
            buttons=[{"label": button or _("Close"), "primary": True,
                      "callback": on_close}]).present()

    @classmethod
    def confirm(cls, parent, title, subtitle="", confirm_label=None,
                cancel_label=None, kind="question", destructive=False,
                on_confirm=None, on_cancel=None):
        cls(parent, kind=kind, title=title, subtitle=subtitle, buttons=[
            {"label": cancel_label or _("Cancel"), "callback": on_cancel},
            {"label": confirm_label or _("OK"),
             "primary": not destructive, "destructive": destructive,
             "callback": on_confirm},
        ]).present()


class HiDPISettingsWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.set_title(_("HiDPI Settings"))
        self.set_default_size(420, -1)
        self.set_resizable(False)

        # Try to set window icon
        icon_path = "/usr/share/icons/Papirus/24x24/apps/lite-dpi.png"
        if shutil.os.path.exists(icon_path):
            self.set_icon_name("lite-dpi")

        # Header bar (plain Gtk so the window follows the Linux-Lite theme)
        header = Gtk.HeaderBar()
        self.set_titlebar(header)

        # Info button in header — symbolic so it recolors to the headerbar
        # foreground (white on the Linux-Lite dark headerbar) in both modes.
        info_button = Gtk.Button(icon_name="help-about-symbolic")
        info_button.set_tooltip_text(_("DPI Info"))
        info_button.connect("clicked", self.on_info_button_clicked)
        header.pack_end(info_button)

        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20,
                              margin_top=20, margin_bottom=20,
                              margin_start=20, margin_end=20)
        self.set_child(content_box)

        # Current DPI status
        status_title = Gtk.Label(label=_("Current Setting"))
        status_title.set_xalign(0)
        status_title.add_css_class("heading")
        content_box.append(status_title)

        current_dpi = self.get_current_dpi()
        status_listbox = Gtk.ListBox()
        status_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        status_listbox.add_css_class("boxed-list")
        status_row = Gtk.ListBoxRow()
        status_row.set_activatable(False)
        sbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                       margin_top=10, margin_bottom=10, margin_start=12, margin_end=12)
        status_icon = Gtk.Image.new_from_icon_name("lite-dpi")
        status_icon.set_pixel_size(28)
        sbox.append(status_icon)
        stext = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        stext.set_valign(Gtk.Align.CENTER)
        stext.set_hexpand(True)
        self._status_title_lbl = Gtk.Label(label=_("{value} DPI").format(value=current_dpi))
        self._status_title_lbl.set_xalign(0)
        self._status_sub_lbl = Gtk.Label(label=self.dpi_to_percentage(current_dpi))
        self._status_sub_lbl.set_xalign(0)
        self._status_sub_lbl.add_css_class("caption")
        stext.append(self._status_title_lbl)
        stext.append(self._status_sub_lbl)
        sbox.append(stext)
        status_row.set_child(sbox)
        status_listbox.append(status_row)
        content_box.append(status_listbox)

        # Scaling options
        scale_title = Gtk.Label(label=_("Scaling Factor"))
        scale_title.set_xalign(0)
        scale_title.add_css_class("heading")
        content_box.append(scale_title)
        scale_desc = Gtk.Label(label=_("Select a scaling factor below"))
        scale_desc.set_xalign(0)
        scale_desc.add_css_class("caption")
        content_box.append(scale_desc)

        # DPI options: (label, dpi_value)
        dpi_options = [
            ("75%", 72),
            ("100%", 96),
            ("125%", 120),
            ("150%", 144),
            ("175%", 168),
            ("200%", 192),
        ]
        
        # Create DPI buttons in a flow box
        flow_box = Gtk.FlowBox()
        flow_box.set_selection_mode(Gtk.SelectionMode.NONE)
        flow_box.set_homogeneous(True)
        flow_box.set_max_children_per_line(3)
        flow_box.set_min_children_per_line(3)
        flow_box.set_row_spacing(10)
        flow_box.set_column_spacing(10)
        
        for label, dpi in dpi_options:
            button = Gtk.Button(label=label)
            button.add_css_class("pill")
            button.set_size_request(100, 45)
            button.connect("clicked", self.on_dpi_button_clicked, dpi)
            flow_box.append(button)
        
        # Wrap flowbox in a ListBox row for consistent styling
        flow_row = Gtk.ListBoxRow()
        flow_row.set_selectable(False)
        flow_row.set_activatable(False)
        flow_row_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        flow_row_box.set_margin_top(10)
        flow_row_box.set_margin_bottom(10)
        flow_row_box.set_margin_start(10)
        flow_row_box.set_margin_end(10)
        flow_row_box.append(flow_box)
        flow_row.set_child(flow_row_box)
        
        scale_listbox = Gtk.ListBox()
        scale_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        scale_listbox.add_css_class("boxed-list")
        scale_listbox.append(flow_row)
        content_box.append(scale_listbox)

        # Actions
        reset_listbox = Gtk.ListBox()
        reset_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        reset_listbox.add_css_class("boxed-list")
        reset_listbox.connect("row-activated", lambda lb, r: self.on_reset_clicked(r))
        reset_row = Gtk.ListBoxRow()
        reset_row.set_activatable(True)
        rbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                       margin_top=10, margin_bottom=10, margin_start=12, margin_end=12)
        reset_icon = Gtk.Image.new_from_icon_name("system-restart")
        reset_icon.set_pixel_size(28)
        rbox.append(reset_icon)
        rtext = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        rtext.set_valign(Gtk.Align.CENTER)
        rtext.set_hexpand(True)
        rt = Gtk.Label(label=_("Reset to Defaults"))
        rt.set_xalign(0)
        rs = Gtk.Label(label=_("Set DPI back to 96 (100%)"))
        rs.set_xalign(0)
        rs.add_css_class("caption")
        rtext.append(rt)
        rtext.append(rs)
        rbox.append(rtext)
        reset_row.set_child(rbox)
        reset_listbox.append(reset_row)
        content_box.append(reset_listbox)
    
    def dpi_to_percentage(self, dpi):
        """Convert DPI value to percentage string"""
        dpi_map = {
            "72": "75%",
            "96": "100%",
            "120": "125%",
            "144": "150%",
            "168": "175%",
            "192": "200%",
        }
        return dpi_map.get(str(dpi), _("Custom"))
    
    def get_current_dpi(self):
        """Get current DPI setting from xrdb or xfconf"""
        try:
            # Try xrdb first
            result = subprocess.run(
                ["xrdb", "-query"],
                capture_output=True,
                text=True,
                timeout=5
            )
            for line in result.stdout.splitlines():
                if "dpi" in line.lower():
                    parts = line.split()
                    if len(parts) >= 2:
                        return parts[-1]
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        
        try:
            # Try xfconf-query
            result = subprocess.run(
                ["xfconf-query", "-c", "xsettings", "-p", "/Xft/DPI"],
                capture_output=True,
                text=True,
                timeout=5
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        
        return _("Unknown")
    
    def set_dpi(self, dpi_value):
        """Set DPI using xfconf-query"""
        try:
            subprocess.run(
                ["xfconf-query", "-c", "xsettings", "-p", "/Xft/DPI", "-s", str(dpi_value)],
                check=True,
                timeout=10
            )
            return True
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
            print(f"Error setting DPI: {e}")
            return False
    
    def on_dpi_button_clicked(self, button, dpi_value):
        """Handle DPI button click"""
        if self.set_dpi(dpi_value):
            self._status_title_lbl.set_label(_("{value} DPI").format(value=dpi_value))
            self._status_sub_lbl.set_label(self.dpi_to_percentage(dpi_value))
            self.show_notification(
                _("HiDPI Settings"),
                _("DPI set to {value} ({pct})").format(value=dpi_value, pct=self.dpi_to_percentage(dpi_value))
            )
            
            # Force window redraw after DPI change
            GLib.idle_add(self._force_redraw)
        else:
            self.show_message_dialog(
                _("Error"),
                _("Failed to set DPI. Make sure xfconf-query is available."),
                is_error=True
            )
    
    def _force_redraw(self):
        """Force the window to redraw after DPI change"""
        # Hide and show to force complete redraw
        self.set_visible(False)
        GLib.timeout_add(50, self._show_window)
        return False
    
    def _show_window(self):
        """Show the window after brief hide"""
        self.set_visible(True)
        self.queue_draw()
        return False
    
    def on_reset_clicked(self, row):
        """Handle reset button click"""
        self.on_dpi_button_clicked(None, 96)
    
    def on_info_button_clicked(self, button):
        """Show DPI/Percentages info dialog"""
        dialog = Gtk.Window(transient_for=self, modal=True)
        dialog.set_title(_("HiDPI Info"))
        dialog.set_default_size(320, 380)

        header = Gtk.HeaderBar()
        header.set_show_title_buttons(False)
        close_button = Gtk.Button(label=_("Close"))
        close_button.connect("clicked", lambda b: dialog.close())
        header.pack_end(close_button)
        dialog.set_titlebar(header)

        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
                              margin_top=20, margin_bottom=20,
                              margin_start=20, margin_end=20)
        dialog.set_child(content_box)

        info_title = Gtk.Label(label=_("DPI / Percentages"))
        info_title.set_xalign(0)
        info_title.add_css_class("heading")
        content_box.append(info_title)
        info_desc = Gtk.Label(label=_("Reference table for DPI values"))
        info_desc.set_xalign(0)
        content_box.append(info_desc)

        # DPI/Percentage data
        dpi_data = [
            ("72", "75%"),
            ("96", "100%"),
            ("120", "125%"),
            ("144", "150%"),
            ("168", "175%"),
            ("192", "200%"),
        ]

        listbox = Gtk.ListBox()
        listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        listbox.add_css_class("boxed-list")

        for dpi, percent in dpi_data:
            row = Gtk.ListBoxRow()
            row.set_activatable(False)
            rbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                           margin_top=8, margin_bottom=8, margin_start=12, margin_end=12)
            rbox.append(Gtk.Image.new_from_icon_name("preferences-desktop-display"))
            rtext = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
            rtext.set_valign(Gtk.Align.CENTER)
            rtext.set_hexpand(True)
            t = Gtk.Label(label=_("{value} DPI").format(value=dpi))
            t.set_xalign(0)
            s = Gtk.Label(label=percent)
            s.set_xalign(0)
            rtext.append(t)
            rtext.append(s)
            rbox.append(rtext)
            row.set_child(rbox)
            listbox.append(row)

        content_box.append(listbox)

        dialog.present()
    
    def show_notification(self, title, message):
        """Show an XFCE notification using notify-send"""
        try:
            subprocess.Popen([
                "notify-send",
                "-i", "preferences-desktop-display",
                title,
                message
            ])
        except FileNotFoundError:
            print(f"notify-send not found: {title} - {message}")
    
    def show_message_dialog(self, title, message, is_error=False):
        """Show a house-style result dialog"""
        LiteDialog.result(self, "error" if is_error else "info", title, message)


class HiDPIApp(Gtk.Application):
    def __init__(self, **kwargs):
        super().__init__(application_id="com.linuxlite.hidpi", **kwargs)

    def do_activate(self):
        win = HiDPISettingsWindow(application=self)
        win.present()


def main():
    app = HiDPIApp()
    app.run(None)


if __name__ == "__main__":
    main()
