using Posix;
using SwaySettings;
using Utils;

public class TimeObj : Object {
    private DateTime date_time;

    public signal void update ();

    public TimeObj () {
        timeout ();
    }

    private void timeout () {
        date_time = new DateTime.now_local ();

        // Compute ms until the next minute boundary
        int sec = date_time.get_second ();
        int usec = date_time.get_microsecond ();
        uint delay_ms = (60 - sec) * 1000 - usec / 1000;
        update ();

        Timeout.add_once (delay_ms, timeout);
    }

    public string get_time () {
        return date_time.format ("%H:%M");
    }

    public string get_date () {
        return date_time.format ("%d %b");
    }
}

static TimeObj time_object;

static Settings self_settings;
static Gtk.Application app;
static AccountsService.Manager user_mgr;
static GtkSessionLock.Instance instance;

private static bool should_daemonize = false;
private static bool should_lock = true;

class Main : Object {
    private static Posix.pid_t parent_pid = int.MIN;

    private static bool activated = false;

    private static unowned ListModel monitors;
    private static ListStore windows;

    private const OptionEntry[] ENTRIES = {
        {
            "daemonize",
            0,
            OptionFlags.NONE,
            OptionArg.NONE,
            ref should_daemonize,
            "Detach from the terminal once locked",
            null
        },
        {
            "debug-do-not-lock",
            0,
            OptionFlags.REVERSE,
            OptionArg.NONE,
            ref should_lock,
            "Doesn't start a session lock, opens in a regular window",
            null
        },
        { null }
    };

    private static void parse_args (owned string[] args) {
        try {
            OptionContext context = new OptionContext ();
            context.set_help_enabled (true);
            context.add_main_entries (ENTRIES, null);
            context.parse_strv (ref args);
        } catch (Error e) {
            error (e.message);
        }
    }

    public static int main (string[] args) {
        parse_args (args);

        if (should_daemonize) {
            daemonize ();
        }

        Gtk.init ();
        Adw.init ();

        user_mgr = new AccountsService.Manager ();
        time_object = new TimeObj ();

        self_settings = new Settings ("org.erikreider.swaysettings");

        app = new Gtk.Application (AppIds.LOCKER,
                                   ApplicationFlags.FLAGS_NONE);
        app.activate.connect ((g_app) => {
            if (activated) {
                return;
            }
            activated = true;
            app.hold ();
            init.begin ();
        });

        try {
            app.register ();
            if (app.get_is_remote ()) {
                signal_daemon ();
                return 0;
            }
        } catch (Error e) {
            error (e.message);
        }
        // Load custom CSS
        Gtk.CssProvider css_provider = new Gtk.CssProvider ();
        css_provider.load_from_resource (
            "/org/erikreider/swaysettings/style/locker.css");
        Widgets.add_style_provider_for_display (Gdk.Display.get_default (),
                                                css_provider,
                                                Gtk.STYLE_PROVIDER_PRIORITY_USER);

        // Init resources
        var theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ());
        theme.add_resource_path ("/org/erikreider/swaysettings/icons");

        return app.run ();
    }

    private static async void init_lock () {
        if (should_lock) {
            GLib.assert (GtkSessionLock.is_supported ());
            instance = new GtkSessionLock.Instance ();
            assert_nonnull (instance);

            instance.locked.connect (locked);
            instance.unlocked.connect (unlocked);
            instance.failed.connect (failed);

            monitors.items_changed.connect (monitors_changed);
        }

        uint n_items = monitors.get_n_items ();
        int count = (int) n_items;
        for (uint i = 0; i < n_items; i++) {
            Gdk.Monitor monitor = (Gdk.Monitor) monitors.get_item (i);
            LockerWindow win = new LockerWindow (app, monitor);
            win.load_content.begin ((obj, res) => {
                win.load_content.end (res);
                if (AtomicInt.dec_and_test (ref count)) {
                    init_lock.callback ();
                }
            });
            windows.insert (i, win);
        }
        yield;
    }

    private static bool inited = false;
    private static async void init () {
        if (!inited) {
            windows = new ListStore (typeof (LockerWindow));

            Gdk.Display ?display = Gdk.Display.get_default ();
            assert_nonnull (display);

            monitors = display.get_monitors ();

            yield init_lock ();

            inited = true;
        }

        if (should_lock) {
            instance.lock ();
            for (uint i = 0; i < windows.get_n_items (); i++) {
                LockerWindow win = (LockerWindow) windows.get_item (i);
                instance.assign_window_to_monitor (win, win.monitor);
            }
        } else {
            // For debugging, doesn't start as a session-lock session
            locked.begin ();
            for (uint i = 0; i < windows.get_n_items (); i++) {
                LockerWindow win = (LockerWindow) windows.get_item (i);
                win.close_request.connect (() => {
                    unlocked ();
                    app.quit ();
                    return false;
                });
                win.present ();
            }
        }
    }

    private static void monitors_changed (uint position,
                                          uint removed,
                                          uint added) {
        for (uint i = 0; i < removed; i++) {
            windows.remove (position + i);
        }

        for (uint i = 0; i < added; i++) {
            Gdk.Monitor monitor = (Gdk.Monitor) monitors.get_item (position +
                                                                   i);
            LockerWindow win = new LockerWindow (app, monitor);
            windows.insert (position + i, win);
            win.load_content.begin ((obj, res) => {
                win.load_content.end (res);
                instance.assign_window_to_monitor (win, monitor);
            });
        }
    }

    private static async void locked () {
        // Wait for all windows to be mapped before daemonizing
        uint timeout_id = 0;
        uint n_items = monitors.get_n_items ();
        int count = (int) n_items;
        for (uint i = 0; i < n_items; i++) {
            LockerWindow win = (LockerWindow) windows.get_item (i);
            if (win.get_mapped () && win.get_realized ()) {
                count--;
                continue;
            }

            ulong map_id = 0;
            map_id = win.map.connect (() => {
                win.disconnect (map_id);
                if (timeout_id == 0) {
                    return;
                }

                if (AtomicInt.dec_and_test (ref count)) {
                    // All windows are mapped, cancel the timeout
                    if (timeout_id > 0) {
                        Source.remove (timeout_id);
                        timeout_id = 0;
                    }
                    locked.callback ();
                }
            });
        }
        timeout_id = Timeout.add_seconds_once (1, () => {
            timeout_id = 0;
            locked.callback ();
        });
        yield;

        // Kill the parent if daemonized
        signal_daemon ();
    }

    private static void unlocked () {
        app.release ();
    }

    private static int lock_tries = 3;
    private static async void failed () {
        GLib.stderr.printf ("Failed to lock. Retrying\n");
        if (lock_tries > 0) {
            lock_tries--;

            yield init ();
        }
    }

    //
    // Daemonization logic
    //

    static int got_sig = 0;
    private static void sig_handler (int sig) {
        AtomicInt.set (ref got_sig, 1);
    }

    private static void daemonize () {
        parent_pid = getpid ();

        if (signal (Posix.Signal.USR2, sig_handler) == SIG_ERR) {
            error ("signal");
        }

        pid_t pid;
        switch (pid = fork ()) {
            case -1:
                error ("Fork PID error");
            case 0:
                break;
            default:
                sigset_t sig_set;
                sigemptyset (out sig_set);
                sigaddset (ref sig_set, Posix.Signal.USR2);
                sigprocmask (SIG_BLOCK, sig_set, null);
                int sig;
                if (sigwait (sig_set, out sig) != 0) {
                    exit (Posix.EXIT_FAILURE);
                }
                exit (Posix.EXIT_SUCCESS);
                break;
        }

        if (setsid () < 0) {
            error ("setsid error");
        }
        switch (pid = fork ()) {
            case -1:
                error ("Fork 2 PID error");
            case 0:
                break;
            default:
                exit (Posix.EXIT_SUCCESS);
                break;
        }
    }

    private static void signal_daemon () {
        if (parent_pid > 0) {
            kill (parent_pid, Posix.Signal.USR2);
        }
    }
}
