/*
 * Copyright 2003-2014 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.util;

import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
* User: fyodor
* Date: 8/27/12
*/
public class SimpleLRUCache<K> {

  private static final int DEFAULT_MAX_SIZE = 20000;
  private static final double FIRST_LEVEL_RATIO = 0.6;
  private static final double CLEANUP_Q1_RATIO = 0.06;

  private final AtomicInteger roomLeftFirstLevel;
  private final AtomicInteger roomLeftSecondLevel;

  private final ConcurrentHashMap<K, K> firstLevelCache; // aka L1
  private final ConcurrentLinkedQueue<K> firstLevelQueue = new ConcurrentLinkedQueue<K>(); // aka Q1

  private final ConcurrentHashMap<K, K> secondLevelCache; // aka L2
  private final ConcurrentLinkedQueue<K> secondLevelQueue = new ConcurrentLinkedQueue<K>(); // aka Q2

  private final ConcurrentHashMap<K, K> transitionalCache = new ConcurrentHashMap<K, K>();

  private int promotesBeforeCleanupInitialValue;
  private final AtomicInteger promotesBeforeCleanup;

  public SimpleLRUCache(int maxSize) {
    final int sizeL1 = (int) (maxSize * FIRST_LEVEL_RATIO);
    final int sizeL2 = (int) (maxSize * (1. - FIRST_LEVEL_RATIO));
    roomLeftFirstLevel = new AtomicInteger(sizeL1);
    roomLeftSecondLevel = new AtomicInteger(sizeL2);
    // compensate HashMap size for default load factor of 0.75
    firstLevelCache = new ConcurrentHashMap<K, K>(sizeL1 * 4 / 3);
    secondLevelCache = new ConcurrentHashMap<K, K>(sizeL2 * 4 / 3);
    promotesBeforeCleanupInitialValue = (int) (maxSize * CLEANUP_Q1_RATIO);
    promotesBeforeCleanup = new AtomicInteger(promotesBeforeCleanupInitialValue);
  }

  public SimpleLRUCache() {
    this(DEFAULT_MAX_SIZE);
  }

  public int size() {
    return firstLevelCache.size() + secondLevelCache.size();
  }

  protected K canonic(K k) {
    return k;
  }

  protected void purged(K k) {}

  @Override
  public String toString() {
    return "LRU["+firstLevelCache.size()+", "+secondLevelCache.size()+"]";
  }

  public K cacheObject (K toCache) {
    K cached = secondLevelCache.get(toCache);
    if (cached != null) {
      return cached;
    }

    cached = firstLevelCache.get(toCache);
    if (cached != null) {
      return primPromote(cached);
    }

    cached = transitionalCache.get(toCache);
    if (cached != null) {
      return cached;
    }

    return primCacheObject(canonic(toCache));
  }

  private K primPromote(K cached) {
    if (lock(cached)) {
      // current thread has a mutex on 'cached'

      K alreadyPromoted = secondLevelCache.putIfAbsent(cached, cached);
      if (alreadyPromoted != null) {
        unlock(cached);
        return alreadyPromoted;
      }
      secondLevelQueue.add(cached);

      if (firstLevelCache.remove(cached, cached)) {
        roomLeftFirstLevel.incrementAndGet();
        if (promotesBeforeCleanup.decrementAndGet() <= 0) {
          cleanupQ1();
        }
      }

      if (roomLeftSecondLevel.decrementAndGet() <= 0) {
        K toDemote = secondLevelQueue.poll();
        assert toDemote != null;

        primCacheObject(toDemote);

        if (lock(toDemote)) {
          if (secondLevelCache.remove(toDemote, toDemote)) {
            roomLeftSecondLevel.incrementAndGet();
          }
          unlock(toDemote);
        } else {
          secondLevelQueue.add(toDemote);
        }
      }

      unlock(cached);
    }
    return cached;
  }

  private K primCacheObject(K canonic) {
    if (lock(canonic)) {
      // current thread has a mutex on 'canonic'

      K alreadyCached = firstLevelCache.putIfAbsent(canonic, canonic);

      if (alreadyCached != null) {
        unlock(canonic);
        return alreadyCached;
      }

      firstLevelQueue.add(canonic);

      if (roomLeftFirstLevel.decrementAndGet() <= 0) {
        K toRemove;
        do {
          toRemove = firstLevelQueue.poll();
          assert toRemove != null;
        } while (!firstLevelCache.containsKey(toRemove));

        if (lock(toRemove)) {
          if (firstLevelCache.remove(toRemove, toRemove)) {
            roomLeftFirstLevel.incrementAndGet();
            purged(toRemove);
          }
          unlock(toRemove);
        } else {
          firstLevelQueue.add(toRemove);
        }
      }

      unlock(canonic);
    }

    return canonic;
  }

  private boolean lock(K cached) {
    return transitionalCache.putIfAbsent(cached, cached) == null;
  }

  private void unlock(K cached) {
    boolean removed = transitionalCache.remove(cached, cached);
    assert removed;
  }

  /**
   * Unlike L2 and Q2, L1 elements can be removed independently from elements in Q1, when promoting L1 element to L2.
   * Afterwards, when L2 elements 'demoted' back to L1 get added to Q1, there are duplicating queue elements.
   * The Q1 is cleaned up after the number o promotes hits the threshold.
   */
  private void cleanupQ1() {
    promotesBeforeCleanup.set(promotesBeforeCleanupInitialValue);
    for (Iterator<K> it = firstLevelQueue.iterator(); it.hasNext();) {
      if (!firstLevelCache.containsKey(it.next())) {
        it.remove();
      }
    }
  }
}
