/*
 * Copyright 2016 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 java.util.stream.Collectors.toSet;

import java.util.List;
import java.util.Random;
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 window and main spaces implements random eviction.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class RandomWindowTinyLfuPolicy implements Policy {
  final Long2ObjectMap<Node> data;
  final PolicyStats policyStats;
  final Admittor admittor;
  final int maximumSize;
  final Random random;
  final Node[] window;
  final Node[] main;

  int windowSize;
  int mainSize;

  public RandomWindowTinyLfuPolicy(double percentMain, RandomWindowTinyLfuSettings settings) {
    String name = String.format("sketch.RandomWindowTinyLfu (%.0f%%)", 100 * (1.0d - percentMain));
    policyStats = new PolicyStats(name);

    admittor = new TinyLfu(settings.config(), policyStats);
    random = new Random(settings.randomSeed());
    data = new Long2ObjectOpenHashMap<>();
    maximumSize = settings.maximumSize();

    int maxMain = (int) (maximumSize * percentMain);
    window = new Node[maximumSize - maxMain + 1];
    main = new Node[maxMain + 1];
  }

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

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

  @Override
  public void record(long key) {
    Node node = data.get(key);
    admittor.record(key);
    if (node == null) {
      node = new Node(key, windowSize);
      policyStats.recordOperation();
      policyStats.recordMiss();
      window[node.index] = node;
      data.put(key, node);
      windowSize++;
      evict();
    } else {
      policyStats.recordOperation();
      policyStats.recordHit();
    }
  }

  /** Evicts if the map exceeds the maximum capacity. */
  private void evict() {
    if (windowSize <= (window.length - 1)) {
      return;
    }

    Node candidate = window[random.nextInt(window.length)];
    removeFromTable(window, candidate);
    windowSize--;

    main[mainSize] = candidate;
    candidate.index = mainSize;
    mainSize++;

    if (data.size() > maximumSize) {
      Node victim = main[random.nextInt(main.length)];
      Node evict = admittor.admit(candidate.key, victim.key) ? victim : candidate;
      removeFromTable(main, evict);
      data.remove(evict.key);
      mainSize--;

      policyStats.recordEviction();
    }
  }

  /** Removes the node from the table and adds the index to the free list. */
  private void removeFromTable(Node[] table, Node node) {
    int last = table.length - 1;
    table[node.index] = table[last];
    table[node.index].index = node.index;
    table[last] = null;
  }

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

    /** Creates a new node. */
    public Node(long key, int index) {
      this.index = index;
      this.key = key;
    }

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

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