/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal.cache.execute;

import static org.junit.Assert.*;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.DataSerializable;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.PartitionResolver;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.cache30.CacheSerializableRunnable;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.PartitionedRegionDUnitTestCase;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

@Category(DistributedTest.class)
public class PRPerformanceTestDUnitTest extends PartitionedRegionDUnitTestCase {

  public PRPerformanceTestDUnitTest() {
    super();
  }

  protected static Cache cache = null;

  Properties props = new Properties();

  VM vm0 = null;

  VM vm1 = null;

  VM vm2 = null;

  VM vm3 = null;

  static final int totalNumBuckets = 13;

  static ArrayList listOfKeys = new ArrayList();

  protected static final byte[] valueArray = new byte[1024];

  public static void createCacheInVm() throws Exception {
    Properties props = new Properties();
    new PRPerformanceTestDUnitTest().createCache(props);
  }

  private void createCache(Properties props) throws Exception {
    DistributedSystem ds = getSystem(props);
    assertNotNull(ds);
    ds.disconnect();
    ds = getSystem(props);
    cache = CacheFactory.create(ds);
    assertNotNull(cache);
  }

  /* SerializableRunnable object to create PR with a partition resolver */
  SerializableRunnable createPrRegionWithPartitionResolver =
      new CacheSerializableRunnable("createPrRegionWithDS") {

        public void run2() throws CacheException {
          AttributesFactory attr = new AttributesFactory();
          PartitionResolver resolver = MonthBasedPartitionResolver.getInstance();
          PartitionAttributesFactory paf = new PartitionAttributesFactory();
          paf.setTotalNumBuckets(totalNumBuckets);
          paf.setPartitionResolver(resolver);
          paf.setLocalMaxMemory(900);
          paf.setRedundantCopies(0);

          PartitionAttributes prAttr = paf.create();
          attr.setPartitionAttributes(prAttr);
          RegionAttributes regionAttribs = attr.create();
          cache.createRegion("PR1", regionAttribs);
          PerformanceTestFunction function = new PerformanceTestFunction();
          FunctionService.registerFunction(function);
        }
      };

  /* SerializableRunnable object to create PR with a partition resolver */

  SerializableRunnable createPrRegionOnlyAccessorWithPartitionResolver =
      new CacheSerializableRunnable("createPrRegionOnlyAccessor") {

        public void run2() throws CacheException {
          AttributesFactory attr = new AttributesFactory();
          PartitionAttributesFactory paf = new PartitionAttributesFactory();
          PartitionResolver resolver = MonthBasedPartitionResolver.getInstance();
          PartitionAttributes prAttr = paf.setLocalMaxMemory(0).setTotalNumBuckets(totalNumBuckets)
              .setPartitionResolver(resolver).setRedundantCopies(0).create();

          attr.setPartitionAttributes(prAttr);
          RegionAttributes regionAttribs = attr.create();
          cache.createRegion("PR1", regionAttribs);

        }
      };

  /**
   * Search the entires PartitionedRegion for the key, to validate that indeed it doesn't exist
   *
   * @returns true if it does exist
   * @param par
   * @param key
   */
  public static boolean searchForKey(PartitionedRegion par, Date key) {
    // Check to make super sure that the key exists
    boolean foundIt = false;
    final int numBucks = par.getTotalNumberOfBuckets();
    for (int b = 0; b < numBucks; b++) {
      if (par.getBucketKeys(b).contains(key)) {
        foundIt = true;
        // getLogWriter().info("Key " + key + " found in bucket " + b);
        break;
      }
    }
    if (!foundIt) {
      LogWriterUtils.getLogWriter().severe("Key " + key + " not found in any bucket");
    }
    return foundIt;
  }

  public void partitionedRegionTest(final String prName, final int noOfEntries) {
    /*
     * Do put() operations through VM with PR having both Accessor and Datastore
     */
    vm0.invoke(new CacheSerializableRunnable("doPutCreateInvalidateOperations1") {
      public void run2() throws CacheException {

        Calendar cal = Calendar.getInstance();
        final Region pr = cache.getRegion(prName);
        if (pr == null) {
          fail(prName + " not created");
        }
        int size = 0;

        size = pr.size();
        assertEquals("Size doesnt return expected value", 0, size);
        assertEquals("isEmpty doesnt return proper state of the PartitionedRegion", true,
            pr.isEmpty());
        assertEquals(0, pr.keySet().size());
        int entries = noOfEntries;
        while (entries > 0) {
          for (int i = 0; i <= 11; i++) {
            int yr = (new Integer((int) (Math.random() * 2100))).intValue();
            int month = i;
            int date = (new Integer((int) (Math.random() * 30))).intValue();
            cal.set(yr, month, date);
            Object key = cal.getTime();
            listOfKeys.add(key);
            assertNotNull(pr);
            // pr.put(key, Integer.toString(i));
            pr.put(key, valueArray);
            // assertIndexDetailsEquals(valueArray, pr.get(key));

          }
          entries--;
        }
      }
    });



    vm0.invoke(new CacheSerializableRunnable("verifyKeysonVM0") {
      public void run2() throws CacheException {

        // Calendar cal = Calendar.getInstance();
        final PartitionedRegion pr = (PartitionedRegion) cache.getRegion(prName);
        if (pr == null) {
          fail(prName + " not created");
        }
        Iterator itr = listOfKeys.iterator();
        while (itr.hasNext()) {
          assertTrue(searchForKey(pr, (Date) itr.next()));

        }
        // Intitial warm up phase ..Do a get of all the keys
        // Iterate over the key and try to get all the values repetitively
        itr = listOfKeys.iterator();
        ArrayList vals = new ArrayList();
        while (itr.hasNext()) {
          Object val = pr.get(itr.next());
          assertNotNull(val);
          vals.add(val);
          // assertTrue(searchForKey(pr, (Date)itr.next()));
        }

        // Call the execute method for each key
        PerformanceTestFunction function = new PerformanceTestFunction();
        FunctionService.registerFunction(function);
        DefaultResultCollector drc = new DefaultResultCollector();
        // final Set allKeysSet = new HashSet();
        final Set singleKeySet = new HashSet();
        Execution dataSet = FunctionService.onRegion(pr);
        vals.clear();
        ArrayList list = new ArrayList();
        itr = listOfKeys.iterator();

        while (itr.hasNext()) {
          singleKeySet.add(itr.next());
          dataSet = dataSet.withFilter(singleKeySet);
          try {
            ResultCollector rc = dataSet.execute(function.getId());
            list = (ArrayList) rc.getResult();
          } catch (Exception ex) {
            LogWriterUtils.getLogWriter().info("Exception Occurred :" + ex.getMessage());
            Assert.fail("Test failed", ex);
          }
          Object val = list.get(0);
          assertNotNull(val);
          vals.add(val);
          singleKeySet.clear();
          // assertTrue(searchForKey(pr, (Date)itr.next()));
        }
        assertEquals(vals.size(), listOfKeys.size());
        // END: warmup

        // Now start the performance count
        itr = listOfKeys.iterator();
        TimeKeeper t = new TimeKeeper();
        vals.clear();
        t.start();
        // ArrayList vals = new ArrayList();
        while (itr.hasNext()) {
          Object val = pr.get(itr.next());
          assertNotNull(val);
          vals.add(val);
          // assertTrue(searchForKey(pr, (Date)itr.next()));
        }

        t.stop();
        LogWriterUtils.getLogWriter().info("Time taken to iterate over " + vals.size()
            + " no. of keys: " + t.getTimeInMs() + " ms");

        // Call the execute method for each key and see if this takes more time

        vals.clear();
        t = new TimeKeeper();
        t.start();
        // ArrayList list = new ArrayList();
        itr = listOfKeys.iterator();

        while (itr.hasNext()) {
          singleKeySet.add(itr.next());
          dataSet = dataSet.withFilter(singleKeySet);
          try {
            ResultCollector rc = dataSet.execute(function.getId());
            list = (ArrayList) rc.getResult();
          } catch (Exception expected) {
            // No data should cause exec to throw
          }
          Object val = list.get(0);
          assertNotNull(val);
          vals.add(val);
          singleKeySet.clear();
        }
        t.stop();
        assertEquals(vals.size(), listOfKeys.size());
        LogWriterUtils.getLogWriter().info("Time taken to iterate over " + vals.size()
            + " no. of keys using FunctionExecution: " + t.getTimeInMs() + " ms");

      }
    });

  }

  /**
   * This is a PartitionedRegion test for Custom Prtitioning . 4 VMs are used to create the PR with
   * and without(Only Accessor) the DataStore.
   */
  @Test
  public void testPartitionedRegionOperationsCustomPartitioning() throws Exception {
    Host host = Host.getHost(0);

    // create the VM(0 - 4)
    vm0 = host.getVM(0);
    vm1 = host.getVM(1);
    vm2 = host.getVM(2);
    vm3 = host.getVM(3);
    final VM accessor = vm0;
    // final VM accessor = vm3;
    // create cache in all vms

    accessor.invoke(() -> PRPerformanceTestDUnitTest.createCacheInVm());
    vm1.invoke(() -> PRPerformanceTestDUnitTest.createCacheInVm());
    vm2.invoke(() -> PRPerformanceTestDUnitTest.createCacheInVm());
    vm3.invoke(() -> PRPerformanceTestDUnitTest.createCacheInVm());



    // Create PR;s in different VM's
    accessor.invoke(createPrRegionOnlyAccessorWithPartitionResolver);
    vm1.invoke(createPrRegionWithPartitionResolver);
    vm2.invoke(createPrRegionWithPartitionResolver);
    vm3.invoke(createPrRegionWithPartitionResolver);

    partitionedRegionTest("/PR1", 50);
    /*
     * destroy the Region.
     */
    destroyTheRegion("/PR1");

    // Create PR;s in different VM's
  }

  public void destroyTheRegion(final String name) {
    /*
     * destroy the Region.
     */
    vm0.invoke(new CacheSerializableRunnable("destroyRegionOp") {

      public void run2() throws CacheException {

        Region pr = cache.getRegion(name);
        if (pr == null) {
          fail(name + " not created");
        }
        pr.destroyRegion();
      }
    });
  }
}


class SerializablePerfMonth implements DataSerializable {
  private int month;

  public SerializablePerfMonth(int month) {
    this.month = month;
  }

  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    this.month = in.readInt();
  }

  public void toData(DataOutput out) throws IOException {
    out.writeInt(this.month);
  }

  public int hashCode() {
    /*
     * if(this.month<4) return 1; else if(this.month >= 4 && this.month < 8) return 2; else return
     * 3;
     *
     */
    return this.month;
  }
}


class TimeKeeper {
  private long startTime = -1;

  private long endTime = -1;

  public void start() {
    startTime = System.currentTimeMillis();
  }

  public void stop() {
    endTime = System.currentTimeMillis();
  }

  public long getTimeInMs() {
    if ((startTime == -1) || (endTime == -1)) {
      System.err.println("call start() and stop() before this method");
      return -1;
    } else if ((endTime == startTime)) {
      System.err.println("the start time and end time are the same...returning 1ms");
      return 1;
    } else
      return (endTime - startTime);
  }
}
