/*
 * Copyright 2011 Google Inc. 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.googlecode.concurrentlinkedhashmap;

import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.collect.Sets;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Node;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.WeightedValue;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import static com.googlecode.concurrentlinkedhashmap.IsEmptyMap.emptyMap;
import static com.googlecode.concurrentlinkedhashmap.IsValidLinkedDeque.validLinkedDeque;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.hasValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

/**
 * A matcher that evaluates a {@link ConcurrentLinkedHashMap} to determine if it
 * is in a valid state.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
@SuppressWarnings("unchecked")
public final class IsValidConcurrentLinkedHashMap<K, V>
    extends TypeSafeDiagnosingMatcher<ConcurrentLinkedHashMap<? extends K, ? extends V>> {

  @Override
  public void describeTo(Description description) {
    description.appendText("valid");
  }

  @Override
  protected boolean matchesSafely(ConcurrentLinkedHashMap<? extends K, ? extends V> map,
      Description description) {
    DescriptionBuilder builder = new DescriptionBuilder(description);

    drain(map);
    checkMap(map, builder);
    checkEvictionDeque(map, builder);
    return builder.matches();
  }

  private void drain(ConcurrentLinkedHashMap<? extends K, ? extends V> map) {
    for (int i = 0; i < map.readBuffers.length; i++) {
      for (;;) {
        map.drainBuffers();

        boolean fullyDrained = map.writeBuffer.isEmpty();
        for (int j = 0; j < map.readBuffers.length; j++) {
          fullyDrained &= (map.readBuffers[i][j].get() == null);
        }
        if (fullyDrained) {
          break;
        }
        map.readBufferReadCount[i]++;
      }
    }
  }

  private void checkMap(ConcurrentLinkedHashMap<? extends K, ? extends V> map,
      DescriptionBuilder builder) {
    builder.expectThat("listenerQueue", map.pendingNotifications.isEmpty(), is(true));
    builder.expectThat("Inconsistent size", map.data.size(), is(map.size()));
    builder.expectThat("weightedSize", map.weightedSize(), is(map.weightedSize.get()));
    builder.expectThat("capacity", map.capacity(), is(map.capacity.get()));
    builder.expectThat("overflow", map.capacity.get(),
        is(greaterThanOrEqualTo(map.weightedSize())));
    builder.expectThat(((ReentrantLock) map.evictionLock).isLocked(), is(false));

    if (map.isEmpty()) {
      builder.expectThat(map, emptyMap());
    }
  }

  private void checkEvictionDeque(ConcurrentLinkedHashMap<? extends K, ? extends V> map,
      DescriptionBuilder builder) {
    LinkedDeque<?> deque = map.evictionDeque;

    checkLinks(map, builder);
    builder.expectThat(deque, hasSize(map.size()));
    validLinkedDeque().matchesSafely(map.evictionDeque, builder.getDescription());
  }

  @SuppressWarnings("rawtypes")
  private void checkLinks(ConcurrentLinkedHashMap<? extends K, ? extends V> map,
      DescriptionBuilder builder) {
    long weightedSize = 0;
    Set<Node> seen = Sets.newIdentityHashSet();
    for (Node node : map.evictionDeque) {
      String errorMsg = String.format("Loop detected: %s, saw %s in %s", node, seen, map);
      builder.expectThat(errorMsg, seen.add(node), is(true));
      weightedSize += ((WeightedValue) node.get()).weight;
      checkNode(map, node, builder);
    }

    builder.expectThat("Size != list length", map.size(), is(seen.size()));
    builder.expectThat("WeightedSize != link weights"
        + " [" + map.weightedSize() + " vs. " + weightedSize + "]"
        + " {size: " + map.size() + " vs. " + seen.size() + "}",
        map.weightedSize(), is(weightedSize));
  }

  @SuppressWarnings("rawtypes")
  private void checkNode(ConcurrentLinkedHashMap<? extends K, ? extends V> map, Node node,
      DescriptionBuilder builder) {
    builder.expectThat(node.key, is(not(nullValue())));
    builder.expectThat(node.get(), is(not(nullValue())));
    builder.expectThat(node.getValue(), is(not(nullValue())));
    builder.expectThat("weight", ((WeightedValue) node.get()).weight,
        is(((EntryWeigher) map.weigher).weightOf(node.key, node.getValue())));

    builder.expectThat("inconsistent", map, hasKey(node.key));
    builder.expectThat("Could not find value: " + node.getValue(), map, hasValue(node.getValue()));
    builder.expectThat("found wrong node", map.data, hasEntry(node.key, node));
  }

  @Factory
  public static <K, V> IsValidConcurrentLinkedHashMap<K, V> valid() {
    return new IsValidConcurrentLinkedHashMap<K, V>();
  }
}
