/*
 *  Entangle: Tethered Camera Control & Capture
 *
 *  Copyright (C) 2009-2018 Daniel P. Berrangé
 *
 *  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 3 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <math.h>
#include <string.h>

#include <glib/gi18n.h>

#include "entangle-control-panel.h"

#include "entangle-control-button.h"
#include "entangle-control-choice.h"
#include "entangle-control-date.h"
#include "entangle-control-group.h"
#include "entangle-control-range.h"
#include "entangle-control-text.h"
#include "entangle-control-toggle.h"
#include "entangle-debug.h"

#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif

struct _EntangleControlPanel
{
    GtkExpander parent;
    EntangleCameraPreferences *cameraPrefs;
    EntangleCamera *camera;

    gulong sigCamera;
    gboolean hasControls;
    gboolean inUpdate;

    GtkWidget *grid;
    gsize rows;
};

G_DEFINE_TYPE(EntangleControlPanel, entangle_control_panel, GTK_TYPE_EXPANDER);

enum
{
    PROP_O,
    PROP_CAMERA,
    PROP_CAMERA_PREFS,
    PROP_HAS_CONTROLS,
};

static void
do_control_remove(GtkWidget *widget, gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlPanel *panel = data;

    gtk_container_remove(GTK_CONTAINER(panel->grid), widget);
}

static void
do_update_control_finish(GObject *src, GAsyncResult *res, gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    GError *error = NULL;

    if (!entangle_camera_save_controls_finish(ENTANGLE_CAMERA(src), res,
                                              &error)) {
        GtkWidget *msg =
            gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
                                   _("Camera control update failed"));
        gtk_window_set_title(GTK_WINDOW(msg),
                             _("Entangle: Camera control update failed"));
        gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(msg), "%s",
                                                 error->message);
        g_signal_connect_swapped(msg, "response",
                                 G_CALLBACK(gtk_widget_destroy), msg);
        gtk_widget_show_all(msg);
        g_error_free(error);
    }
}

static gboolean
do_refresh_control_entry_idle(gpointer data)
{
    GtkWidget *widget = GTK_WIDGET(data);
    EntangleControlPanel *panel = g_object_get_data(G_OBJECT(widget), "panel");
    GObject *control = g_object_get_data(G_OBJECT(widget), "control");
    gchar *text;

    panel->inUpdate = TRUE;
    g_object_get(control, "value", &text, NULL);
    ENTANGLE_DEBUG("Notified control entry '%s' ('%s') with '%s'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)), text);

    if (GTK_IS_LABEL(widget))
        gtk_label_set_text(GTK_LABEL(widget), text);
    else
        gtk_entry_set_text(GTK_ENTRY(widget), text);
    g_free(text);
    panel->inUpdate = FALSE;
    return FALSE;
}

static void
do_refresh_control_entry(GObject *object G_GNUC_UNUSED,
                         GParamSpec *pspec G_GNUC_UNUSED,
                         gpointer data)
{
    g_idle_add(do_refresh_control_entry_idle, data);
}

static void
do_update_control_entry(GtkWidget *widget,
                        GdkEventFocus *ev G_GNUC_UNUSED,
                        gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlText *control =
        g_object_get_data(G_OBJECT(widget), "control");
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(data);
    const char *text;

    if (panel->inUpdate)
        return;

    text = gtk_entry_get_text(GTK_ENTRY(widget));

    ENTANGLE_DEBUG("Updated control entry '%s' ('%s') with '%s'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)), text);
    g_object_set(control, "value", text, NULL);

    entangle_camera_save_controls_async(panel->camera, NULL,
                                        do_update_control_finish, panel);
}

static gboolean
do_refresh_control_range_idle(gpointer data)
{
    GtkWidget *widget = GTK_WIDGET(data);
    EntangleControlPanel *panel = g_object_get_data(G_OBJECT(widget), "panel");
    GObject *control = g_object_get_data(G_OBJECT(widget), "control");
    gfloat val;

    panel->inUpdate = TRUE;
    g_object_get(control, "value", &val, NULL);
    ENTANGLE_DEBUG("Notified control range '%s' ('%s') with '%lf'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)),
                   (double)val);

    if (GTK_IS_LABEL(widget)) {
        gchar *text = g_strdup_printf("%0.02f", (double)val);
        gtk_label_set_text(GTK_LABEL(widget), text);
        g_free(text);
    } else {
        gtk_range_set_value(GTK_RANGE(widget), val);
    }
    panel->inUpdate = FALSE;
    return FALSE;
}

static void
do_refresh_control_range(GObject *object G_GNUC_UNUSED,
                         GParamSpec *pspec G_GNUC_UNUSED,
                         gpointer data)
{
    g_idle_add(do_refresh_control_range_idle, data);
}

static void
do_update_control_range(GtkRange *widget G_GNUC_UNUSED,
                        GtkScrollType scroll G_GNUC_UNUSED,
                        gdouble value,
                        gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlRange *control =
        g_object_get_data(G_OBJECT(widget), "control");
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(data);

    if (panel->inUpdate)
        return;

    ENTANGLE_DEBUG("Updated control range '%s' ('%s') with '%lf'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)),
                   value);
    g_object_set(control, "value", (double)value, NULL);

    entangle_camera_save_controls_async(panel->camera, NULL,
                                        do_update_control_finish, panel);
}

static gboolean
do_refresh_control_combo_idle(gpointer data)
{
    GtkWidget *widget = GTK_WIDGET(data);
    EntangleControlPanel *panel = g_object_get_data(G_OBJECT(widget), "panel");
    GObject *control = g_object_get_data(G_OBJECT(widget), "control");
    gchar *text;

    panel->inUpdate = TRUE;
    g_object_get(control, "value", &text, NULL);
    ENTANGLE_DEBUG("Notified control combo '%s' ('%s') with '%s'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)), text);

    if (GTK_IS_LABEL(widget)) {
        gtk_label_set_text(GTK_LABEL(widget), text);
    } else {
        GtkListStore *store =
            GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(widget)));
        int active = 0;
        gtk_list_store_clear(store);
        for (int n = 0; n < entangle_control_choice_entry_count(
                                ENTANGLE_CONTROL_CHOICE(control));
             n++) {
            GtkTreeIter iter;
            if (g_strcmp0(text, entangle_control_choice_entry_get(
                                    ENTANGLE_CONTROL_CHOICE(control), n)) == 0)
                active = n;
            gtk_list_store_append(store, &iter);
            gtk_list_store_set(store, &iter, 0,
                               entangle_control_choice_entry_get(
                                   ENTANGLE_CONTROL_CHOICE(control), n),
                               -1);
        }
        gtk_combo_box_set_active(GTK_COMBO_BOX(widget), active);
    }
    g_free(text);
    panel->inUpdate = FALSE;

    return FALSE;
}

static void
do_refresh_control_combo(GObject *object G_GNUC_UNUSED,
                         GParamSpec *pspec G_GNUC_UNUSED,
                         gpointer data)
{
    g_idle_add(do_refresh_control_combo_idle, data);
}

static void
do_update_control_combo(GtkComboBox *widget, gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlChoice *control =
        g_object_get_data(G_OBJECT(widget), "control");
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(data);
    GtkTreeIter iter;
    char *text = NULL;
    GtkTreeModel *model = gtk_combo_box_get_model(widget);

    if (panel->inUpdate)
        return;

    if (gtk_combo_box_get_active_iter(widget, &iter))
        gtk_tree_model_get(model, &iter, 0, &text, -1);

    ENTANGLE_DEBUG("Updated control combo '%s' ('%s') with '%s'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)), text);
    g_object_set(control, "value", text, NULL);

    g_free(text);

    entangle_camera_save_controls_async(panel->camera, NULL,
                                        do_update_control_finish, panel);
}

static gboolean
do_refresh_control_toggle_idle(gpointer data)
{
    GtkWidget *widget = GTK_WIDGET(data);
    EntangleControlPanel *panel = g_object_get_data(G_OBJECT(widget), "panel");
    GObject *control = g_object_get_data(G_OBJECT(widget), "control");
    gboolean state;

    panel->inUpdate = TRUE;
    g_object_get(control, "value", &state, NULL);
    ENTANGLE_DEBUG("Notified control toggle '%s' ('%s') with '%d'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)),
                   state);

    if (GTK_IS_LABEL(widget))
        gtk_label_set_text(GTK_LABEL(widget), state ? _("On") : _("Off"));
    else
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), state);
    panel->inUpdate = FALSE;
    return FALSE;
}

static void
do_refresh_control_toggle(GObject *object G_GNUC_UNUSED,
                          GParamSpec *pspec G_GNUC_UNUSED,
                          gpointer data)
{
    g_idle_add(do_refresh_control_toggle_idle, data);
}

static void
do_update_control_toggle(GtkToggleButton *widget, gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlChoice *control =
        g_object_get_data(G_OBJECT(widget), "control");
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(data);
    gboolean active;

    if (panel->inUpdate)
        return;

    active = gtk_toggle_button_get_active(widget);
    ENTANGLE_DEBUG("Updated control toggle '%s' ('%s') with '%d'",
                   entangle_control_get_path(ENTANGLE_CONTROL(control)),
                   entangle_control_get_label(ENTANGLE_CONTROL(control)),
                   active);
    g_object_set(control, "value", active, NULL);

    entangle_camera_save_controls_async(panel->camera, NULL,
                                        do_update_control_finish, panel);
}

static gboolean
do_update_control_readonly_idle(gpointer data)
{
    GtkWidget *widget = GTK_WIDGET(data);
    GObject *control = g_object_get_data(G_OBJECT(widget), "control");
    gboolean state;

    g_object_get(control, "readonly", &state, NULL);
    gtk_widget_set_sensitive(widget, !state);

    return FALSE;
}

static void
do_update_control_readonly(GObject *object G_GNUC_UNUSED,
                           GParamSpec *pspec G_GNUC_UNUSED,
                           gpointer data)
{
    g_idle_add(do_update_control_readonly_idle, data);
}

static void
do_setup_control(EntangleControlPanel *panel,
                 EntangleControl *control,
                 GtkContainer *box,
                 gint row)
{
    GtkWidget *label = NULL;
    GtkWidget *value = NULL;
    gboolean needLabel = TRUE;

    ENTANGLE_DEBUG("Build control %d %s", entangle_control_get_id(control),
                   entangle_control_get_label(control));

    if (ENTANGLE_IS_CONTROL_BUTTON(control)) {
        needLabel = FALSE;
        value = gtk_button_new_with_label(entangle_control_get_label(control));
        if (entangle_control_get_readonly(control))
            gtk_widget_set_sensitive(value, FALSE);
        g_signal_connect(control, "notify::readonly",
                         G_CALLBACK(do_update_control_readonly), value);
    } else if (ENTANGLE_IS_CONTROL_CHOICE(control)) {
        GtkCellRenderer *cell;
        GtkListStore *store;
        char *text;
        int active = -1;

        /*
         * Need todo better here
         *
         *  If there's only two entries 0/1, turn into toggle
         *  If there's a continuous sequence of numbers turn
         *      into a spinbutton
         *
         *  Some sequences of numbers are nonsene, and need to
         *  be turned in to real labels.
         *
         *   eg Shutter speed 0.00025 should be presented 1/4000
         */

        store = gtk_list_store_new(1, G_TYPE_STRING);
        value = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
        g_object_unref(store);

        cell = gtk_cell_renderer_text_new();
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(value), cell, TRUE);
        gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(value), cell, "text", 0,
                                       NULL);

        g_object_get(control, "value", &text, NULL);
        for (int n = 0; n < entangle_control_choice_entry_count(
                                ENTANGLE_CONTROL_CHOICE(control));
             n++) {
            GtkTreeIter iter;
            if (g_strcmp0(text, entangle_control_choice_entry_get(
                                    ENTANGLE_CONTROL_CHOICE(control), n)) == 0)
                active = n;
            gtk_list_store_append(store, &iter);
            gtk_list_store_set(store, &iter, 0,
                               entangle_control_choice_entry_get(
                                   ENTANGLE_CONTROL_CHOICE(control), n),
                               -1);
        }
        g_free(text);

        if (entangle_control_get_readonly(control))
            gtk_widget_set_sensitive(value, FALSE);
        gtk_combo_box_set_active(GTK_COMBO_BOX(value), active);

#ifdef GDK_WINDOWING_WAYLAND
        /* Hack until https://gitlab.gnome.org/GNOME/gtk/issues/1463 is fixed */
        if (GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(GTK_WIDGET(value))) &&
            (entangle_control_choice_entry_count(
                 ENTANGLE_CONTROL_CHOICE(control)) > 15)) {
            gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(value), 1);
        }
#endif

        g_signal_connect(value, "changed", G_CALLBACK(do_update_control_combo),
                         panel);
        g_signal_connect(control, "notify::value",
                         G_CALLBACK(do_refresh_control_combo), value);
        g_signal_connect(control, "notify::readonly",
                         G_CALLBACK(do_update_control_readonly), value);
    } else if (ENTANGLE_IS_CONTROL_DATE(control)) {
        int date;

        value = gtk_entry_new();
        g_object_get(control, "value", &date, NULL);
        if (entangle_control_get_readonly(control))
            gtk_widget_set_sensitive(value, FALSE);
        // gtk_entry_set_text(GTK_ENTRY(value), text);
    } else if (ENTANGLE_IS_CONTROL_RANGE(control)) {
        gfloat offset;
        gdouble min =
            entangle_control_range_get_min(ENTANGLE_CONTROL_RANGE(control));
        gdouble max =
            entangle_control_range_get_max(ENTANGLE_CONTROL_RANGE(control));
        gboolean forceReadonly = FALSE;

        if (fabs(min - max) < 0.005) {
            forceReadonly = TRUE;
            max += 1;
        }

        value = gtk_scale_new_with_range(
            GTK_ORIENTATION_HORIZONTAL, min, max,
            entangle_control_range_get_step(ENTANGLE_CONTROL_RANGE(control)));
        g_object_get(control, "value", &offset, NULL);
        gtk_range_set_value(GTK_RANGE(value), offset);
        if (entangle_control_get_readonly(control) || forceReadonly)
            gtk_widget_set_sensitive(value, FALSE);
        g_signal_connect(value, "change-value",
                         G_CALLBACK(do_update_control_range), panel);
        g_signal_connect(control, "notify::value",
                         G_CALLBACK(do_refresh_control_range), value);
        g_signal_connect(control, "notify::readonly",
                         G_CALLBACK(do_update_control_readonly), value);
    } else if (ENTANGLE_IS_CONTROL_TEXT(control)) {
        const char *text;

        value = gtk_entry_new();
        g_object_get(control, "value", &text, NULL);
        gtk_entry_set_text(GTK_ENTRY(value), text);
        if (entangle_control_get_readonly(control))
            gtk_widget_set_sensitive(value, FALSE);
        g_signal_connect(value, "focus-out-event",
                         G_CALLBACK(do_update_control_entry), panel);
        g_signal_connect(control, "notify::value",
                         G_CALLBACK(do_refresh_control_entry), value);
        g_signal_connect(control, "notify::readonly",
                         G_CALLBACK(do_update_control_readonly), value);
    } else if (ENTANGLE_IS_CONTROL_TOGGLE(control)) {
        gboolean active;
        needLabel = FALSE;
        value = gtk_check_button_new_with_label(
            entangle_control_get_label(control));
        g_object_get(control, "value", &active, NULL);
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(value), active);
        if (entangle_control_get_readonly(control))
            gtk_widget_set_sensitive(value, FALSE);
        g_signal_connect(value, "toggled", G_CALLBACK(do_update_control_toggle),
                         panel);
        g_signal_connect(control, "notify::value",
                         G_CALLBACK(do_refresh_control_toggle), value);
        g_signal_connect(control, "notify::readonly",
                         G_CALLBACK(do_update_control_readonly), value);
    }

    if (needLabel) {
        label = gtk_label_new(entangle_control_get_label(control));
        gtk_widget_set_tooltip_text(label, entangle_control_get_info(control));
        gtk_widget_set_halign(label, GTK_ALIGN_END);
        gtk_grid_attach(GTK_GRID(box), label, 0, row, 1, 1);

        g_object_set_data(G_OBJECT(label), "panel", panel);
        g_object_set_data(G_OBJECT(label), "control", control);
        gtk_widget_show(label);

        gtk_widget_set_hexpand(value, TRUE);
        gtk_widget_set_halign(value, GTK_ALIGN_FILL);
        gtk_grid_attach(GTK_GRID(box), value, 1, row, 1, 1);

        g_object_set_data(G_OBJECT(value), "panel", panel);
        g_object_set_data(G_OBJECT(value), "control", control);
        gtk_widget_show(value);
    } else {
        gtk_widget_set_hexpand(value, TRUE);
        gtk_widget_set_halign(value, GTK_ALIGN_FILL);
        gtk_grid_attach(GTK_GRID(box), value, 0, row, 2, 1);

        g_object_set_data(G_OBJECT(value), "panel", panel);
        g_object_set_data(G_OBJECT(value), "control", control);
        gtk_widget_show(value);
    }
}

static gchar **
entangle_control_panel_get_default_controls(EntangleControlGroup *root)
{
    gchar **controls = NULL;
    gsize ncontrols = 0;

    if (entangle_control_group_get_by_path(root,
                                           "/main/capturesettings/f-number")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/capturesettings/f-number");
    } else if (entangle_control_group_get_by_path(
                   root, "/main/capturesettings/aperture")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/capturesettings/aperture");
    }

    if (entangle_control_group_get_by_path(
            root, "/main/capturesettings/shutterspeed2")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/capturesettings/shutterspeed2");
    } else if (entangle_control_group_get_by_path(
                   root, "/main/capturesettings/shutterspeed")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/capturesettings/shutterspeed");
    }

    if (entangle_control_group_get_by_path(root, "/main/imgsettings/iso")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/imgsettings/iso");
    }

    if (entangle_control_group_get_by_path(root,
                                           "/main/imgsettings/whitebalance")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/imgsettings/whitebalance");
    }

    if (entangle_control_group_get_by_path(
            root, "/main/capturesettings/imagequality")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/capturesettings/imagequality");
    } else if (entangle_control_group_get_by_path(
                   root, "/main/imgsettings/imageformat")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/imgsettings/imageformat");
    }

    if (entangle_control_group_get_by_path(root,
                                           "/main/imgsettings/imagesize")) {
        controls = g_renew(gchar *, controls, ncontrols + 1);
        controls[ncontrols++] = g_strdup("/main/imgsettings/imagesize");
    }

    controls = g_renew(gchar *, controls, ncontrols + 1);
    controls[ncontrols++] = NULL;

    return controls;
}

static GList *
do_get_control_list(EntangleControlGroup *group)
{
    gsize i;
    GList *controls = NULL;

    for (i = 0; i < entangle_control_group_count(group); i++) {
        EntangleControl *control = entangle_control_group_get(group, i);

        if (ENTANGLE_IS_CONTROL_GROUP(control)) {
            GList *children =
                do_get_control_list(ENTANGLE_CONTROL_GROUP(control));

            controls = g_list_concat(controls, children);
        } else {
            controls = g_list_append(controls, control);
        }
    }

    return controls;
}

static int
compare_control(gconstpointer a, gconstpointer b)
{
    EntangleControl *ac = (EntangleControl *)a;
    EntangleControl *bc = (EntangleControl *)b;

    return strcmp(entangle_control_get_label(ac),
                  entangle_control_get_label(bc));
}

static gboolean
is_control_enabled(gchar **controls, const gchar *check)
{
    gsize i;

    if (!controls)
        return FALSE;

    for (i = 0; controls[i] != NULL; i++)
        if (g_str_equal(controls[i], check))
            return TRUE;
    return FALSE;
}

static void
do_setup_controls(EntangleControlPanel *panel);

static void
do_reset_controls(GtkWidget *src G_GNUC_UNUSED, EntangleControlPanel *panel)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(panel));

    gtk_container_foreach(GTK_CONTAINER(panel->grid), do_control_remove, panel);
    panel->rows = 0;
    entangle_camera_preferences_set_controls(panel->cameraPrefs, NULL);
    do_setup_controls(panel);
}

static void
do_update_control_prefs(EntangleControlPanel *panel)
{
    const gchar **controlnames = g_new0(const gchar *, panel->rows + 1);
    gsize i;

    for (i = 0; i < panel->rows; i++) {
        GtkWidget *widget = gtk_grid_get_child_at(GTK_GRID(panel->grid), 0, i);
        EntangleControl *control =
            g_object_get_data(G_OBJECT(widget), "control");
        controlnames[i] = entangle_control_get_path(control);
    }
    controlnames[panel->rows] = NULL;
    entangle_camera_preferences_set_controls(
        panel->cameraPrefs, (const gchar *const *)controlnames);
    g_free(controlnames);
}

static void
do_add_control(EntangleControlPanel *panel, EntangleControl *control)
{
    gsize i;

    for (i = 0; i < panel->rows; i++) {
        GtkWidget *widget = gtk_grid_get_child_at(GTK_GRID(panel->grid), 0, i);
        EntangleControl *that = g_object_get_data(G_OBJECT(widget), "control");

        if (that == control)
            return;
    }

    gtk_grid_insert_row(GTK_GRID(panel->grid), panel->rows);
    do_setup_control(panel, control, GTK_CONTAINER(panel->grid), panel->rows++);
    do_update_control_prefs(panel);
}

static void
do_remove_control(EntangleControlPanel *panel, EntangleControl *control)
{
    gsize i;

    for (i = 0; i < panel->rows; i++) {
        GtkWidget *widget = gtk_grid_get_child_at(GTK_GRID(panel->grid), 0, i);
        EntangleControl *that = g_object_get_data(G_OBJECT(widget), "control");

        if (that == control) {
            gtk_grid_remove_row(GTK_GRID(panel->grid), i);
            panel->rows--;
            break;
        }
    }
    do_update_control_prefs(panel);
}

static void
do_addremove_control(GtkWidget *src, EntangleControlPanel *panel)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(panel));

    EntangleControl *control;

    control = g_object_get_data(G_OBJECT(src), "control");
    g_return_if_fail(ENTANGLE_IS_CONTROL(control));

    if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(src)))
        do_add_control(panel, control);
    else
        do_remove_control(panel, control);
}

static GtkWidget *
do_create_control_menu(EntangleControlPanel *panel,
                       EntangleControlGroup *group,
                       gchar **controlnames)
{
    GList *controls = do_get_control_list(group);
    GList *tmp;
    GtkWidget *menu = gtk_menu_new();
    GtkWidget *item;

    tmp = controls = g_list_sort(controls, compare_control);

    while (tmp) {
        EntangleControl *control = tmp->data;
        item = gtk_check_menu_item_new_with_label(
            entangle_control_get_label(control));

        g_object_set_data(G_OBJECT(item), "control", control);
        g_signal_connect(item, "toggled", G_CALLBACK(do_addremove_control),
                         panel);

        gtk_container_add(GTK_CONTAINER(menu), item);

        if (is_control_enabled(controlnames,
                               entangle_control_get_path(control)))
            gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);

        tmp = tmp->next;
    }

    g_list_free(controls);

    item = gtk_separator_menu_item_new();
    gtk_container_add(GTK_CONTAINER(menu), item);

    item = gtk_menu_item_new_with_label(_("Reset controls"));
    gtk_container_add(GTK_CONTAINER(menu), item);
    g_signal_connect(item, "activate", G_CALLBACK(do_reset_controls), panel);

    gtk_widget_show_all(menu);

    return menu;
}

static void
do_setup_controls(EntangleControlPanel *panel)
{
    EntangleControlGroup *root;
    GtkWidget *settingsButton;
    gchar **controls;
    GtkWidget *menu;
    gsize i;

    root = entangle_camera_get_controls(panel->camera, NULL);

    controls = entangle_camera_preferences_get_controls(panel->cameraPrefs);
    if (!controls || !controls[0]) {
        controls = entangle_control_panel_get_default_controls(root);
        entangle_camera_preferences_set_controls(panel->cameraPrefs,
                                                 (const char *const *)controls);
    }

    for (i = 0; controls[i] != NULL; i++) {
        EntangleControl *control =
            entangle_control_group_get_by_path(root, controls[i]);
        if (control)
            do_setup_control(panel, control, GTK_CONTAINER(panel->grid),
                             panel->rows++);
    }

    menu = do_create_control_menu(panel, root, controls);

    settingsButton = gtk_menu_button_new();
    gtk_container_add(
        GTK_CONTAINER(settingsButton),
        gtk_image_new_from_icon_name("emblem-system-symbolic",
                                     GTK_ICON_SIZE_SMALL_TOOLBAR));
    gtk_menu_button_set_popup(GTK_MENU_BUTTON(settingsButton), menu);
    gtk_widget_set_hexpand(settingsButton, TRUE);
    gtk_widget_set_halign(settingsButton, GTK_ALIGN_END);
    gtk_widget_set_margin_end(settingsButton, 6);

    gtk_grid_attach(GTK_GRID(panel->grid), settingsButton, 1, panel->rows, 2,
                    1);

    gtk_widget_show_all(GTK_WIDGET(panel));
    g_object_unref(root);
    g_strfreev(controls);
}

static void
do_setup_camera(EntangleControlPanel *panel)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(panel));

    EntangleControlGroup *ctrls = NULL;

    gtk_container_foreach(GTK_CONTAINER(panel->grid), do_control_remove, panel);
    panel->rows = 0;

    if (!panel->camera) {
        GtkWidget *label = gtk_label_new(_("No camera connected"));

        gtk_widget_set_hexpand(label, TRUE);
        gtk_widget_set_halign(label, GTK_ALIGN_FILL);
        gtk_grid_attach(GTK_GRID(panel->grid), label, 0, 0, 2, 1);
        gtk_widget_show_all(GTK_WIDGET(panel));
    } else if ((ctrls = entangle_camera_get_controls(panel->camera, NULL)) ==
               NULL) {
        GtkWidget *label = gtk_label_new(_("No controls available"));
        gtk_widget_set_hexpand(label, TRUE);
        gtk_widget_set_halign(label, GTK_ALIGN_FILL);
        gtk_grid_attach(GTK_GRID(panel->grid), label, 0, 0, 2, 1);
        gtk_widget_show_all(GTK_WIDGET(panel));
    } else {
        do_setup_controls(panel);
    }
    if (ctrls)
        g_object_unref(ctrls);
}

static void
do_update_camera(GObject *object G_GNUC_UNUSED,
                 GParamSpec *pspec G_GNUC_UNUSED,
                 gpointer data)
{
    g_return_if_fail(ENTANGLE_IS_CONTROL_PANEL(data));

    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(data);

    if (panel->camera) {
        g_object_unref(panel->camera);
        panel->camera = NULL;
    }
    panel->camera = entangle_camera_preferences_get_camera(panel->cameraPrefs);
    if (panel->camera)
        g_object_ref(panel->camera);

    do_setup_camera(panel);
}

static void
entangle_control_panel_get_property(GObject *object,
                                    guint prop_id,
                                    GValue *value,
                                    GParamSpec *pspec)
{
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(object);

    switch (prop_id) {
    case PROP_CAMERA_PREFS:
        g_value_set_object(value, panel->cameraPrefs);
        break;

    case PROP_CAMERA:
        g_value_set_object(value, panel->camera);
        break;

    case PROP_HAS_CONTROLS:
        g_value_set_boolean(value, panel->hasControls);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_control_panel_set_property(GObject *object,
                                    guint prop_id,
                                    const GValue *value,
                                    GParamSpec *pspec)
{
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(object);

    ENTANGLE_DEBUG("Set prop on control panel %d", prop_id);

    switch (prop_id) {
    case PROP_CAMERA_PREFS:
        panel->cameraPrefs = g_value_get_object(value);
        panel->sigCamera =
            g_signal_connect(panel->cameraPrefs, "notify::camera",
                             G_CALLBACK(do_update_camera), panel);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_control_panel_finalize(GObject *object)
{
    EntangleControlPanel *panel = ENTANGLE_CONTROL_PANEL(object);

    if (panel->camera)
        g_object_unref(panel->camera);

    if (panel->cameraPrefs) {
        g_signal_handler_disconnect(panel->cameraPrefs, panel->sigCamera);
        g_object_unref(panel->cameraPrefs);
    }

    G_OBJECT_CLASS(entangle_control_panel_parent_class)->finalize(object);
}

static void
entangle_control_panel_class_init(EntangleControlPanelClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->finalize = entangle_control_panel_finalize;
    object_class->get_property = entangle_control_panel_get_property;
    object_class->set_property = entangle_control_panel_set_property;

    g_object_class_install_property(
        object_class, PROP_CAMERA_PREFS,
        g_param_spec_object(
            "camera-prefs", "Camera prefs", "Camera preferences to manage",
            ENTANGLE_TYPE_CAMERA_PREFERENCES,
            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));

    g_object_class_install_property(
        object_class, PROP_CAMERA,
        g_param_spec_object("camera", "Camera", "Camera to manage",
                            ENTANGLE_TYPE_CAMERA,
                            G_PARAM_READABLE | G_PARAM_STATIC_NAME |
                                G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));

    g_object_class_install_property(
        object_class, PROP_HAS_CONTROLS,
        g_param_spec_boolean("has-controls", "Has Controls", "Has Controls",
                             FALSE,
                             G_PARAM_READABLE | G_PARAM_STATIC_NAME |
                                 G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
}

EntangleControlPanel *
entangle_control_panel_new(EntangleCameraPreferences *prefs)
{
    return ENTANGLE_CONTROL_PANEL(
        g_object_new(ENTANGLE_TYPE_CONTROL_PANEL, "camera-prefs", prefs,
                     "label", "Camera settings", "expanded", TRUE, NULL));
}

static void
entangle_control_panel_init(EntangleControlPanel *panel)
{
    gtk_container_set_border_width(GTK_CONTAINER(panel), 0);

    panel->grid = gtk_grid_new();
    gtk_grid_set_row_spacing(GTK_GRID(panel->grid), 6);
    gtk_grid_set_column_spacing(GTK_GRID(panel->grid), 6);
    gtk_container_set_border_width(GTK_CONTAINER(panel->grid), 6);
    gtk_widget_set_hexpand(panel->grid, TRUE);
    gtk_widget_set_halign(panel->grid, GTK_ALIGN_FILL);

    gtk_container_add(GTK_CONTAINER(panel), panel->grid);

    do_setup_camera(panel);
}

/**
 * entangle_control_panel_get_camera_preferences:
 * @panel: the control widget
 *
 * Get the camera preferences whose controls are displayed
 *
 * Returns: (transfer none): the camera preferences or NULL
 */
EntangleCameraPreferences *
entangle_control_panel_get_camera_preferences(EntangleControlPanel *panel)
{
    g_return_val_if_fail(ENTANGLE_IS_CONTROL_PANEL(panel), NULL);

    return panel->cameraPrefs;
}

gboolean
entangle_control_panel_get_has_controls(EntangleControlPanel *panel)
{
    g_return_val_if_fail(ENTANGLE_IS_CONTROL_PANEL(panel), FALSE);

    return panel->hasControls;
}

/*
 * Local variables:
 *  c-indent-level: 4
 *  c-basic-offset: 4
 *  indent-tabs-mode: nil
 *  tab-width: 8
 * End:
 */
