/*
 * Copyright (C) 2011 The Guava Authors
 *
 * 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.google.common.collect;

import static com.google.common.collect.MapMakerInternalMap.DRAIN_THRESHOLD;
import static com.google.common.collect.MapMakerInternalMapTest.SMALL_MAX_SIZE;
import static com.google.common.collect.MapMakerInternalMapTest.allEvictingMakers;
import static com.google.common.collect.MapMakerInternalMapTest.assertNotified;
import static com.google.common.collect.MapMakerInternalMapTest.checkAndDrainRecencyQueue;
import static com.google.common.collect.MapMakerInternalMapTest.checkEvictionQueues;
import static com.google.common.collect.MapMakerInternalMapTest.checkExpirationTimes;

import com.google.common.base.Function;
import com.google.common.collect.ComputingConcurrentHashMap.ComputingMapAdapter;
import com.google.common.collect.MapMaker.RemovalCause;
import com.google.common.collect.MapMakerInternalMap.ReferenceEntry;
import com.google.common.collect.MapMakerInternalMap.Segment;
import com.google.common.collect.MapMakerInternalMapTest.DummyEntry;
import com.google.common.collect.MapMakerInternalMapTest.DummyValueReference;
import com.google.common.collect.MapMakerInternalMapTest.QueuingRemovalListener;
import com.google.common.testing.NullPointerTester;

import junit.framework.TestCase;

import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;

/**
 * @author Charles Fry
 */
public class ComputingConcurrentHashMapTest extends TestCase {

  private static <K, V> ComputingConcurrentHashMap<K, V> makeComputingMap(
      MapMaker maker, Function<? super K, ? extends V> computingFunction) {
    return new ComputingConcurrentHashMap<K, V>(
        maker, computingFunction);
  }

  private static <K, V> ComputingMapAdapter<K, V> makeAdaptedMap(
      MapMaker maker, Function<? super K, ? extends V> computingFunction) {
    return new ComputingMapAdapter<K, V>(
        maker, computingFunction);
  }

  private MapMaker createMapMaker() {
    MapMaker maker = new MapMaker();
    maker.useCustomMap = true;
    return maker;
  }

  // constructor tests

  public void testComputingFunction() {
    Function<Object, Object> computingFunction = new Function<Object, Object>() {
      @Override
      public Object apply(Object from) {
        return from;
      }
    };
    ComputingConcurrentHashMap<Object, Object> map =
        makeComputingMap(createMapMaker(), computingFunction);
    assertSame(computingFunction, map.computingFunction);
  }

  // computation tests

  public void testCompute() throws ExecutionException {
    CountingFunction computingFunction = new CountingFunction();
    ComputingConcurrentHashMap<Object, Object> map =
        makeComputingMap(createMapMaker(), computingFunction);
    assertEquals(0, computingFunction.getCount());

    Object key = new Object();
    Object value = map.getOrCompute(key);
    assertEquals(1, computingFunction.getCount());
    assertEquals(value, map.getOrCompute(key));
    assertEquals(1, computingFunction.getCount());
  }

  public void testComputeNull() {
    Function<Object, Object> computingFunction = new ConstantLoader<Object, Object>(null);
    ComputingMapAdapter<Object, Object> map = makeAdaptedMap(createMapMaker(), computingFunction);
    try {
      map.get(new Object());
      fail();
    } catch (NullPointerException expected) {}
  }

  public void testRecordReadOnCompute() throws ExecutionException {
    CountingFunction computingFunction = new CountingFunction();
    for (MapMaker maker : allEvictingMakers()) {
      ComputingConcurrentHashMap<Object, Object> map =
          makeComputingMap(maker.concurrencyLevel(1), computingFunction);
      Segment<Object, Object> segment = map.segments[0];
      List<ReferenceEntry<Object, Object>> writeOrder = Lists.newLinkedList();
      List<ReferenceEntry<Object, Object>> readOrder = Lists.newLinkedList();
      for (int i = 0; i < SMALL_MAX_SIZE; i++) {
        Object key = new Object();
        int hash = map.hash(key);

        map.getOrCompute(key);
        ReferenceEntry<Object, Object> entry = segment.getEntry(key, hash);
        writeOrder.add(entry);
        readOrder.add(entry);
      }

      checkEvictionQueues(map, segment, readOrder, writeOrder);
      checkExpirationTimes(map);
      assertTrue(segment.recencyQueue.isEmpty());

      // access some of the elements
      Random random = new Random();
      List<ReferenceEntry<Object, Object>> reads = Lists.newArrayList();
      Iterator<ReferenceEntry<Object, Object>> i = readOrder.iterator();
      while (i.hasNext()) {
        ReferenceEntry<Object, Object> entry = i.next();
        if (random.nextBoolean()) {
          map.getOrCompute(entry.getKey());
          reads.add(entry);
          i.remove();
          assertTrue(segment.recencyQueue.size() <= DRAIN_THRESHOLD);
        }
      }
      int undrainedIndex = reads.size() - segment.recencyQueue.size();
      checkAndDrainRecencyQueue(map, segment, reads.subList(undrainedIndex, reads.size()));
      readOrder.addAll(reads);

      checkEvictionQueues(map, segment, readOrder, writeOrder);
      checkExpirationTimes(map);
    }
  }

  public void testComputeExistingEntry() throws ExecutionException {
    CountingFunction computingFunction = new CountingFunction();
    ComputingConcurrentHashMap<Object, Object> map =
        makeComputingMap(createMapMaker(), computingFunction);
    assertEquals(0, computingFunction.getCount());

    Object key = new Object();
    Object value = new Object();
    map.put(key, value);

    assertEquals(value, map.getOrCompute(key));
    assertEquals(0, computingFunction.getCount());
  }

  public void testComputePartiallyCollectedKey() throws ExecutionException {
    MapMaker maker = createMapMaker().concurrencyLevel(1);
    CountingFunction computingFunction = new CountingFunction();
    ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction);
    Segment<Object, Object> segment = map.segments[0];
    AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table;
    assertEquals(0, computingFunction.getCount());

    Object key = new Object();
    int hash = map.hash(key);
    Object value = new Object();
    int index = hash & (table.length() - 1);

    DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null);
    DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry);
    entry.setValueReference(valueRef);
    table.set(index, entry);
    segment.count++;

    assertSame(value, map.getOrCompute(key));
    assertEquals(0, computingFunction.getCount());
    assertEquals(1, segment.count);

    entry.clearKey();
    assertNotSame(value, map.getOrCompute(key));
    assertEquals(1, computingFunction.getCount());
    assertEquals(2, segment.count);
  }

  public void testComputePartiallyCollectedValue() throws ExecutionException {
    MapMaker maker = createMapMaker().concurrencyLevel(1);
    CountingFunction computingFunction = new CountingFunction();
    ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction);
    Segment<Object, Object> segment = map.segments[0];
    AtomicReferenceArray<ReferenceEntry<Object, Object>> table = segment.table;
    assertEquals(0, computingFunction.getCount());

    Object key = new Object();
    int hash = map.hash(key);
    Object value = new Object();
    int index = hash & (table.length() - 1);

    DummyEntry<Object, Object> entry = DummyEntry.create(key, hash, null);
    DummyValueReference<Object, Object> valueRef = DummyValueReference.create(value, entry);
    entry.setValueReference(valueRef);
    table.set(index, entry);
    segment.count++;

    assertSame(value, map.getOrCompute(key));
    assertEquals(0, computingFunction.getCount());
    assertEquals(1, segment.count);

    valueRef.clear(null);
    assertNotSame(value, map.getOrCompute(key));
    assertEquals(1, computingFunction.getCount());
    assertEquals(1, segment.count);
  }

  @SuppressWarnings("deprecation") // test of deprecated method
  public void testComputeExpiredEntry() throws ExecutionException {
    MapMaker maker = createMapMaker().expireAfterWrite(1, TimeUnit.NANOSECONDS);
    CountingFunction computingFunction = new CountingFunction();
    ComputingConcurrentHashMap<Object, Object> map = makeComputingMap(maker, computingFunction);
    assertEquals(0, computingFunction.getCount());

    Object key = new Object();
    Object one = map.getOrCompute(key);
    assertEquals(1, computingFunction.getCount());

    Object two = map.getOrCompute(key);
    assertNotSame(one, two);
    assertEquals(2, computingFunction.getCount());
  }

  public void testRemovalListener_replaced() {
    // TODO(user): May be a good candidate to play with the MultithreadedTestCase
    final CountDownLatch startSignal = new CountDownLatch(1);
    final CountDownLatch computingSignal = new CountDownLatch(1);
    final CountDownLatch doneSignal = new CountDownLatch(1);
    final Object computedObject = new Object();

    Function<Object, Object> computingFunction = new Function<Object, Object>() {
      @Override
      public Object apply(Object key) {
        computingSignal.countDown();
        try {
          startSignal.await();
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
        return computedObject;
      }
    };

    QueuingRemovalListener<Object, Object> listener =
        new QueuingRemovalListener<Object, Object>();
    MapMaker maker = (MapMaker) createMapMaker().removalListener(listener);
    final ComputingConcurrentHashMap<Object, Object> map =
        makeComputingMap(maker, computingFunction);
    assertTrue(listener.isEmpty());

    final Object one = new Object();
    final Object two = new Object();
    final Object three = new Object();

    new Thread() {
      @Override
      public void run() {
        try {
          map.getOrCompute(one);
        } catch (ExecutionException e) {
          throw new RuntimeException(e);
        }
        doneSignal.countDown();
      }
    }.start();

    try {
      computingSignal.await();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }

    map.put(one, two);
    startSignal.countDown();

    try {
      doneSignal.await();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }

    assertNotNull(map.putIfAbsent(one, three)); // force notifications
    assertNotified(listener, one, computedObject, RemovalCause.REPLACED);
    assertTrue(listener.isEmpty());
  }

  // computing functions

  private static class CountingFunction implements Function<Object, Object> {
    private final AtomicInteger count = new AtomicInteger();

    @Override
    public Object apply(Object from) {
      count.incrementAndGet();
      return new Object();
    }

    public int getCount() {
      return count.get();
    }
  }

  public void testNullParameters() throws Exception {
    NullPointerTester tester = new NullPointerTester();
    Function<Object, Object> computingFunction = new IdentityLoader<Object>();
    tester.testAllPublicInstanceMethods(makeComputingMap(createMapMaker(), computingFunction));
  }

  static final class ConstantLoader<K, V> implements Function<K, V> {
    private final V constant;

    public ConstantLoader(V constant) {
      this.constant = constant;
    }

    @Override
    public V apply(K key) {
      return constant;
    }
  }

  static final class IdentityLoader<T> implements Function<T, T> {
    @Override
    public T apply(T key) {
      return key;
    }
  }

}