/*
 * 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;

/**
 * An adaption of the 2Q algorithm used by OpenBSD and memcached. Unlike the original 2Q algorithm,
 * non-resident entries are not retained in the TU-Q policy. For details see OpenBSD description at
 * <a href="http://www.tedunangst.com/flak/post/2Q-buffer-cache-algorithm">2Q buffer cache
 * algorithm</a> and memcached's at <a href="https://github.com/memcached/memcached/pull/97">[Work
 * In Progress] LRU rework</a>.
 * <p>
 * "We retain the key three working set distinction. In the OpenBSD code, they are named hot, cold,
 * and warm, and each is an LRU queue. New buffers start hot. They stay that way as long as they
 * remain on the hot queue. Eventually, a buffer will slip from the end of the hot queue onto the
 * front of the cold queue. (We preserve the data, not just the address.) When a new buffer is
 * needed, we recycle one from the tail of the cold queue. The oldest and coldest. If, on the other
 * hand, we have a cache hit on a cold buffer, it turns into a warm buffer and goes to the front of
 * the warm queue. Then as the warm queue lengthens, buffers start slipping from the end onto the
 * cold queue. Both the hot and warm queues are capped at one third of memory each to ensure
 * balance."
 * <p>
 * Scan resistance is achieved by means of the warm queue. Transient data will pass from hot queue
 * to cold queue and be recycled. Responsiveness is maintained by making the warm queue LRU so that
 * expired long term set buffers fade away."
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public class TuQueuePolicy implements Policy {
  private final Long2ObjectMap<Node> data;
  private final PolicyStats policyStats;
  private final int maximumSize;

  private int sizeHot;
  private final int maxHot;
  private final Node headHot;

  private int sizeWarm;
  private final int maxWarm;
  private final Node headWarm;

  private int sizeCold;
  private final Node headCold;

  public TuQueuePolicy(Config config) {
    TuQueueSettings settings = new TuQueueSettings(config);

    this.headHot = new Node();
    this.headWarm = new Node();
    this.headCold = new Node();
    this.maximumSize = settings.maximumSize();
    this.data = new Long2ObjectOpenHashMap<>();
    this.policyStats = new PolicyStats("two-queue.TuQueue");
    this.maxHot = (int) (maximumSize * settings.percentHot());
    this.maxWarm = (int) (maximumSize * settings.percentWarm());
  }

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

  @Override
  public void record(long key) {
    policyStats.recordOperation();
    Node node = data.get(key);
    if (node == null) {
      policyStats.recordMiss();
      onMiss(key);
    } else {
      policyStats.recordHit();
      onHit(node);
    }
  }

  private void onHit(Node node) {
    if (node.type == QueueType.HOT) {
      node.moveToTail(headHot);
    } else if (node.type == QueueType.WARM) {
      node.moveToTail(headWarm);
    } else if (node.type == QueueType.COLD) {
      // If we have a cache hit on a cold buffer, it turns into a warm buffer and goes to the
      // front of the warm queue. Then as the warm queue lengthens, buffers start slipping from
      // the end onto the cold queue.
      node.remove();
      sizeCold--;

      node.type = QueueType.WARM;
      node.appendToTail(headWarm);
      sizeWarm++;

      if (sizeWarm > maxWarm) {
        Node demoted = headWarm.next;
        demoted.remove();
        sizeWarm--;
        demoted.type = QueueType.COLD;
        demoted.appendToTail(headCold);
        sizeCold++;
      }
    } else {
      throw new IllegalStateException();
    }
  }

  /** Adds the entry to the cache as HOT, overflowing to the COLD queue, and evicts if necessary. */
  private void onMiss(long key) {
    Node node = new Node(key);
    node.type = QueueType.HOT;
    node.appendToTail(headHot);
    data.put(key, node);
    sizeHot++;

    if (sizeHot > maxHot) {
      Node demoted = headHot.next;
      demoted.remove();
      sizeHot--;
      demoted.appendToTail(headCold);
      demoted.type = QueueType.COLD;
      sizeCold++;
      evict();
    }
  }

  private void evict() {
    if (data.size() > maximumSize) {
      Node victim = headCold.next;
      data.remove(victim.key);
      victim.remove();
      sizeCold--;

      policyStats.recordEviction();
    }
  }

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

  @Override
  public void finished() {
    checkState(sizeHot + sizeWarm + sizeCold == data.size());
  }

  enum QueueType {
    HOT,
    WARM,
    COLD,
  }

  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;
    }

    /** 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 = null;
      type = null;
    }

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

  static final class TuQueueSettings extends BasicSettings {

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

    public double percentHot() {
      double percentHot = config().getDouble("tu-queue.percent-hot");
      checkState(percentHot < 1.0);
      return percentHot;
    }

    public double percentWarm() {
      double percentWarm = config().getDouble("tu-queue.percent-warm");
      checkState(percentWarm < 1.0);
      return percentWarm;
    }
  }
}
