"""
D-Bus laptop battery monitor applet for the Panorama panel.
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public Licence as published by
the Free Software Foundation, either version 3 of the Licence, 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 Licence for more details.

You should have received a copy of the GNU General Public Licence
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from __future__ import annotations

import threading

import panorama_panel
import locale
from pathlib import Path
from pydbus import SystemBus, SessionBus

import gi
gi.require_version("Gtk", "4.0")

from gi.repository import Gtk, Gdk, GLib


module_directory = Path(__file__).resolve().parent

locale.bindtextdomain("panorama-battery-monitor", module_directory / "locale")
_ = lambda x: locale.dgettext("panorama-battery-monitor", x)


def get_battery_icon(fraction, charging):
    if fraction < 1/32:
        base_icon = "battery-empty"
    elif fraction < 1/16:
        base_icon = "battery-caution"
    elif fraction < 1/4:
        base_icon = "battery-low"
    elif fraction < 1/2:
        base_icon = "battery-medium"
    elif fraction < 7/8:
        base_icon = "battery-good"
    else:
        base_icon = "battery-full"

    if charging:
        return base_icon + "-charging"

    return base_icon


class BatteryMonitor(panorama_panel.Applet):
    name = _("Laptop battery")
    description = _("Check laptop battery charge")

    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
        super().__init__(orientation=orientation, config=config)
        if config is None:
            config = {}

        self.bus = SystemBus()
        self.session_bus = SessionBus()
        self.notification_service = None
        self.upower = self.bus.get("org.freedesktop.UPower")

        self.button = Gtk.MenuButton(has_frame=False)
        self.icon = Gtk.Image(pixel_size=config.get("icon_size", 24))
        self.button.set_child(self.icon)
        self.append(self.button)

        self.low_threshold = config.get("low_threshold", 15/100)
        self.low_notification = None
        self.critical_threshold = config.get("critical_threshold", 5/100)
        self.critical_notification = None

        self.popover = Gtk.Popover()
        self.popover_content = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
        self.popover_content.set_size_request(320, -1)
        self.popover.set_child(self.popover_content)

        self.button.set_popover(self.popover)

        self.update_batteries()
        GLib.timeout_add_seconds(config.get("refresh_interval", 10), self.update_batteries)

    def update_batteries(self):
        self.popover_content.remove_all()

        total_energy = max_energy = 0
        any_charging = False

        for device_path in self.upower.EnumerateDevices():
            device = self.bus.get("org.freedesktop.UPower", device_path)

            # Filter only batteries
            if device.Type == 2:
                # This is in Wh but we don't care
                total_energy += device.Energy
                max_energy += device.EnergyFull

                if device.State == 1:
                    any_charging = True

                box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8)
                charge_fraction = device.Energy / device.EnergyFull
                meter = Gtk.ProgressBar(show_text=True, fraction=charge_fraction)
                box.append(meter)
                charge_label = Gtk.Label.new(_("{current} Wh of {full} Wh").format(current=locale.format_string("%.1f", device.Energy), full=locale.format_string("%.1f", device.EnergyFull)))
                charge_label.set_halign(Gtk.Align.CENTER)
                charge_label.set_xalign(0.5)
                box.append(charge_label)
                name_label = Gtk.Label.new(device.Vendor + " " + device.Model)
                name_label.set_halign(Gtk.Align.CENTER)
                name_label.set_xalign(0.5)
                box.prepend(name_label)
                box.set_margin_start(8)
                box.set_margin_end(8)
                box.set_margin_top(8)
                box.set_margin_bottom(8)
                row = Gtk.ListBoxRow(child=box, activatable=False)
                self.popover_content.append(row)

        if not max_energy:
            return False

        self.button.set_tooltip_text(f"{locale.format_string("%d", total_energy / max_energy * 1000)}‰")

        self.icon.set_from_icon_name(get_battery_icon(total_energy / max_energy, any_charging))

        GLib.idle_add(self.do_notifications, total_energy, max_energy, any_charging)

        return True

    def do_notifications(self, total_energy, max_energy, any_charging):
        if self.session_bus.get("org.freedesktop.Notifications"):
            self.notification_service = self.session_bus.get("org.freedesktop.Notifications")
        else:
            return

        # This uses threading to make sure it doesn't hang while waiting to deliver the
        # notification
        if not any_charging and total_energy / max_energy < self.low_threshold:
            if not self.low_notification:
                def send_notification():
                    self.low_notification = self.notification_service.Notify(
                        _("Battery"),
                        self.low_notification or self.critical_notification or 0,
                        "battery-low",
                        _("The energy in the battery is low."),
                        "",
                        [],
                        {},
                        -1
                    )
                threading.Thread(target=send_notification, daemon=True).start()

        elif self.low_notification:
            threading.Thread(
                target=lambda: self.notification_service.CloseNotification(self.low_notification),
                daemon=True
            ).start()

            self.low_notification = None

        if not any_charging and total_energy / max_energy < self.critical_threshold:
            if not self.critical_notification:
                def send_notification():
                    self.critical_notification = self.notification_service.Notify(
                        _("Battery"),
                        self.low_notification or self.critical_notification or 0,
                        "battery-empty",
                        _("The energy in the battery is very low; the device will shut down soon."),
                        "",
                        [],
                        {},
                        -1
                    )
                threading.Thread(target=send_notification, daemon=True).start()
        elif self.critical_notification:
            threading.Thread(
                target=lambda: self.notification_service.CloseNotification(self.critical_notification),
                daemon=True
            ).start()

            self.critical_notification = None

    def get_config(self):
        return {
            "icon_size": self.icon.get_pixel_size(),
            "low_threshold": self.low_threshold,
            "critical_threshold": self.critical_threshold,
        }

    def shutdown(self, app):
        return
