// Copyright (c) 2023 INRIA
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org)
//
// $URL: https://github.com/CGAL/cgal/blob/v6.2-beta1/Property_map/include/CGAL/Property_container.h $
// $Id: include/CGAL/Property_container.h 431cfb5357d $
// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial
//
// Author(s)     : Jackson Campolattaro

#ifndef CGAL_PROPERTY_CONTAINER_H
#define CGAL_PROPERTY_CONTAINER_H

#include <CGAL/assertions.h>

#include <map>
#include <optional>
#include <iterator>

#include <boost/property_map/property_map.hpp>

#ifndef DOXYGEN_RUNNING

namespace CGAL::Properties::Experimental {

template <typename Index>
class Property_array_base {
public:

  Property_array_base() = default;

  Property_array_base(const Property_array_base<Index>& rhs) = delete;

  virtual ~Property_array_base() = default;

  // Declare virtual functions here, for things which need to be done within the Property container
  // todo: these should mostly be private, and made available using friend

  virtual std::shared_ptr<Property_array_base<Index>> empty_clone(const std::vector<bool>& active_indices) = 0;

  virtual std::shared_ptr<Property_array_base<Index>> clone(const std::vector<bool>& active_indices) = 0;

  virtual void copy(const Property_array_base<Index>& other) = 0;

  // desactived as MSVC 2017 as an issue with that but it is not currently used.
#if 0
  virtual void move(Property_array_base<Index>&& other) = 0;
#endif

  virtual void append(const Property_array_base<Index>& other) = 0;

  virtual void resize(std::size_t n) = 0;

  virtual void shrink_to_fit() = 0;

  virtual void swap(Index a, Index b) = 0;

  virtual void reset(Index i) = 0;

  virtual const std::type_info& type() const = 0;

  virtual void transfer_from(const Property_array_base<Index>& other_base, Index other_index, Index this_index) = 0;

};

/*!
 * \brief Indexed storage for arbitrary types
 *
 * todo: make this effectively private, prioritize the use of Property_array_handle
 *
 * @tparam T
 */
template <typename Index, typename T>
class Property_array : public Property_array_base<Index> {

  std::vector<T> m_data;
  const std::vector<bool>& m_active_indices;
  T m_default_value;

public:

  using value_type = T;
  using reference = typename std::vector<T>::reference;
  using const_reference = typename std::vector<T>::const_reference;
  using iterator = typename std::vector<T>::iterator;
  using const_iterator = typename std::vector<T>::const_iterator;

  Property_array(const std::vector<bool>& active_indices, const T& default_value) :
    m_data(), m_active_indices(active_indices), m_default_value(default_value) {

    m_data.resize(active_indices.size(), m_default_value);
  }

  virtual std::shared_ptr<Property_array_base<Index>> empty_clone(const std::vector<bool>& active_indices) override {
    return std::make_shared<Property_array<Index, T>>(active_indices, m_default_value);
  }

  virtual std::shared_ptr<Property_array_base<Index>> clone(const std::vector<bool>& active_indices) override {
    auto new_array = std::make_shared<Property_array<Index, T>>(active_indices, m_default_value);
    new_array->m_data = m_data;
    return new_array;
  }

  virtual void copy(const Property_array_base<Index>& other_base) override {
    auto& other = dynamic_cast<const Property_array<Index, T>&>(other_base);
    m_data = other.m_data;
    CGAL_precondition(m_active_indices.size() == m_data.size());
  }

// deactivated as MSVC 2017 has an issue with that but it is not currently used.
#if 0
  virtual void move(Property_array_base<Index>&& other_base) override {
    auto&& other = static_cast<Property_array<Index, T>&&>(other_base);
    m_data = std::move(other.m_data);
    CGAL_precondition(m_active_indices.size() == m_data.size());
  }
#endif

  virtual void append(const Property_array_base<Index>& other_base) override {
    auto& other = dynamic_cast<const Property_array<Index, T>&>(other_base);
    CGAL_precondition(m_data.size() + other.m_data.size() == m_active_indices.size());
    m_data.insert(m_data.end(), other.m_data.begin(), other.m_data.end());
  }

  virtual void resize(std::size_t n) override {
    CGAL_precondition(m_active_indices.size() == n);
    m_data.resize(n, m_default_value);
  };

  virtual void shrink_to_fit() override {
    m_data.shrink_to_fit();
  }

  virtual void swap(Index a, Index b) override {
    // todo: maybe cast to index, instead of casting index to size?
    CGAL_precondition(std::size_t(a) < m_data.size() && std::size_t(b) < m_data.size());
    std::iter_swap(m_data.begin() + a, m_data.begin() + b);
  };

  virtual void reset(Index i) override {
    CGAL_precondition(std::size_t(i) < m_data.size());
    m_data[std::size_t(i)] = m_default_value;
  };

  virtual const std::type_info& type() const override { return typeid(T); };

  virtual void transfer_from(const Property_array_base<Index>& other_base,
                             Index other_index, Index this_index) override {

    CGAL_precondition(other_base.type() == type());
    auto& other = dynamic_cast<const Property_array<Index, T>&>(other_base);
    CGAL_precondition(std::size_t(other_index) < other.capacity() && std::size_t(this_index) < capacity());
    m_data[this_index] = other.m_data[other_index];
  }

public:

  // todo: there's not really a good reason to use these, maybe they should be removed

  [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); }

  [[nodiscard]] std::size_t capacity() const { return m_data.size(); }

  const_reference operator[](Index i) const {
    CGAL_precondition(std::size_t(i) < m_data.size());
    return m_data[std::size_t(i)];
  }

  reference operator[](Index i) {
    CGAL_precondition(std::size_t(i) < m_data.size());
    return m_data[std::size_t(i)];
  }

  iterator begin() { return m_data.begin(); }

  iterator end() { return m_data.end(); }

  const_iterator begin() const { return m_data.begin(); }

  const_iterator end() const { return m_data.end(); }

public:

  bool operator==(const Property_array<Index, T>& other) const {
    return &other == this;
  }

  bool operator!=(const Property_array<Index, T>& other) const { return !operator==(other); }

};

// todo: property maps/array handles should go in their own file

// todo: add const/read-only handle
template <typename Index, typename T>
class Property_array_handle {

  std::reference_wrapper<Property_array<Index, T>> m_array;

public:

  // Necessary for use as a boost::property_type
  using key_type = Index;
  using value_type = T;
  using reference = typename std::vector<T>::reference;
  using const_reference = typename std::vector<T>::const_reference;
  using category = boost::lvalue_property_map_tag;

  using iterator = typename std::vector<T>::iterator;
  using const_iterator = typename std::vector<T>::const_iterator;

  Property_array_handle(Property_array<Index, T>& array) : m_array(array) {}

  [[nodiscard]] std::size_t size() const { return m_array.get().size(); }

  [[nodiscard]] std::size_t capacity() const { return m_array.get().capacity(); }

  Property_array<Index, T>& array() const { return m_array.get(); }

  // todo: This might not be needed, if the other operator[] is made const
  const_reference operator[](Index i) const { return m_array.get()[i]; }

  reference operator[](Index i) { return m_array.get()[i]; }

  // todo: maybe these can be const, in an lvalue property map?
  iterator begin() { return m_array.get().begin(); }

  iterator end() { return m_array.get().end(); }

  const_iterator begin() const { return m_array.get().begin(); }

  const_iterator end() const { return m_array.get().end(); }

  bool operator==(const Property_array_handle<Index, T>& other) const { return other.m_array.get() == m_array.get(); }

  bool operator!=(const Property_array_handle<Index, T>& other) const { return !operator==(other); }

  inline friend reference get(Property_array_handle<Index, T> p, const Index& i) { return p[i]; }

  inline friend void put(Property_array_handle<Index, T> p, const Index& i, const T& v) { p[i] = v; }

};

template <typename Index = std::size_t>
class Property_container {

  std::multimap<std::string, std::shared_ptr<Property_array_base<Index>>> m_properties;
  std::vector<bool> m_active_indices{};
  bool m_has_deleted_elements = false;

public:

  template<typename T>
  using Array = Property_array<Index, T>;

  Property_container() = default;

  Property_container(const Property_container<Index>& other) {
    m_active_indices = other.m_active_indices;
    m_has_deleted_elements = other.m_has_deleted_elements;

    for (auto [name, array] : other.m_properties) {
      // todo: this could probably be made faster using emplace_hint
      m_properties.emplace(
        name,
        array->clone(m_active_indices)
      );
    }
  }

  Property_container(Property_container<Index>&& other) { *this = std::move(other); }

  // This is not exactly an assignment as existing unique properties are kept.
  Property_container<Index>& operator=(const Property_container<Index>& other) {
    m_active_indices = other.m_active_indices;
    m_has_deleted_elements = other.m_has_deleted_elements;

    for (auto [name, array] : other.m_properties) {
      // search if property already exists
      auto range = m_properties.equal_range(name);
      auto it = range.first;
      for (; it != range.second; it++) {
        if (typeid(*array) == typeid((*it->second)))
          break;
      }

      if (it != range.second)
        it->second->copy(*array);
      else
        m_properties.emplace(name, array->clone(m_active_indices));
    }

    return *this;
  }

  // This is not exactly an assignment as existing unique properties are kept.
  Property_container<Index>& operator=(Property_container<Index>&& other) {
    m_active_indices = std::move(other.m_active_indices);
    m_has_deleted_elements = other.m_has_deleted_elements;

    for (auto [name, array] : other.m_properties) {
      // search if property already exists
      auto range = m_properties.equal_range(name);
      auto it = range.first;
      for (; it != range.second; it++) {
        if (typeid(*array) == typeid((*it->second)))
          break;
      }

      if (it != range.second)
        it->second->copy(std::move(*array));
      else
        m_properties.emplace(name, array->clone(m_active_indices));
    }

    // The moved-from property map should retain all of its properties, but contain 0 elements
    other.resize(0);
    return *this;
  }

  template <typename T>
  std::pair<std::reference_wrapper<Property_array<Index, T>>, bool>
  get_or_add_property(const std::string& name, const T default_value = T()) {
    auto range = m_properties.equal_range(name);
    for (auto it = range.first; it != range.second; it++) {
      Property_array<Index, T>* typed_array_ptr = dynamic_cast<Property_array<Index, T>*>(it->second.get());
      if (typed_array_ptr != nullptr)
        return { {*typed_array_ptr}, false };
    }

    auto it = m_properties.emplace(
      name,
      std::make_shared<Property_array<Index, T>>(
        m_active_indices,
        default_value
      )
    );

    return {{*dynamic_cast<Property_array<Index, T>*>(it->second.get())}, true};
  }

  template <typename T>
  Property_array<Index, T>& add_property(const std::string& name, const T default_value = T()) {
    // todo: I'm not settled on the naming, but it's really convenient to have a function like this
    auto [array, created] = get_or_add_property(name, default_value);
    CGAL_precondition(created);
    return array.get();
  }

/*
  // todo: misleading name, maybe it could be add_same_properties?
  void copy_properties(const Property_container<Index>& other) {
    for (auto [name, other_array]: other.m_properties) {
      // If this container doesn't have any property by this name, add it (with the same type as in other)
      if (!property_exists(name))
        m_property_arrays.emplace(name, other_array->empty_clone(m_active_indices));
    }
  }*/

  template <typename T>
  const Property_array<Index, T>& get_property(const std::string& name) const {
    return *(get_property_if_exists<T>(name));
  }

  template <typename T>
  Property_array<Index, T>& get_property(const std::string& name) {
    return *(get_property_if_exists<T>(name));
  }

  template <typename T>
  std::optional<std::reference_wrapper<Property_array<Index, T>>> get_property_if_exists(const std::string& name) const {
    auto range = m_properties.equal_range(name);
    for (auto it = range.first; it != range.second; it++) {
      Property_array<Index, T>* typed_array_ptr = dynamic_cast<Property_array<Index, T>*>(it->second.get());
      if (typed_array_ptr != nullptr)
        return *typed_array_ptr;
    }

    return {};
  }

  template <typename T>
  bool property_exists(const std::string& name) const {
    auto range = m_properties.equal_range(name);

    for (auto it = range.first; it != range.second; it++) {
      Property_array<Index, T>* typed_array_ptr = dynamic_cast<Property_array<Index, T>*>(it->second.get());
      if (typed_array_ptr != nullptr)
        return true;
    }

    return false;
  }

  /*!
   * Removes all properties with the name from the container.
   *
   * @param name
   * @return number of removed properties.
   */
  std::size_t remove_properties(const std::string& name) { return m_properties.erase(name); }

  template <typename T>
  bool remove_property(const Property_array<Index, T>& arrayToRemove) {
    const Property_array_base<Index>* ref = dynamic_cast<const Property_array_base<Index>*>(&arrayToRemove);
    for (auto it = m_properties.begin(); it != m_properties.end(); it++) {
      auto const& [name, array] = *it;
      if (array.get() == ref) {
        m_properties.erase(it);
        return true;
      }
    }
    return false;
  }

  void remove_all_properties_except(const std::vector<std::string>& preserved_names) {
    // todo: if this is used often, it should take a parameter pack instead of a vector
    // A fold expression could then be used in place of std::find for better performance
    for (auto it = m_properties.begin(); it != m_properties.end();) {
      auto const& [name, array] = *it;
      if (std::find(preserved_names.begin(), preserved_names.end(), name) == preserved_names.end())
        it = m_properties.erase(it);
      else
        it++;
    }
  }

  std::vector<std::string> properties() const {
    std::vector<std::string> property_names{};
    for (auto const& [name, _]: m_properties)
      property_names.emplace_back(name);
    return property_names;
  }

  std::size_t num_properties() const { return m_properties.size(); }

/* Deactivated as there may be several Property_maps with different types but the same name.
  const std::type_info& property_type(const std::string& name) const {
    if (auto it = m_property_arrays.find(name); it != m_property_arrays.end())
      return it->second->type();
    else
      return typeid(void);
  }*/

public:
  void resize(std::size_t n) {

    m_active_indices.resize(n);
    for (auto [name, array] : m_properties)
      array->resize(n);

    std::fill(m_active_indices.begin(), m_active_indices.end(), true);

    m_has_deleted_elements = false;
  }

  [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); }

  [[nodiscard]] std::size_t capacity() const { return m_active_indices.size(); }

  Index emplace_back() {

    // Expand the storage and return the last element
    m_active_indices.push_back(true);

    for (auto [name, array] : m_properties)
      array->resize(capacity());

    auto first_new_index = Index(capacity() - 1);
    reset(first_new_index);
    return first_new_index;
  }

  Index emplace() {

    if (!m_has_deleted_elements)
      return emplace_back();

    // If there are empty slots, return the index of one of them and mark it as full
    auto first_unused = std::find_if(m_active_indices.begin(), m_active_indices.end(), [](bool used) { return !used; });
    if (first_unused != m_active_indices.end()) {
      *first_unused = true;
      auto index = Index(std::distance(m_active_indices.begin(), first_unused));
      reset(index);
      return index;
    }

    m_has_deleted_elements = false;

    return emplace_back();
  }

  Index emplace_group_back(std::size_t n) {

    // Expand the storage and return the start of the new region
    m_active_indices.resize(capacity() + n, true);

    for (auto [name, array] : m_properties)
      array->resize(capacity());

    return Index(capacity() - n);
  }

  Index emplace_group(std::size_t n) {

    if (!m_has_deleted_elements)
      return emplace_group_back(n);

    auto search_start = m_active_indices.begin();
    while (search_start != m_active_indices.end()) {

      // Find the first unused cell
      auto unused_begin = std::find_if(
        search_start, m_active_indices.end(),
        [](bool used) { return !used; }
      );

      auto unused_end = unused_begin;

      // Determine if the group fits
      if (std::distance(unused_begin, m_active_indices.end()) >= static_cast<typename std::iterator_traits<std::vector<bool>::iterator>::difference_type>(n))
        unused_end = std::find_if(
          unused_begin, (std::min)(unused_begin + n, m_active_indices.end()),
          [](bool used) { return used; }
      );

      // If the discovered range was large enough
      if (std::distance(unused_begin, unused_end) >= static_cast<typename std::iterator_traits<std::vector<bool>::iterator>::difference_type>(n)) {

        // Mark the indices as used, and reset the properties of each of them
        // todo: it would be better to provide a function to set a range
        for (auto it = unused_begin; it < unused_end; ++it) {
          *it = true;
          reset(Index(std::distance(m_active_indices.begin(), it)));
        }

        // Return the first index of the range
        return Index(std::distance(m_active_indices.begin(), unused_begin));
      }

      // If we didn't find a large enough region, continue our search after the end
      search_start = unused_end;
    }

    // If no empty regions were found, expand the storage
    return emplace_group_back(n);
  }

  void swap(Index a, Index b) {
    for (auto [name, array]: m_properties)
      array->swap(a, b);
  }

  void reset(Index i) {
    for (auto [name, array]: m_properties)
      array->reset(i);
  }

  void erase(Index i) {
    m_active_indices[i] = false;
    m_has_deleted_elements = true;
    for (auto [name, array]: m_properties)
      array->reset(i);
  }

  bool is_erased(Index i) const {
    return !m_active_indices[i];
  }

  // todo: I'd prefer to eliminate this, if possible
  void mark_active(Index i) {
    return m_active_indices[i] = true;
  }

  void mark_inactive(Index i) {
    return m_active_indices[i] = false;
  }

  std::vector<Index> active_list() const {
    std::vector<Index> indices;
    for (std::size_t i = 0; i < m_active_indices.size(); ++i)
      if (m_active_indices[i]) indices.emplace_back(i);
    return indices;
  }

  std::vector<Index> inactive_list() const {
    std::vector<Index> indices;
    for (std::size_t i = 0; i < m_active_indices.size(); ++i)
      if (!m_active_indices[i]) indices.emplace_back(i);
    return indices;
  }

  void shrink_to_fit() {
    for (auto [name, array]: m_properties)
      array->shrink_to_fit();
  }

  /*!
   * Adds the elements of the other container to this container for each property which is present in this container.
   *
   * Gaps in both containers are preserved, and all elements of the other container are guaranteed
   * to appear after the elements of this container.
   * Properties in this container which don't appear in the other container are extended with default values.
   * Properties in the other container which don't appear in this one are not included.
   * todo: merge() would be useful as well, but could break contiguous regions in the other container
   *
   * @param other
   */
  void append(const Property_container<Index>& other) {

    m_active_indices.insert(m_active_indices.end(), other.m_active_indices.begin(), other.m_active_indices.end());
    for (auto [name, array]: m_properties) {

      auto range = other.m_properties.equal_range(name);
      auto it = range.first;
      for (; it != range.second; it++) {
        if (typeid(array.get()) == typeid((it->second.get())))
          break;
      }

      if (it != range.second)
        array->append(*it->second.get());
      else
        array->resize(m_active_indices.size());
    }
  }

/*
  // todo: maybe should be renamed to transfer_from, but I'd rather remove this functionality entirely
  void transfer(const Property_container<Index>& other, Index other_index, Index this_index) {
    CGAL_precondition(other.m_property_arrays.size() == m_property_arrays.size());
    for (auto [name, array]: m_property_arrays) {
      auto other_array = other.m_property_arrays.at(name);
      array->transfer_from(*other_array, other_index, this_index);
    }
  }*/

  // todo: maybe a compress() method?
};

}

#endif // DOXYGEN_RUNNING

#endif //CGAL_PROPERTY_CONTAINER_H
