#ifndef CACHE_HPP
#define CACHE_HPP

#include <cstddef>
#include <limits>
#include <memory>
#include <mutex>
#include <unordered_map>
#include "cache_policy.hpp"

namespace caches
{

// Base class for caching algorithms
template <typename Key, typename Value, typename Policy = NoCachePolicy<Key>>
class fixed_sized_cache
{
 public:

  using iterator = typename std::unordered_map<Key, Value>::iterator;

  using const_iterator =
      typename std::unordered_map<Key, Value>::const_iterator;

  using operation_guard = typename std::lock_guard<std::mutex>;

  fixed_sized_cache(
          size_t max_size,
          const Policy& policy = Policy())
      : max_cache_size{max_size},
        cache_policy(policy)
  {
    if (max_cache_size == 0)
    {
      max_cache_size = std::numeric_limits<size_t>::max();
    }
  }

  void Put(const Key& key, const Value& value)
  {
    operation_guard{safe_op};
    auto elem_it = FindElem(key);

    if (elem_it == cache_items_map.end())
    {
      // add new element to the cache
      if (Size() + 1 > max_cache_size)
      {
        auto disp_candidate_key = cache_policy.ReplCandidate();

        Erase(disp_candidate_key);
      }

      Insert(key, value);
    }
    else
    {
      // update previous value
      Update(key, value);
    }
  }

  bool Contains(const Key& key)
  {
    operation_guard{safe_op};
    auto elem_it = FindElem(key);
    return elem_it != cache_items_map.end();
  }

  const Value& Get(const Key& key) const
  {
    operation_guard{safe_op};
    auto elem_it = FindElem(key);

    if (elem_it == cache_items_map.end())
    {
      throw std::range_error{"No such element in the cache"};
    }
    cache_policy.Touch(key);

    return elem_it->second;
  }

  const size_t Size() const
  {
    operation_guard{safe_op};

    return cache_items_map.size();
  }

  // return a key of a displacement candidate
  void Clear()
  {
    operation_guard{safe_op};

    cache_policy.Clear();
    cache_items_map.clear();
  }

 protected:

  void Insert(const Key& key, const Value& value)
  {
    cache_policy.Insert(key);
    cache_items_map.emplace(std::make_pair(key, value));
  }

  void Erase(const Key& key)
  {
    cache_policy.Erase(key);
    cache_items_map.erase(key);
  }

  void Update(const Key& key, const Value& value)
  {
    cache_policy.Touch(key);
    cache_items_map[key] = value;
  }

  const_iterator FindElem(const Key& key) const
  {
    return cache_items_map.find(key);
  }


private:

  std::unordered_map<Key, Value> cache_items_map;

  mutable Policy cache_policy;
  mutable std::mutex safe_op;

  size_t max_cache_size;

};
}

#endif  // CACHE_HPP