/*
 * This file is part of budgie-desktop
 *
 * Copyright © 2015-2022 Budgie Desktop Developers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */

#include "applet.h"
#include "budgie-enums.h"

/**
 * SECTION:applet
 * @Short_description: Budgie Panel GTK+ Widget
 * @Title: BudgieApplet
 *
 * The BudgieApplet is the main event when it comes to providing a Budgie
 * Panel extension. This is the widget that is visible to the user, and is
 * provided by, and instaniated by, your #BudgiePlugin.
 *
 * Those implementing applets have a specific API available to them in order
 * to achieve better integration with the overall desktop. At the bare minimum
 * you should at least ensure that your applet respects the sizes exposed by
 * the managing panel, via the #BudgieApplet::panel-size-changed signal.
 *
 * BudgieApplet extends #GtkEventBox to leave you free to make your own choices
 * on internal applet layout and configuration. Do note, however, that the
 * panel implementation will not call #gtk_widget_show_all, it is solely
 * your responsibility to ensure all of your contents are displayed. This
 * is to enable applet's to contextually hide part of their user interface
 * when required.
 *
 */
enum {
	PROP_PREFIX = 1,
	PROP_SCHEMA,
	PROP_ACTIONS,
	N_PROPS
};

enum {
	APPLET_SIZE_CHANGED = 0,
	APPLET_POSITION_CHANGED,
	N_SIGNALS
};

struct _BudgieAppletPrivate {
	char* prefix;
	char* schema;
	BudgiePanelAction actions;
};

G_DEFINE_TYPE_WITH_PRIVATE(BudgieApplet, budgie_applet, GTK_TYPE_EVENT_BOX)

static GParamSpec* obj_properties[N_PROPS] = {NULL};
static guint applet_signals[N_SIGNALS] = {0};

static void budgie_applet_set_property(GObject* object, guint id, const GValue* value, GParamSpec* spec) {
	BudgieApplet* self = BUDGIE_APPLET(object);

	switch (id) {
		case PROP_PREFIX:
			budgie_applet_set_settings_prefix(self, g_value_get_string(value));
			break;
		case PROP_SCHEMA:
			budgie_applet_set_settings_schema(self, g_value_get_string(value));
			break;
		case PROP_ACTIONS:
			self->priv->actions = g_value_get_flags(value);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, id, spec);
			break;
	}
}

static void budgie_applet_get_property(GObject* object, guint id, GValue* value, GParamSpec* spec) {
	BudgieApplet* self = BUDGIE_APPLET(object);

	switch (id) {
		case PROP_PREFIX:
			g_value_set_string(value, budgie_applet_get_settings_prefix(self));
			break;
		case PROP_SCHEMA:
			g_value_set_string(value, budgie_applet_get_settings_schema(self));
			break;
		case PROP_ACTIONS:
			g_value_set_flags(value, self->priv->actions);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, id, spec);
			break;
	}
}

/**
 * budgie_applet_invoke_action:
 * @action: Action to invoke
 *
 * Invoke the given action on this applet. This action will only be one
 * that has been declared in supported actions bitmask.
 *
 * To allow better integration between the Budgie Desktop, and the applets
 * that live within it, the panel will relay actions to applets that have
 * set their #BudgieApplet:supported-actions to a matching bitmask.
 *
 * For example, if we wish to listen for Menu Key events, we can simply do
 * the following in C:
 *
 * |[<!-- language="C" -->
 *
 *      static void my_applet_invoke_action(BudgieApplet *applet, BudgiePanelAction action)
 *      {
 *              if (action == BUDGIE_PANEL_ACTION_MENU) {
 *                      my_applet_do_predict_the_lottery(MY_APPLET(applet));
 *              }
 *      }
 *
 *      static void my_class_init(GObjectClass *class)
 *      {
 *              MyClass *mc = MY_CLASS(klass);
 *              ..
 *              mc->invoke_action = my_applet_invoke_action;
 *      }
 * ]|
 *
 * Likewise, a Vala implementation might look like the following:
 * |[<!-- language="Vala" -->
 *
 *      public override void invoke_action(Budgie.PanelAction action)
 *      {
 *          if (action == Budgie.PanelAction.MENU) {
 *              this.predict_the_lottery();
 *          }
 *      }
 * ]|
 */
void budgie_applet_invoke_action(BudgieApplet* self, BudgiePanelAction action) {
	BudgieAppletClass* klazz = NULL;

	if (!BUDGIE_IS_APPLET(self)) {
		return;
	}

	klazz = BUDGIE_APPLET_GET_CLASS(self);

	if (klazz->invoke_action) {
		klazz->invoke_action(self, action);
	}
}

/**
 * budgie_applet_supports_settings:
 *
 * Implementations should override this to return TRUE if they support
 * a settings UI
 *
 * Returns: true if this implementation supports a Settings UI
 */
gboolean budgie_applet_supports_settings(BudgieApplet* self) {
	BudgieAppletClass* klazz = NULL;

	if (!BUDGIE_IS_APPLET(self)) {
		return FALSE;
	}

	klazz = BUDGIE_APPLET_GET_CLASS(self);
	if (!klazz->supports_settings) {
		return FALSE;
	}
	return klazz->supports_settings(self);
}

/**
 * budgie_applet_get_settings_ui:
 *
 * For applets that need to expose settings, they should both override the
 * #BudgieApplet::supports_settings method and return a new widget instance
 * whenever this function is invoked.
 *
 * This UI will live in the Raven sidebar within the Budgie Desktop, and
 * will be destroyed as soon as it's not being used. It's advisable to keep
 * this widget implementation light, and to prefer vertical space.
 *
 * Returns: (transfer full) (nullable): A GTK Settings UI
 */
GtkWidget* budgie_applet_get_settings_ui(BudgieApplet* self) {
	BudgieAppletClass* klazz = NULL;

	if (!BUDGIE_IS_APPLET(self)) {
		return NULL;
	}

	klazz = BUDGIE_APPLET_GET_CLASS(self);
	if (!klazz->get_settings_ui) {
		return NULL;
	}
	return klazz->get_settings_ui(self);
}

/**
 * budgie_applet_get_applet_settings:
 * @uuid: UUID for this instance
 *
 * If your #BudgiePlugin implementation passes the UUID to your BudgieApplet
 * implementation on construction, you can take advantage of per-instance
 * settings.
 *
 * For most applets, global GSettings keys are more than suffice. However,
 * in some situations, it may be beneficial to enable multiple unique instances
 * of your applet, each with their own configuration.
 *
 * To facilitate this, use this function to create a new relocatable settings
 * instance using your UUID. Make sure you set the #BudgieApplet:settings-schema
 * and #BudgieApplet:settings-prefix properties first.
 *
 * Returns: (transfer full): A newly created #GSettings for this applet instance
 */
GSettings* budgie_applet_get_applet_settings(BudgieApplet* self, gchar* uuid) {
	GSettings* settings = NULL;
	gchar* path = NULL;

	if (!self || !self->priv || !self->priv->schema || !self->priv->prefix) {
		return NULL;
	}

	path = g_strdup_printf("%s/{%s}/", self->priv->prefix, uuid);
	if (!path) {
		return NULL;
	}

	settings = g_settings_new_with_path(self->priv->schema, path);
	g_free(path);
	return settings;
}

static void budgie_applet_dispose(GObject* g_object) {
	BudgieApplet* self = BUDGIE_APPLET(g_object);

	g_clear_pointer(&self->priv->prefix, g_free);
	g_clear_pointer(&self->priv->schema, g_free);

	G_OBJECT_CLASS(budgie_applet_parent_class)->dispose(g_object);
}

static void budgie_applet_class_init(BudgieAppletClass* klazz) {
	GObjectClass* obj_class = G_OBJECT_CLASS(klazz);

	obj_class->get_property = budgie_applet_get_property;
	obj_class->set_property = budgie_applet_set_property;
	obj_class->dispose = budgie_applet_dispose;

	klazz->update_popovers = NULL;

	/* Todo, make the PREFIX/SCHEMA G_PARAM_CONSTRUCT_ONLY */

	/**
	 * BudgieApplet:settings-prefix:
	 *
	 * The GSettings schema path prefix for this applet
	 *
	 * For applets that require unique instance configuration, the
	 * panel management must know where to initialise the settings
	 * within the tree. The path takes the form:
	 *
	 * `$SETTINGS_PREFIX/{$UUID}`
	 *
	 * As an example, the Budgie Menu Applet set's the `settings-prefix`
	 * to:
	 * `/com/solus-project/budgie-panel/instance/budgie-menu`.
	 *
	 * This results in relocatable schemas being created at:
	 *
	 * `/com/solus-project/budgie-panel/instance/budgie-menu/{$UUID}`
	 */
	obj_properties[PROP_PREFIX] = g_param_spec_string(
		"settings-prefix", "GSettings schema prefix", "Set the GSettings schema prefix",
		NULL, G_PARAM_READWRITE);

	/**
	 * BudgieApplet:settings-schema:
	 *
	 * The ID of the GSettings schema used by this applet
	 *
	 * This only takes effect when you've also set #BudgieApplet:settings-prefix,
	 * and is used by the panel managemen to both initialise and delete your per-instance
	 * settings, respectively.
	 *
	 * As an example, the Budgie Menu Applet uses the schema:
	 *
	 * `com.solus-project.budgie-menu`
	 *
	 * as defined by the accompanying gschema XML file. Providing an incorrect
	 * schema ID is considered programmer error.
	 */
	obj_properties[PROP_SCHEMA] = g_param_spec_string(
		"settings-schema", "GSettings relocatable schema ID", "Set the GSettings relocatable schema ID",
		NULL, G_PARAM_READWRITE);

	/**
	 * BudgieApplet:supported-actions:
	 *
	 * The actions supported by this applet instance
	 */
	obj_properties[PROP_ACTIONS] = g_param_spec_flags(
		"supported-actions", "Supported panel actions", "Get/set the supported panel actions",
		BUDGIE_TYPE_PANEL_ACTION, BUDGIE_PANEL_ACTION_NONE, G_PARAM_READWRITE);

	/**
	 * BudgieApplet::panel-size-changed:
	 * @applet: The applet receiving the signal
	 * @panel_size: The new panel size
	 * @icon_size: Larget possible icon size for the panel
	 * @small_icon_size: Smaller icon that will still fit on the panel
	 *
	 * Used to notify this applet of a change in the panel size
	 */
	applet_signals[APPLET_SIZE_CHANGED] = g_signal_new(
		"panel-size-changed",
		BUDGIE_TYPE_APPLET,
		G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
		G_STRUCT_OFFSET(BudgieAppletClass, panel_size_changed),
		NULL, NULL, NULL,
		G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT);

	/**
	 * BudgieApplet::panel-position-changed:
	 * @applet: The applet receiving the signal
	 * @position: The new position (screen edge)
	 *
	 * Used to notify this applet of a change in the panel's placement
	 * on screen, so that it may adjust its own layout to better suit
	 * the geometry.
	 */
	applet_signals[APPLET_POSITION_CHANGED] = g_signal_new(
		"panel-position-changed",
		BUDGIE_TYPE_APPLET,
		G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
		G_STRUCT_OFFSET(BudgieAppletClass, panel_position_changed),
		NULL, NULL, NULL,
		G_TYPE_NONE, 1, BUDGIE_TYPE_PANEL_POSITION);

	g_object_class_install_properties(obj_class, N_PROPS, obj_properties);
}

/**
 * budgie_applet_set_settings_prefix:
 *
 * Utility function for Python usage. See: #BudgieApplet:settings-prefix
 */
void budgie_applet_set_settings_prefix(BudgieApplet* self, const gchar* prefix) {
	if (!self || !prefix) {
		return;
	}

	BudgieAppletPrivate* priv = self->priv;
	if (priv->prefix) {
		g_free(priv->prefix);
	}
	priv->prefix = g_strdup(prefix);
}

/**
 * budgie_applet_get_settings_prefix:
 *
 * Utility function for Python usage. See: #BudgieApplet:settings-prefix
 */
const gchar* budgie_applet_get_settings_prefix(BudgieApplet* self) {
	if (!self) {
		return NULL;
	}
	return (const gchar*) self->priv->prefix;
}

/**
 * budgie_applet_set_settings_schema:
 *
 * Utility function for Python usage. See #BudgieApplet:settings-schema
 */
void budgie_applet_set_settings_schema(BudgieApplet* self, const gchar* schema) {
	if (!self || !schema) {
		return;
	}

	BudgieAppletPrivate* priv = self->priv;
	;
	if (priv->schema) {
		g_free(priv->schema);
	}
	priv->schema = g_strdup(schema);
}

/**
 * budgie_applet_get_settings_schema:
 *
 * Utility function for Python usage. See #BudgieApplet:settings-schema
 */
const gchar* budgie_applet_get_settings_schema(BudgieApplet* self) {
	if (!self) {
		return NULL;
	}
	return (const gchar*) self->priv->schema;
}

/**
 * budgie_applet_update_popovers:
 * @manager: (nullable)
 *
 * This virtual method should be implemented by panel applets that wish
 * to support #GtkPopover's natively. As each Budgie Panel may house multiple
 * GtkPopover widgets, each one must be registered with the @manager.
 *
 * During this call, it is safe to store a reference to the @manager. In
 * this call you should invoke #BudgiePopoverManager::register_popover to
 * register your popover with the panel manager.
 *
 * Each registered popover joins the global menu system of popovers in the
 * panel. It is a requirement to register, otherwise the panel will not
 * know when to expand and collapse the main panel harness to accommodate
 * the GtkPopover.
 *
 */
void budgie_applet_update_popovers(BudgieApplet* self, BudgiePopoverManager* manager) {
	if (!self) {
		return;
	}
	BudgieAppletClass* klazz = BUDGIE_APPLET_GET_CLASS(self);

	if (klazz->update_popovers) {
		klazz->update_popovers(self, manager);
	}
}

/**
 * budgie_applet_get_supported_actions:
 *
 * Utility function for Python bindings. See #BudgieApplet:supported-actions
 */
BudgiePanelAction budgie_applet_get_supported_actions(BudgieApplet* self) {
	if (!self) {
		return BUDGIE_PANEL_ACTION_NONE;
	}
	return self->priv->actions;
}

static void budgie_applet_init(BudgieApplet* self) {
	self->priv = budgie_applet_get_instance_private(self);

	gtk_widget_set_can_focus(GTK_WIDGET(self), FALSE);
}

/**
 * budgie_applet_new:
 *
 * Returns: (transfer full): A new BudgieApplet
 */
BudgieApplet* budgie_applet_new() {
	return g_object_new(BUDGIE_TYPE_APPLET, NULL);
}
