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

/**
 * "Segmented LRU is based on the observation that objects with at least two accesses are much more
 * popular than those with only one access during a short interval. In Segmented LRU, cache space is
 * partitioned into two segments: probationary segment and protected segment.
 * <p>
 * New objects (with only one access) are first faulted into the probationary segment, whereas
 * objects with two or more accesses are kept in the protected segment. When a probationary object
 * gets one more reference, it will change to the protected segment. When the whole cache space
 * becomes full, the least recently used object in the probationary segment will first be replaced.
 * The protected segment is finite in size. When it gets full, the overflowed will be re-cached in
 * probationary segment. Since objects in protected segment have to go a longer way before being
 * evicted, popular object or an object with more accesses tends to be kept in cache for longer
 * time." from <a href="
 * http://www.is.kyusan-u.ac.jp/~chengk/pub/papers/compsac00_A07-07.pdf">LRU-SP: A Size-Adjusted and
 * Popularity-Aware LRU Replacement Algorithm for Web Caching</a>
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class SegmentedLruPolicy implements Policy {
  static final Node UNLINKED = new Node();

  final Long2ObjectMap<Node> data;
  final PolicyStats policyStats;
  final Node headProtected;
  final Node headProbation;
  final Admittor admittor;
  final int maxProtected;
  final int maximumSize;

  int sizeProtected;

  public SegmentedLruPolicy(Admission admission, Config config) {
    this.policyStats = new PolicyStats(admission.format("linked.SegmentedLru"));
    this.admittor = admission.from(config, policyStats);

    SegmentedLruSettings settings = new SegmentedLruSettings(config);
    this.headProtected = new Node();
    this.headProbation = new Node();
    this.maximumSize = settings.maximumSize();
    this.data = new Long2ObjectOpenHashMap<>();
    this.maxProtected = (int) (maximumSize * settings.percentProtected());
  }

  /** 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 SegmentedLruPolicy(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);
    } else {
      onHit(node);
    }
  }

  private void onHit(Node node) {
    if (node.type == QueueType.PROTECTED) {
      node.moveToTail(headProtected);
    } else {
      sizeProtected++;
      if (sizeProtected > maxProtected) {
        Node demote = headProtected.next;
        demote.remove();
        demote.type = QueueType.PROBATION;
        demote.appendToTail(headProbation);
        sizeProtected--;
      }
      node.remove();
      node.type = QueueType.PROTECTED;
      node.appendToTail(headProtected);
    }
    policyStats.recordHit();
  }

  private void onMiss(long key) {
    Node node = new Node(key);
    data.put(key, node);
    policyStats.recordMiss();
    node.appendToTail(headProbation);
    node.type = QueueType.PROBATION;
    evict(node);
  }

  private void evict(Node candidate) {
    if (data.size() > maximumSize) {
      Node victim = (maxProtected == 0)
          ? headProtected.next // degrade to LRU
          : headProbation.next;
      policyStats.recordEviction();

      boolean admit = admittor.admit(candidate.key, victim.key);
      if (admit) {
        evictEntry(victim);
      } else {
        evictEntry(candidate);
      }
    }
  }

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

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

  enum QueueType {
    PROTECTED,
    PROBATION,
  }

  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 SegmentedLruSettings extends BasicSettings {

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

    public double percentProtected() {
      return config().getDouble("segmented-lru.percent-protected");
    }
  }
}
