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

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

import java.util.List;
import java.util.Set;

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

/**
 * The Window TinyLfu algorithm where the eden and main spaces implement LRU. This simpler version
 * comes at the cost of not capturing recency as effectively as Segmented LRU does.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class LruWindowTinyLfuPolicy implements Policy {
  private final Long2ObjectMap<Node> data;
  private final PolicyStats policyStats;
  private final Admittor admittor;
  private final Node headEden;
  private final Node headMain;
  private final int maxEden;
  private final int maxMain;

  private int sizeEden;
  private int sizeMain;

  public LruWindowTinyLfuPolicy(double percentMain, LruWindowTinyLfuSettings settings) {
    String name = String.format("sketch.LruWindowTinyLfu (%.0f%%)", 100 * (1.0d - percentMain));
    this.policyStats = new PolicyStats(name);

    this.admittor = new TinyLfu(settings.config(), policyStats);
    this.maxMain = (int) (settings.maximumSize() * percentMain);
    this.maxEden = settings.maximumSize() - maxMain;
    this.data = new Long2ObjectOpenHashMap<>();
    this.headEden = new Node();
    this.headMain = new Node();
  }

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

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

  @Override
  public void record(long key) {
    policyStats.recordOperation();
    Node node = data.get(key);
    admittor.record(key);

    if (node == null) {
      node = new Node(key, Status.EDEN);
      node.appendToTail(headEden);
      data.put(key, node);
      sizeEden++;
      evict();

      policyStats.recordMiss();
    } else if (node.status == Status.EDEN) {
      node.moveToTail(headEden);
      policyStats.recordHit();
    } else if (node.status == Status.MAIN) {
      node.moveToTail(headMain);
      policyStats.recordHit();
    } else {
      throw new IllegalStateException();
    }
  }

  /** Evicts if the map exceeds the maximum capacity. */
  private void evict() {
    if (sizeEden <= maxEden) {
      return;
    }

    Node candidate = headEden.next;
    candidate.remove();
    sizeEden--;

    candidate.appendToTail(headMain);
    candidate.status = Status.MAIN;
    sizeMain++;

    if (sizeMain > maxMain) {
      Node victim = headMain.next;
      Node evict = admittor.admit(candidate.key, victim.key) ? victim : candidate;
      data.remove(evict.key);
      evict.remove();
      sizeMain--;

      policyStats.recordEviction();
    }
  }

  @Override
  public void finished() {
    checkState(data.values().stream().filter(n -> n.status == Status.EDEN).count() == sizeEden);
    checkState(sizeEden + sizeMain == data.size());
    checkState(data.size() <= maxEden + maxMain);
  }

  enum Status {
    EDEN, MAIN
  }

  /** A node on the double-linked list. */
  static final class Node {
    final long key;

    Status status;
    Node prev;
    Node next;

    /** Creates a new sentinel node. */
    public Node() {
      this.key = Integer.MIN_VALUE;
      this.prev = this;
      this.next = this;
    }

    /** Creates a new, unlinked node. */
    public Node(long key, Status status) {
      this.status = status;
      this.key = key;
    }

    public void moveToTail(Node head) {
      remove();
      appendToTail(head);
    }

    /** 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() {
      prev.next = next;
      next.prev = prev;
      next = prev = null;
    }

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

  static final class LruWindowTinyLfuSettings extends BasicSettings {
    public LruWindowTinyLfuSettings(Config config) {
      super(config);
    }
    public List<Double> percentMain() {
      return config().getDoubleList("lru-window-tiny-lfu.percent-main");
    }
  }
}
