import { execAsync } from "ags/process";
import { timeout } from "ags/time";
import { monitorFile, readFile } from "ags/file";
import GObject, { register, getter } from "ags/gobject";

import AstalIO from "gi://AstalIO";
import Gio from "gi://Gio?version=2.0";
import GLib from "gi://GLib?version=2.0";
import { decoder, encoder } from "./utils";


export { Wallpaper };

type WalData = {
    checksum: string;
    wallpaper: string;
    alpha: number;
    special: {
        background: string;
        foreground: string;
        cursor: string;
    };
    colors: {
        color0: string;
        color1: string;
        color2: string;
        color3: string;
        color4: string;
        color5: string;
        color6: string;
        color7: string;
        color8: string;
        color9: string;
        color10: string;
        color11: string;
        color12: string;
        color13: string;
        color14: string;
        color15: string;
    };
};

@register({ GTypeName: "Wallpaper" })
class Wallpaper extends GObject.Object {
    private static instance: Wallpaper;
    #wallpaper: (string|undefined);
    #splash: boolean = true;
    #monitor: Gio.FileMonitor;
    #hyprpaperFile: Gio.File;
    #wallpapersPath: string;
    #ignoreWatch: boolean = false;

    @getter(Boolean)
    public get splash() { return this.#splash; }
    public set splash(showSplash: boolean) {
        this.#splash = showSplash;
        this.notify("splash");
    }

    /** current wallpaper's complete path
    * can be an empty string if undefined */
    @getter(String)
    public get wallpaper() { return this.#wallpaper ?? ""; }
    public set wallpaper(newValue: string) { this.setWallpaper(newValue); }

    public get wallpapersPath() { return this.#wallpapersPath; }

    constructor() {
        super();

        this.#wallpapersPath = GLib.getenv("WALLPAPERS") ?? 
            `${GLib.get_home_dir()}/wallpapers`;

        this.#hyprpaperFile = Gio.File.new_for_path(`${
            GLib.get_user_config_dir()}/hypr/hyprpaper.conf`);

        this.getWallpaper().then((wall) => {
            if(wall?.trim()) this.#wallpaper = wall.trim();
        });

        let tmeout: (AstalIO.Time|undefined) = undefined;

        this.#monitor = monitorFile(this.#hyprpaperFile.get_path()!, (_, event) => {
            if(event !== Gio.FileMonitorEvent.CHANGED && event !== Gio.FileMonitorEvent.CREATED &&
              event !== Gio.FileMonitorEvent.MOVED_IN)
                return;

            if(tmeout) return;
            else tmeout = timeout(1500, () => tmeout = undefined);

            if(this.#ignoreWatch) {
                this.#ignoreWatch = false;
                return;
            }

            const [ loaded, text ] = this.#hyprpaperFile.load_contents(null);
            if(!loaded) 
                console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!");

            const content = decoder.decode(text);

            if(content) {
                let setWall: boolean = true;

                for(const line of content.split('\n')) {
                    if(line.trim().startsWith('#'))
                        continue;

                    const lineSplit = line.split('=');
                    const key = lineSplit[0].trim(),
                        value = lineSplit.filter((_, i) => i !== 0).join('=').trim();

                    switch(key) {
                        case "splash": 
                            this.splash = (/(yes|true|on|enable|enabled|1).*/.test(value)) ? true : false;
                            break;

                        case "wallpaper":
                            if(this.#wallpaper !== value && setWall) {
                                this.setWallpaper(value, false);
                                setWall = false; // wallpaper already set
                            }

                            break;
                    }
                }
            }
        });
    }

    vfunc_dispose(): void {
        this.#monitor.cancel();
    }

    public static getDefault(): Wallpaper {
        if(!this.instance)
            this.instance = new Wallpaper();

        return this.instance;
    }

    private writeChanges(): void {
        this.#ignoreWatch = true; // tell monitor to ignore file replace
        this.#hyprpaperFile.replace_async(null, false,
            Gio.FileCreateFlags.REPLACE_DESTINATION,
            GLib.PRIORITY_DEFAULT, null, (_, result) => {
                const res = this.#hyprpaperFile.replace_finish(result);
                if(res) {
                    // success
                    this.#ignoreWatch = true; // tell monitor to ignore this change
                    res.write_bytes_async(encoder.encode(`# This file was automatically generated by color-shell

                        preload = ${this.#wallpaper}
                        splash = ${this.#splash}
                        wallpaper = , ${this.#wallpaper}`.split('\n').map(str => str.trimStart()).join('\n')),
                        GLib.PRIORITY_DEFAULT, null, (_, asyncRes) => {
                            if(_!.write_finish(asyncRes)) res.flush(null);
                            res.close(null);
                        }
                    );

                    return;
                }

                console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`);
            }
        );
    }

    public getData(): WalData {
        const content = readFile(`${GLib.getenv("XDG_CACHE_HOME")}/wal/colors.json`);
        return JSON.parse(content) as WalData;
    }

    public async getWallpaper(): Promise<string|undefined> {
        return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => {
            const loaded: (string|undefined) = stdout.split('=')[1]?.trim();

            if(!loaded) 
                console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`);

            return loaded;
        }).catch((err: Gio.IOErrorEnum) => {
            console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`);
            return undefined;
        });
    }

    public reloadColors(): void {
        execAsync(`wal -t --cols16 darken -i "${this.#wallpaper}"`).then(() => {
            console.log("Wallpaper: reloaded shell colors");
        }).catch(r => {
            console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`);
        });
    }

    public setWallpaper(path: string|Gio.File, write: boolean = true): void {
        execAsync("hyprctl hyprpaper unload all").then(() => 
            execAsync(`hyprctl hyprpaper preload ${path}`).then(() => 
                execAsync(`hyprctl hyprpaper wallpaper ${path}`).then(() => {
                    this.#wallpaper = (typeof path === "string") ? path : path.get_path()!;
                    this.reloadColors();
                    write && this.writeChanges();
                }).catch(r => {
                    console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`);
                })
            ).catch(r => {
                console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`);
            })
        ).catch(r => {
            console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`);
        });
    }

    public async pickWallpaper(): Promise<string|undefined> {
        return (await execAsync(`zenity --file-selection`).then(wall => {
            if(!wall.trim()) return undefined;

            this.setWallpaper(wall);
            return wall;
        }).catch(r => {
            console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`);
            return undefined;
        }));
    }
}
