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

import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.Properties;
import java.util.Random;

import org.junit.Test;

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.EvictionAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.util.ObjectSizer;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.JvmSizeUtils;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.eviction.EvictionCounters;
import org.apache.geode.internal.cache.eviction.MemoryLRUController;
import org.apache.geode.internal.size.ReflectionSingleObjectSizer;
import org.apache.geode.internal.size.WellKnownClassSizer;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;

/**
 * Tests the basic functionality of the memory lru eviction controller and its statistics.
 *
 *
 * @since GemFire 3.2
 */

public class MemLRUEvictionControllerDUnitTest extends JUnit4CacheTestCase {

  private static boolean usingMain = false;

  /**
   * Creates a new <code>MemLRUEvictionControllerDUnitTest</code>
   */
  public MemLRUEvictionControllerDUnitTest() {
    super();
  }

  /**
   * Returns the <code>EvictionStatistics</code> for the given region
   */
  private EvictionCounters getLRUStats(Region region) {
    final LocalRegion l = (LocalRegion) region;
    return l.getEvictionController().getCounters();
  }

  private int getEntryOverhead(Region region) {
    LocalRegion lRegion = (LocalRegion) region;
    return ((MemoryLRUController) lRegion.getEvictionController()).getPerEntryOverhead();
  }

  // ////// Test Methods

  /**
   * Carefully verifies that region operations effect the {@link EvictionCounters} as expected.
   */
  @Test
  public void testRegionOperations() throws CacheException {

    int threshold = 4;

    final String name = getUniqueName();
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    factory.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(threshold));

    Region region;
    if (usingMain) {
      DistributedSystem system = DistributedSystem.connect(new Properties());
      Cache cache = CacheFactory.create(system);
      region = cache.createRegion("Test", factory.create());

    } else {
      region = createRegion(name, factory.create());
    }

    EvictionCounters lruStats = getLRUStats(region);
    assertNotNull(lruStats);

    String sampleKey = "10000";
    int stringSize = getObjectHeaderSize() // String object
        + (2 * 4) + JvmSizeUtils.getReferenceSize(); // 2 ints and a reference on a string
    stringSize = roundUpSize(stringSize);

    int charArraySize = sampleKey.length() * 2 + getObjectHeaderSize() // char array
                                                                       // object
        + 4; // length of char array
    charArraySize = roundUpSize(charArraySize);
    assertEquals(stringSize, ReflectionSingleObjectSizer.sizeof(String.class));
    assertEquals(roundUpSize(getObjectHeaderSize() + 4),
        (new ReflectionSingleObjectSizer()).sizeof(new char[0]));
    assertEquals(charArraySize, (new ReflectionSingleObjectSizer()).sizeof(new char[5]));

    int keySize = stringSize + charArraySize;
    assertEquals(keySize, WellKnownClassSizer.sizeof(sampleKey));
    assertEquals(keySize, ObjectSizer.DEFAULT.sizeof(sampleKey));
    // now that keys are inlined the keySize is 0
    keySize = 0;
    byte[] sampleValue = new byte[1000];
    int valueSize = sampleValue.length + getObjectHeaderSize() // byte array object;
        + 4; // length of byte array
    valueSize = roundUpSize(valueSize);
    int entrySize = keySize + valueSize + getEntryOverhead(region);
    assertEquals(valueSize, ObjectSizer.DEFAULT.sizeof(sampleValue));

    for (int i = 1; i <= 100; i++) {
      Object key = String.valueOf(10000 + i);
      Object value = new byte[1000];
      region.put(key, value);
      assertEquals(i * entrySize, lruStats.getCounter());
      assertEquals(0, lruStats.getEvictions());
    }

    for (int i = 100; i >= 1; i--) {
      Object key = String.valueOf(10000 + i);
      region.destroy(key);
      assertEquals((i - 1) * entrySize, lruStats.getCounter());
      assertEquals(0, lruStats.getEvictions());
    }
  }

  /**
   * Make sure that we only size a class the first time we see the class instance.
   *
   */
  @Test
  public void testSizeClassesOnce() throws CacheException {
    int threshold = 4;

    final String name = getUniqueName();
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    factory.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(threshold));

    Region region = createRegion(name, factory.create());

    EvictionCounters lruStats = getLRUStats(region);
    assertNotNull(lruStats);

    TestObject object = new TestObject(50);
    // keySize is 0 because it is inlined
    int keySize = 0; // ObjectSizer.DEFAULT.sizeof(new String("10000"));
    int valueSize = ObjectSizer.DEFAULT.sizeof(object);
    int entrySize = keySize + valueSize + getEntryOverhead(region);

    Random ran = new Random();
    for (int i = 1; i <= 100; i++) {
      Object key = String.valueOf(10000 + i);
      // Use a randomly sized object.
      Object value = new TestObject(ran.nextInt(100));
      region.put(key, value);
      assertEquals(i * entrySize, lruStats.getCounter());
      assertEquals(0, lruStats.getEvictions());
    }

    for (int i = 100; i >= 1; i--) {
      Object key = String.valueOf(10000 + i);
      region.destroy(key);
      assertEquals((i - 1) * entrySize, lruStats.getCounter());
      assertEquals(0, lruStats.getEvictions());
    }
  }

  /**
   * Prints out the number of bytes that a region entry occupies in the VM.
   */
  @Test
  public void testEntryOverHead() throws Exception {
    final String name = getUniqueName();
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    factory.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(50));

    Region region;
    region = createRegion(name, factory.create());

    String s = "Each entry occupies " + getEntryOverhead(region) + " bytes";
    region.getCache().getDistributedSystem().getLogWriter().info(s);
  }

  /**
   * Class used in testCustomObjectSizer
   *
   * @since GemFire 5.0
   */
  class CustomObjectSizer implements ObjectSizer {
    private final int dataSize;

    private boolean wasCalled = false;

    public CustomObjectSizer(int dataSize) {
      this.dataSize = dataSize;
    }

    @Override
    public int sizeof(Object o) {
      wasCalled = true;
      return dataSize;
    }

    public boolean wasCalled() {
      return wasCalled;
    }
  }

  /**
   * Validate that a custom {@link ObjectSizer} is called, configured propertly, and actually limits
   * the size of the <code>Region</code>.
   *
   */
  @Test
  public void testCustomObjectSizer() throws Exception {
    final String name = getUniqueName();
    final int entrySize = 1024 * 1024;
    final int numEntries = 3;
    AttributesFactory factory = new AttributesFactory();
    CustomObjectSizer cs = new CustomObjectSizer(entrySize);
    factory.setScope(Scope.LOCAL);
    factory.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(1, cs));
    Region r = createRegion(name, factory.create());

    for (int size = 0; size < numEntries; size++) {
      // changed to a boolean[] because byte[] does not cause a call to ObjectSizer.sizeof
      // What was calling it before was the key object. But now that keys are inlined we
      // no longer call ObjectSizer.sizeof for the key.
      r.put(size, new boolean[entrySize]);
    }
    assertTrue("ObjectSizer was not triggered", cs.wasCalled());
    long actualSize = getLRUStats(r).getCounter();
    int bytesPut = (entrySize * numEntries);
    assertTrue("Expected bytes put: " + bytesPut + " is not < " + actualSize,
        actualSize < bytesPut);
  }

  public static void main(String[] args) throws Exception {
    usingMain = true;
    (new MemLRUEvictionControllerDUnitTest()).testRegionOperations();
  }

  private static class TestObject {
    private final byte[] field;

    public TestObject(int size) {
      field = new byte[size];
    }
  }
}
