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

import static com.google.common.base.Preconditions.checkState;
import static java.util.stream.Collectors.toSet;

import java.util.Arrays;
import java.util.Set;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.Admission;
import com.github.benmanes.caffeine.cache.simulator.admission.Admittor;
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.typesafe.config.Config;

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

/**
 * "Quadruply-segmented LRU. Four queues are maintained at levels 0 to 3. On a cache miss, the item
 * is inserted at the head of queue 0. On a cache hit, the item is moved to the head of the next
 * higher queue (items in queue 3 move to the head of queue 3). Each queue is allocated 1/4 of the
 * total cache size and items are evicted from the tail of a queue to the head of the next lower
 * queue to maintain the size invariants. Items evicted from queue 0 are evicted from the cache."
 *
 * For more details, see <a href="http://www.cs.cornell.edu/~qhuang/papers/sosp_fbanalysis.pdf"An
 * Analysis of Facebook Photo Caching</a>.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class S4LruPolicy implements Policy {
  private final Long2ObjectMap<Node> data;
  private final PolicyStats policyStats;
  private final Admittor admittor;
  private final int maximumSize;
  private final Node[] headQ;
  private final int[] sizeQ;
  private final int levels;

  public S4LruPolicy(Admission admission, Config config) {
    this.policyStats = new PolicyStats(admission.format("linked.S4Lru"));
    this.admittor = admission.from(config, policyStats);
    S4LruSettings settings = new S4LruSettings(config);
    this.data = new Long2ObjectOpenHashMap<>();
    this.maximumSize = settings.maximumSize();
    this.levels = settings.levels();
    this.headQ = new Node[levels];
    this.sizeQ = new int[levels];

    Arrays.setAll(headQ, Node::sentinel);
  }

  /** Returns all variations of this policy based on the configuration parameters. */
  public static Set<Policy> policies(Config config) {
    BasicSettings settings = new BasicSettings(config);
    return settings.admission().stream().map(admission ->
      new S4LruPolicy(admission, config)
    ).collect(toSet());
  }

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

  private void onHit(Node node) {
    node.remove();
    sizeQ[node.level]--;
    if (node.level < (levels - 1)) {
      node.level++;
    }

    Node head = headQ[node.level];
    node.appendToTail(head);
    sizeQ[node.level]++;

    adjust();
  }

  private void onMiss(long key) {
    Node node = new Node(key);
    data.put(key, node);
    node.appendToTail(headQ[0]);
    sizeQ[0]++;

    adjust();
    evict(node);
  }

  private void adjust() {
    int maxPerLevel = maximumSize / levels;
    for (int i = levels - 1; i > 0; i--) {
      if (sizeQ[i] > maxPerLevel) {
        Node demote = headQ[i].next;
        demote.remove();
        sizeQ[i]--;

        demote.level = i - 1;
        sizeQ[demote.level]++;
        demote.appendToTail(headQ[demote.level]);
      }
    }
  }

  private void evict(Node candidate) {
    if (data.size() > maximumSize) {
      policyStats.recordEviction();

      Node victim = headQ[0].next;
      boolean admit = admittor.admit(candidate.key, victim.key);
      if (admit) {
        evictEntry(victim);
        sizeQ[0]--;
      } else {
        evictEntry(candidate);
        sizeQ[candidate.level]--;
      }
    }
  }

  private void evictEntry(Node node) {
    data.remove(node.key);
    node.remove();
  }

  @Override
  public void finished() {
    for (int i = 0; i < levels; i++) {
      int level = i;
      long count = data.values().stream().filter(node -> node.level == level).count();
      checkState(count == sizeQ[i]);
    }
  }

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

  static final class Node {
    final long key;

    Node prev;
    Node next;
    int level;

    Node(long key) {
      this.key = key;
    }

    static Node sentinel(int level) {
      Node node = new Node(Long.MIN_VALUE);
      node.level = level;
      node.prev = node;
      node.next = node;
      return node;
    }

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

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

      prev.next = next;
      next.prev = prev;
      prev = next = null;
    }

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

  static final class S4LruSettings extends BasicSettings {
    public S4LruSettings(Config config) {
      super(config);
    }
    public int levels() {
      return config().getInt("s4lru.levels");
    }
  }
}
