#!/usr/bin/env python3

import gi
import os
import subprocess
import json
import threading
import time
import math
import sys
from datetime import datetime

try:
    # Find all running instances of this script
    pid_output = subprocess.check_output(["pgrep", "-f", "bluelightfilter"]).decode().split()
    current_pid = str(os.getpid())
    for pid in pid_output:
        if pid != current_pid:
            # Kill the old instance
            subprocess.call(["kill", pid])
            time.sleep(0.2)
except Exception:
    pass
gi.require_version("Gtk", "3.0")
gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import Gtk, GLib, AyatanaAppIndicator3

APP_NAME = "BlueLightFilter"
ICON_NAME = "bluelightfilter"
CONFIG_PATH = os.path.expanduser("~/.config/BlueLightFilter/config.json")


class BlueLightFilter:
    def __init__(self):
        self.default_config = {
            "enabled": True,
            "output": "auto",
            "gamma_r": 0.8,
            "gamma_g": 0.7,
            "gamma_b": 0.6,
            "mode": "manual",
            "latitude": 0,
            "longitude": 0,
        }

        self.config = self.load_config()
        self.outputs = self.get_connected_outputs()

        if self.config.get("output") not in self.outputs:
            self.config["output"] = "auto"
            self.save_config()

        self.current_gamma = (1.0, 1.0, 1.0)
        self.transition_running = False

        self.create_indicator()

        if "WAYLAND_DISPLAY" in os.environ:
            self.show_wayland_warning()

        GLib.timeout_add_seconds(10, self.monitor_hotplug_watch)
        GLib.timeout_add_seconds(60, self.refresh_status)

        if self.config.get("enabled"):
            self.apply_gamma()

    # ---------------- CONFIG ----------------

    def load_config(self):
        if not os.path.exists(CONFIG_PATH):
            os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
            self.save_config(self.default_config)
            return dict(self.default_config)

        try:
            with open(CONFIG_PATH) as f:
                data = json.load(f)
                merged = dict(self.default_config)
                merged.update(data)
                return merged
        except Exception:
            return dict(self.default_config)

    def save_config(self, data=None):
        if data:
            self.config = data
        os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
        with open(CONFIG_PATH, "w") as f:
            json.dump(self.config, f, indent=4)

    # ---------------- XRANDR ----------------

    def get_connected_outputs(self):
        try:
            result = subprocess.check_output(["xrandr"]).decode()
            return [l.split()[0] for l in result.splitlines() if " connected" in l]
        except Exception:
            return ["default"]

    def is_output_active(self, output):
        try:
            result = subprocess.check_output(["xrandr"]).decode()
            for line in result.splitlines():
                if line.startswith(output) and " connected" in line and "+" in line:
                    return True
        except Exception:
            pass
        return False

    def apply_gamma_values(self, r, g, b):
        for out in self.outputs:
            if not self.is_output_active(out):
                continue
            subprocess.call([
                "xrandr", "--output", out,
                "--gamma", f"{r}:{g}:{b}"
            ])

    def transition_gamma(self, r, g, b, duration=1.0, steps=20):
        if self.transition_running:
            return

        self.transition_running = True
        sr, sg, sb = self.current_gamma

        def worker():
            for i in range(1, steps + 1):
                t = i / steps
                nr = sr + (r - sr) * t
                ng = sg + (g - sg) * t
                nb = sb + (b - sb) * t

                GLib.idle_add(self.apply_gamma_values, nr, ng, nb)
                time.sleep(duration / steps)

            self.current_gamma = (r, g, b)
            self.transition_running = False

        threading.Thread(target=worker, daemon=True).start()

    def apply_gamma(self):
        if not self.config.get("enabled"):
            self.reset_gamma()
            return

        mode = self.config.get("mode", "manual")

        if mode == "intel":
            if os.path.exists("/sys/class/backlight/intel_backlight"):
                try:
                    with open("/sys/class/backlight/intel_backlight/max_brightness") as f:
                        maxb = int(f.read().strip())
                    with open("/sys/class/backlight/intel_backlight/brightness", "w") as f:
                        f.write(str(int(maxb * 0.5)))
                    return
                except Exception:
                    pass

        if mode == "sunset":
            rise, set_ = self.compute_sun_times(
                self.config["latitude"], self.config["longitude"]
            )
            if rise and set_:
                now = datetime.now()
                if not (now > set_ or now < rise):
                    self.reset_gamma()
                    return

        r = self.config["gamma_r"]
        g = self.config["gamma_g"]
        b = self.config["gamma_b"]

        self.transition_gamma(r, g, b)
        self.update_tray_icon()
        self.rebuild_menu()

    def reset_gamma(self):
        self.transition_gamma(1.0, 1.0, 1.0)
        self.update_tray_icon()
        self.rebuild_menu()

    # ---------------- SUN ----------------

    def compute_sun_times(self, lat, lon):
        try:
            lat_rad = math.radians(lat)
            n = datetime.utcnow().timetuple().tm_yday

            m = (0.9856 * n) - 3.289
            L = m + (1.916 * math.sin(math.radians(m))) + (0.020 * math.sin(math.radians(2 * m))) + 282.634
            L %= 360

            RA = math.degrees(math.atan(0.91764 * math.tan(math.radians(L))))
            RA %= 360
            RA /= 15

            sinDec = 0.39782 * math.sin(math.radians(L))
            cosDec = math.cos(math.asin(sinDec))

            cosH = (math.cos(math.radians(90.833)) - (sinDec * math.sin(lat_rad))) / (cosDec * math.cos(lat_rad))
            if cosH > 1 or cosH < -1:
                return None, None

            Hrise = (360 - math.degrees(math.acos(cosH))) / 15
            Hset = math.degrees(math.acos(cosH)) / 15

            T_rise = Hrise + RA - (0.06571 * n) - 6.622
            T_set = Hset + RA - (0.06571 * n) - 6.622

            def fix(t):
                t %= 24
                return datetime.now().replace(hour=int(t), minute=int((t * 60) % 60), second=0)

            return fix(T_rise), fix(T_set)
        except Exception:
            return None, None

    # ---------------- STATUS ----------------

    def get_status_text(self):
        if not self.config.get("enabled"):
            return "✖️ Disabled"

        mode = self.config.get("mode")

        if mode == "manual":
            return "💡 Manual Mode"

        if mode == "intel":
            return "🔅 Intel Mode"

        if mode == "sunset":
            rise, set_ = self.compute_sun_times(
                self.config["latitude"], self.config["longitude"]
            )
            if rise and set_:
                now = datetime.now()
                if now > set_ or now < rise:
                    return "🌙 Night Mode"
                else:
                    return "🌞️ Day Mode"

        return "Unknown"

    # ---------------- UI ----------------

    def create_indicator(self):
        self.indicator = AyatanaAppIndicator3.Indicator.new(
            "bluelightfilter",
            ICON_NAME,
            AyatanaAppIndicator3.IndicatorCategory.APPLICATION_STATUS
        )
        self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)
        self.rebuild_menu()

    def update_tray_icon(self):
        icon = ICON_NAME + "-active" if self.config.get("enabled") else ICON_NAME
        self.indicator.set_icon(icon)

    def rebuild_menu(self):
        menu = Gtk.Menu()

        status_item = Gtk.MenuItem(label=self.get_status_text())
        status_item.set_sensitive(False)
        menu.append(status_item)
        menu.append(Gtk.SeparatorMenuItem())

        toggle = Gtk.CheckMenuItem(label="Enabled")
        toggle.set_active(self.config.get("enabled"))
        toggle.connect("toggled", self.toggle_enabled)
        menu.append(toggle)

        prefs = Gtk.MenuItem(label="⚙️ Preferences")
        prefs.connect("activate", self.open_preferences_window)
        menu.append(prefs)

        about = Gtk.MenuItem(label="❓️ About")
        about.connect("activate", self.show_about_dialog)
        menu.append(about)

        quit_item = Gtk.MenuItem(label="❌ Quit")
        quit_item.connect("activate", self.quit)
        menu.append(quit_item)

        menu.show_all()
        self.indicator.set_menu(menu)

    def toggle_enabled(self, widget):
        self.config["enabled"] = widget.get_active()
        self.save_config()
        self.apply_gamma()

    def show_about_dialog(self, *args):
        dialog = Gtk.AboutDialog()
        dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
        dialog.set_program_name(APP_NAME)
        dialog.set_version("1.0.7")
        dialog.set_comments("Simple blue light filter using xrandr")
        dialog.set_website("https://www.pclosdebian.com/")
        dialog.set_license("© 2026 WTFPL – Do What the Fuck You Want to Public License.\n\nEveryone is permitted to copy and distribute verbatim or modified\ncopies of this license document, and changing it is allowed as long\nas the name is changed.")
        dialog.set_icon_from_file("/usr/share/icons/hicolor/48x48/apps/bluelightfilter-active.png")
        dialog.set_logo_icon_name(ICON_NAME + "-active")
        dialog.run()
        dialog.destroy()

    def quit(self, *args):
        Gtk.main_quit()

    # ---------------- PREFS ----------------

    def open_preferences_window(self, *args):
        win = Gtk.Window(title="Preferences")
        win.set_default_size(420, 260)
        win.set_position(Gtk.WindowPosition.CENTER)
        win.set_modal(True)
        win.set_resizable(False)

        grid = Gtk.Grid(margin=10, row_spacing=10, column_spacing=10)
        win.add(grid)

        # --- Mode ---
        grid.attach(Gtk.Label(label="Mode:"), 0, 0, 1, 1)
        combo = Gtk.ComboBoxText()
        combo.append_text("Manual")
        combo.append_text("Sunset")
        combo.append_text("Intel")
        mode_map = {"manual": 0, "sunset": 1, "intel": 2}
        combo.set_active(mode_map.get(self.config["mode"], 0))

        def change_mode(c):
            modes = ["manual", "sunset", "intel"]
            self.config["mode"] = modes[c.get_active()]
            self.save_config()
            self.apply_gamma()

        combo.connect("changed", change_mode)
        grid.attach(combo, 1, 0, 1, 1)

        # --- Gamma sliders ---
        def slider(label_text, key, row):
            lbl = Gtk.Label(label=label_text)
            lbl.set_halign(Gtk.Align.START)
            grid.attach(lbl, 0, row, 1, 1)

            scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0.1, 1.0, 0.01)
            scale.set_value(self.config[key])
            scale.set_hexpand(True)
            scale.set_halign(Gtk.Align.FILL)
            scale.set_digits(2)

            def on_change(s):
                self.config[key] = round(s.get_value(), 2)
                self.save_config()
                if self.config["mode"] == "manual":
                    self.apply_gamma()

            scale.connect("value-changed", on_change)
            grid.attach(scale, 1, row, 1, 1)

        slider("Red Gamma:", "gamma_r", 1)
        slider("Green Gamma:", "gamma_g", 2)
        slider("Blue Gamma:", "gamma_b", 3)

        # --- Autostart ---
        auto = Gtk.CheckButton(label="Start at login")
        auto.set_active(os.path.exists(os.path.expanduser("~/.config/autostart/bluelightfilter.desktop")))
        auto.connect("toggled", lambda w: self.set_autostart(w.get_active()))
        grid.attach(auto, 0, 4, 2, 1)

        win.show_all()

    # ---------------- AUTOSTART ----------------

    def set_autostart(self, enabled):
        path = os.path.expanduser("~/.config/autostart")
        os.makedirs(path, exist_ok=True)
        desktop = os.path.join(path, "bluelightfilter.desktop")

        if enabled:
            with open(desktop, "w") as f:
                f.write("""[Desktop Entry]
Type=Application
Exec=bluelightfilter
Name=Blue Light Filter
""")
        else:
            if os.path.exists(desktop):
                os.remove(desktop)

    # ---------------- HOTPLUG ----------------

    def monitor_hotplug_watch(self):
        current = self.get_connected_outputs()
        if current != self.outputs:
            self.outputs = current
            self.apply_gamma()
        return True

    def refresh_status(self):
        self.rebuild_menu()
        return True

    # ---------------- WARN ----------------

    def show_wayland_warning(self):
        dialog = Gtk.MessageDialog(
            message_type=Gtk.MessageType.WARNING,
            buttons=Gtk.ButtonsType.OK,
            text="Wayland detected"
        )
        dialog.format_secondary_text("xrandr may not work.")
        dialog.run()
        dialog.destroy()


if __name__ == "__main__":
    app = BlueLightFilter()
    Gtk.main()
