#ifndef STAR_ID_MAP_HPP
#define STAR_ID_MAP_HPP

#include "StarMap.hpp"
#include "StarMathCommon.hpp"
#include "StarDataStream.hpp"

namespace Star {

STAR_EXCEPTION(IdMapException, StarException);

// Maps key ids to values with auto generated ids in a given id range.  Tries
// to cycle through ids as new values are added and avoid re-using ids until
// the id space wraps around.
template <typename BaseMap>
class IdMapWrapper : private BaseMap {
public:
  typedef typename BaseMap::iterator iterator;
  typedef typename BaseMap::const_iterator const_iterator;
  typedef typename BaseMap::key_type key_type;
  typedef typename BaseMap::value_type value_type;
  typedef typename BaseMap::mapped_type mapped_type;

  typedef key_type IdType;
  typedef value_type ValueType;
  typedef mapped_type MappedType;

  IdMapWrapper();
  IdMapWrapper(IdType min, IdType max);

  // New valid id that does not exist in this map.  Tries not to immediately
  // recycle ids, to avoid temporally close id repeats.
  IdType nextId();

  // Throws exception if key already exists
  void add(IdType id, MappedType mappedType);

  // Add with automatically allocated id
  IdType add(MappedType mappedType);

  void clear();

  bool operator==(IdMapWrapper const& rhs) const;
  bool operator!=(IdMapWrapper const& rhs) const;

  using BaseMap::keys;
  using BaseMap::values;
  using BaseMap::pairs;
  using BaseMap::contains;
  using BaseMap::size;
  using BaseMap::empty;
  using BaseMap::get;
  using BaseMap::ptr;
  using BaseMap::maybe;
  using BaseMap::take;
  using BaseMap::maybeTake;
  using BaseMap::remove;
  using BaseMap::value;
  using BaseMap::begin;
  using BaseMap::end;
  using BaseMap::erase;

  template <typename Base>
  friend DataStream& operator>>(DataStream& ds, IdMapWrapper<Base>& map);
  template <typename Base>
  friend DataStream& operator<<(DataStream& ds, IdMapWrapper<Base> const& map);

private:
  IdType m_min;
  IdType m_max;
  IdType m_nextId;
};

template <class Key, class Value>
using IdMap = IdMapWrapper<Map<Key, Value>>;

template <class Key, class Value>
using IdHashMap = IdMapWrapper<HashMap<Key, Value>>;

template <typename BaseMap>
IdMapWrapper<BaseMap>::IdMapWrapper()
  : m_min(lowest<IdType>()), m_max(highest<IdType>()), m_nextId(m_min) {}

template <typename BaseMap>
IdMapWrapper<BaseMap>::IdMapWrapper(IdType min, IdType max)
  : m_min(min), m_max(max), m_nextId(m_min) {
  starAssert(m_max > m_min);
}

template <typename BaseMap>
auto IdMapWrapper<BaseMap>::nextId() -> IdType {
  if ((IdType)BaseMap::size() > m_max - m_min)
    throw IdMapException("No id space left in IdMapWrapper");

  IdType nextId = m_nextId;
  while (BaseMap::contains(nextId))
    nextId = cycleIncrement(nextId, m_min, m_max);
  m_nextId = cycleIncrement(nextId, m_min, m_max);
  return nextId;
}

template <typename BaseMap>
void IdMapWrapper<BaseMap>::add(IdType id, MappedType mappedType) {
  if (!BaseMap::insert(make_pair(move(id), move(mappedType))).second)
    throw IdMapException::format("IdMapWrapper::add(id, value) called with pre-existing id '{}'", outputAny(id));
}

template <typename BaseMap>
auto IdMapWrapper<BaseMap>::add(MappedType mappedType) -> IdType {
  auto id = nextId();
  BaseMap::insert(id, mappedType);
  return id;
}

template <typename BaseMap>
void IdMapWrapper<BaseMap>::clear() {
  BaseMap::clear();
  m_nextId = m_min;
}

template <typename BaseMap>
bool IdMapWrapper<BaseMap>::operator==(IdMapWrapper const& rhs) const {
  return tie(m_min, m_max) == tie(rhs.m_min, rhs.m_max) && BaseMap::operator==(rhs);
}

template <typename BaseMap>
bool IdMapWrapper<BaseMap>::operator!=(IdMapWrapper const& rhs) const {
  return !operator==(rhs);
}

template <typename BaseMap>
DataStream& operator>>(DataStream& ds, IdMapWrapper<BaseMap>& map) {
  ds.readMapContainer((BaseMap&)map);
  ds.read(map.m_min);
  ds.read(map.m_max);
  ds.read(map.m_nextId);
  return ds;
}

template <typename BaseMap>
DataStream& operator<<(DataStream& ds, IdMapWrapper<BaseMap> const& map) {
  ds.writeMapContainer((BaseMap const&)map);
  ds.write(map.m_min);
  ds.write(map.m_max);
  ds.write(map.m_nextId);
  return ds;
}

}

#endif