#include <gtk/gtk.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libintl.h>
#include <locale.h>

#include <glib.h>

#include <stdio.h>
#include <string.h>

#ifndef LOCALEDIR
#define LOCALEDIR "/usr/share/locale"
#endif

#define _(String) gettext(String)
#define N_(String) String

enum {
    COL_KEY = 0,
    COL_VALUE,
    COL_TOOLTIP,
    COL_CHOICES,
    COL_CHOICE_MODEL,
    N_COLS
};

static const char *excluded_roots[] = {
    "keyboard",
    "mouse",
    "windowRules",
    NULL
};

typedef struct {
    gchar *config_path;
    gchar *reference_path;
    gboolean backup;
} CliOptions;

typedef struct {
    CliOptions opts;
    xmlDocPtr doc;
    GPtrArray *keys;
    GtkListStore *store;
    GtkTreeModelFilter *filter;
    gchar *query;
    gchar *original_xml;
    GtkWidget *status;
    GtkWidget *window;
    GtkWidget *view;
    GtkWidget *detail_btn;
    GtkTreeViewColumn *key_col;
    GtkTreeViewColumn *value_col;
    gboolean show_all;
} GuiState;

static const char *brief_keys[] = {
    "desktops.names.name",
    "desktops.number",
    "placement.policy",
    "resize.popupShow",
    "theme.font.name",
    "theme.font.size",
    "theme.name",
    "theme.titlebar.layout",
    "windowSwitcher.osd@show",
    "windowSwitcher.osd@style",
    NULL
};

typedef struct {
    const gchar *key;
    const gchar *hint;
    const gchar *choices;
} KeyMeta;

static const KeyMeta key_meta[] = {
    { "core.decoration", N_("Who draws window decorations."), "server|client" },
    { "core.gap", N_("Outer gap around tiled/maximized windows in pixels."), NULL },
    { "core.adaptiveSync", N_("Enable adaptive sync (VRR)."), "yes|no" },
    { "core.allowTearing", N_("Allow tearing when supported."), "yes|no" },
    { "core.autoEnableOutputs", N_("Auto-enable newly connected outputs."), "yes|no" },
    { "core.reuseOutputMode", N_("Reuse previous output mode after reconnect."), "yes|no" },
    { "core.xwaylandPersistence", N_("Keep Xwayland running between clients."), "yes|no" },
    { "core.primarySelection", N_("Enable primary selection clipboard."), "yes|no" },
    { "placement.policy", N_("Policy for placing newly opened windows."), "center|automatic|cursor|cascade" },
    { "theme.fallbackAppIcon", N_("Fallback icon name for windows without icon."), NULL },
    { "theme.titlebar.layout", N_("Titlebar buttons layout."), NULL },
    { "theme.titlebar.showTitle", N_("Show window title text in titlebar."), "yes|no" },
    { "theme.cornerRadius", N_("Window corner radius."), NULL },
    { "theme.keepBorder", N_("Keep border around maximized windows."), "yes|no" },
    { "theme.maximizedDecoration", N_("Decoration mode for maximized windows."), "titlebar|border|none" },
    { "theme.dropShadows", N_("Draw drop shadows."), "yes|no" },
    { "theme.dropShadowsOnTiled", N_("Draw shadows for tiled windows."), "yes|no" },
    { "windowSwitcher@preview", N_("Show live preview in window switcher."), "yes|no" },
    { "windowSwitcher@outlines", N_("Show outlines while switching."), "yes|no" },
    { "windowSwitcher@unshade", N_("Unshade windows when selecting."), "yes|no" },
    { "windowSwitcher.osd@show", N_("Show OSD window switcher popup."), "yes|no" },
    { "windowSwitcher.osd@style", N_("Window switcher visual style."), "classic|thumbnail" },
    { "windowSwitcher.osd@output", N_("Output for switcher OSD."), "all|focused|cursor" },
    { "windowSwitcher.order", N_("Order in window switcher."), "focus|age" },
    { "resistance.screenEdgeStrength", N_("Resistance when moving to screen edge."), NULL },
    { "resistance.windowEdgeStrength", N_("Resistance when snapping to other windows."), NULL },
    { "resistance.unSnapThreshold", N_("Drag threshold to unsnap tiled windows."), NULL },
    { "resistance.unMaximizeThreshold", N_("Drag threshold to unmaximize windows."), NULL },
    { "resize.popupShow", N_("When resize/move popup is shown."), "Never|Always|Nonpixel" },
    { "resize.drawContents", N_("Redraw client contents during resize."), "yes|no" },
    { "snapping.overlay@enabled", N_("Enable snap overlay previews."), "yes|no" },
    { "snapping.topMaximize", N_("Maximize when moved to top edge."), "yes|no" },
    { "snapping.notifyClient", N_("Notify clients when snapped."), "always|region|edge|never" },
    { "focus.followMouse", N_("Focus windows under pointer."), "yes|no" },
    { "focus.followMouseRequiresMovement", N_("Require pointer movement before focus-follow."), "yes|no" },
    { "focus.raiseOnFocus", N_("Raise window when focused."), "yes|no" },
    { "menu.showIcons", N_("Show icons in menu entries."), "yes|no" },
    { "magnifier.useFilter", N_("Use filtered scaling in magnifier."), "yes|no" },
    { "touch.mouseEmulation", N_("Emulate mouse with touch input."), "yes|no" },
    { "tablet@mouseEmulation", N_("Emulate mouse with tablet input."), "yes|no" },
    { "tabletTool@motion", N_("Tablet tool motion mode."), "absolute|relative" },
    { "tablet.map@to", N_("Mouse button mapped from tablet action."), "Left|Middle|Right" },
    { "tablet.map@button", N_("Tablet trigger mapped to mouse button."), "Tip|Stylus|Stylus2" },
    { NULL, NULL, NULL }
};

static const KeyMeta *meta_for_key(const gchar *key)
{
    gint i;

    for (i = 0; key_meta[i].key != NULL; i++)
        if (g_strcmp0(key_meta[i].key, key) == 0)
            return &key_meta[i];
    return NULL;
}

static gboolean is_boolean_value(const gchar *value)
{
    gchar *lower;
    gboolean ret;

    if (value == NULL || value[0] == '\0')
        return FALSE;
    lower = g_ascii_strdown(value, -1);
    ret = (g_strcmp0(lower, "yes") == 0 ||
           g_strcmp0(lower, "no") == 0 ||
           g_strcmp0(lower, "true") == 0 ||
           g_strcmp0(lower, "false") == 0 ||
           g_strcmp0(lower, "on") == 0 ||
           g_strcmp0(lower, "off") == 0 ||
           g_strcmp0(lower, "1") == 0 ||
           g_strcmp0(lower, "0") == 0);
    g_free(lower);
    return ret;
}

static gchar *tooltip_for_key(const gchar *key)
{
    const KeyMeta *meta = meta_for_key(key);
    const gchar *hint;
    GString *tip;

    if (g_strcmp0(key, "desktops.names.name") == 0)
        hint = _("Base name for virtual desktops. Saved as Name1, Name2, ... according to desktops.number.");
    else if (g_strcmp0(key, "desktops.number") == 0 || g_strcmp0(key, "desktops@number") == 0)
        hint = _("Number of virtual desktops. Increasing adds new names (D1, D2, ...), decreasing removes extra names.");
    else if (g_strcmp0(key, "theme.name") == 0)
        hint = _("Theme name. Select from installed Openbox themes.");
    else if (g_strcmp0(key, "theme.font.name") == 0)
        hint = _("Font family used by window title and decoration text.");
    else if (g_strcmp0(key, "theme.font.size") == 0)
        hint = _("Font size for window title and decoration text.");
    else if (meta != NULL && meta->hint != NULL)
        hint = _(meta->hint);
    else
        hint = _("General setting.");

    tip = g_string_new(hint);
    g_string_append_printf(tip, "\n%s %s", _("Field:"), key);
    if (meta != NULL && meta->choices != NULL)
        g_string_append_printf(tip, "\n%s %s", _("Allowed values:"), meta->choices);

    return g_string_free(tip, FALSE);
}

static gint str_ptr_compare(gconstpointer a, gconstpointer b)
{
    return g_strcmp0(*(char * const *)a, *(char * const *)b);
}

static gboolean ptr_array_has_str(GPtrArray *arr, const gchar *s)
{
    guint i;

    for (i = 0; i < arr->len; i++) {
        const gchar *v = g_ptr_array_index(arr, i);

        if (g_strcmp0(v, s) == 0)
            return TRUE;
    }
    return FALSE;
}

static gchar **system_theme_choices(const gchar *current)
{
    static const gchar *roots[] = {
        "/usr/local/share/themes",
        "/usr/share/themes",
        NULL
    };
    GPtrArray *arr = g_ptr_array_new_with_free_func(g_free);
    gint i;

    for (i = 0; roots[i] != NULL; i++) {
        GDir *dir = g_dir_open(roots[i], 0, NULL);
        const gchar *name;

        if (dir == NULL)
            continue;
        while ((name = g_dir_read_name(dir)) != NULL) {
            gchar *themerc = g_build_filename(roots[i], name, "openbox-3", "themerc", NULL);

            if (g_file_test(themerc, G_FILE_TEST_EXISTS) && !ptr_array_has_str(arr, name))
                g_ptr_array_add(arr, g_strdup(name));
            g_free(themerc);
        }
        g_dir_close(dir);
    }

    if (current != NULL && current[0] != '\0' && !ptr_array_has_str(arr, current))
        g_ptr_array_add(arr, g_strdup(current));

    g_ptr_array_sort(arr, str_ptr_compare);

    if (arr->len == 0) {
        g_ptr_array_free(arr, TRUE);
        return NULL;
    }

    {
        gchar **out = g_new0(gchar *, arr->len + 1);
        guint k;

        for (k = 0; k < arr->len; k++)
            out[k] = g_strdup(g_ptr_array_index(arr, k));
        g_ptr_array_free(arr, TRUE);
        return out;
    }
}

static gchar **choices_for_key(const gchar *key, const gchar *current)
{
    const KeyMeta *meta = meta_for_key(key);

    if (g_strcmp0(key, "theme.name") == 0)
        return system_theme_choices(current);

    if (meta != NULL && meta->choices != NULL)
        return g_strsplit(meta->choices, "|", -1);
    if (is_boolean_value(current))
        return g_strsplit("yes|no", "|", -1);
    return NULL;
}

static void print_usage(void)
{
    puts(_("labsetup - configure non-hotkey labwc settings"));
    puts("");
    puts(_("Usage:"));
    puts("  labsetup [--file PATH] [--reference PATH] list [--values]");
    puts("  labsetup [--file PATH] [--reference PATH] get KEY");
    puts("  labsetup [--file PATH] [--reference PATH] set KEY VALUE [--no-backup]");
    puts("  labsetup [--file PATH] [--reference PATH] unset KEY [--no-backup]");
    puts("  labsetup [--file PATH] [--reference PATH] gui [--no-backup]");
    puts("  labsetup [--file PATH] [--reference PATH]");
    puts("");
    puts(_("Notes:"));
    puts(_("  - default config: ~/.config/labwc/rc.xml"));
    puts(_("  - default reference: ~/ptheme-labwc-src/rc.xml.all (optional)"));
    puts(_("  - no command opens GTK editor"));
}

static gchar *default_config_path(void)
{
    return g_build_filename(g_get_home_dir(), ".config", "labwc", "rc.xml", NULL);
}

static gchar *default_reference_path(void)
{
    return g_build_filename(g_get_home_dir(), "ptheme-labwc-src", "rc.xml.all", NULL);
}

static xmlDocPtr load_doc_or_die(const char *path)
{
    xmlDocPtr doc = xmlReadFile(path, NULL, XML_PARSE_NOBLANKS);
    if (doc == NULL)
        g_error("Config not found or invalid XML: %s", path);
    return doc;
}

static xmlDocPtr load_doc(const char *path)
{
    return xmlReadFile(path, NULL, XML_PARSE_NOBLANKS);
}

static gboolean is_excluded_root(const char *name)
{
    int i;

    for (i = 0; excluded_roots[i] != NULL; i++)
        if (g_strcmp0(excluded_roots[i], name) == 0)
            return TRUE;
    return FALSE;
}

static gboolean has_element_children(xmlNodePtr node)
{
    xmlNodePtr child;

    for (child = node->children; child != NULL; child = child->next)
        if (child->type == XML_ELEMENT_NODE)
            return TRUE;
    return FALSE;
}

static gchar *node_text_trimmed(xmlNodePtr node)
{
    xmlChar *raw = xmlNodeGetContent(node);
    gchar *text = g_strdup((const char *)(raw ? raw : BAD_CAST ""));
    gchar *trimmed = g_strstrip(text);
    gchar *out = g_strdup(trimmed);

    if (raw != NULL)
        xmlFree(raw);
    g_free(text);
    return out;
}

static void catalog_add_node(xmlNodePtr node, const gchar *path, GHashTable *set)
{
    xmlAttrPtr attr;
    gchar *key;
    xmlNodePtr child;

    for (attr = node->properties; attr != NULL; attr = attr->next) {
        key = g_strdup_printf("%s@%s", path, (const char *)attr->name);
        g_hash_table_add(set, key);
    }

    if (!has_element_children(node)) {
        gchar *text = node_text_trimmed(node);

        if (text[0] != '\0')
            g_hash_table_add(set, g_strdup(path));
        g_free(text);
    }

    for (child = node->children; child != NULL; child = child->next) {
        gchar *child_path;

        if (child->type != XML_ELEMENT_NODE)
            continue;
        child_path = g_strdup_printf("%s.%s", path, (const char *)child->name);
        catalog_add_node(child, child_path, set);
        g_free(child_path);
    }
}

static void catalog_add_doc(xmlDocPtr doc, GHashTable *set)
{
    xmlNodePtr root = xmlDocGetRootElement(doc);
    xmlNodePtr child;

    if (root == NULL)
        return;

    for (child = root->children; child != NULL; child = child->next) {
        gchar *path;

        if (child->type != XML_ELEMENT_NODE)
            continue;
        if (is_excluded_root((const char *)child->name))
            continue;
        path = g_strdup((const char *)child->name);
        catalog_add_node(child, path, set);
        g_free(path);
    }
}

static gint str_compare(gconstpointer a, gconstpointer b)
{
    return g_strcmp0(*(char * const *)a, *(char * const *)b);
}

static GPtrArray *build_catalog(const CliOptions *opts, xmlDocPtr config_doc)
{
    GHashTable *set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
    GHashTableIter iter;
    gpointer key;
    GPtrArray *arr = g_ptr_array_new_with_free_func(g_free);
    xmlDocPtr ref_doc = NULL;

    catalog_add_doc(config_doc, set);

    if (opts->reference_path && g_file_test(opts->reference_path, G_FILE_TEST_EXISTS)) {
        ref_doc = load_doc(opts->reference_path);
        if (ref_doc != NULL)
            catalog_add_doc(ref_doc, set);
    }

    g_hash_table_iter_init(&iter, set);
    while (g_hash_table_iter_next(&iter, &key, NULL))
        g_ptr_array_add(arr, g_strdup((const char *)key));

    g_ptr_array_sort(arr, str_compare);
    g_hash_table_destroy(set);

    if (ref_doc != NULL)
        xmlFreeDoc(ref_doc);
    return arr;
}

static gboolean split_key(const gchar *key, gchar **path, gchar **attr)
{
    gchar *at;

    *path = g_strdup(key);
    *attr = NULL;

    at = strrchr(*path, '@');
    if (at == NULL)
        return TRUE;
    if (at == *path || at[1] == '\0')
        return FALSE;

    *at = '\0';
    *attr = g_strdup(at + 1);
    return TRUE;
}

static xmlNodePtr find_child_by_name(xmlNodePtr parent, const gchar *name)
{
    xmlNodePtr child;

    for (child = parent->children; child != NULL; child = child->next)
        if (child->type == XML_ELEMENT_NODE && g_strcmp0((const char *)child->name, name) == 0)
            return child;
    return NULL;
}

static xmlNodePtr find_path_node(xmlDocPtr doc, const gchar *dot_path)
{
    gchar **parts = g_strsplit(dot_path, ".", -1);
    gint i;
    xmlNodePtr node = xmlDocGetRootElement(doc);

    if (node == NULL) {
        g_strfreev(parts);
        return NULL;
    }

    for (i = 0; parts[i] != NULL; i++) {
        node = find_child_by_name(node, parts[i]);
        if (node == NULL)
            break;
    }

    g_strfreev(parts);
    return node;
}

static xmlNodePtr ensure_path_node(xmlDocPtr doc, const gchar *dot_path)
{
    gchar **parts = g_strsplit(dot_path, ".", -1);
    gint i;
    xmlNodePtr node = xmlDocGetRootElement(doc);

    if (node == NULL) {
        node = xmlNewNode(NULL, BAD_CAST "labwc_config");
        xmlDocSetRootElement(doc, node);
    }

    for (i = 0; parts[i] != NULL; i++) {
        xmlNodePtr next = find_child_by_name(node, parts[i]);

        if (next == NULL)
            next = xmlNewChild(node, NULL, BAD_CAST parts[i], NULL);
        node = next;
    }

    g_strfreev(parts);
    return node;
}

static gchar *get_value(xmlDocPtr doc, const gchar *key)
{
    gchar *path = NULL, *attr = NULL;
    xmlNodePtr node;

    if (!split_key(key, &path, &attr)) {
        g_free(path);
        return NULL;
    }
    node = find_path_node(doc, path);
    if (node == NULL) {
        g_free(path);
        g_free(attr);
        return NULL;
    }

    if (attr != NULL) {
        xmlChar *prop = xmlGetProp(node, BAD_CAST attr);
        gchar *value = prop ? g_strdup((const char *)prop) : NULL;

        if (prop != NULL)
            xmlFree(prop);
        g_free(path);
        g_free(attr);
        return value;
    }

    if (g_strcmp0(key, "desktops.names.name") == 0) {
        gchar *raw = node_text_trimmed(node);
        gsize len = strlen(raw);

        while (len > 0 && g_ascii_isdigit(raw[len - 1])) {
            raw[len - 1] = '\0';
            len--;
        }
        while (len > 0 && g_ascii_isspace(raw[len - 1])) {
            raw[len - 1] = '\0';
            len--;
        }
        g_free(path);
        g_free(attr);
        return raw;
    }

    g_free(path);
    g_free(attr);
    return node_text_trimmed(node);
}

static gboolean set_value(xmlDocPtr doc, const gchar *key, const gchar *value)
{
    gchar *path = NULL, *attr = NULL;
    xmlNodePtr node;

    if (g_strcmp0(key, "desktops.names.name") == 0) {
        xmlNodePtr desktops = ensure_path_node(doc, "desktops");
        xmlNodePtr names = ensure_path_node(doc, "desktops.names");
        xmlNodePtr child;
        xmlChar *attr_num;
        gint count = 0;

        attr_num = xmlGetProp(desktops, BAD_CAST "number");
        if (attr_num != NULL) {
            count = (gint)g_ascii_strtoll((const gchar *)attr_num, NULL, 10);
            xmlFree(attr_num);
        }
        if (count <= 0) {
            xmlNodePtr number_node = find_child_by_name(desktops, "number");

            if (number_node != NULL) {
                gchar *num_text = node_text_trimmed(number_node);

                count = (gint)g_ascii_strtoll(num_text, NULL, 10);
                g_free(num_text);
            }
        }
        if (count <= 0) {
            for (child = names->children; child != NULL; child = child->next)
                if (child->type == XML_ELEMENT_NODE && g_strcmp0((const char *)child->name, "name") == 0)
                    count++;
        }
        if (count <= 0)
            count = 1;

        child = names->children;
        while (child != NULL) {
            xmlNodePtr next = child->next;

            if (child->type == XML_ELEMENT_NODE && g_strcmp0((const char *)child->name, "name") == 0) {
                xmlUnlinkNode(child);
                xmlFreeNode(child);
            }
            child = next;
        }

        for (gint i = 1; i <= count; i++) {
            gchar *label = g_strdup_printf("%s%d", value, i);

            xmlNewChild(names, NULL, BAD_CAST "name", BAD_CAST label);
            g_free(label);
        }
        return TRUE;
    }

    if (g_strcmp0(key, "desktops.number") == 0 || g_strcmp0(key, "desktops@number") == 0) {
        gint count = (gint)g_ascii_strtoll(value, NULL, 10);

        if (count > 0) {
            xmlNodePtr names = ensure_path_node(doc, "desktops.names");
            xmlNodePtr child;
            GPtrArray *existing = g_ptr_array_new();

            for (child = names->children; child != NULL; child = child->next) {
                if (child->type == XML_ELEMENT_NODE && g_strcmp0((const char *)child->name, "name") == 0)
                    g_ptr_array_add(existing, child);
            }

            for (gint i = (gint)existing->len - 1; i >= count; i--) {
                xmlNodePtr n = g_ptr_array_index(existing, i);

                xmlUnlinkNode(n);
                xmlFreeNode(n);
            }

            for (gint i = (gint)existing->len + 1; i <= count; i++) {
                gchar *label = g_strdup_printf("D%d", i);

                xmlNewChild(names, NULL, BAD_CAST "name", BAD_CAST label);
                g_free(label);
            }
            g_ptr_array_free(existing, TRUE);
        }
    }

    if (!split_key(key, &path, &attr)) {
        g_free(path);
        return FALSE;
    }
    node = ensure_path_node(doc, path);

    if (attr != NULL)
        xmlSetProp(node, BAD_CAST attr, BAD_CAST value);
    else
        xmlNodeSetContent(node, BAD_CAST value);

    g_free(path);
    g_free(attr);
    return TRUE;
}

static gboolean unset_value(xmlDocPtr doc, const gchar *key)
{
    gchar *path = NULL, *attr = NULL;
    xmlNodePtr node;

    if (!split_key(key, &path, &attr)) {
        g_free(path);
        return FALSE;
    }
    node = find_path_node(doc, path);
    if (node == NULL) {
        g_free(path);
        g_free(attr);
        return FALSE;
    }

    if (attr != NULL) {
        int ret = xmlUnsetProp(node, BAD_CAST attr);

        g_free(path);
        g_free(attr);
        return ret == 0;
    }

    xmlNodeSetContent(node, BAD_CAST "");
    g_free(path);
    g_free(attr);
    return TRUE;
}

static gboolean backup_file(const gchar *path)
{
    gchar *contents = NULL;
    gsize len = 0;
    gchar *backup;
    GError *err = NULL;
    gboolean ok;

    if (!g_file_test(path, G_FILE_TEST_EXISTS))
        return TRUE;
    if (!g_file_get_contents(path, &contents, &len, &err)) {
        g_printerr("Backup read failed: %s\n", err->message);
        g_error_free(err);
        return FALSE;
    }

    backup = g_strconcat(path, ".bak", NULL);
    ok = g_file_set_contents(backup, contents, len, &err);
    if (!ok) {
        g_printerr("Backup write failed: %s\n", err->message);
        g_error_free(err);
    }

    g_free(backup);
    g_free(contents);
    return ok;
}

static gboolean save_doc(xmlDocPtr doc, const gchar *path, gboolean backup)
{
    if (backup && !backup_file(path))
        return FALSE;
    return xmlSaveFormatFileEnc(path, doc, "UTF-8", 1) >= 0;
}

static gchar *serialize_doc(xmlDocPtr doc)
{
    xmlChar *buf = NULL;
    gint size = 0;
    gchar *out = NULL;

    xmlDocDumpFormatMemoryEnc(doc, &buf, &size, "UTF-8", 1);
    if (buf != NULL) {
        out = g_strndup((const gchar *)buf, size);
        xmlFree(buf);
    }
    return out;
}

static gboolean validate_doc_syntax(xmlDocPtr doc, gchar **error_text)
{
    gchar *serialized;
    xmlDocPtr check;

    *error_text = NULL;
    serialized = serialize_doc(doc);
    if (serialized == NULL || serialized[0] == '\0') {
        *error_text = g_strdup(_("XML check failed: cannot serialize document"));
        g_free(serialized);
        return FALSE;
    }

    check = xmlReadMemory(serialized, (int)strlen(serialized), "rc.xml", "UTF-8", XML_PARSE_NOBLANKS);
    g_free(serialized);
    if (check == NULL) {
        *error_text = g_strdup(_("XML check failed: generated config is not well-formed"));
        return FALSE;
    }

    xmlFreeDoc(check);
    return TRUE;
}

static gchar *build_changes_preview(GuiState *st, guint *out_count)
{
    xmlDocPtr old_doc = NULL;
    GPtrArray *keys;
    GString *preview = g_string_new("");
    guint i;
    guint count = 0;

    if (st->original_xml != NULL && st->original_xml[0] != '\0')
        old_doc = xmlReadMemory(st->original_xml,
                                (int)strlen(st->original_xml),
                                "rc.xml",
                                "UTF-8",
                                XML_PARSE_NOBLANKS);

    keys = build_catalog(&st->opts, st->doc);
    for (i = 0; i < keys->len; i++) {
        const gchar *key = g_ptr_array_index(keys, i);
        gchar *old_v = old_doc ? get_value(old_doc, key) : NULL;
        gchar *new_v = get_value(st->doc, key);
        const gchar *old_s = (old_v && old_v[0]) ? old_v : "<unset>";
        const gchar *new_s = (new_v && new_v[0]) ? new_v : "<unset>";

        if (g_strcmp0(old_s, new_s) != 0) {
            g_string_append_printf(preview,
                                   "%s\n  - %s\n  + %s\n\n",
                                   key,
                                   old_s,
                                   new_s);
            count++;
        }
        g_free(old_v);
        g_free(new_v);
    }

    if (old_doc)
        xmlFreeDoc(old_doc);
    g_ptr_array_free(keys, TRUE);
    *out_count = count;
    return g_string_free(preview, FALSE);
}

static gboolean confirm_apply_with_diff(GuiState *st)
{
    guint count = 0;
    gchar *diff = build_changes_preview(st, &count);
    GtkWidget *dlg;
    GtkWidget *area;
    GtkWidget *box;
    GtkWidget *label;
    GtkWidget *scroll;
    GtkWidget *view;
    GtkTextBuffer *buf;
    GtkWidget *btn;
    gint response;
    gboolean proceed;

    dlg = gtk_dialog_new_with_buttons(_("Apply changes"),
                                      GTK_WINDOW(st->window),
                                      GTK_DIALOG_MODAL,
                                      _("Cancel"), GTK_RESPONSE_CANCEL,
                                      _("Apply"), GTK_RESPONSE_OK,
                                      NULL);
    gtk_window_set_default_size(GTK_WINDOW(dlg), 720, 420);

    btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dlg), GTK_RESPONSE_CANCEL);
    if (btn != NULL) {
        gtk_button_set_image(GTK_BUTTON(btn),
                             gtk_image_new_from_icon_name("gtk-cancel", GTK_ICON_SIZE_BUTTON));
        gtk_button_set_always_show_image(GTK_BUTTON(btn), TRUE);
    }

    btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dlg), GTK_RESPONSE_OK);
    if (btn != NULL) {
        gtk_button_set_image(GTK_BUTTON(btn),
                             gtk_image_new_from_icon_name("gtk-save", GTK_ICON_SIZE_BUTTON));
        gtk_button_set_always_show_image(GTK_BUTTON(btn), TRUE);
    }

    area = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
    gtk_container_set_border_width(GTK_CONTAINER(box), 8);
    gtk_container_add(GTK_CONTAINER(area), box);

    if (count == 0)
        label = gtk_label_new(_("No changes detected."));
    else
        label = gtk_label_new(_("Review changes before apply:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
    gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);

    scroll = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
                                   GTK_POLICY_AUTOMATIC,
                                   GTK_POLICY_AUTOMATIC);
    gtk_widget_set_vexpand(scroll, TRUE);

    view = gtk_text_view_new();
    gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
    gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE);
    gtk_text_view_set_monospace(GTK_TEXT_VIEW(view), TRUE);
    buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
    gtk_text_buffer_set_text(buf,
                             (count == 0) ? _("Configuration is unchanged.") : diff,
                             -1);
    gtk_container_add(GTK_CONTAINER(scroll), view);
    gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0);

    gtk_widget_show_all(dlg);
    response = gtk_dialog_run(GTK_DIALOG(dlg));
    proceed = (response == GTK_RESPONSE_OK);
    gtk_widget_destroy(dlg);
    g_free(diff);
    return proceed;
}

static void update_column_widths(GuiState *st)
{
    gtk_tree_view_column_set_sizing(st->key_col, GTK_TREE_VIEW_COLUMN_FIXED);
    gtk_tree_view_column_set_sizing(st->value_col, GTK_TREE_VIEW_COLUMN_FIXED);
    gtk_tree_view_column_set_fixed_width(st->key_col, 500);
    gtk_tree_view_column_set_fixed_width(st->value_col, 100);
    gtk_tree_view_column_set_expand(st->key_col, FALSE);
    gtk_tree_view_column_set_expand(st->value_col, FALSE);
}

static void expand_columns_for_edit(GuiState *st)
{
    gtk_tree_view_column_set_sizing(st->key_col, GTK_TREE_VIEW_COLUMN_FIXED);
    gtk_tree_view_column_set_sizing(st->value_col, GTK_TREE_VIEW_COLUMN_FIXED);
    gtk_tree_view_column_set_fixed_width(st->key_col, 500);
    gtk_tree_view_column_set_fixed_width(st->value_col, 100);
}

static int cmd_list(const CliOptions *opts, gboolean with_values)
{
    guint i;
    guint width = 1;
    xmlDocPtr doc = load_doc_or_die(opts->config_path);
    GPtrArray *keys = build_catalog(opts, doc);

    if (!with_values) {
        for (i = 0; i < keys->len; i++)
            puts((const char *)g_ptr_array_index(keys, i));
    } else {
        for (i = 0; i < keys->len; i++) {
            guint len = (guint)strlen((const char *)g_ptr_array_index(keys, i));

            if (len > width)
                width = len;
        }
        for (i = 0; i < keys->len; i++) {
            const gchar *key = g_ptr_array_index(keys, i);
            gchar *value = get_value(doc, key);

            printf("%-*s  %s\n", width, key, value && value[0] ? value : "<unset>");
            g_free(value);
        }
    }

    g_ptr_array_free(keys, TRUE);
    xmlFreeDoc(doc);
    return 0;
}

static int cmd_get(const CliOptions *opts, const gchar *key)
{
    xmlDocPtr doc = load_doc_or_die(opts->config_path);
    gchar *value = get_value(doc, key);

    if (value == NULL || value[0] == '\0') {
        g_printerr("Key not set: %s\n", key);
        g_free(value);
        xmlFreeDoc(doc);
        return 1;
    }
    puts(value);

    g_free(value);
    xmlFreeDoc(doc);
    return 0;
}

static int cmd_set(const CliOptions *opts, const gchar *key, const gchar *value)
{
    xmlDocPtr doc = load_doc_or_die(opts->config_path);

    if (!set_value(doc, key, value)) {
        g_printerr("Invalid key: %s\n", key);
        xmlFreeDoc(doc);
        return 1;
    }
    if (!save_doc(doc, opts->config_path, opts->backup)) {
        g_printerr("Failed to write %s\n", opts->config_path);
        xmlFreeDoc(doc);
        return 1;
    }

    xmlFreeDoc(doc);
    return 0;
}

static int cmd_unset(const CliOptions *opts, const gchar *key)
{
    xmlDocPtr doc = load_doc_or_die(opts->config_path);

    if (!unset_value(doc, key)) {
        g_printerr("Key not set: %s\n", key);
        xmlFreeDoc(doc);
        return 1;
    }
    if (!save_doc(doc, opts->config_path, opts->backup)) {
        g_printerr("Failed to write %s\n", opts->config_path);
        xmlFreeDoc(doc);
        return 1;
    }

    xmlFreeDoc(doc);
    return 0;
}

static gboolean filter_visible(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
{
    GuiState *st = user_data;
    gchar *key = NULL;
    gchar *lower = NULL;
    gboolean visible;
    gboolean allowed = FALSE;
    gint i;

    gtk_tree_model_get(model, iter, COL_KEY, &key, -1);

    if (st->show_all)
        allowed = TRUE;
    else {
        for (i = 0; brief_keys[i] != NULL; i++) {
            if (g_strcmp0(key, brief_keys[i]) == 0) {
                allowed = TRUE;
                break;
            }
        }
    }

    if (!allowed) {
        g_free(key);
        return FALSE;
    }

    if (st->query == NULL || st->query[0] == '\0') {
        g_free(key);
        return TRUE;
    }

    lower = g_ascii_strdown(key, -1);
    visible = (g_strrstr(lower, st->query) != NULL);
    g_free(lower);
    g_free(key);
    return visible;
}

static void update_detail_button(GuiState *st)
{
    GtkWidget *img;

    if (st->show_all) {
        gtk_button_set_label(GTK_BUTTON(st->detail_btn), _("Brief"));
        img = gtk_image_new_from_icon_name("gtk-remove", GTK_ICON_SIZE_BUTTON);
    } else {
        gtk_button_set_label(GTK_BUTTON(st->detail_btn), _("Detailed"));
        img = gtk_image_new_from_icon_name("gtk-add", GTK_ICON_SIZE_BUTTON);
    }
    gtk_button_set_image(GTK_BUTTON(st->detail_btn), img);
    gtk_button_set_always_show_image(GTK_BUTTON(st->detail_btn), TRUE);
}

static void gui_set_status(GuiState *st, const gchar *text)
{
    gtk_label_set_text(GTK_LABEL(st->status), text);
}

static gboolean gui_reload(GuiState *st)
{
    guint i;
    gchar *msg;

    if (st->doc != NULL) {
        xmlFreeDoc(st->doc);
        st->doc = NULL;
    }
    if (st->keys != NULL) {
        g_ptr_array_free(st->keys, TRUE);
        st->keys = NULL;
    }

    st->doc = load_doc(st->opts.config_path);
    if (st->doc == NULL) {
        msg = g_strdup_printf(_("Cannot load %s"), st->opts.config_path);
        gui_set_status(st, msg);
        g_free(msg);
        return FALSE;
    }

    g_free(st->original_xml);
    st->original_xml = serialize_doc(st->doc);

    st->keys = build_catalog(&st->opts, st->doc);
    gtk_list_store_clear(st->store);

    for (i = 0; i < st->keys->len; i++) {
        const gchar *key = g_ptr_array_index(st->keys, i);
        gchar *value = get_value(st->doc, key);
        gchar *tip = tooltip_for_key(key);
        gchar **choices = choices_for_key(key, value);
        gchar *choice_text = choices ? g_strjoinv("|", choices) : g_strdup("");
        GtkListStore *choice_model = NULL;
        GtkTreeIter iter;

        if (choices != NULL) {
            gint j;

            choice_model = gtk_list_store_new(1, G_TYPE_STRING);
            for (j = 0; choices[j] != NULL; j++) {
                GtkTreeIter it;

                gtk_list_store_append(choice_model, &it);
                gtk_list_store_set(choice_model, &it, 0, choices[j], -1);
            }
        }

        gtk_list_store_append(st->store, &iter);
        gtk_list_store_set(st->store, &iter,
                           COL_KEY, key,
                           COL_VALUE, (value && value[0]) ? value : "",
                           COL_TOOLTIP, tip,
                           COL_CHOICES, choice_text,
                           COL_CHOICE_MODEL, choice_model,
                           -1);
        if (choice_model != NULL)
            g_object_unref(choice_model);
        g_free(value);
        g_free(tip);
        g_free(choice_text);
        g_strfreev(choices);
    }

    msg = g_strdup_printf(_("Loaded %u keys from %s"), st->keys->len, st->opts.config_path);
    gui_set_status(st, msg);
    g_free(msg);
    update_column_widths(st);
    return TRUE;
}

static void on_search_changed(GtkEditable *editable, gpointer user_data)
{
    GuiState *st = user_data;
    gchar *text = gtk_editable_get_chars(editable, 0, -1);

    g_free(st->query);
    st->query = g_ascii_strdown(text, -1);
    g_free(text);
    gtk_tree_model_filter_refilter(st->filter);
}

static void on_detail_clicked(GtkButton *button, gpointer user_data)
{
    GuiState *st = user_data;

    (void)button;
    st->show_all = !st->show_all;
    update_detail_button(st);
    gtk_tree_model_filter_refilter(st->filter);
}

static void on_cancel_clicked(GtkButton *button, gpointer user_data)
{
    GuiState *st = user_data;

    (void)button;
    gtk_widget_destroy(st->window);
}

static void on_save_clicked(GtkButton *button, gpointer user_data)
{
    GuiState *st = user_data;
    GError *err = NULL;
    gchar *xml_err = NULL;

    (void)button;
    if (st->doc == NULL)
        return;
    if (!validate_doc_syntax(st->doc, &xml_err)) {
        gui_set_status(st, xml_err ? xml_err : _("XML check failed"));
        g_free(xml_err);
        return;
    }
    if (!confirm_apply_with_diff(st)) {
        gui_set_status(st, _("Apply canceled"));
        return;
    }
    if (!save_doc(st->doc, st->opts.config_path, st->opts.backup)) {
        gui_set_status(st, _("Save failed"));
        return;
    }

    g_free(st->original_xml);
    st->original_xml = serialize_doc(st->doc);

    if (g_spawn_command_line_sync("labwc -r", NULL, NULL, NULL, &err))
        gui_set_status(st, _("Saved and reloaded (labwc -r)"));
    else {
        gchar *msg = g_strdup_printf(_("Saved, but reload failed: %s"), err->message);

        gui_set_status(st, msg);
        g_free(msg);
        g_error_free(err);
    }
}

static gboolean model_iter_from_filter_path(GuiState *st, const gchar *path_str, GtkTreeIter *iter)
{
    GtkTreePath *fpath = gtk_tree_path_new_from_string(path_str);
    GtkTreePath *cpath;
    gboolean ok;

    if (fpath == NULL)
        return FALSE;

    cpath = gtk_tree_model_filter_convert_path_to_child_path(st->filter, fpath);
    gtk_tree_path_free(fpath);
    if (cpath == NULL)
        return FALSE;

    ok = gtk_tree_model_get_iter(GTK_TREE_MODEL(st->store), iter, cpath);
    gtk_tree_path_free(cpath);
    return ok;
}

static void set_value_from_row(GuiState *st, const gchar *path_str, const gchar *text)
{
    GtkTreeIter iter;
    gchar *key = NULL;
    gchar *old_value = NULL;
    gchar *trimmed;

    if (st->doc == NULL)
        return;
    if (!model_iter_from_filter_path(st, path_str, &iter))
        return;

    gtk_tree_model_get(GTK_TREE_MODEL(st->store), &iter,
                       COL_KEY, &key,
                       COL_VALUE, &old_value,
                       -1);
    trimmed = g_strdup(text ? text : "");
    g_strstrip(trimmed);

    if (trimmed[0] == '\0') {
        gui_set_status(st, _("Empty value is not allowed"));
        if (old_value != NULL)
            gtk_list_store_set(st->store, &iter, COL_VALUE, old_value, -1);
    } else {
        gtk_list_store_set(st->store, &iter, COL_VALUE, trimmed, -1);
        set_value(st->doc, key, trimmed);
    }

    g_free(trimmed);
    g_free(key);
    g_free(old_value);
}

static void on_combo_edited(GtkCellRendererCombo *renderer,
                            gchar *path_str,
                            gchar *new_text,
                            gpointer user_data)
{
    GuiState *st = user_data;

    (void)renderer;
    set_value_from_row(st, path_str, new_text);
    update_column_widths(st);
}

static void on_combo_editing_started(GtkCellRenderer *renderer,
                                     GtkCellEditable *editable,
                                     gchar *path,
                                     gpointer user_data)
{
    GuiState *st = user_data;

    (void)renderer;
    (void)editable;
    (void)path;
    expand_columns_for_edit(st);
}

static void on_combo_editing_canceled(GtkCellRenderer *renderer, gpointer user_data)
{
    GuiState *st = user_data;

    (void)renderer;
    update_column_widths(st);
}

static void combo_cell_data_func(GtkTreeViewColumn *column,
                                 GtkCellRenderer *renderer,
                                 GtkTreeModel *model,
                                 GtkTreeIter *iter,
                                 gpointer data)
{
    gchar *choices = NULL;
    GtkTreeModel *cmodel = NULL;
    gboolean has_choices;

    (void)column;
    (void)data;
    gtk_tree_model_get(model, iter,
                       COL_CHOICES, &choices,
                       COL_CHOICE_MODEL, &cmodel,
                       -1);
    has_choices = (choices != NULL && choices[0] != '\0');
    g_object_set(renderer, "visible", TRUE, "editable", TRUE, NULL);

    if (has_choices && cmodel != NULL) {
        g_object_set(renderer,
                     "model", cmodel,
                     "text-column", 0,
                     "has-entry", FALSE,
                     NULL);
    } else {
        g_object_set(renderer,
                     "model", NULL,
                     "has-entry", TRUE,
                     "text-column", 0,
                     NULL);
    }

    if (cmodel != NULL)
        g_object_unref(cmodel);
    g_free(choices);
}

static void on_window_destroy(GtkWidget *widget, gpointer user_data)
{
    GuiState *st = user_data;

    (void)widget;
    if (st->doc)
        xmlFreeDoc(st->doc);
    if (st->keys)
        g_ptr_array_free(st->keys, TRUE);
    g_free(st->query);
    g_free(st->original_xml);
    g_free(st->opts.config_path);
    g_free(st->opts.reference_path);
    g_free(st);
    gtk_main_quit();
}

static gboolean on_window_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
    GuiState *st = user_data;
    guint key = event->keyval;

    (void)widget;

    if ((event->state & GDK_CONTROL_MASK) && (key == GDK_KEY_s || key == GDK_KEY_S)) {
        on_save_clicked(NULL, st);
        return TRUE;
    }
    if (key == GDK_KEY_Escape) {
        gtk_widget_destroy(st->window);
        return TRUE;
    }
    return FALSE;
}

static int cmd_gui(const CliOptions *opts)
{
    GuiState *st;
    GtkWidget *root, *toolbar, *icon, *search, *cancel_btn, *save_btn;
    GtkWidget *scrolled, *view;
    GtkCellRenderer *combo_renderer;
    GtkTreeViewColumn *col;

    gtk_init(NULL, NULL);

    st = g_new0(GuiState, 1);
    st->opts.config_path = g_strdup(opts->config_path);
    st->opts.reference_path = g_strdup(opts->reference_path);
    st->opts.backup = opts->backup;
    st->show_all = FALSE;

    st->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(st->window), _("labsetup"));
    gtk_window_set_default_size(GTK_WINDOW(st->window), 700, 460);
    gtk_window_set_icon_name(GTK_WINDOW(st->window), "labwc");
    g_signal_connect(st->window, "destroy", G_CALLBACK(on_window_destroy), st);
    g_signal_connect(st->window, "key-press-event", G_CALLBACK(on_window_key_press), st);

    root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
    gtk_container_set_border_width(GTK_CONTAINER(root), 8);
    gtk_container_add(GTK_CONTAINER(st->window), root);

    toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
    gtk_box_pack_start(GTK_BOX(root), toolbar, FALSE, FALSE, 0);

    icon = gtk_image_new_from_icon_name("labwc", GTK_ICON_SIZE_LARGE_TOOLBAR);
    gtk_box_pack_start(GTK_BOX(toolbar), icon, FALSE, FALSE, 0);

    search = gtk_search_entry_new();
    gtk_entry_set_placeholder_text(GTK_ENTRY(search), _("Search key"));
    g_signal_connect(search, "changed", G_CALLBACK(on_search_changed), st);
    gtk_box_pack_start(GTK_BOX(toolbar), search, TRUE, TRUE, 0);

    st->detail_btn = gtk_button_new_with_label(_("Detailed"));
    update_detail_button(st);
    g_signal_connect(st->detail_btn, "clicked", G_CALLBACK(on_detail_clicked), st);
    gtk_box_pack_start(GTK_BOX(toolbar), st->detail_btn, FALSE, FALSE, 0);

    cancel_btn = gtk_button_new_with_label(_("Cancel"));
    gtk_button_set_image(GTK_BUTTON(cancel_btn),
                         gtk_image_new_from_icon_name("gtk-cancel", GTK_ICON_SIZE_BUTTON));
    gtk_button_set_always_show_image(GTK_BUTTON(cancel_btn), TRUE);
    g_signal_connect(cancel_btn, "clicked", G_CALLBACK(on_cancel_clicked), st);
    gtk_box_pack_start(GTK_BOX(toolbar), cancel_btn, FALSE, FALSE, 0);

    save_btn = gtk_button_new_with_label(_("Apply"));
    gtk_button_set_image(GTK_BUTTON(save_btn),
                         gtk_image_new_from_icon_name("gtk-save", GTK_ICON_SIZE_BUTTON));
    gtk_button_set_always_show_image(GTK_BUTTON(save_btn), TRUE);
    g_signal_connect(save_btn, "clicked", G_CALLBACK(on_save_clicked), st);
    gtk_box_pack_start(GTK_BOX(toolbar), save_btn, FALSE, FALSE, 0);

    st->store = gtk_list_store_new(N_COLS,
                                   G_TYPE_STRING,
                                   G_TYPE_STRING,
                                   G_TYPE_STRING,
                                   G_TYPE_STRING,
                                   G_TYPE_OBJECT);
    st->filter = GTK_TREE_MODEL_FILTER(gtk_tree_model_filter_new(GTK_TREE_MODEL(st->store), NULL));
    gtk_tree_model_filter_set_visible_func(st->filter, filter_visible, st, NULL);

    view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(st->filter));
    st->view = view;
    gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(view), COL_TOOLTIP);

    col = gtk_tree_view_column_new_with_attributes(_("Key"), gtk_cell_renderer_text_new(), "text", COL_KEY, NULL);
    gtk_tree_view_column_set_resizable(col, TRUE);
    gtk_tree_view_column_set_expand(col, TRUE);
    gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
    st->key_col = col;

    combo_renderer = gtk_cell_renderer_combo_new();
    g_signal_connect(combo_renderer, "edited", G_CALLBACK(on_combo_edited), st);
    g_signal_connect(combo_renderer, "editing-started", G_CALLBACK(on_combo_editing_started), st);
    g_signal_connect(combo_renderer, "editing-canceled", G_CALLBACK(on_combo_editing_canceled), st);
    col = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(col, _("Value"));
    gtk_tree_view_column_pack_start(col, combo_renderer, TRUE);
    gtk_tree_view_column_add_attribute(col, combo_renderer, "text", COL_VALUE);
    gtk_tree_view_column_set_cell_data_func(col, combo_renderer, combo_cell_data_func, st, NULL);
    gtk_tree_view_column_set_resizable(col, TRUE);
    gtk_tree_view_column_set_expand(col, TRUE);
    gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
    st->value_col = col;

    scrolled = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
                                   GTK_POLICY_AUTOMATIC,
                                   GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(scrolled), view);
    gtk_box_pack_start(GTK_BOX(root), scrolled, TRUE, TRUE, 0);

    st->status = gtk_label_new(st->opts.config_path);
    gtk_label_set_xalign(GTK_LABEL(st->status), 0.0f);
    gtk_box_pack_start(GTK_BOX(root), st->status, FALSE, FALSE, 0);

    gui_reload(st);

    gtk_widget_show_all(st->window);
    gtk_main();
    return 0;
}

static int parse_common_options(int argc, char **argv, CliOptions *opts, int *idx)
{
    while (*idx < argc) {
        if (g_strcmp0(argv[*idx], "--file") == 0) {
            (*idx)++;
            if (*idx >= argc)
                return 1;
            g_free(opts->config_path);
            opts->config_path = g_strdup(argv[*idx]);
        } else if (g_strcmp0(argv[*idx], "--reference") == 0) {
            (*idx)++;
            if (*idx >= argc)
                return 1;
            g_free(opts->reference_path);
            opts->reference_path = g_strdup(argv[*idx]);
        } else if (argv[*idx][0] == '-') {
            if (g_strcmp0(argv[*idx], "-h") == 0 || g_strcmp0(argv[*idx], "--help") == 0)
                return 2;
            return 1;
        } else {
            return 0;
        }
        (*idx)++;
    }
    return 0;
}

int main(int argc, char **argv)
{
    CliOptions opts = {0};
    int i = 1;
    int st;
    const gchar *cmd = "gui";

    setlocale(LC_ALL, "");
    bindtextdomain("labsetup", LOCALEDIR);
    bind_textdomain_codeset("labsetup", "UTF-8");
    textdomain("labsetup");

    xmlInitParser();

    opts.config_path = default_config_path();
    opts.reference_path = default_reference_path();
    opts.backup = TRUE;

    st = parse_common_options(argc, argv, &opts, &i);
    if (st == 2) {
        print_usage();
        goto done_ok;
    }
    if (st != 0) {
        print_usage();
        goto done_fail;
    }

    if (i < argc) {
        cmd = argv[i];
        i++;
    }

    if (g_strcmp0(cmd, "list") == 0) {
        gboolean values = FALSE;

        while (i < argc) {
            if (g_strcmp0(argv[i], "--values") == 0)
                values = TRUE;
            else {
                print_usage();
                goto done_fail;
            }
            i++;
        }
        st = cmd_list(&opts, values);
    } else if (g_strcmp0(cmd, "get") == 0) {
        if (i >= argc || i + 1 != argc) {
            print_usage();
            goto done_fail;
        }
        st = cmd_get(&opts, argv[i]);
    } else if (g_strcmp0(cmd, "set") == 0) {
        const gchar *key = NULL, *value = NULL;

        while (i < argc) {
            if (g_strcmp0(argv[i], "--no-backup") == 0)
                opts.backup = FALSE;
            else if (key == NULL)
                key = argv[i];
            else if (value == NULL)
                value = argv[i];
            else {
                print_usage();
                goto done_fail;
            }
            i++;
        }

        if (key == NULL || value == NULL) {
            print_usage();
            goto done_fail;
        }
        st = cmd_set(&opts, key, value);
    } else if (g_strcmp0(cmd, "unset") == 0) {
        const gchar *key = NULL;

        while (i < argc) {
            if (g_strcmp0(argv[i], "--no-backup") == 0)
                opts.backup = FALSE;
            else if (key == NULL)
                key = argv[i];
            else {
                print_usage();
                goto done_fail;
            }
            i++;
        }

        if (key == NULL) {
            print_usage();
            goto done_fail;
        }
        st = cmd_unset(&opts, key);
    } else if (g_strcmp0(cmd, "gui") == 0) {
        while (i < argc) {
            if (g_strcmp0(argv[i], "--no-backup") == 0)
                opts.backup = FALSE;
            else {
                print_usage();
                goto done_fail;
            }
            i++;
        }
        st = cmd_gui(&opts);
    } else if (g_strcmp0(cmd, "-h") == 0 || g_strcmp0(cmd, "--help") == 0 || g_strcmp0(cmd, "help") == 0) {
        print_usage();
        goto done_ok;
    } else {
        print_usage();
        goto done_fail;
    }

    g_free(opts.config_path);
    g_free(opts.reference_path);
    xmlCleanupParser();
    return st;

done_fail:
    g_free(opts.config_path);
    g_free(opts.reference_path);
    xmlCleanupParser();
    return 1;

done_ok:
    g_free(opts.config_path);
    g_free(opts.reference_path);
    xmlCleanupParser();
    return 0;
}
