/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <o3tl/safeint.hxx>

#include <unx/gtk/glomenu.h>

struct GLOMenu
{
    GMenuModel const  parent_instance;

    GArray      *items;
};

typedef GMenuModelClass GLOMenuClass;

#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
#if defined __clang__
#if __has_warning("-Wdeprecated-volatile")
#pragma clang diagnostic ignored "-Wdeprecated-volatile"
#endif
#endif
#endif
G_DEFINE_TYPE (GLOMenu, g_lo_menu, G_TYPE_MENU_MODEL);
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

namespace {

struct item
{
    GHashTable*     attributes;     // Item attributes.
    GHashTable*     links;          // Item links.
};

}

static void
g_lo_menu_struct_item_init (struct item *menu_item)
{
    menu_item->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, reinterpret_cast<GDestroyNotify>(g_variant_unref));
    menu_item->links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
}

/* We treat attribute names the same as GSettings keys:
 * - only lowercase ascii, digits and '-'
 * - must start with lowercase
 * - must not end with '-'
 * - no consecutive '-'
 * - not longer than 1024 chars
 */
static bool
valid_attribute_name (const gchar *name)
{
    gint i;

    if (!g_ascii_islower (name[0]))
        return false;

    for (i = 1; name[i]; i++)
    {
        if (name[i] != '-' &&
                !g_ascii_islower (name[i]) &&
                !g_ascii_isdigit (name[i]))
            return false;

        if (name[i] == '-' && name[i + 1] == '-')
            return false;
    }

    if (name[i - 1] == '-')
        return false;

    if (i > 1024)
        return false;

    return true;
}

/*
 * GLOMenu
 */

static gboolean
g_lo_menu_is_mutable (GMenuModel*)
{
    // Menu is always mutable.
    return true;
}

static gint
g_lo_menu_get_n_items (GMenuModel *model)
{
    g_return_val_if_fail (model != nullptr, 0);
    GLOMenu *menu = G_LO_MENU (model);
    g_return_val_if_fail (menu->items != nullptr, 0);

    return menu->items->len;
}

gint
g_lo_menu_get_n_items_from_section (GLOMenu *menu,
                                    gint     section)
{
    g_return_val_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len, 0);

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_val_if_fail (model != nullptr, 0);

    gint length = model->items->len;

    g_object_unref (model);

    return length;
}

static void
g_lo_menu_get_item_attributes (GMenuModel  *model,
                               gint         position,
                               GHashTable **table)
{
    GLOMenu *menu = G_LO_MENU (model);
    *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).attributes);
}

static void
g_lo_menu_get_item_links (GMenuModel  *model,
                          gint         position,
                          GHashTable **table)
{
    GLOMenu *menu = G_LO_MENU (model);
    *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).links);
}

void
g_lo_menu_insert (GLOMenu     *menu,
                  gint         position,
                  const gchar *label)
{
    g_lo_menu_insert_section (menu, position, label, nullptr);
}

void
g_lo_menu_insert_in_section (GLOMenu     *menu,
                             gint         section,
                             gint         position,
                             const gchar *label)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len);

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    g_lo_menu_insert (model, position, label);

    g_object_unref (model);
}

GLOMenu *
g_lo_menu_new()
{
    return G_LO_MENU( g_object_new (G_TYPE_LO_MENU, nullptr) );
}

static void
g_lo_menu_set_attribute_value (GLOMenu     *menu,
                               gint         position,
                               const gchar *attribute,
                               GVariant    *value)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (attribute != nullptr);
    g_return_if_fail (valid_attribute_name (attribute));

    if (position >= static_cast<gint>(menu->items->len))
        return;

    struct item menu_item = g_array_index (menu->items, struct item, position);

    if (value != nullptr)
        g_hash_table_insert (menu_item.attributes, g_strdup (attribute), g_variant_ref_sink (value));
    else
        g_hash_table_remove (menu_item.attributes, attribute);
}

static GVariant*
g_lo_menu_get_attribute_value_from_item_in_section (GLOMenu            *menu,
                                                    gint                section,
                                                    gint                position,
                                                    const gchar        *attribute,
                                                    const GVariantType *type)
{
    GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section));

    g_return_val_if_fail (model != nullptr, nullptr);

    GVariant *value = g_menu_model_get_item_attribute_value (model,
                                                             position,
                                                             attribute,
                                                             type);

    g_object_unref (model);

    return value;
}

void
g_lo_menu_set_label (GLOMenu     *menu,
                     gint         position,
                     const gchar *label)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GVariant *value;

    if (label != nullptr)
        value = g_variant_new_string (label);
    else
        value = nullptr;

    g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_LABEL, value);
}

void
g_lo_menu_set_icon (GLOMenu     *menu,
                    gint         position,
                    const GIcon *icon)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GVariant *value;

    if (icon != nullptr)
    {
#if GLIB_CHECK_VERSION(2,38,0)
        value = g_icon_serialize (const_cast<GIcon*>(icon));
#else
        value = nullptr;
#endif
    }
    else
        value = nullptr;

#ifndef G_MENU_ATTRIBUTE_ICON
#    define G_MENU_ATTRIBUTE_ICON "icon"
#endif

    g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ICON, value);
    if (value)
      g_variant_unref (value);
}

void
g_lo_menu_set_label_to_item_in_section (GLOMenu     *menu,
                                        gint         section,
                                        gint         position,
                                        const gchar *label)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    g_lo_menu_set_label (model, position, label);

    // Notify the update.
    g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1);

    g_object_unref (model);
}

void
g_lo_menu_set_icon_to_item_in_section (GLOMenu     *menu,
                                       gint         section,
                                       gint         position,
                                       const GIcon *icon)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    g_lo_menu_set_icon (model, position, icon);

    // Notify the update.
    g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1);

    g_object_unref (model);
}

gchar *
g_lo_menu_get_label_from_item_in_section (GLOMenu *menu,
                                          gint     section,
                                          gint     position)
{
    g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr);

    GVariant *label_value = g_lo_menu_get_attribute_value_from_item_in_section (menu,
                                                                                section,
                                                                                position,
                                                                                G_MENU_ATTRIBUTE_LABEL,
                                                                                G_VARIANT_TYPE_STRING);

    gchar *label = nullptr;

    if (label_value)
    {
        label = g_variant_dup_string (label_value, nullptr);
        g_variant_unref (label_value);
    }

    return label;
}

void
g_lo_menu_set_action_and_target_value (GLOMenu     *menu,
                                       gint         position,
                                       const gchar *action,
                                       GVariant    *target_value)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GVariant *action_value;

    if (action != nullptr)
    {
        action_value = g_variant_new_string (action);
    }
    else
    {
        action_value = nullptr;
        target_value = nullptr;
    }

    g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ACTION, action_value);
    g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_TARGET, target_value);
    g_lo_menu_set_attribute_value (menu, position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, nullptr);

    g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 1);
}

void
g_lo_menu_set_action_and_target_value_to_item_in_section (GLOMenu     *menu,
                                                          gint         section,
                                                          gint         position,
                                                          const gchar *command,
                                                          GVariant    *target_value)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    g_lo_menu_set_action_and_target_value (model, position, command, target_value);

    g_object_unref (model);
}

void
g_lo_menu_set_accelerator_to_item_in_section (GLOMenu     *menu,
                                              gint         section,
                                              gint         position,
                                              const gchar *accelerator)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    GVariant *value;

    if (accelerator != nullptr)
        value = g_variant_new_string (accelerator);
    else
        value = nullptr;

    g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_ACCELERATOR, value);

    // Notify the update.
    g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1);

    g_object_unref (model);
}

gchar *
g_lo_menu_get_accelerator_from_item_in_section (GLOMenu *menu,
                                                gint     section,
                                                gint     position)
{
    g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr);

    GVariant *accel_value = g_lo_menu_get_attribute_value_from_item_in_section (menu,
                                                                                section,
                                                                                position,
                                                                                G_LO_MENU_ATTRIBUTE_ACCELERATOR,
                                                                                G_VARIANT_TYPE_STRING);

    gchar *accel = nullptr;

    if (accel_value != nullptr)
    {
        accel = g_variant_dup_string (accel_value, nullptr);
        g_variant_unref (accel_value);
    }

    return accel;
}

void
g_lo_menu_set_command_to_item_in_section (GLOMenu     *menu,
                                          gint         section,
                                          gint         position,
                                          const gchar *command)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    GVariant *value;

    if (command != nullptr)
        value = g_variant_new_string (command);
    else
        value = nullptr;

    g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_COMMAND, value);

    // Notify the update.
    g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1);

    g_object_unref (model);
}

gchar *
g_lo_menu_get_command_from_item_in_section (GLOMenu *menu,
                                            gint     section,
                                            gint     position)
{
    g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr);

    GVariant *command_value = g_lo_menu_get_attribute_value_from_item_in_section (menu,
                                                                                  section,
                                                                                  position,
                                                                                  G_LO_MENU_ATTRIBUTE_COMMAND,
                                                                                  G_VARIANT_TYPE_STRING);

    gchar *command = nullptr;

    if (command_value != nullptr)
    {
        command = g_variant_dup_string (command_value, nullptr);
        g_variant_unref (command_value);
    }

    return command;
}

static void
g_lo_menu_set_link (GLOMenu     *menu,
                    gint         position,
                    const gchar *link,
                    GMenuModel  *model)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (link != nullptr);
    g_return_if_fail (valid_attribute_name (link));

    if (position < 0 || o3tl::make_unsigned(position) >= menu->items->len)
        position = menu->items->len - 1;

    struct item menu_item = g_array_index (menu->items, struct item, position);

    if (model != nullptr)
        g_hash_table_insert (menu_item.links, g_strdup (link), g_object_ref (model));
    else
        g_hash_table_remove (menu_item.links, link);
}

void
g_lo_menu_insert_section (GLOMenu     *menu,
                          gint         position,
                          const gchar *label,
                          GMenuModel  *section)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    if (position < 0 || o3tl::make_unsigned(position) > menu->items->len)
        position = menu->items->len;

    struct item menu_item;

    g_lo_menu_struct_item_init(&menu_item);

    g_array_insert_val (menu->items, position, menu_item);

    g_lo_menu_set_label (menu, position, label);
    g_lo_menu_set_link (menu, position, G_MENU_LINK_SECTION, section);

    g_menu_model_items_changed (G_MENU_MODEL (menu), position, 0, 1);
}

void
g_lo_menu_new_section (GLOMenu     *menu,
                       gint         position,
                       const gchar *label)
{
    GMenuModel *section = G_MENU_MODEL (g_lo_menu_new());

    g_lo_menu_insert_section (menu, position, label, section);

    g_object_unref (section);
}

GLOMenu *
g_lo_menu_get_section (GLOMenu *menu,
                       gint section)
{
    g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr);

    return G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class)
                      ->get_item_link (G_MENU_MODEL (menu), section, G_MENU_LINK_SECTION));
}

void
g_lo_menu_new_submenu_in_item_in_section (GLOMenu *menu,
                                          gint     section,
                                          gint     position)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len);

    GLOMenu* model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    if (0 <= position && o3tl::make_unsigned(position) < model->items->len) {
        GMenuModel* submenu = G_MENU_MODEL (g_lo_menu_new());

        g_lo_menu_set_link (model, position, G_MENU_LINK_SUBMENU, submenu);

        g_object_unref (submenu);

        g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1);

        g_object_unref (model);
    }
}

GLOMenu *
g_lo_menu_get_submenu_from_item_in_section (GLOMenu *menu,
                                            gint     section,
                                            gint     position)
{
    g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr);
    g_return_val_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len, nullptr);

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_val_if_fail (model != nullptr, nullptr);

    GLOMenu *submenu = nullptr;

    if (0 <= position && o3tl::make_unsigned(position) < model->items->len)
        submenu = G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class)
                ->get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU));
        //submenu = g_menu_model_get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU);

    g_object_unref (model);

    return submenu;
}

void
g_lo_menu_set_submenu_action_to_item_in_section (GLOMenu     *menu,
                                                 gint         section,
                                                 gint         position,
                                                 const gchar *action)
{
    g_return_if_fail (G_IS_LO_MENU (menu));

    GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section));

    g_return_if_fail (model != nullptr);

    GVariant *value;

    if (action != nullptr)
        value = g_variant_new_string (action);
    else
        value = nullptr;

    g_lo_menu_set_attribute_value (G_LO_MENU (model), position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, value);

    // Notify the update.
    g_menu_model_items_changed (model, position, 1, 1);

    g_object_unref (model);
}

static void
g_lo_menu_clear_item (struct item *menu_item)
{
    if (menu_item->attributes != nullptr)
        g_hash_table_unref (menu_item->attributes);
    if (menu_item->links != nullptr)
        g_hash_table_unref (menu_item->links);
}

void
g_lo_menu_remove (GLOMenu *menu,
                  gint     position)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (0 <= position && o3tl::make_unsigned(position) < menu->items->len);

    g_lo_menu_clear_item (&g_array_index (menu->items, struct item, position));
    g_array_remove_index (menu->items, position);
    g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 0);
}

void
g_lo_menu_remove_from_section (GLOMenu *menu,
                               gint     section,
                               gint     position)
{
    g_return_if_fail (G_IS_LO_MENU (menu));
    g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len);

    GLOMenu *model = g_lo_menu_get_section (menu, section);

    g_return_if_fail (model != nullptr);

    g_lo_menu_remove (model, position);

    g_object_unref (model);
}

static void
g_lo_menu_finalize (GObject *object)
{
    GLOMenu *menu = G_LO_MENU (object);
    struct item *items;
    gint n_items;
    gint i;

    n_items = menu->items->len;
    items = reinterpret_cast<struct item *>(g_array_free (menu->items, FALSE));
    for (i = 0; i < n_items; i++)
        g_lo_menu_clear_item (&items[i]);
    g_free (items);

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

static void
g_lo_menu_init (GLOMenu *menu)
{
    menu->items = g_array_new (FALSE, FALSE, sizeof (struct item));
}

static void
g_lo_menu_class_init (GLOMenuClass *klass)
{
    GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass);
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = g_lo_menu_finalize;

    model_class->is_mutable = g_lo_menu_is_mutable;
    model_class->get_n_items = g_lo_menu_get_n_items;
    model_class->get_item_attributes = g_lo_menu_get_item_attributes;
    model_class->get_item_links = g_lo_menu_get_item_links;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
