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

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

import static org.junit.Assert.*;

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;

import java.io.File;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.EvictionAction;
import org.apache.geode.cache.EvictionAlgorithm;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.util.ObjectSizer;
import org.apache.geode.cache30.CacheTestCase;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.SharedLibrary;
import org.apache.geode.internal.cache.lru.HeapLRUCapacityController;
import org.apache.geode.internal.cache.lru.Sizeable;
import org.apache.geode.internal.size.ReflectionSingleObjectSizer;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.LogWriterUtils;

@Category(DistributedTest.class)
public class EvictionObjectSizerDUnitTest extends JUnit4CacheTestCase {

  protected static Cache cache = null;

  static int maxEnteries = 20;

  static int maxSizeInMb = 20;

  protected static Region region = null;

  private static final long serialVersionUID = 1L;

  // Removed CONCURRENT_OVERHEAD since it is included in the regionEntrySize.

  public EvictionObjectSizerDUnitTest() {
    super();
  }

  /**
   * Without object sizer
   */
  @Test
  public void testWithoutObjectSizerForHeapLRU() {
    prepareScenario(EvictionAlgorithm.LRU_HEAP, null);

    // Size of overhead=
    // 49(HeapLRUCapacityController)((PartitionedRegion)region).getEvictionController()).getPerEntryOverhead()
    // Size of Integer key= 0 (it is inlined)
    // Size of Byte Array(1 MB)= 1024 * 1024 + 8 (byte object) + 4 ( size of object) + 4 (rounded up
    // to nearest word)
    // = 1048592
    // Total Size of each entry should be= 1048592
    putData("PR1", 2, 1);
    int keySize = 0;
    int valueSize =
        SharedLibrary.getObjectHeaderSize() + 4 /* array length */ + (1024 * 1024) /* bytes */;
    valueSize = (int) ReflectionSingleObjectSizer.roundUpSize(valueSize);
    int entrySize = keySize + valueSize
        + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
            .getPerEntryOverhead();
    verifySize("PR1", 2, entrySize);
    assertEquals(2 * entrySize, ((PartitionedRegion) region).getEvictionController().getLRUHelper()
        .getStats().getCounter());
  }

  /**
   * With object sizer for standard objects.Key -Integer Value ByteArray
   * 
   */
  @Test
  public void testObjectSizerForHeapLRU_StandardObjects() {
    prepareScenario(EvictionAlgorithm.LRU_HEAP, new TestObjectSizerImpl());

    // Size of overhead= 49
    // Size of Integer key= 0(inlined)
    // Size of Byte Array(1 MB) + overhead (16 bytes)= 1048592 + 16
    // Total Size of each entry should be= 1048592
    putData("PR1", 2, 1);
    {
      int keySize = 0;
      int valueSize =
          SharedLibrary.getObjectHeaderSize() + 4 /* array length */ + (1024 * 1024) /* bytes */;
      valueSize = (int) ReflectionSingleObjectSizer.roundUpSize(valueSize);
      int entrySize = keySize + valueSize
          + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
              .getPerEntryOverhead();
      verifySize("PR1", 2, entrySize);
    }

    // Size of overhead= 49
    // Size of Integer key= 0(inlined)
    // Size of Byte Array(2 MB) + overhead= 2097152 + 16
    // Total Size of each entry should be= 2097201

    {
      putData("PR1", 2, 2);
      int keySize = 0;
      int valueSize = SharedLibrary.getObjectHeaderSize() + 4 /* array length */
          + (1024 * 1024 * 2) /* bytes */;
      valueSize = (int) ReflectionSingleObjectSizer.roundUpSize(valueSize);
      int entrySize = keySize + valueSize
          + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
              .getPerEntryOverhead();
      verifySize("PR1", 2, entrySize);
    }
  }

  /**
   * With object sizer for customized value object implementing ObjectSizer .Key -Integer Value
   * TestNonSizerObject
   */
  @Test
  public void testObjectSizerForHeapLRU_CustomizedNonSizerObject() {
    prepareScenario(EvictionAlgorithm.LRU_HEAP, new TestObjectSizerImpl());

    // Size of overhead= 49
    // Size of Integer key= 0(inlined)
    // Size of byte array 0 + size of overhead(16)
    // Total Size of each entry should be= 54
    putCustomizedData(1, new byte[0]);
    {
      int keySize = 0;
      int valueSize = SharedLibrary.getObjectHeaderSize() + 4 /* array length */ + 0 /* bytes */;
      valueSize = (int) ReflectionSingleObjectSizer.roundUpSize(valueSize);
      int entrySize = keySize + valueSize
          + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
              .getPerEntryOverhead();
      LogWriterUtils.getLogWriter()
          .info("testObjectSizerForHeapLRU_CustomizedNonSizerObject expected= " + entrySize);
      assertEquals(entrySize, getSizeOfCustomizedData(1));
    }

    // Size of overhead= 49
    // Size of Integer key= 0(inlined)
    // Size of byte array 4 + size of overhead(12)
    // Total Size of each entry should be= 59
    putCustomizedData(2, new byte[4]);
    {
      int keySize = 0;
      int valueSize = SharedLibrary.getObjectHeaderSize() + 4 /* array length */ + 4 /* bytes */;
      valueSize = (int) ReflectionSingleObjectSizer.roundUpSize(valueSize);
      int entrySize = keySize + valueSize
          + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
              .getPerEntryOverhead();
      LogWriterUtils.getLogWriter()
          .info("testObjectSizerForHeapLRU_CustomizedNonSizerObject expected= " + entrySize);
      assertEquals(entrySize, getSizeOfCustomizedData(2));
    }
  }

  /**
   * With object sizer for customized value object implementing ObjectSizer .Key -Integer Value
   * TestObjectSizerImpl
   */
  @Test
  public void testObjectSizerForHeapLRU_CustomizedSizerObject() {
    prepareScenario(EvictionAlgorithm.LRU_HEAP, new TestObjectSizerImpl());

    // Size of overhead= 49
    // Size of Integer key= 0(inlined)
    // Size of TestObjectSizerImpl= 160 (serialized size), changed to 156 because package changed to
    // org.apache.geode
    // Total Size of entry should be= 71
    putCustomizedData(1, new TestObjectSizerImpl());
    int expected = (0 + 156 + (Sizeable.PER_OBJECT_OVERHEAD * 2)
        + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
            .getPerEntryOverhead());
    LogWriterUtils.getLogWriter()
        .info("testObjectSizerForHeapLRU_CustomizedSizerObject expected= " + expected);
    assertEquals(expected, getSizeOfCustomizedData(1));
    assertEquals(expected, ((PartitionedRegion) region).getEvictionController().getLRUHelper()
        .getStats().getCounter());
  }

  /**
   * With object sizer for customized key and value objects.
   */
  @Test
  public void testObjectSizerForHeapLRU_CustomizedSizerObjects() {
    prepareScenario(EvictionAlgorithm.LRU_HEAP, new TestObjectSizerImpl());

    // Size of overhead= 49
    // Size of TestNonSizerObject key= 1(customized)
    // Size of TestObjectSizerImpl= 160 (serialized size), changed to 156 because package changed to
    // org.apache.geode
    // Total Size of entry should be= 72
    putCustomizedObjects(new TestNonSizerObject("1"), new TestObjectSizerImpl());
    int expected = (1 + 156 + (Sizeable.PER_OBJECT_OVERHEAD * 2)
        + ((HeapLRUCapacityController) ((PartitionedRegion) region).getEvictionController())
            .getPerEntryOverhead());
    LogWriterUtils.getLogWriter()
        .info("testObjectSizerForHeapLRU_CustomizedSizerObjects expected= " + expected);
    assertEquals(expected, getSizeOfCustomizedObject(new TestNonSizerObject("1")));
    assertEquals(expected, ((PartitionedRegion) region).getEvictionController().getLRUHelper()
        .getStats().getCounter());
  }

  public void prepareScenario(EvictionAlgorithm evictionAlgorithm, ObjectSizer sizer) {
    createCache();
    createPartitionedRegion(true, evictionAlgorithm, "PR1", 1, 1, 10000, sizer);
  }

  public void createCache() {
    try {
      Properties props = new Properties();
      DistributedSystem ds = getSystem(props);
      assertNotNull(ds);
      ds.disconnect();
      ds = getSystem(props);
      cache = CacheFactory.create(ds);
      assertNotNull(cache);
      LogWriterUtils.getLogWriter().info("cache= " + cache);
      LogWriterUtils.getLogWriter().info("cache closed= " + cache.isClosed());
      cache.getResourceManager().setEvictionHeapPercentage(50);
    } catch (Exception e) {
      Assert.fail("Failed while creating the cache", e);
    }
  }

  public static void createPartitionedRegion(boolean setEvictionOn,
      EvictionAlgorithm evictionAlgorithm, String regionName, int totalNoOfBuckets,
      int evictionAction, int evictorInterval, ObjectSizer sizer) {

    final AttributesFactory factory = new AttributesFactory();
    PartitionAttributesFactory partitionAttributesFactory = new PartitionAttributesFactory()
        .setRedundantCopies(totalNoOfBuckets == 4 ? 0 : 1).setTotalNumBuckets(totalNoOfBuckets);
    factory.setConcurrencyChecksEnabled(false);
    factory.setPartitionAttributes(partitionAttributesFactory.create());
    if (setEvictionOn) {
      if (evictionAlgorithm.isLRUHeap()) {
        factory.setEvictionAttributes(EvictionAttributes.createLRUHeapAttributes(sizer,
            evictionAction == 1 ? EvictionAction.LOCAL_DESTROY : EvictionAction.OVERFLOW_TO_DISK));
      } else if (evictionAlgorithm.isLRUMemory()) {
        factory.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(maxSizeInMb,
            sizer,
            evictionAction == 1 ? EvictionAction.LOCAL_DESTROY : EvictionAction.OVERFLOW_TO_DISK));
      } else {
        factory.setEvictionAttributes(EvictionAttributes.createLRUEntryAttributes(maxEnteries,
            evictionAction == 1 ? EvictionAction.LOCAL_DESTROY : EvictionAction.OVERFLOW_TO_DISK));
      }
      if (evictionAction == 2) {
        factory.setDiskSynchronous(true);
        final File[] diskDirs = new File[1];
        diskDirs[0] =
            new File("Partitioned_Region_Eviction/" + "LogFile" + "_" + OSProcess.getId());
        diskDirs[0].mkdirs();
        factory.setDiskStoreName(cache.createDiskStoreFactory().setDiskDirs(diskDirs)
            .create("EvictionObjectSizerDUnitTest").getName());
      }
    }

    region = cache.createRegion(regionName, factory.create());
    assertNotNull(region);
    LogWriterUtils.getLogWriter().info("Partitioned Region created Successfully :" + region);
  }

  /**
   * returns data size in bytes
   */
  public static int putData(final String regionName, final int noOfElememts,
      final int sizeOfElement) {
    int result = 0;
    final Region pr = cache.getRegion(regionName);
    for (int counter = 1; counter <= noOfElememts; counter++) {
      byte[] baValue = new byte[sizeOfElement * 1024 * 1024];
      int baSize = CachedDeserializableFactory.getByteSize(baValue);
      result += baSize;
      pr.put(new Integer(counter), baValue);
    }
    return result;
  }

  public static void verifySize(String regionName, int noOfElememts, int entrySize) {
    final Region pr = cache.getRegion(regionName);
    for (final Iterator i =
        ((PartitionedRegion) pr).getDataStore().getAllLocalBuckets().iterator(); i.hasNext();) {
      final Map.Entry entry = (Map.Entry) i.next();
      final BucketRegion bucketRegion = (BucketRegion) entry.getValue();
      if (bucketRegion == null) {
        continue;
      } else {
        AbstractLRURegionMap map = (AbstractLRURegionMap) bucketRegion.entries;
        if (map == null || map.size() == 0) {
          continue;
        }
        LogWriterUtils.getLogWriter().info("Checking for entry in bucket region: " + bucketRegion);
        for (int counter = 1; counter <= noOfElememts; counter++) {
          assertEquals(entrySize,
              ((AbstractLRURegionEntry) map.getEntry(new Integer(counter))).getEntrySize());
        }
      }
    }
  }

  private void putCustomizedData(int counter, Object object) {
    final Region pr = cache.getRegion("PR1");
    pr.put(new Integer(counter), object);

  }

  private void putCustomizedObjects(Object key, Object value) {
    final Region pr = cache.getRegion("PR1");
    pr.put(key, value);

  }

  private int getSizeOfCustomizedData(int counter) {
    final Region pr = cache.getRegion("PR1");
    for (final Iterator i =
        ((PartitionedRegion) pr).getDataStore().getAllLocalBuckets().iterator(); i.hasNext();) {
      final Map.Entry entry = (Map.Entry) i.next();
      final BucketRegion bucketRegion = (BucketRegion) entry.getValue();
      if (bucketRegion == null) {
        continue;
      } else {
        AbstractLRURegionMap map = (AbstractLRURegionMap) bucketRegion.entries;
        return ((AbstractLRURegionEntry) map.getEntry(new Integer(counter))).getEntrySize();
      }
    }
    return 0;
  }

  private int getSizeOfCustomizedObject(Object object) {
    final Region pr = cache.getRegion("PR1");
    for (final Iterator i =
        ((PartitionedRegion) pr).getDataStore().getAllLocalBuckets().iterator(); i.hasNext();) {
      final Map.Entry entry = (Map.Entry) i.next();
      final BucketRegion bucketRegion = (BucketRegion) entry.getValue();
      if (bucketRegion == null) {
        continue;
      } else {
        AbstractLRURegionMap map = (AbstractLRURegionMap) bucketRegion.entries;
        AbstractLRURegionEntry re = (AbstractLRURegionEntry) map.getEntry(object);
        if (re != null) {
          return re.getEntrySize();
        }
      }
    }
    return 0;
  }

}
