/* eslint-disable no-param-reassign, max-classes-per-file */

/**
 * Simple implementation of the Doubly-Linked List Node
 * that is used in LRUCache class below.
 */
class LinkedListNode {
  /**
   * Creates a doubly-linked list node.
   * @param {string} key
   * @param {any} val
   * @param {LinkedListNode} prev
   * @param {LinkedListNode} next
   */
  constructor(key, val, prev = null, next = null) {
    this.key = key;
    this.val = val;
    this.prev = prev;
    this.next = next;
  }
}

/**
 * Implementation of the LRU (Least Recently Used) Cache
 * based on the HashMap and Doubly Linked List data-structures.
 *
 * Current implementation allows to have fast O(1) (in average) read and write operations.
 *
 * At any moment in time the LRU Cache holds not more that "capacity" number of items in it.
 */
class LRUCache {
  /**
   * Creates a cache instance of a specific capacity.
   * @param {number} capacity
   */
  constructor(capacity) {
    this.capacity = capacity; // How many items to store in cache at max.
    this.nodesMap = {}; // The quick links to each linked list node in cache.
    this.size = 0; // The number of items that is currently stored in the cache.
    this.head = new LinkedListNode(); // The Head (first) linked list node.
    this.tail = new LinkedListNode(); // The Tail (last) linked list node.
  }

  /**
   * Returns the cached value by its key.
   * Time complexity: O(1) in average.
   * @param {string} key
   * @returns {any}
   */
  get(key) {
    if (this.nodesMap[key] === undefined) return undefined;
    const node = this.nodesMap[key];
    this.promote(node);
    return node.val;
  }

  /**
   * Sets the value to cache by its key.
   * Time complexity: O(1) in average.
   * @param {string} key
   * @param {any} val
   */
  set(key, val) {
    if (this.nodesMap[key]) {
      const node = this.nodesMap[key];
      node.val = val;
      this.promote(node);
    } else {
      const node = new LinkedListNode(key, val);
      this.append(node);
    }
  }

  /**
   * Promotes the node to the end of the linked list.
   * It means that the node is most frequently used.
   * It also reduces the chance for such node to get evicted from cache.
   * @param {LinkedListNode} node
   */
  promote(node) {
    this.evict(node);
    this.append(node);
  }

  /**
   * Appends a new node to the end of the cache linked list.
   * @param {LinkedListNode} node
   */
  append(node) {
    this.nodesMap[node.key] = node;

    if (!this.head.next) {
      // First node to append.
      this.head.next = node;
      this.tail.prev = node;
      node.prev = this.head;
      node.next = this.tail;
    } else {
      // Append to an existing tail.
      const oldTail = this.tail.prev;
      oldTail.next = node;
      node.prev = oldTail;
      node.next = this.tail;
      this.tail.prev = node;
    }

    this.size += 1;

    if (this.size > this.capacity) {
      this.evict(this.head.next);
    }
  }

  /**
   * Evicts (removes) the node from cache linked list.
   * @param {LinkedListNode} node
   */
  evict(node) {
    delete this.nodesMap[node.key];
    this.size -= 1;

    const prevNode = node.prev;
    const nextNode = node.next;

    // If one and only node.
    if (prevNode === this.head && nextNode === this.tail) {
      this.head.next = null;
      this.tail.prev = null;
      this.size = 0;
      return;
    }

    // If this is a Head node.
    if (prevNode === this.head) {
      nextNode.prev = this.head;
      this.head.next = nextNode;
      return;
    }

    // If this is a Tail node.
    if (nextNode === this.tail) {
      prevNode.next = this.tail;
      this.tail.prev = prevNode;
      return;
    }

    // If the node is in the middle.
    prevNode.next = nextNode;
    nextNode.prev = prevNode;
  }
}

export default LRUCache;