/*
 *  This file is part of Xfce (https://gitlab.xfce.org).
 *
 *  Copyright (c) 2021 Jan Ziak <0xe2.0x9a.0x9b@xfce.org>
 *  Copyright (c) 2023 Błażej Szczygieł
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Library General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#pragma once

#include <glib.h>
#include <gtk/gtk.h>
#include <libxfce4panel/libxfce4panel.h>
#include <functional>
#include <memory>

namespace xfce4 {

using namespace std;

using Connection = weak_ptr<pair<gpointer, gulong>>;

template<typename GReturnType, typename ObjectType, typename ReturnType, typename... Args>
struct ConnectionHandlerData
{
    using FunctionType = ReturnType (ObjectType *, Args...);

    shared_ptr<Connection::element_type> m_connection;
    function<FunctionType> m_handler;

    ConnectionHandlerData (const function<FunctionType> &handler)
        : m_handler (handler)
    {}

    static GReturnType
    call (ObjectType *object, Args... args, gpointer data)
    {
        auto h = reinterpret_cast<ConnectionHandlerData *> (data);
        return h->m_handler (object, args...);
    }

    static void
    destroy (gpointer data, GClosure *)
    {
        delete reinterpret_cast<ConnectionHandlerData *> (data);
    }
};

template<typename GReturnType, typename ObjectType, typename ReturnType, typename... Args>
static Connection
connect (ObjectType *object, const char *signal, const function<ReturnType (ObjectType *, Args...)> &handler, bool after = false)
{
    using CHD = ConnectionHandlerData<GReturnType, ObjectType, ReturnType, Args...>;

    auto data = new CHD (handler);

    auto id = g_signal_connect_data(
        object,
        signal,
        (GCallback) CHD::call,
        data,
        CHD::destroy,
        after ? G_CONNECT_AFTER : (GConnectFlags) 0
    );

    if (G_UNLIKELY(id == 0))
    {
        delete data;
        return Connection ();
    }

    data->m_connection = make_shared<Connection::element_type> (object, id);
    return data->m_connection;
}

static inline void
disconnect (const Connection &connection)
{
    if (auto connectionPtr = connection.lock ())
        g_signal_handler_disconnect (connectionPtr->first, connectionPtr->second);
}

/*
 * Partially type-safe functions for making signal -> handler connections.
 *
 * Only for signals that do not pass extra arguments to the handler.
 * An example of a signal incompatible with these functions is "response" generated by GtkDialog.
 *
 * If the handler is a C++ λ-function, variables captured by the handler
 * are automatically destroyed (via calling the corresponding C++ destructor)
 * when the object/widget generating the signal gets destroyed.
 */

static inline Connection
connect (GtkColorButton *widget, const char *signal, const function<void (GtkColorButton *)> &handler)
{
    return connect<void> (widget, signal, handler);
}

static inline Connection
connect (GtkComboBox *widget, const char *signal, const function<void (GtkComboBox *)> &handler)
{
    return connect<void> (widget, signal, handler);
}

static inline Connection
connect (GtkEntry *widget, const char *signal, const function<void (GtkEntry *)> &handler)
{
    return connect<void> (widget, signal, handler);
}

static inline Connection
connect (GtkSpinButton *widget, const char *signal, const function<void (GtkSpinButton *)> &handler)
{
    return connect<void> (widget, signal, handler);
}

static inline Connection
connect (GtkToggleButton *widget, const char *signal, const function<void (GtkToggleButton *)> &handler)
{
    return connect<void> (widget, signal, handler);
}

/*
 * Type-safe functions for making signal -> handler connections.
 *
 * If the handler is a C++ λ-function, variables captured by the handler
 * are automatically destroyed (via calling the corresponding C++ destructor)
 * when the object/widget generating the signal gets destroyed.
 */

class PluginShape
{
    /* Whether the panel plugin size is forced to be a square. Otherwise, the plugin can be a rectangle. */

    PluginShape (gboolean rectangle)
        : m_rectangle (rectangle)
    {}

    const bool m_rectangle;

public:
    operator gboolean () const
    {
        return m_rectangle;
    }

    static PluginShape Square ()
    {
        return false;
    }
    static PluginShape Rectangle ()
    {
        return true;
    }
};

class Propagation
{
    /* Whether to prevent the event from propagating to other handlers */

    Propagation (gboolean stop)
        : m_stop (stop)
    {}

    const bool m_stop;

public:
    operator gboolean () const
    {
        return m_stop;
    }

    static Propagation Propagate ()
    {
        return false;
    }
    static Propagation Stop ()
    {
        return true;
    }
};

class TooltipTime
{
    /* Whether to show the tooltip now or later */

    TooltipTime (gboolean now)
        : m_now (now)
    {}

    const bool m_now;

public:
    operator gboolean () const
    {
        return m_now;
    }

    static TooltipTime Later ()
    {
        return false;
    }
    static TooltipTime Now ()
    {
        return true;
    }
};

/*
 * Connection functions, with links to associated GTK documentation.
*/

using ButtonHandler                     = Propagation (GtkWidget *widget, GdkEventButton *event);
using ChangedHandler_ComboBox           = void        (GtkComboBox *widget);
using ChangeValueHandler_Range          = Propagation (GtkRange *widget, GtkScrollType *scroll, gdouble value);
using CheckResizeHandler                = void        (GtkContainer *widget);
using ClickHandler                      = void        (GtkButton *widget);
using ColorSetHandler                   = void        (GtkColorButton *widget);
using DestroyHandler                    = void        (GtkWidget *widget);
using DrawHandler1                      = Propagation (cairo_t *cr);
using DrawHandler2                      = Propagation (GtkWidget *widget, cairo_t *cr);
using EditedHandler                     = void        (GtkCellRendererText *object, gchar *path, gchar *new_text);
using EnterNotifyHandler                = Propagation (GtkWidget *widget, GdkEventCrossing *event);
using FontSetHandler                    = void        (GtkFontButton *widget);
using LeaveNotifyHandler                = Propagation (GtkWidget *widget, GdkEventCrossing *event);
using ResponseHandler                   = void        (GtkDialog *widget, gint response);
using ToggledHandler_CellRendererToggle = void        (GtkCellRendererToggle *object, gchar *path);
using ToggledHandler_ToggleButton       = void        (GtkToggleButton *widget);
using TooltipHandler                    = TooltipTime (GtkWidget *widget, gint x, gint y, bool keyboard, GtkTooltip *tooltip);
using ValueChangedHandler_Adjustment    = void        (GtkAdjustment *object);
using ValueChangedHandler_SpinButton    = void        (GtkSpinButton *widget);

/* http://docs.gtk.org/gtk3/signal.Widget.draw.html */
static inline Connection
connect_after_draw (GtkWidget *widget, const function<DrawHandler2> &handler)
{
    return connect<gboolean> (widget, "draw", handler, true);
}

static inline Connection
connect_after_draw (GtkWidget *widget, const function<DrawHandler1> &handler)
{
    return connect_after_draw(widget, [handler](GtkWidget *, cairo_t *cr) {
        return handler(cr);
    });
}

/* http://docs.gtk.org/gtk3/signal.Widget.button-press-event.html */
static inline Connection
connect_button_press (GtkWidget *widget, const function<ButtonHandler> &handler)
{
    return connect<gboolean> (widget, "button-press-event", handler);
}

/* http://docs.gtk.org/gtk3/signal.ComboBox.changed.html */
static inline Connection
connect_changed (GtkComboBox *widget, const function<ChangedHandler_ComboBox> &handler)
{
    return connect<void> (widget, "changed", handler);
}

/* http://docs.gtk.org/gtk3/signal.Range.change-value.html */
static inline Connection
connect_change_value (GtkRange *widget, const function<ChangeValueHandler_Range> &handler)
{
    return connect<gboolean> (widget, "change-value", handler);
}

/* http://docs.gtk.org/gtk3/signal.Container.check-resize.html */
static inline Connection
connect_check_resize (GtkContainer *widget, const function<CheckResizeHandler> &handler)
{
    return connect<void> (widget, "check-resize", handler);
}

/* http://docs.gtk.org/gtk3/signal.Button.clicked.html */
static inline Connection
connect_clicked (GtkButton *widget, const function<ClickHandler> &handler)
{
    return connect<void> (widget, "clicked", handler);
}

/* http://docs.gtk.org/gtk3/signal.ColorButton.color-set.html */
static inline Connection
connect_color_set (GtkColorButton *widget, const function<ColorSetHandler> &handler)
{
    return connect<void> (widget, "color-set", handler);
}

/* http://docs.gtk.org/gtk3/signal.Widget.destroy.html */
static inline Connection
connect_destroy (GtkWidget *widget, const function<DestroyHandler> &handler)
{
    return connect<void> (widget, "destroy", handler);
}

static inline Connection
connect_draw (GtkWidget *widget, const function<DrawHandler2> &handler)
{
    return connect<gboolean> (widget, "draw", handler);
}

static inline Connection
connect_draw (GtkWidget *widget, const function<DrawHandler1> &handler)
{
    return connect_draw(widget, [handler](GtkWidget *, cairo_t *cr) {
        return handler(cr);
    });
}

/* http://docs.gtk.org/gtk3/signal.CellRendererText.edited.html */
static inline Connection
connect_edited (GtkCellRendererText *object, const function<EditedHandler> &handler)
{
    return connect<void> (object, "edited", handler);
}

/* http://docs.gtk.org/gtk3/signal.Widget.enter-notify-event.html
 *
 * Note: GTK+ documentation contains an error.
 *       The event actually passed to the handler is a pointer to GdkEventCrossing.
 */
static inline Connection
connect_enter_notify (GtkWidget *widget, const function<EnterNotifyHandler> &handler)
{
    return connect<gboolean> (widget, "enter-notify-event", handler);
}

/* http://docs.gtk.org/gtk3/signal.FontButton.font-set.html */
static inline Connection
connect_font_set (GtkFontButton *widget, const function<FontSetHandler> &handler)
{
    return connect<void> (widget, "font-set", handler);
}

/* http://docs.gtk.org/gtk3/signal.Widget.leave-notify-event.html
 *
 * Note: GTK+ documentation contains an error.
 *       The event actually passed to the handler is a pointer to GdkEventCrossing.
 */
static inline Connection
connect_leave_notify (GtkWidget *widget, const function<LeaveNotifyHandler> &handler)
{
    return connect<gboolean> (widget, "leave-notify-event", handler);
}

/* http://docs.gtk.org/gtk3/signal.Widget.query-tooltip.html */
static inline Connection
connect_query_tooltip (GtkWidget *widget, const function<TooltipHandler> &handler)
{
    return connect<gboolean> (widget, "query-tooltip", handler);
}

/* http://docs.gtk.org/gtk3/signal.Dialog.response.html */
static inline Connection
connect_response (GtkDialog *widget, const function<ResponseHandler> &handler)
{
    return connect<void> (widget, "response", handler);
}

/* http://docs.gtk.org/gtk3/signal.CellRendererToggle.toggled.html */
static inline Connection
connect_toggled (GtkCellRendererToggle *object, const function<ToggledHandler_CellRendererToggle> &handler)
{
    return  connect<void> (object, "toggled", handler);
}

/* http://docs.gtk.org/gtk3/signal.ToggleButton.toggled.html */
static inline Connection
connect_toggled (GtkToggleButton *widget, const function<ToggledHandler_ToggleButton> &handler)
{
    return connect<void> (widget, "toggled", handler);
}

/* http://docs.gtk.org/gtk3/signal.Adjustment.value-changed.html */
static inline Connection
connect_value_changed (GtkAdjustment *object, const function<ValueChangedHandler_Adjustment> &handler)
{
    return connect<void> (object, "value_changed", handler);
}

/* http://docs.gtk.org/gtk3/signal.SpinButton.value-changed.html */
static inline Connection
connect_value_changed (GtkSpinButton *widget, const function<ValueChangedHandler_SpinButton> &handler)
{
    return connect<void> (widget, "value_changed", handler);
}

/*
 * Links to documentation:
 *
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-about
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-configure-plugin
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-free-data
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-mode-changed
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-save
 * http://developer.xfce.org/xfce4-panel/XfcePanelPlugin.html#XfcePanelPlugin-size-changed
 */

using PluginHandler     = void        (XfcePanelPlugin *plugin);
using ModeChangeHandler = void        (XfcePanelPlugin *plugin, XfcePanelPluginMode mode);
using SizeChangeHandler = PluginShape (XfcePanelPlugin *plugin, guint size);

static inline Connection
connect_about (XfcePanelPlugin *plugin, const function<PluginHandler> &handler)
{
    return connect<void> (plugin, "about", handler);
}

static inline Connection
connect_configure_plugin (XfcePanelPlugin *plugin, const function<PluginHandler> &handler)
{
    return connect<void> (plugin, "configure-plugin", handler);
}

static inline Connection
connect_free_data (XfcePanelPlugin *plugin, const function<PluginHandler> &handler)
{
    return connect<void> (plugin, "free-data", handler);
}

static inline Connection
connect_mode_changed (XfcePanelPlugin *plugin, const function<ModeChangeHandler> &handler)
{
    return connect<void> (plugin, "mode-changed", handler);
}

static inline Connection
connect_save (XfcePanelPlugin *plugin, const function<PluginHandler> &handler)
{
    return connect<void> (plugin, "save", handler);
}

static inline Connection
connect_size_changed (XfcePanelPlugin *plugin, const function<SizeChangeHandler> &handler)
{
    return connect<gboolean> (plugin, "size-changed", handler);
}

}
