/******************************************************************************
 *
 *  Copyright 2020 Google, Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

#pragma once

#include <bluetooth/log.h>

#include <functional>
#include <iterator>
#include <list>
#include <mutex>
#include <optional>
#include <thread>
#include <unordered_map>

namespace bluetooth {

namespace common {

template <typename K, typename V>
class LegacyLruCache {
public:
  using Node = std::pair<K, V>;
  /**
   * Constructor of the cache
   *
   * @param capacity maximum size of the cache
   * @param log_tag, keyword to put at the head of log.
   */
  LegacyLruCache(const size_t& capacity, const std::string& log_tag) : capacity_(capacity) {
    if (capacity_ == 0) {
      // don't allow invalid capacity
      log::fatal("{} unable to have 0 LRU Cache capacity", log_tag);
    }
  }

  // delete copy constructor
  LegacyLruCache(LegacyLruCache const&) = delete;
  LegacyLruCache& operator=(LegacyLruCache const&) = delete;

  ~LegacyLruCache() { Clear(); }

  /**
   * Clear the cache
   */
  void Clear() {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    lru_map_.clear();
    node_list_.clear();
  }

  /**
   * Same as Get, but return an iterator to the accessed element
   *
   * Modifying the returned iterator does not warm up the cache
   *
   * @param key
   * @return pointer to the underlying value to allow in-place modification
   * nullptr when not found, will be invalidated when the key is evicted
   */
  V* Find(const K& key) {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    auto map_iterator = lru_map_.find(key);
    if (map_iterator == lru_map_.end()) {
      return nullptr;
    }
    node_list_.splice(node_list_.begin(), node_list_, map_iterator->second);
    return &(map_iterator->second->second);
  }

  /**
   * Get the value of a key, and move the key to the head of cache, if there is
   * one
   *
   * @param key
   * @param value, output parameter of value of the key
   * @return true if the cache has the key
   */
  bool Get(const K& key, V* value) {
    log::assert_that(value != nullptr, "assert failed: value != nullptr");
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    auto value_ptr = Find(key);
    if (value_ptr == nullptr) {
      return false;
    }
    *value = *value_ptr;
    return true;
  }

  /**
   * Check if the cache has the input key, move the key to the head
   * if there is one
   *
   * @param key
   * @return true if the cache has the key
   */
  bool HasKey(const K& key) {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    return Find(key) != nullptr;
  }

  /**
   * Put a key-value pair to the head of cache
   *
   * @param key
   * @param value
   * @return evicted node if tail value is popped, std::nullopt if no value
   * is popped. std::optional can be treated as a boolean as well
   */
  std::optional<Node> Put(const K& key, V value) {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    auto value_ptr = Find(key);
    if (value_ptr != nullptr) {
      // hasKey() calls get(), therefore already move the node to the head
      *value_ptr = std::move(value);
      return std::nullopt;
    }

    // remove tail
    std::optional<Node> ret = std::nullopt;
    if (lru_map_.size() == capacity_) {
      lru_map_.erase(node_list_.back().first);
      ret = std::move(node_list_.back());
      node_list_.pop_back();
    }
    // insert to dummy next;
    node_list_.emplace_front(key, std::move(value));
    lru_map_.emplace(key, node_list_.begin());
    return ret;
  }

  /**
   * Delete a key from cache
   *
   * @param key
   * @return true if deleted successfully
   */
  bool Remove(const K& key) {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    auto map_iterator = lru_map_.find(key);
    if (map_iterator == lru_map_.end()) {
      return false;
    }

    // remove from the list
    node_list_.erase(map_iterator->second);

    // delete key from map
    lru_map_.erase(map_iterator);

    return true;
  }

  /**
   * Return size of the cache
   *
   * @return size of the cache
   */
  int Size() const {
    std::lock_guard<std::recursive_mutex> lock(lru_mutex_);
    return lru_map_.size();
  }

private:
  std::list<Node> node_list_;
  size_t capacity_;
  std::unordered_map<K, typename std::list<Node>::iterator> lru_map_;
  mutable std::recursive_mutex lru_mutex_;
};

}  // namespace common
}  // namespace bluetooth
