/* Copyright (C) 2016 The giomm Development Team
 *
 * 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/>.
 */

#include <glibmm/object.h>
#include <giomm/listmodel.h>
#include <vector>
#include <type_traits>

_DEFS(giomm,gio)
_PINCLUDE(glibmm/private/object_p.h)

namespace Gio
{

/** A simple implementation of Gio::ListModel that stores all items in memory.
 *
 * The templated subclass ListStore<> provides better compile-time type safety.
 *
 * It provides insertions, deletions, and lookups in logarithmic time
 * with a fast path for the common case of iterating the list linearly.
 *
 * @newin{2,50}
 */
class GIOMM_API ListStoreBase
: public Glib::Object,
  public ListModel
{
  _CLASS_GOBJECT(ListStoreBase, GListStore, G_LIST_STORE, Glib::Object, GObject, , , GIOMM_API)
  _IMPLEMENTS_INTERFACE(ListModel)
  _DO_NOT_DERIVE_GTYPE dnl// GListStore is a final type
  _ABI_AS_WITH_DERIVED_GTYPE dnl// Remove when we can break ABI
  _STRUCT_NOT_HIDDEN

protected:
  _WRAP_CTOR(ListStoreBase(GType item_type), g_list_store_new)

public:
  _WRAP_CREATE(GType item_type)

#m4 _CONVERSION(`const Glib::RefPtr<Glib::ObjectBase>&',`gpointer',`($3)->gobj()')

  _WRAP_METHOD(void insert(guint position, const Glib::RefPtr<Glib::ObjectBase>& item), g_list_store_insert, newin "2,50")

  /** A slot that will be called to compare two items.
   * The slot should return a negative integer if the first item comes before the second,
   * 0 if they are equal, or a positive integer if the first value comes after the second.
   * For instance,
   * @code
   * int on_compare_item(const Glib::RefPtr<const Glib::ObjectBase>& item1, const Glib::RefPtr<const Glib::ObjectBase>& item2);
   * @endcode
   *
   * @newin{2,50}
   */
  using SlotCompare = sigc::slot<int, const Glib::RefPtr<const Glib::ObjectBase>&, const Glib::RefPtr<const Glib::ObjectBase>&>;

  _WRAP_METHOD(guint insert_sorted(const Glib::RefPtr<Glib::ObjectBase>& item,
    const SlotCompare& slot{compare_func}), g_list_store_insert_sorted,
    slot_name slot, slot_callback ListStoreBase_CompareDataFunc, no_slot_copy, newin "2,50")

  _WRAP_METHOD(void sort(const SlotCompare& slot{compare_func}), g_list_store_sort,
    slot_name slot, slot_callback ListStoreBase_CompareDataFunc, no_slot_copy, newin "2,50")

  _WRAP_METHOD(void append(const Glib::RefPtr<Glib::ObjectBase>& item), g_list_store_append, newin "2,50")
  _WRAP_METHOD(void remove(guint position), g_list_store_remove, newin "2,50")
  _WRAP_METHOD(void remove_all(), g_list_store_remove_all, newin "2,50")

  /** Removes @a n_removals items and adds @a additions.size() items.
   * @a additions must contain items of type property_item_type() or derived from it.
   * Empty RefPtr is not permitted.
   *
   * This function is more efficient than insert() and remove(), because it only emits
   * ListModel::signal_items_changed() once for the change.
   *
   * The parameters @a position and @a n_removals must be correct (i.e.
   * @a position + @a n_removals must be less than or equal to the length of
   * the list at the time this function is called).
   *
   * @newin{2,50}
   *
   * @param position The position at which to make the change.
   * @param n_removals The number of items to remove.
   * @param additions The items to add.
   */
  void splice(guint position, guint n_removals,
    const std::vector<Glib::RefPtr<Glib::ObjectBase>>& additions);
  _IGNORE(g_list_store_splice)

  _WRAP_PROPERTY("item-type", GType, newin "2,50")

}; // end class ListStoreBase

/** A simple implementation of Gio::ListModel that stores all items in memory.
 *
 * It provides insertions, deletions, and lookups in logarithmic time
 * with a fast path for the common case of iterating the list linearly.
 *
 * @newin{2,50}
 *
 * @tparam T_item Base class of the items in the ListStore. All items must
 *                be of type T_item or a type derived from T_item.
 *                T_item must be Glib::ObjectBase or a type derived from Glib::ObjectBase.
 */
template <typename T_item>
class ListStore : public ListStoreBase
{
  static_assert(std::is_base_of<Glib::ObjectBase, T_item>::value,
    "T_item must be Glib::ObjectBase or derived from Glib::ObjectBase.");

protected:
  ListStore();

public:
  static Glib::RefPtr<ListStore> create();

  /** Get the item at @a position.
   * If @a position is greater than or equal to the number of
   * items in @a list, an empty Glib::RefPtr is returned.
   *
   * An empty Glib::RefPtr is never returned for an index that is less than the length
   * of the list.  See ListModel::get_n_items().
   *
   * @newin{2,50}
   *
   * @param position The position of the item to fetch.
   * @return The object at @a position.
   */
  Glib::RefPtr<T_item> get_item(guint position);

  /** Get the item at @a position.
   * If @a position is greater than or equal to the number of
   * items in @a list, an empty Glib::RefPtr is returned.
   *
   * An empty Glib::RefPtr is never returned for an index that is less than the length
   * of the list.  See ListModel::get_n_items().
   *
   * @newin{2,50}
   *
   * @param position The position of the item to fetch.
   * @return The object at @a position.
   */
  Glib::RefPtr<const T_item> get_item(guint position) const;

  /** Inserts @a item at @a position.
   * @a item must be of type ListStoreBase::property_item_type() or derived from it.
   * @a position must be smaller than the length of the list, or equal to it to append.
   *
   * Use splice() to insert multiple items at the same time efficiently.
   *
   * @newin{2,50}
   *
   * @param position The position at which to insert the new item.
   * @param item The new item.
   */
  void insert(guint position, const Glib::RefPtr<T_item>& item);

  /** A slot that will be called to compare two items.
   * The slot should return a negative integer if the first item comes before the second,
   * 0 if they are equal, or a positive integer if the first value comes after the second.
   * For instance,
   * @code
   * int on_compare_item(const Glib::RefPtr<const T_item>& item1, const Glib::RefPtr<const T_item>& item2);
   * @endcode
   *
   * @newin{2,50}
   */
  using SlotCompare = sigc::slot<int, const Glib::RefPtr<const T_item>&, const Glib::RefPtr<const T_item>&>;

  /** Inserts @a item at a position to be determined by the @a slot.
   *
   * The list must already be sorted before calling this function or the
   * result is undefined.  Usually you would approach this by only ever
   * inserting items by way of this function.
   *
   * @newin{2,50}
   *
   * @param item The new item.
   * @param slot Pairwise comparison function for sorting.
   * @return The position at which @a item was inserted.
   */
  guint insert_sorted(const Glib::RefPtr<T_item>& item, const SlotCompare& slot);

  /** Sorts the items according to @a slot.
   *
   * @newin{2,50}
   *
   * @param slot Pairwise comparison function for sorting.
   */
  void sort(const SlotCompare& slot);

  /** Appends @a item.
   * @a item must be of type ListStoreBase::property_item_type() or derived from it.
   *
   * Use splice() to append multiple items at the same time efficiently.
   *
   * @newin{2,50}
   *
   * @param item The new item.
   */
  void append(const Glib::RefPtr<T_item>& item);

  /** Removes @a n_removals items and adds @a additions.size() items.
   * @a additions must contain items of type ListStoreBase::property_item_type()
   * or derived from it. Empty RefPtr is not permitted.
   *
   * This function is more efficient than insert() and remove(), because it only emits
   * ListModel::signal_items_changed() once for the change.
   *
   * The parameters @a position and @a n_removals must be correct (i.e.
   * @a position + @a n_removals must be less than or equal to the length of
   * the list at the time this function is called).
   *
   * @newin{2,50}
   *
   * @param position The position at which to make the change.
   * @param n_removals The number of items to remove.
   * @param additions The items to add.
   */
  void splice(guint position, guint n_removals,
    const std::vector<Glib::RefPtr<T_item>>& additions);

private:
  static int compare_data_func(gconstpointer a, gconstpointer b, gpointer user_data);
}; // end class ListStore

#ifndef DOXYGEN_SHOULD_SKIP_THIS

template <typename T_item>
ListStore<T_item>::ListStore()
: ListStoreBase(T_item::get_base_type())
{ }

template <typename T_item>
Glib::RefPtr<ListStore<T_item>> ListStore<T_item>::create()
{
  return Glib::RefPtr<ListStore<T_item>>(new ListStore<T_item>());
}

template <typename T_item>
Glib::RefPtr<T_item> ListStore<T_item>::get_item(guint position)
{
  return Glib::RefPtr<T_item>::cast_dynamic(ListModel::get_object(position));
}

template <typename T_item>
Glib::RefPtr<const T_item> ListStore<T_item>::get_item(guint position) const
{
  return const_cast<ListStore<T_item>*>(this)->get_item(position);
}

template <typename T_item>
void ListStore<T_item>::insert(guint position, const Glib::RefPtr<T_item>& item)
{
  ListStoreBase::insert(position, item);
}

template <typename T_item>
guint ListStore<T_item>::insert_sorted(
  const Glib::RefPtr<T_item>& item, const SlotCompare& slot)
{
  // Use the original slot (not a copy).
  auto slot_copy = const_cast<SlotCompare*>(&slot);

  return g_list_store_insert_sorted(gobj(), item->gobj(), &compare_data_func, slot_copy);
}

template <typename T_item>
void ListStore<T_item>::sort(const SlotCompare& slot)
{
  // Use the original slot (not a copy).
  auto slot_copy = const_cast<SlotCompare*>(&slot);

  g_list_store_sort(gobj(), &compare_data_func, slot_copy);
}

template <typename T_item>
void ListStore<T_item>::append(const Glib::RefPtr<T_item>& item)
{
  ListStoreBase::append(item);
}

template <typename T_item>
void ListStore<T_item>::splice(guint position, guint n_removals,
  const std::vector<Glib::RefPtr<T_item>>& additions)
{
  const std::size_t n_additions = additions.size();
  std::unique_ptr<gpointer[]> g_additions{new gpointer[n_additions]};
  for (std::size_t i = 0; i < n_additions; i++)
  {
    g_additions[i] = additions[i]->gobj();
  }
  g_list_store_splice(gobj(), position, n_removals, g_additions.get(), n_additions);
}

template <typename T_item>
int ListStore<T_item>::compare_data_func(gconstpointer a, gconstpointer b, gpointer user_data)
{
  auto slot = static_cast<SlotCompare*>(user_data);

  // cast_dynamic is necessary if T_item is a user-derived class, such as
  // class MyObject : public Glib::Object
  const Glib::RefPtr<const T_item> item_a = Glib::RefPtr<T_item>::cast_dynamic(
    Glib::wrap(static_cast<typename T_item::BaseObjectType*>(const_cast<gpointer>(a)), true));
  const Glib::RefPtr<const T_item> item_b = Glib::RefPtr<T_item>::cast_dynamic(
    Glib::wrap(static_cast<typename T_item::BaseObjectType*>(const_cast<gpointer>(b)), true));

  return (*slot)(item_a, item_b);
}

#endif // DOXYGEN_SHOULD_SKIP_THIS

} // namespace Gio
