/*
 * Copyright 2015 Ben Manes. All Rights Reserved.
 *
 * 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.
 */
package com.github.benmanes.caffeine.cache.simulator.policy.two_queue;

import static com.google.common.base.Preconditions.checkState;

import java.util.Set;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.policy.Policy;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import com.typesafe.config.Config;

import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;

/**
 * The 2Q algorithm. This algorithm uses a queue for items that are seen once (IN), a queue for
 * items seen multiple times (MAIN), and a non-resident queue for evicted items that are being
 * monitored (OUT). The maximum size of the IN and OUT queues must be tuned with the authors
 * recommending 20% and 50% of the maximum size, respectively.
 * <p>
 * This implementation is based on the pseudo code provided by the authors in their paper
 * <a href="http://www.vldb.org/conf/1994/P439.PDF">2Q: A Low Overhead High Performance Buffer
 * Management Replacement Algorithm</a>. For consistency with other policies, this version places
 * the next item to be removed at the head and most recently added at the tail of the queue.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class TwoQueuePolicy implements Policy {
  static final Node UNLINKED = new Node();

  final Long2ObjectMap<Node> data;
  final PolicyStats policyStats;
  final int maximumSize;

  int sizeIn;
  final int maxIn;
  final Node headIn;

  int sizeOut;
  final int maxOut;
  final Node headOut;

  int sizeMain;
  final Node headMain;

  public TwoQueuePolicy(Config config) {
    TwoQueueSettings settings = new TwoQueueSettings(config);

    this.headIn = new Node();
    this.headOut = new Node();
    this.headMain = new Node();
    this.maximumSize = settings.maximumSize();
    this.data = new Long2ObjectOpenHashMap<>();
    this.maxIn = (int) (maximumSize * settings.percentIn());
    this.policyStats = new PolicyStats("two-queue.TwoQueue");
    this.maxOut = (int) (maximumSize * settings.percentOut());
  }

  /** Returns all variations of this policy based on the configuration parameters. */
  public static Set<Policy> policies(Config config) {
    return ImmutableSet.of(new TwoQueuePolicy(config));
  }

  @Override
  @SuppressWarnings({"PMD.ConfusingTernary", "PMD.SwitchStmtsShouldHaveDefault"})
  public void record(long key) {
    // On accessing a page X :
    //   if X is in Am then
    //     move X to the head of Am
    //   else if (X is in Alout) then
    //     reclaimfor(X)
    //     add X to the head of Am
    //   else if (X is in Alin)
    //     // do nothing
    //   else // X is in no queue
    //     reclaimfor(X)
    //     add X to the head of Alin
    //   end if

    policyStats.recordOperation();
    Node node = data.get(key);
    if (node != null) {
      switch (node.type) {
        case MAIN:
          node.moveToTail(headMain);
          policyStats.recordHit();
          return;
        case OUT:
          node.remove();
          sizeOut--;

          reclaimfor(node);

          node.appendToTail(headMain);
          node.type = QueueType.MAIN;
          sizeMain++;

          policyStats.recordMiss();
          return;
        case IN:
          // do nothing
          policyStats.recordHit();
          return;
      }
    } else {
      node = new Node(key);
      node.type = QueueType.IN;

      reclaimfor(node);
      node.appendToTail(headIn);
      sizeIn++;

      policyStats.recordMiss();
    }
  }

  private void reclaimfor(Node node) {
    // if there are free page slots then
    //   put X into a free page slot
    // else if (size(Alin) > Kin)
    //   page out the tail of Alin, call it Y
    //   add identifier of Y to the head of Alout
    //   if (size(Alout) > Kout)
    //     remove identifier of Z from the tail of Alout
    //   end if
    //   put X into the reclaimed page slot
    // else
    //   page out the tail of Am, call it Y
    //   // do not put it on Alout; it hasn’t been accessed for a while
    //   put X into the reclaimed page slot
    // end if

    if ((sizeMain + sizeIn) < maximumSize) {
      data.put(node.key, node);
    } else if (sizeIn > maxIn) {
      // IN is full, move to OUT
      Node n = headIn.next;
      n.remove();
      sizeIn--;
      n.appendToTail(headOut);
      n.type = QueueType.OUT;
      sizeOut++;

      if (sizeOut > maxOut) {
        // OUT is full, drop oldest
        policyStats.recordEviction();
        Node victim = headOut.next;
        data.remove(victim.key);
        victim.remove();
        sizeOut--;
      }
      data.put(node.key, node);
    } else {
      // OUT has room, evict from MAIN
      policyStats.recordEviction();
      Node victim = headMain.next;
      data.remove(victim.key);
      victim.remove();
      sizeMain--;
      data.put(node.key, node);
    }
  }

  @Override
  public PolicyStats stats() {
    return policyStats;
  }

  enum QueueType {
    MAIN,
    IN,
    OUT,
  }

  static final class Node {
    final long key;

    Node prev;
    Node next;
    QueueType type;

    Node() {
      this.key = Long.MIN_VALUE;
      this.prev = this;
      this.next = this;
    }

    Node(long key) {
      this.key = key;
      this.prev = UNLINKED;
      this.next = UNLINKED;
    }

    /** Appends the node to the tail of the list. */
    public void appendToTail(Node head) {
      Node tail = head.prev;
      head.prev = this;
      tail.next = this;
      next = head;
      prev = tail;
    }

    /** Moves the node to the tail. */
    public void moveToTail(Node head) {
      // unlink
      prev.next = next;
      next.prev = prev;

      // link
      next = head;
      prev = head.prev;
      head.prev = this;
      prev.next = this;
    }

    /** Removes the node from the list. */
    public void remove() {
      checkState(key != Long.MIN_VALUE);

      prev.next = next;
      next.prev = prev;
      prev = next = UNLINKED; // mark as unlinked
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("key", key)
          .add("type", type)
          .toString();
    }
  }

  static final class TwoQueueSettings extends BasicSettings {

    public TwoQueueSettings(Config config) {
      super(config);
    }

    public double percentIn() {
      return config().getDouble("two-queue.percent-in");
    }

    public double percentOut() {
      return config().getDouble("two-queue.percent-out");
    }
  }
}
