/*
    Copyright (c) 2016 - 2026 Christian Schoenebeck

    This file is part of "gigedit" and released under the terms of the
    GNU General Public License version 2.
*/

#include "global.h"
#include "ManagedWindow.h"
#include <typeinfo>
#ifdef GLIB_HEADER_FILE
# include GLIB_HEADER_FILE(glib.h)
#else
# include <glib.h>
#endif

///////////////////////////////////////////////////////////////////////////
// class 'ManagedWindow'

ManagedWindow::ManagedWindow() :
    m_parent(NULL),
    m_listenOnConfigureEvents(false)
{
    initManagedWindow();
}

ManagedWindow::ManagedWindow(Gtk::Window& parent) :
    m_parent(&parent),
    m_listenOnConfigureEvents(false)
{
    if (Settings::singleton()->subwindowsFollowMainWindow)
        set_transient_for(parent);

    initManagedWindow();
}

ManagedWindow::~ManagedWindow() {
    m_connectionSubwindowsFollowMainWindow.disconnect();
    m_connectionOnceRestoreWindowDimensions.disconnect();
    m_connectionOnceEnableListeningConfigureEvents.disconnect();
}

void ManagedWindow::initManagedWindow() {
    m_connectionSubwindowsFollowMainWindow =
        Settings::singleton()->subwindowsFollowMainWindow.get_proxy().signal_changed().connect([this]() {
            const bool follow = Settings::singleton()->subwindowsFollowMainWindow;
            onSubwindowsFollowMainWindowChanged(follow);
        });

    m_connectionOnceRestoreWindowDimensions = Glib::signal_idle().connect(
        [this]() {
            restoreWindowDimensions();

            // replicate connect_once() behaviour:
            m_connectionOnceRestoreWindowDimensions.disconnect();
            return false; // don't call aagain
        },
        // timeout starts given amount of ms after main loop became idle again:
        0
    );
    //HACK: Gtk does not support to distinguish between user caused window resize/move and programmtical window resize/move, so as a workaround we ignore such events for a certain amount of time while this window is constructing
    m_connectionOnceEnableListeningConfigureEvents = Glib::signal_idle().connect(
        [this]() {
            enableListeningConfigureEvents();

            // replicate connect_once() behaviour:
            m_connectionOnceEnableListeningConfigureEvents.disconnect();
            return false; // don't call aagain
        },
        // timeout starts given amount of ms after main loop became idle again:
        300
    );
}

#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
bool ManagedWindow::on_configure_event(Gdk::EventConfigure& event) {
    GdkEventConfigure* e = event.gobj();
#else
bool ManagedWindow::on_configure_event(GdkEventConfigure* e) {
#endif
    //printf("on_configure_event x=%d y=%d w=%d h=%d\n", e->x, e->y, e->width, e->height);
    if (m_listenOnConfigureEvents) {
        //printf("reset event throttle timer\n");
        // invalidate timer (so it won't fire)
        if (m_eventThrottleTimer) m_eventThrottleTimer->destroy();
        // restart timer
        m_eventThrottleTimer = Glib::TimeoutSource::create(300);
        const GdkEventConfigure ev = *e;
        m_eventThrottleTimer->connect([this, ev]() -> bool {
            return saveWindowDimensions(ev.x, ev.y, ev.width, ev.height);
        });
        m_eventThrottleTimer->attach(Glib::MainContext::get_default());
    }
#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
    return Gtk::Window::on_configure_event(event);
#else
    return Gtk::Window::on_configure_event(e);
#endif
}

void ManagedWindow::onSubwindowsFollowMainWindowChanged(bool follow) {
    Gtk::Window* parent = parentWindow();
    if (!parent) return;

    if (follow) {
        set_transient_for(*parent);
    } else {
        #if defined(__APPLE__)
        // detaching a transient does not work on macOS
        return;
        #else
        // Gtkmm(3) does not have a method to detach a transient, so use C-API
        gtk_window_set_transient_for(gobj(), NULL);
        #endif
    }

    // ensure window would still be on a visible position after transient change
    restoreWindowDimensions();
}

bool ManagedWindow::saveWindowDimensions(int x, int y, int w, int h) {
    printf("saveWindowDimensions(%d,%d,%d,%d)\n",x,y,w,h);
    if (*windowSettingX() != x) *windowSettingX() = x;
    if (*windowSettingY() != y) *windowSettingY() = y;
    if (*windowSettingWidth() != w) *windowSettingWidth() = w;
    if (*windowSettingHeight() != h) *windowSettingHeight() = h;

    // prevent (transient) window to become invisible when dragging it off-screen
    if (get_transient_for())
        restoreWindowDimensions();

    return false; // kill timeout which called this method
}

void ManagedWindow::restoreWindowDimensions() {
    int x = -1, y = -1, w = -1, h = -1;

    if (Settings::singleton()->autoRestoreWindowDimension) {
        x = *windowSettingX();
        y = *windowSettingY();
        w = *windowSettingWidth();
        h = *windowSettingHeight();
    }
    if (x < 0 || y < 0)
        get_position(x, y);
    if (w < 0 || h < 0)
        get_size(w, h);

    printf("restoreWindowDimensions(%d,%d,%d,%d)\n",x,y,w,h);

    // force a minimum size
    if (w < 50) {
        printf("clamp w=%d -> w=%d\n", w, 50);
        w = 50;
    }
    if (h < 50) {
        printf("clamp h=%d -> h=%d\n", h, 50);
        h = 50;
    }

    int monitorNr = -1;
    Gdk::Rectangle rect;

    Gtk::Window* transientParent = get_transient_for();
    if (transientParent) { // transient (parent window) ...
        // clamp coordinates to parent window monitor's dimensions (i.e. force
        // this dialog to appear on same monitor on a multi-display setup, as
        // some systems like macOS won't allow a transient subwindow to appear
        // on another monitor than its parent window)
        Glib::RefPtr<Gdk::Window> gdkWindow = transientParent->get_window();
        if (!gdkWindow)
            goto apply_dimensions;

        int xParent, yParent;
        gdkWindow->get_root_origin(xParent, yParent);

        Glib::RefPtr<Gdk::Screen> screen = gdkWindow->get_screen();
        monitorNr = screen->get_monitor_at_point(xParent, yParent);
        screen->get_monitor_geometry(monitorNr, rect);

        goto apply_constraints;
    } else { // no transient (parent window) ...
        // clamp coordinates to this monitor's dimensions (to prevent window to
        // appear on either an invisible position or where it's barely
        // impossible to move the window back on screen by mouse)
        Glib::RefPtr<Gdk::Window> gdkWindow = get_window();
        if (!gdkWindow)
            goto apply_dimensions;

        Glib::RefPtr<Gdk::Screen> screen = gdkWindow->get_screen();
        monitorNr = screen->get_monitor_at_point(x + w/2, y + h/2);
        screen->get_monitor_geometry(monitorNr, rect);

        goto apply_constraints;
    }

    apply_constraints:

    if (x + w/2 < rect.get_x()) {
        printf("clamp x=%d", x);
        x = rect.get_x();
        printf(" -> x=%d to left edge of monitor %d\n", x, monitorNr);
    } else if (x + w/2 > rect.get_x() + rect.get_width()) {
        printf("clamp x=%d", x);
        x = rect.get_x() + rect.get_width() - w;
        printf(" -> x=%d to right edge of monitor %d\n", x, monitorNr);
    }

    if (y + h/2 < rect.get_y()) {
        printf("clamp y=%d", y);
        y = rect.get_y();
        printf(" -> y=%d to top edge of monitor %d\n", y, monitorNr);
    } else if (y + h/2 > rect.get_y() + rect.get_height()) {
        printf("clamp y=%d", y);
        y = rect.get_y() + rect.get_height() - h;
        printf(" -> y=%d to bottom edge of monitor %d\n", y, monitorNr);
    }

    apply_dimensions:

    move(x, y);
    resize(w, h);
}

///////////////////////////////////////////////////////////////////////////
// class 'ManagedDialog'

ManagedDialog::ManagedDialog()
    : Gtk::Dialog(), m_listenOnConfigureEvents(false)
{
    initManagedDialog();
}

ManagedDialog::ManagedDialog(const Glib::ustring& title, bool modal)
    : Gtk::Dialog(title, modal), m_listenOnConfigureEvents(false)
{
    initManagedDialog();
}

ManagedDialog::ManagedDialog(const Glib::ustring& title, Gtk::Window& parent, bool modal)
    : Gtk::Dialog(title, parent, modal), m_listenOnConfigureEvents(false)
{
    // force this dialog to be always relative to its parent window
    // (this prevents issues on multi-display systems, e.g. on macOS, where a
    // dialog position on another display than its parent window is considered
    // an illegal position, causing the dialog to become invisible)
    set_transient_for(parent);

    initManagedDialog();
}

// ManagedDialog::ManagedDialog(const Glib::ustring& title, Gtk::DialogFlags flags)
//     : Gtk::Dialog(title, flags), m_listenOnConfigureEvents(false)
// {
//     initManagedDialog();
// }

ManagedDialog::~ManagedDialog() {
    m_connectionOnceRestoreWindowDimensions.disconnect();
    m_connectionOnceEnableListeningConfigureEvents.disconnect();
}

void ManagedDialog::initManagedDialog() {
    m_connectionOnceRestoreWindowDimensions = Glib::signal_idle().connect(
        [this]() {
            restoreWindowDimensions();

            // replicate connect_once() behaviour:
            m_connectionOnceRestoreWindowDimensions.disconnect();
            return false; // don't call again
        },
        // timeout starts given amount of ms after main loop became idle again:
        0
    );
    //HACK: Gtk does not support to distinguish between user caused window resize/move and programmtical window resize/move, so as a workaround we ignore such events for a certain amount of time while this window is constructing
    m_connectionOnceEnableListeningConfigureEvents = Glib::signal_idle().connect(
        [this]() {
            enableListeningConfigureEvents();

            // replicate connect_once() behaviour:
            m_connectionOnceEnableListeningConfigureEvents.disconnect();
            return false; // don't call again
        },
        // timeout starts given amount of ms after main loop became idle again:
        300
    );
}

#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
bool ManagedDialog::on_configure_event(Gdk::EventConfigure& event) {
    GdkEventConfigure* e = event.gobj();
#else
bool ManagedDialog::on_configure_event(GdkEventConfigure* e) {
#endif
    //printf("on_configure_event x=%d y=%d w=%d h=%d\n", e->x, e->y, e->width, e->height);
    if (m_listenOnConfigureEvents) {
        //printf("reset event throttle timer\n");
        // invalidate timer (so it won't fire)
        if (m_eventThrottleTimer) m_eventThrottleTimer->destroy();
        // restart timer
        m_eventThrottleTimer = Glib::TimeoutSource::create(300);
        const GdkEventConfigure ev = *e;
        m_eventThrottleTimer->connect([this, ev]() -> bool {
            return saveWindowDimensions(ev.x, ev.y, ev.width, ev.height);
        });
        m_eventThrottleTimer->attach(Glib::MainContext::get_default());
    }
#if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
    return Gtk::Dialog::on_configure_event(event);
#else
    return Gtk::Dialog::on_configure_event(e);
#endif
}

bool ManagedDialog::saveWindowDimensions(int x, int y, int w, int h) {
    printf("saveDialogDimensions(%d,%d,%d,%d)\n",x,y,w,h);
    if (*windowSettingX() != x) *windowSettingX() = x;
    if (*windowSettingY() != y) *windowSettingY() = y;
    if (*windowSettingWidth() != w) *windowSettingWidth() = w;
    if (*windowSettingHeight() != h) *windowSettingHeight() = h;

    // prevent dialog to become invisible when dragging it off-screen
    restoreWindowDimensions();

    return false; // kill timeout which called this method
}

void ManagedDialog::restoreWindowDimensions() {
    int x = -1, y = -1, w = -1, h = -1;

    if (Settings::singleton()->autoRestoreWindowDimension) {
        x = *windowSettingX();
        y = *windowSettingY();
        w = *windowSettingWidth();
        h = *windowSettingHeight();
    }
    if (x < 0 || y < 0)
        get_position(x, y);
    if (w < 0 || h < 0)
        get_size(w, h);

    printf("restoreDialogDimensions(%d,%d,%d,%d)\n",x,y,w,h);

    // force a minimum size
    if (w < 50) {
        printf("clamp w=%d -> w=%d\n", w, 50);
        w = 50;
    }
    if (h < 50) {
        printf("clamp h=%d -> h=%d\n", h, 50);
        h = 50;
    }

    // clamp coordinates to parent window monitor's dimensions (i.e. force this
    // dialog to appear on same monitor on a multi-display setup, as some
    // systems like macOS won't allow a dialog to appear on another monitor than
    // its parent window)
    Gtk::Window* parent = get_transient_for();
    if (parent) {
        Glib::RefPtr<Gdk::Window> gdkWindow = parent->get_window();
        if (gdkWindow) {
            int xParent, yParent;
            gdkWindow->get_root_origin(xParent, yParent);

            Glib::RefPtr<Gdk::Screen> screen = gdkWindow->get_screen();
            int monitorNr = screen->get_monitor_at_point(xParent, yParent);

            Gdk::Rectangle rect;
            screen->get_monitor_geometry(monitorNr, rect);

            if (x + w/2 < rect.get_x()) {
                printf("clamp x=%d", x);
                x = rect.get_x();
                printf(" -> x=%d to left edge of monitor %d\n", x, monitorNr);
            } else if (x + w/2 > rect.get_x() + rect.get_width()) {
                printf("clamp x=%d", x);
                x = rect.get_x() + rect.get_width() - w;
                printf(" -> x=%d to right edge of monitor %d\n", x, monitorNr);
            }

            if (y + h/2 < rect.get_y()) {
                printf("clamp y=%d", y);
                y = rect.get_y();
                printf(" -> y=%d to top edge of monitor %d\n", y, monitorNr);
            } else if (y + h/2 > rect.get_y() + rect.get_height()) {
                printf("clamp y=%d", y);
                y = rect.get_y() + rect.get_height() - h;
                printf(" -> y=%d to bottom edge of monitor %d\n", y, monitorNr);
            }
        }
    }

    move(x, y);
    resize(w, h);
}
