package org.infinispan.query.distributed;

import static org.testng.AssertJUnit.assertEquals;

import java.util.concurrent.TimeUnit;

import org.apache.lucene.search.Query;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
import org.infinispan.commons.api.BasicCacheContainer;
import org.infinispan.commons.util.Util;
import org.infinispan.context.Flag;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.query.CacheQuery;
import org.infinispan.query.Search;
import org.infinispan.query.SearchManager;
import org.infinispan.query.queries.faceting.Car;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.annotations.Test;

/**
 * Stress test running on an indexed Cache which is storing the index in a distributed Lucene Directory
 *
 * This test takes approximately 30 seconds: better increment the number of iterations.
 * It takes approximately half a second to insert 2000 entries in the indexed cache
 * when running DIST in 4 nodes, and it's much faster when run in single node.
 *
 * TestNG enables assertions: these have an impact on Lucene so better run it as a main!
 *
 * @author Adrian Nistor (C) 2013 Red Hat Inc.
 * @author Sanne Grinovero (C) 2013 Red Hat Inc.
 */
@Test(groups = "profiling", testName = "query.distributed.PerfTest", singleThreaded = true)
public class PerfTest extends MultipleCacheManagersTest {

   private static final int NUM_NODES = 4;
   private static final int LOG_ON_EACH = 2000;
   private static final int NUMBER_OF_ITERATIONS = 50;

   private static final String[] neededCacheNames = {
         BasicCacheContainer.DEFAULT_CACHE_NAME,
         "LuceneIndexesMetadata",
         "LuceneIndexesData",
         "LuceneIndexesLocking",
   };

   @Override
   protected void createCacheManagers() throws Throwable {
      for (int i = 0; i < NUM_NODES; i++) {
         EmbeddedCacheManager cacheManager = TestCacheManagerFactory.fromXml("indexing-perf.xml");
         registerCacheManager(cacheManager);
      }
      waitForClusterToForm(neededCacheNames);
   }

   public void testIndexing() throws Exception {
      int carId = 0;
      int cacheId = 0;
      final long start = System.nanoTime();
      for (int outherLoop = 0; outherLoop < NUMBER_OF_ITERATIONS; outherLoop++) {
         final Cache<String, Car> cache = getWriteOnlyCache(cacheId++ % NUM_NODES);
         System.out.print("Using " + cacheId + ": " + cache +"\t");
         final long blockStart = System.nanoTime();
         cache.startBatch();
         for (int innerLoop = 0; innerLoop < LOG_ON_EACH; innerLoop++) {
            carId++;
            cache.put("car" + carId, new Car("megane", "blue", 300 + carId));
            carId++;
            cache.put("car" + carId, new Car("bmw", "blue", 300 + carId));
         }
         cache.endBatch(true);
         System.out.println("Inserted " + LOG_ON_EACH + " cars in " + Util.prettyPrintTime(System.nanoTime() - blockStart, TimeUnit.NANOSECONDS));
      }
      System.out.println("Test took " + Util.prettyPrintTime(System.nanoTime() - start, TimeUnit.NANOSECONDS));

      verifyFindsCar(carId / 2, "megane");
   }

   private Cache<String, Car> getWriteOnlyCache(int cacheId) {
      Cache<String, Car> cache = cache(cacheId);
      AdvancedCache<String,Car> advancedCache = cache.getAdvancedCache();
      AdvancedCache<String, Car> withFlags = advancedCache.withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_INDEX_CLEANUP);
      return withFlags;
   }

   private void verifyFindsCar(int expectedCount, String carMake) {
      for (int i = 0; i < NUM_NODES; i++) {
         verifyFindsCar(cache(i), expectedCount, carMake);
      }
   }

   private void verifyFindsCar(Cache cache, int expectedCount, String carMake) {
      SearchManager searchManager = Search.getSearchManager(cache);
      QueryBuilder carQueryBuilder = searchManager.buildQueryBuilderForClass(Car.class).get();
      Query fullTextQuery = carQueryBuilder.keyword().onField("make").matching(carMake).createQuery();
      CacheQuery<?> cacheQuery = searchManager.getQuery(fullTextQuery, Car.class);
      assertEquals(expectedCount, cacheQuery.getResultSize());
   }

   /**
    * Recommended tuning options:
    * -Xms4g -Xmx4g -XX:+UseParallelGC -Djava.net.preferIPv4Stack=true -Dorg.jboss.resolver.warning=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -Dcom.arjuna.ats.arjuna.coordinator.CoordinatorEnvironmentBean.asyncPrepare=true -XX:+UseLargePages -Djava.awt.headless=true -Dlog4j.configurationFile=log4j2.xml
    *
    * Diagnostic options:
    * -Xms6g -Xmx6g -XX:+UseParallelGC -Djava.net.preferIPv4Stack=true -Dorg.jboss.resolver.warning=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -Dcom.arjuna.ats.arjuna.coordinator.CoordinatorEnvironmentBean.asyncPrepare=true -XX:+UseLargePages -Djava.awt.headless=true -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:-PrintTLAB -XX:+PrintHeapAtGCExtended -XX:+PrintAdaptiveSizePolicy -XX:+PrintGCApplicationStoppedTime -XX:-PrintGCApplicationConcurrentTime -Xloggc:/tmp/lucene.gclog -XX:+ParallelRefProcEnabled -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=delay=10s,duration=24h,filename=/tmp/flight_record_lucene.jfr,settings=/opt/flightrecorder/profile_2ms -Dlog4j.configurationFile=log4j2.xml
    */
   public static void main(String[] args) throws Throwable {
      PerfTest test = new PerfTest();
      test.createBeforeClass();
      try {
         test.testIndexing();
      }
      finally {
         test.destroy();
      }
   }

}
