import { execAsync, register, timeout, Variable, type Time } from "astal";
import { App, Astal, Gtk, Widget } from "astal/gtk3";
import cairo from "cairo";
import AstalWp from "gi://AstalWp";
import Cairo from "gi://cairo";
import Pango from "gi://Pango";
import PangoCairo from "gi://PangoCairo";
import { osds as config } from "../config";
import Monitors, { type Monitor } from "../services/monitors";
import { PopupWindow } from "../utils/widgets";

const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;

const mix = (a: number, b: number, r: number) => a * r + b * (1 - r);

const pangoWeightToStr = (weight: Pango.Weight) => {
    switch (weight) {
        case Pango.Weight.ULTRALIGHT:
            return "UltraLight";
        case Pango.Weight.LIGHT:
            return "Light";
        case Pango.Weight.BOLD:
            return "Bold";
        case Pango.Weight.ULTRABOLD:
            return "UltraBold";
        case Pango.Weight.HEAVY:
            return "Heavy";
        default:
            return "Normal";
    }
};

const SliderOsd = ({
    fillIcons,
    monitor,
    type,
    windowSetup,
    className = "",
    initValue,
    drawAreaSetup,
}: {
    fillIcons?: boolean;
    monitor?: Monitor;
    type: "volume" | "brightness";
    windowSetup: (self: Widget.Window, show: () => void) => void;
    className?: string;
    initValue: number;
    drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void;
}) => (
    <PopupWindow
        name={type}
        monitor={monitor?.id}
        keymode={Astal.Keymode.NONE}
        anchor={config[type].position}
        margin={config[type].margin}
        setup={self => {
            let time: Time | null = null;
            const hideAfterTimeout = () => {
                time?.cancel();
                time = timeout(config[type].hideDelay, () => self.hide());
            };
            self.connect("show", hideAfterTimeout);
            windowSetup(self, () => {
                self.show();
                hideAfterTimeout();
            });
        }}
    >
        <box className={type}>
            <drawingarea
                className={`inner ${className}`}
                css={"font-size: " + initValue + "px;"}
                setup={self => {
                    const halfPi = Math.PI / 2;
                    const vertical =
                        config[type].position === Astal.WindowAnchor.LEFT ||
                        config[type].position === Astal.WindowAnchor.RIGHT;

                    const icon = Variable("");
                    drawAreaSetup(self, icon);
                    self.hook(icon, () => self.queue_draw());

                    // Init size
                    const styleContext = self.get_style_context();
                    const width = getNumStyle(styleContext, "min-width");
                    const height = getNumStyle(styleContext, "min-height");
                    if (vertical) self.set_size_request(height, width);
                    else self.set_size_request(width, height);

                    let fontDesc: Pango.FontDescription | null = null;

                    self.connect("draw", (_, cr: cairo.Context) => {
                        const parent = self.get_parent();
                        if (!parent) return;

                        const styleContext = self.get_style_context();
                        const pContext = parent.get_style_context();

                        let width = getNumStyle(styleContext, "min-width");
                        let height = getNumStyle(styleContext, "min-height");

                        const progressValue = getNumStyle(styleContext, "font-size");
                        let radius = getNumStyle(pContext, "border-radius");
                        // Flatten when near 0, do before swap cause its simpler
                        radius = Math.min(radius, Math.min(width * progressValue, height) / 2);

                        if (vertical) [width, height] = [height, width]; // Swap if vertical
                        self.set_size_request(width, height);

                        const progressPosition = vertical
                            ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom
                            : width * progressValue - radius;

                        const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
                        cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);

                        // Background
                        if (vertical) {
                            cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left
                            cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right
                            cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
                        } else {
                            cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
                            cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
                            cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
                        }
                        cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
                        cr.fill();

                        const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL);
                        cr.setAntialias(cairo.Antialias.BEST);

                        // Progress number, at top/right
                        let nw = 0;
                        let nh = 0;
                        if (config[type].showValue) {
                            const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100)));
                            [nw, nh] = numLayout.get_pixel_size();
                            let diff;
                            if (vertical) {
                                diff = ((1 - progressValue) * height) / nh;
                                cr.moveTo((width - nw) / 2, radius / 2);
                            } else {
                                diff = ((1 - progressValue) * width) / nw;
                                cr.moveTo(width - nw - radius, (height - nh) / 2);
                            }
                            diff = Math.max(0, Math.min(1, diff));

                            cr.setSourceRGBA(
                                mix(bg.red, fg.red, diff),
                                mix(bg.green, fg.green, diff),
                                mix(bg.blue, fg.blue, diff),
                                mix(bg.alpha, fg.alpha, diff)
                            );

                            PangoCairo.show_layout(cr, numLayout);
                        }

                        // Progress icon, follows progress
                        if (fontDesc === null) {
                            const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight);
                            const size = getNumStyle(pContext, "font-size") * 1.5;
                            fontDesc = Pango.font_description_from_string(
                                `Material Symbols Rounded ${weight} ${size}px`
                            );
                            // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout
                            if (fillIcons) fontDesc.set_variations("FILL=1");
                        }

                        const iconLayout = PangoCairo.create_layout(cr);
                        iconLayout.set_font_description(fontDesc);
                        iconLayout.set_text(icon.get(), -1);

                        const [iw, ih] = iconLayout.get_pixel_size();
                        let diff;
                        if (vertical) {
                            diff = (progressValue * height) / ih;
                            cr.moveTo(
                                (width - iw) / 2,
                                Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius))
                            );
                        } else {
                            diff = (progressValue * width) / iw;
                            cr.moveTo(
                                Math.min(
                                    width - nw * 1.1 - iw - radius,
                                    Math.max(0, progressPosition - iw / 2 - radius)
                                ),
                                (height - ih) / 2
                            );
                        }
                        diff = Math.max(0, Math.min(1, diff));

                        cr.setSourceRGBA(
                            mix(fg.red, bg.red, diff),
                            mix(fg.green, bg.green, diff),
                            mix(fg.blue, bg.blue, diff),
                            mix(fg.alpha, bg.alpha, diff)
                        );

                        PangoCairo.show_layout(cr, iconLayout);
                    });
                }}
            />
        </box>
    </PopupWindow>
);

const Volume = ({ audio }: { audio: AstalWp.Audio }) => (
    <SliderOsd
        fillIcons
        type="volume"
        windowSetup={(self, show) => {
            self.hook(audio.defaultSpeaker, "notify::volume", show);
            self.hook(audio.defaultSpeaker, "notify::mute", show);
        }}
        className={audio.defaultSpeaker.mute ? "mute" : ""}
        initValue={audio.defaultSpeaker.volume}
        drawAreaSetup={(self, icon) => {
            const updateIcon = () => {
                if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones");
                else if (audio.defaultSpeaker.mute) icon.set("no_sound");
                else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute");
                else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down");
                else icon.set("volume_up");
            };
            updateIcon();
            self.hook(audio.defaultSpeaker, "notify::icon", updateIcon);
            self.hook(audio.defaultSpeaker, "notify::mute", () => {
                updateIcon();
                self.toggleClassName("mute", audio.defaultSpeaker.mute);
            });
            self.hook(audio.defaultSpeaker, "notify::volume", () => {
                updateIcon();
                self.css = `font-size: ${audio.defaultSpeaker.volume}px`;
            });
        }}
    />
);

const Brightness = ({ monitor }: { monitor: Monitor }) => (
    <SliderOsd
        monitor={monitor}
        type="brightness"
        windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)}
        initValue={monitor.brightness}
        drawAreaSetup={(self, icon) => {
            const update = () => {
                if (monitor.brightness > 0.66) icon.set("brightness_high");
                else if (monitor.brightness > 0.33) icon.set("brightness_medium");
                else if (monitor.brightness > 0) icon.set("brightness_low");
                else icon.set("brightness_empty");
                self.css = `font-size: ${monitor.brightness}px`;
            };
            self.hook(monitor, "notify::brightness", update);
            update();
        }}
    />
);

@register()
class LockOsd extends Widget.Window {
    readonly lockType: "caps" | "num";

    #timeout: Time | null = null;

    constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) {
        super({
            visible: false,
            name: `lock-${type}`,
            application: App,
            namespace: `caelestia-lock-${type}`,
            anchor:
                Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
            exclusivity: Astal.Exclusivity.IGNORE,
        });

        this.lockType = type;
        this.#update();

        this.add(
            <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}>
                <label vexpand className="icon" label={icon} />
                <label vexpand className="text" label={type.slice(0, 1).toUpperCase() + type.slice(1) + "lock"} />
            </box>
        );

        // Clickthrough
        this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region()));

        // Move over when other indicator opens/closes
        this.hook(App, "window-toggled", (_, window) => {
            if (window !== this && window instanceof LockOsd) {
                const child = this.get_child();
                if (!child) return;
                this[right ? "marginLeft" : "marginRight"] = window.visible
                    ? child.get_preferred_width()[1] + config.lock.spacing
                    : 0;
            }
        });
    }

    #update() {
        execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`)
            .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1")))
            .catch(console.error);
    }

    show() {
        super.show();
        this.#update();
        this.#timeout?.cancel();
        this.#timeout = timeout(config.lock[this.lockType].hideDelay, () => this.hide());
    }
}

export default () => {
    if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />;
    Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />);

    <LockOsd type="caps" icon="keyboard_capslock" />;
    <LockOsd right type="num" icon="filter_1" />;

    return null;
};
