/*
 * Copyright © 2018 Benjamin Otte
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 */

#include "config.h"

#include "gtkselectionmodel.h"

#include "gtkbitset.h"
#include "gtkintl.h"
#include "gtkmarshalers.h"

/**
 * SECTION:gtkselectionmodel
 * @Title: GtkSelectionModel
 * @Short_description: An extension of the list model interface that handles selections
 * @See_also: #GListModel, #GtkSingleSelection
 *
 * #GtkSelectionModel is an interface that extends the #GListModel interface by
 * adding support for selections. This support is then used by widgets using list
 * models to add the ability to select and unselect various items.
 *
 * GTK provides default implementations of the most common selection modes such
 * as #GtkSingleSelection, so you will only need to implement this interface if
 * you want detailed control about how selections should be handled.
 *
 * A #GtkSelectionModel supports a single boolean per item indicating if an item
 * is selected or not. This can be queried via gtk_selection_model_is_selected().
 * When the selected state of one or more items changes, the model will emit the
 * #GtkSelectionModel::selection-changed signal by calling the
 * gtk_selection_model_selection_changed() function. The positions given in that
 * signal may have their selection state changed, though that is not a requirement.
 * If new items added to the model via the #GListModel::items-changed signal are
 * selected or not is up to the implementation.
 *
 * Note that items added via #GListModel::items-changed may already be selected
 * and no #GtkSelectionModel::selection-changed will be emitted for them. So to
 * track which items are selected, it is necessary to listen to both signals.
 *
 * Additionally, the interface can expose functionality to select and unselect
 * items. If these functions are implemented, GTK's list widgets will allow users
 * to select and unselect items. However, #GtkSelectionModels are free to only
 * implement them partially or not at all. In that case the widgets will not
 * support the unimplemented operations.
 *
 * When selecting or unselecting is supported by a model, the return values of
 * the selection functions do *not* indicate if selection or unselection happened.
 * They are only meant to indicate complete failure, like when this mode of
 * selecting is not supported by the model.
 *
 * Selections may happen asynchronously, so the only reliable way to find out
 * when an item was selected is to listen to the signals that indicate selection.
 */

G_DEFINE_INTERFACE (GtkSelectionModel, gtk_selection_model, G_TYPE_LIST_MODEL)

enum {
  SELECTION_CHANGED,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static gboolean
gtk_selection_model_default_is_selected (GtkSelectionModel *model,
                                         guint              position)
{
  GtkBitset *bitset;
  gboolean selected;

  bitset = gtk_selection_model_get_selection_in_range (model, position, 1);
  selected = gtk_bitset_contains (bitset, position);
  gtk_bitset_unref (bitset);

  return selected;
}

static GtkBitset *
gtk_selection_model_default_get_selection_in_range (GtkSelectionModel *model,
                                                    guint              position,
                                                    guint              n_items)
{
  GtkBitset *bitset;
  guint i;

  bitset = gtk_bitset_new_empty ();

  for (i = position; i < position + n_items; i++)
    {
      if (gtk_selection_model_is_selected (model, i))
        gtk_bitset_add (bitset, i);
    }

  return bitset;
}

static gboolean
gtk_selection_model_default_select_item (GtkSelectionModel *model,
                                         guint              position,
                                         gboolean           unselect_rest)
{
  GtkBitset *selected;
  GtkBitset *mask;
  gboolean result;

  selected = gtk_bitset_new_empty ();
  gtk_bitset_add (selected, position);
  if (unselect_rest)
    {
      mask = gtk_bitset_new_empty ();
      gtk_bitset_add_range (mask, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
    }
  else
    {
      mask = gtk_bitset_ref (selected);
    }

  result = gtk_selection_model_set_selection (model, selected, mask);

  gtk_bitset_unref (selected);
  gtk_bitset_unref (mask);

  return result;
}

static gboolean
gtk_selection_model_default_unselect_item (GtkSelectionModel *model,
                                           guint              position)
{
  GtkBitset *selected;
  GtkBitset *mask;
  gboolean result;

  selected = gtk_bitset_new_empty ();
  mask = gtk_bitset_new_empty ();
  gtk_bitset_add (mask, position);

  result = gtk_selection_model_set_selection (model, selected, mask);

  gtk_bitset_unref (selected);
  gtk_bitset_unref (mask);

  return result;
}

static gboolean
gtk_selection_model_default_select_range (GtkSelectionModel *model,
                                          guint              position,
                                          guint              n_items,
                                          gboolean           unselect_rest)
{
  GtkBitset *selected;
  GtkBitset *mask;
  gboolean result;

  selected = gtk_bitset_new_empty ();
  gtk_bitset_add_range (selected, position, n_items);
  if (unselect_rest)
    {
      mask = gtk_bitset_new_empty ();
      gtk_bitset_add_range (mask, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
    }
  else
    {
      mask = gtk_bitset_ref (selected);
    }

  result = gtk_selection_model_set_selection (model, selected, mask);

  gtk_bitset_unref (selected);
  gtk_bitset_unref (mask);

  return result;
}

static gboolean
gtk_selection_model_default_unselect_range (GtkSelectionModel *model,
                                            guint              position,
                                            guint              n_items)
{
  GtkBitset *selected;
  GtkBitset *mask;
  gboolean result;

  selected = gtk_bitset_new_empty ();
  mask = gtk_bitset_new_empty ();
  gtk_bitset_add_range (mask, position, n_items);

  result = gtk_selection_model_set_selection (model, selected, mask);

  gtk_bitset_unref (selected);
  gtk_bitset_unref (mask);

  return result;
}

static gboolean
gtk_selection_model_default_select_all (GtkSelectionModel *model)
{
  return gtk_selection_model_select_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)), FALSE);
}

static gboolean
gtk_selection_model_default_unselect_all (GtkSelectionModel *model)
{
  return gtk_selection_model_unselect_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
}

static gboolean
gtk_selection_model_default_set_selection (GtkSelectionModel *model,
                                           GtkBitset         *selected,
                                           GtkBitset         *mask)
{
  return FALSE;
}

static void
gtk_selection_model_default_init (GtkSelectionModelInterface *iface)
{
  iface->is_selected = gtk_selection_model_default_is_selected;
  iface->get_selection_in_range = gtk_selection_model_default_get_selection_in_range;
  iface->select_item = gtk_selection_model_default_select_item;
  iface->unselect_item = gtk_selection_model_default_unselect_item;
  iface->select_range = gtk_selection_model_default_select_range;
  iface->unselect_range = gtk_selection_model_default_unselect_range;
  iface->select_all = gtk_selection_model_default_select_all;
  iface->unselect_all = gtk_selection_model_default_unselect_all;
  iface->set_selection = gtk_selection_model_default_set_selection;

  /**
   * GtkSelectionModel::selection-changed
   * @model: a #GtkSelectionModel
   * @position: The first item that may have changed
   * @n_items: number of items with changes
   *
   * Emitted when the selection state of some of the items in @model changes.
   *
   * Note that this signal does not specify the new selection state of the items,
   * they need to be queried manually.
   * It is also not necessary for a model to change the selection state of any of
   * the items in the selection model, though it would be rather useless to emit
   * such a signal.
   */
  signals[SELECTION_CHANGED] =
    g_signal_new ("selection-changed",
                  GTK_TYPE_SELECTION_MODEL,
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  _gtk_marshal_VOID__UINT_UINT,
                  G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT);
  g_signal_set_va_marshaller (signals[SELECTION_CHANGED],
                              GTK_TYPE_SELECTION_MODEL,
                              _gtk_marshal_VOID__UINT_UINTv);
}

/**
 * gtk_selection_model_is_selected:
 * @model: a #GtkSelectionModel
 * @position: the position of the item to query
 *
 * Checks if the given item is selected.
 *
 * Returns: %TRUE if the item is selected
 **/
gboolean
gtk_selection_model_is_selected (GtkSelectionModel *model,
                                 guint              position)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), 0);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->is_selected (model, position);
}

/**
 * gtk_selection_model_get_selection:
 * @model: a #GtkSelectionModel
 *
 * Gets the set containing all currently selected items in the model.
 *
 * This function may be slow, so if you are only interested in single item,
 * consider using gtk_selection_model_is_selected() or if you are only
 * interested in a few consider gtk_selection_model_get_selection_in_range().
 *
 * Returns: (transfer full): a #GtkBitset containing all the values currently
 *     selected in @model. If no items are selected, the bitset is empty.
 *     The bitset must not be modified.
 **/
GtkBitset *
gtk_selection_model_get_selection (GtkSelectionModel *model)
{
  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), gtk_bitset_new_empty ());

  return gtk_selection_model_get_selection_in_range (model, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
}

/**
 * gtk_selection_model_get_selection_in_range:
 * @model: a #GtkSelectionModel
 * @position: start of the queired range
 * @n_items: number of items in the queried range
 *
 * Gets a set containing a set where the values in the range `[position,
 * position + n_items)` match the selected state of the items in that range.
 * All values outside that range are undefined.
 *
 * This function is an optimization for gtk_selection_model_get_selection() when
 * you are only interested in part of the model's selected state. A common use
 * case is in response to the #GtkSelectionModel::selection-changed signal.
 *
 * Returns: A #GtkBitset that matches the selection state for the given state
 *     with all other values being undefined.
 *     The bitset must not be modified.
 **/
GtkBitset *
gtk_selection_model_get_selection_in_range (GtkSelectionModel *model,
                                            guint              position,
                                            guint              n_items)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), gtk_bitset_new_empty ());

  if (n_items == 0)
    return gtk_bitset_new_empty ();

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->get_selection_in_range (model, position, n_items);
}

/**
 * gtk_selection_model_select_item:
 * @model: a #GtkSelectionModel
 * @position: the position of the item to select
 * @unselect_rest: whether previously selected items should be unselected
 *
 * Requests to select an item in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean the item was selected.
 */
gboolean
gtk_selection_model_select_item (GtkSelectionModel *model,
                                 guint              position,
                                 gboolean           unselect_rest)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->select_item (model, position, unselect_rest);
}

/**
 * gtk_selection_model_unselect_item:
 * @model: a #GtkSelectionModel
 * @position: the position of the item to unselect
 *
 * Requests to unselect an item in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean the item was unselected.
 */
gboolean
gtk_selection_model_unselect_item (GtkSelectionModel *model,
                                   guint              position)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->unselect_item (model, position);
}

/**
 * gtk_selection_model_select_range:
 * @model: a #GtkSelectionModel
 * @position: the first item to select
 * @n_items: the number of items to select
 * @unselect_rest: whether previously selected items should be unselected
 *
 * Requests to select a range of items in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean the range was selected.
 */
gboolean
gtk_selection_model_select_range (GtkSelectionModel *model,
                                  guint              position,
                                  guint              n_items,
                                  gboolean           unselect_rest)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->select_range (model, position, n_items, unselect_rest);
}

/**
 * gtk_selection_model_unselect_range:
 * @model: a #GtkSelectionModel
 * @position: the first item to unselect
 * @n_items: the number of items to unselect
 *
 * Requests to unselect a range of items in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean the range was unselected.
 */
gboolean
gtk_selection_model_unselect_range (GtkSelectionModel *model,
                                    guint              position,
                                    guint              n_items)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->unselect_range (model, position, n_items);
}

/**
 * gtk_selection_model_select_all:
 * @model: a #GtkSelectionModel
 *
 * Requests to select all items in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean that all items are now selected.
 */
gboolean
gtk_selection_model_select_all (GtkSelectionModel *model)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->select_all (model);
}

/**
 * gtk_selection_model_unselect_all:
 * @model: a #GtkSelectionModel
 *
 * Requests to unselect all items in the model.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean that all items are now unselected.
 */
gboolean
gtk_selection_model_unselect_all (GtkSelectionModel *model)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->unselect_all (model);
}

/**
 * gtk_selection_model_set_selection:
 * @model: a #GtkSelectionModel
 * @selected: bitmask specifying if items should be selected or
 *     unselected
 * @mask: bitmask specifying which items should be updated
 *
 * This is the most advanced selection updating method that allows
 * the most fine-grained control over selection changes.
 * If you can, you should try the simpler versions, as implementations
 * are more likely to implement support for those.
 *
 * Requests that the selection state of all positions set in @mask be
 * updated to the respective value in the @selected bitmask.
 *
 * In pseudocode, it would look something like this:
 *
 * |[<!-- language="C" -->
 * for (i = 0; i < n_items; i++)
 *   {
 *     // don't change values not in the mask
 *     if (!gtk_bitset_contains (mask, i))
 *       continue;
 *
 *     if (gtk_bitset_contains (selected, i))
 *       select_item (i);
 *     else
 *       unselect_item (i);
 *   }
 *
 * gtk_selection_model_selection_changed (model, first_changed_item, n_changed_items);
 * ]|
 *
 * @mask and @selected must not be modified. They may refer to the same bitset,
 * which would mean that every item in the set should be selected.
 *
 * Returns: %TRUE if this action was supported and no fallback should be
 *     tried. This does not mean that all items were updated according
 *     to the inputs.
 **/
gboolean
gtk_selection_model_set_selection (GtkSelectionModel *model,
                                   GtkBitset         *selected,
                                   GtkBitset         *mask)
{
  GtkSelectionModelInterface *iface;

  g_return_val_if_fail (GTK_IS_SELECTION_MODEL (model), FALSE);
  g_return_val_if_fail (selected != NULL, FALSE);
  g_return_val_if_fail (mask != NULL, FALSE);

  iface = GTK_SELECTION_MODEL_GET_IFACE (model);
  return iface->set_selection (model, selected, mask);
}

/**
 * gtk_selection_model_selection_changed:
 * @model: a #GtkSelectionModel
 * @position: the first changed item
 * @n_items: the number of changed items
 *
 * Helper function for implementations of #GtkSelectionModel.
 * Call this when a the selection changes to emit the
 * #GtkSelectionModel::selection-changed signal.
 */
void
gtk_selection_model_selection_changed (GtkSelectionModel *model,
                                       guint              position,
                                       guint              n_items)
{
  g_return_if_fail (GTK_IS_SELECTION_MODEL (model));
  g_return_if_fail (n_items > 0);
  g_return_if_fail (position + n_items <= g_list_model_get_n_items (G_LIST_MODEL (model)));

  g_signal_emit (model, signals[SELECTION_CHANGED], 0, position, n_items);
}

