/*
 * Copyright 2014 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;

import static com.github.benmanes.caffeine.testing.IsEmptyMap.emptyMap;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;

import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import com.github.benmanes.caffeine.cache.Async.AsyncWeigher;
import com.github.benmanes.caffeine.cache.References.WeakKeyReference;
import com.github.benmanes.caffeine.cache.TimerWheel.Sentinel;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher;
import com.github.benmanes.caffeine.testing.Awaits;
import com.github.benmanes.caffeine.testing.DescriptionBuilder;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

/**
 * A matcher that evaluates a {@link BoundedLocalCache} to determine if it is in a valid state.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class IsValidBoundedLocalCache<K, V>
    extends TypeSafeDiagnosingMatcher<BoundedLocalCache<K, V>> {
  DescriptionBuilder desc;

  @Override
  public void describeTo(Description description) {
    description.appendText("valid bounded cache");
    if (desc.getDescription() != description) {
      description.appendText(desc.getDescription().toString());
    }
  }

  @Override
  protected boolean matchesSafely(BoundedLocalCache<K, V> cache, Description description) {
    desc = new DescriptionBuilder(description);

    drain(cache);
    checkReadBuffer(cache);

    checkCache(cache);
    checkTimerWheel(cache);
    checkEvictionDeque(cache);

    if (!desc.matches()) {
      throw new AssertionError(desc.getDescription().toString());
    }
    return true;
  }

  private void drain(BoundedLocalCache<K, V> cache) {
    do {
      cache.cleanUp();
    } while (cache.buffersWrites() && cache.writeBuffer().size() > 0);
  }

  private void checkReadBuffer(BoundedLocalCache<K, V> cache) {
    Buffer<?> buffer = cache.readBuffer;
    Awaits.await().until(() -> {
      cache.cleanUp();
      return (buffer.size() == 0) && buffer.reads() == buffer.writes();
    });
  }

  private void checkCache(BoundedLocalCache<K, V> cache) {
    try {
      if (cache.evictionLock.tryLock(5, TimeUnit.SECONDS)) {
        cache.evictionLock.unlock();
      } else {
        desc.expected("Maintenance lock can be acquired");
      }
    } catch (InterruptedException e) {
      desc.expected("Maintenance lock can be acquired: " + Throwables.getStackTraceAsString(e));
    }

    desc.expectThat("Inconsistent size", cache.data.size(), is(cache.size()));
    if (cache.evicts()) {
      cache.evictionLock.lock();
      long weightedSize = cache.weightedSize();
      cache.evictionLock.unlock();
      desc.expectThat("overflow", cache.maximum(), is(greaterThanOrEqualTo(weightedSize)));
    }

    if (cache.isEmpty()) {
      desc.expectThat("empty map", cache, emptyMap());
    }

    for (Node<K, V> node : cache.data.values()) {
      checkNode(cache, node, desc);
    }
  }

  private void checkTimerWheel(BoundedLocalCache<K, V> cache) {
    if (!cache.expiresVariable()) {
      return;
    }

    Set<Node<K, V>> seen = Sets.newIdentityHashSet();
    for (int i = 0; i < cache.timerWheel().wheel.length; i++) {
      for (int j = 0; j < cache.timerWheel().wheel[i].length; j++) {
        Node<K, V> sentinel = cache.timerWheel().wheel[i][j];
        desc.expectThat("Wrong sentinel prev",
            sentinel.getPreviousInVariableOrder().getNextInVariableOrder(), sameInstance(sentinel));
        desc.expectThat("Wrong sentinel next",
            sentinel.getNextInVariableOrder().getPreviousInVariableOrder(), sameInstance(sentinel));
        desc.expectThat("Sentinel must be first element", sentinel, instanceOf(Sentinel.class));

        for (Node<K, V> node = sentinel.getNextInVariableOrder();
            node != sentinel; node = node.getNextInVariableOrder()) {
          Node<K, V> next = node.getNextInVariableOrder();
          Node<K, V> prev = node.getPreviousInVariableOrder();
          long duration = node.getVariableTime() - cache.timerWheel().nanos;
          desc.expectThat("Expired", duration, greaterThan(0L));
          desc.expectThat("Loop detected", seen.add(node), is(true));
          desc.expectThat("Wrong prev", prev.getNextInVariableOrder(), is(sameInstance(node)));
          desc.expectThat("Wrong next", next.getPreviousInVariableOrder(), is(sameInstance(node)));
        }
      }
    }
    desc.expectThat("Timers != Entries", seen, hasSize(cache.size()));
  }

  private void checkEvictionDeque(BoundedLocalCache<K, V> cache) {
    if (cache.evicts()) {
      ImmutableList<LinkedDeque<Node<K, V>>> deques = ImmutableList.of(
          cache.accessOrderEdenDeque(),
          cache.accessOrderProbationDeque(),
          cache.accessOrderProtectedDeque());
      checkLinks(cache, deques, desc);
      checkDeque(cache.accessOrderEdenDeque(), desc);
      checkDeque(cache.accessOrderProbationDeque(), desc);
    } else if (cache.expiresAfterAccess()) {
      checkLinks(cache, ImmutableList.of(cache.accessOrderEdenDeque()), desc);
      checkDeque(cache.accessOrderEdenDeque(), desc);
    }

    if (cache.expiresAfterWrite()) {
      checkLinks(cache, ImmutableList.of(cache.writeOrderDeque()), desc);
      checkDeque(cache.writeOrderDeque(), desc);
    }
  }

  private void checkDeque(LinkedDeque<Node<K, V>> deque, DescriptionBuilder desc) {
    IsValidLinkedDeque.<Node<K, V>>validLinkedDeque().matchesSafely(deque, desc.getDescription());
  }

  private void checkLinks(BoundedLocalCache<K, V> cache,
      ImmutableList<LinkedDeque<Node<K, V>>> deques, DescriptionBuilder desc) {
    int size = 0;
    long weightedSize = 0;
    Set<Node<K, V>> seen = Sets.newIdentityHashSet();
    for (LinkedDeque<Node<K, V>> deque : deques) {
      size += deque.size();
      weightedSize += scanLinks(cache, seen, deque, desc);
    }
    if (cache.size() != size) {
      desc.expectThat(() -> "deque size " + deques, size, is(cache.size()));
    }

    Supplier<String> errorMsg = () -> String.format(
        "Size != list length; pending=%s, additional: %s", cache.writeBuffer().size(),
        Sets.difference(seen, ImmutableSet.copyOf(cache.data.values())));
    desc.expectThat(errorMsg, cache.size(), is(seen.size()));

    final long weighted = weightedSize;
    if (cache.evicts()) {
      Supplier<String> error = () -> String.format(
          "WeightedSize != link weights [%d vs %d] {%d vs %d}",
          cache.adjustedWeightedSize(), weighted, seen.size(), cache.size());
      desc.expectThat("non-negative weight", weightedSize, is(greaterThanOrEqualTo(0L)));
      desc.expectThat(error, cache.adjustedWeightedSize(), is(weightedSize));
    }
  }

  private long scanLinks(BoundedLocalCache<K, V> cache, Set<Node<K, V>> seen,
      LinkedDeque<Node<K, V>> deque, DescriptionBuilder desc) {
    long weightedSize = 0;
    Node<?, ?> prev = null;
    for (Node<K, V> node : deque) {
      Supplier<String> errorMsg = () -> String.format(
          "Loop detected: %s, saw %s in %s", node, seen, cache);
      desc.expectThat("wrong previous", deque.getPrevious(node), is(prev));
      desc.expectThat(errorMsg, seen.add(node), is(true));
      weightedSize += node.getWeight();
      prev = node;
    }
    return weightedSize;
  }

  private void checkNode(BoundedLocalCache<K, V> cache, Node<K, V> node, DescriptionBuilder desc) {
    Weigher<? super K, ? super V> weigher = cache.weigher;
    V value = node.getValue();
    K key = node.getKey();

    desc.expectThat("weight", node.getWeight(), is(greaterThanOrEqualTo(0)));

    boolean canCheckWeight = weigher == CacheWeigher.RANDOM;
    if (weigher instanceof AsyncWeigher) {
      canCheckWeight = ((AsyncWeigher<?, ?>) weigher).delegate == CacheWeigher.RANDOM;
    }
    if (canCheckWeight) {
      desc.expectThat("weight", node.getWeight(), is(weigher.weigh(key, value)));
    }

    if (cache.collectKeys()) {
      if ((key != null) && (value != null)) {
        desc.expectThat("inconsistent", cache.containsKey(key), is(true));
      }
      desc.expectThat("Invalid reference type",
          node.getKeyReference(), instanceOf(WeakKeyReference.class));
    } else {
      desc.expectThat("not null key", key, is(not(nullValue())));
    }
    desc.expectThat("found wrong node", cache.data.get(node.getKeyReference()), is(node));

    if (!cache.collectValues()) {
      desc.expectThat("not null value", value, is(not(nullValue())));
      if ((key != null) && !cache.hasExpired(node, cache.expirationTicker().read())) {
        desc.expectThat(() -> "Could not find key: " + key + ", value: " + value,
            cache.containsValue(value), is(true));
      }
    }

    if (cache.refreshAfterWrite()) {
      desc.expectThat("infinite timestamp", node.getWriteTime(), is(not(Long.MAX_VALUE)));
    }

    if (value instanceof CompletableFuture<?>) {
      CompletableFuture<?> future = (CompletableFuture<?>) value;
      boolean success = future.isDone() && !future.isCompletedExceptionally();
      desc.expectThat("future is done", success, is(true));
      desc.expectThat("not null value", future.getNow(null), is(not(nullValue())));
    }
  }

  public static <K, V> IsValidBoundedLocalCache<K, V> valid() {
    return new IsValidBoundedLocalCache<>();
  }
}
