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

import org.apache.geode.InternalGemFireException;
import org.apache.geode.StatisticsFactory;
import org.apache.geode.cache.CacheCallback;
import org.apache.geode.cache.EvictionAction;
import org.apache.geode.cache.Region;
import org.apache.geode.internal.cache.BucketRegion;
import org.apache.geode.internal.cache.PlaceHolderDiskRegion;
import org.apache.geode.internal.i18n.LocalizedStrings;

import java.io.IOException;
import java.io.Serializable;
import java.util.Properties;

/**
 * Eviction controllers that extend this class evict the least recently used (LRU) entry in the
 * region whose capacity they controller. In order to provide an efficient computation of the LRU
 * entry, GemFire uses special internal data structures for managing the contents of a region. As a
 * result, there are several restrictions that are placed on regions whose capacity is governed by
 * an LRU algorithm.
 *
 * <UL>
 *
 * <LI>If the capacity of a region is to be controlled by an LRU algorithm, then the region must be
 * <b>created</b> with {@link org.apache.geode.cache.EvictionAttributes}
 *
 * <LI>The eviction controller of a region governed by an LRU algorithm cannot be changed.</LI>
 *
 * <LI>An LRU algorithm cannot be applied to a region after the region has been created.</LI>
 *
 * </UL>
 *
 * LRU algorithms also specify what {@linkplain org.apache.geode.cache.EvictionAction action} should
 * be performed upon the least recently used entry when the capacity is reached. Currently, there
 * are two supported actions: {@linkplain org.apache.geode.cache.EvictionAction#LOCAL_DESTROY
 * locally destroying} the entry (which is the
 * {@linkplain org.apache.geode.cache.EvictionAction#DEFAULT_EVICTION_ACTION default}), thus freeing
 * up space in the VM, and {@linkplain org.apache.geode.cache.EvictionAction#OVERFLOW_TO_DISK
 * overflowing} the value of the entry to disk.
 *
 * <P>
 *
 * {@link org.apache.geode.cache.EvictionAttributes Eviction controllers} that use an LRU algorithm
 * maintain certain region-dependent state (such as the maximum number of entries allowed in the
 * region). As a result, an instance of <code>LRUAlgorithm</code> cannot be shared among multiple
 * regions. Attempts to create a region with a LRU-based capacity controller that has already been
 * used to create another region will result in an {@link IllegalStateException} being thrown.
 *
 * @since GemFire 3.2
 */
public abstract class LRUAlgorithm implements CacheCallback, Serializable, Cloneable {

  /**
   * The key for setting the <code>eviction-action</code> property of an <code>LRUAlgorithm</code>
   */
  public static final String EVICTION_ACTION = "eviction-action";

  /** What to do upon eviction */
  protected EvictionAction evictionAction;

  /** Used to dynamically track the changing region limit. */
  protected transient LRUStatistics stats;

  /** The helper created by this LRUAlgorithm */
  private transient EnableLRU helper;

  protected BucketRegion bucketRegion;

  /**
   * Creates a new <code>LRUAlgorithm</code> with the given {@linkplain EvictionAction eviction
   * action}.
   */
  protected LRUAlgorithm(EvictionAction evictionAction, Region region) {
    bucketRegion = (BucketRegion) (region instanceof BucketRegion ? region : null);
    setEvictionAction(evictionAction);
    this.helper = createLRUHelper();
  }

  /**
   * Used to hook up a bucketRegion late during disk recover.
   */
  public void setBucketRegion(Region r) {
    if (r instanceof BucketRegion) {
      this.bucketRegion = (BucketRegion) r;
      this.bucketRegion.setLimit(getLimit());
    }
  }

  /**
   * Sets the action that is performed on the least recently used entry when it is evicted from the
   * VM.
   *
   * @throws IllegalArgumentException If <code>evictionAction</code> specifies an unknown eviction
   *         action.
   *
   * @see EvictionAction
   */
  protected void setEvictionAction(EvictionAction evictionAction) {
    this.evictionAction = evictionAction;
  }

  /**
   * Gets the action that is performed on the least recently used entry when it is evicted from the
   * VM.
   *
   * @return one of the following constants: {@link EvictionAction#LOCAL_DESTROY},
   *         {@link EvictionAction#OVERFLOW_TO_DISK}
   */
  public EvictionAction getEvictionAction() {
    return this.evictionAction;
  }

  /**
   * For internal use only. Returns a helper object used internally by the GemFire cache
   * implementation.
   */
  public EnableLRU getLRUHelper() {
    synchronized (this) {
      // Synchronize with readObject/writeObject to avoid race
      // conditions with copy sharing. See bug 31047.
      return this.helper;
    }
  }

  private void writeObject(java.io.ObjectOutputStream out) throws IOException {

    synchronized (this) { // See bug 31047
      out.writeObject(this.evictionAction);
    }
  }

  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {

    synchronized (this) { // See bug 31047
      this.evictionAction = (EvictionAction) in.readObject();
      this.helper = createLRUHelper();
    }
  }

  /**
   * Creates a new <code>LRUHelper</code> tailed for this LRU algorithm implementation.
   */
  protected abstract EnableLRU createLRUHelper();

  /**
   * Returns the "limit" as defined by this LRU algorithm
   */
  public abstract long getLimit();

  /**
   * Set the limiting parameter used to determine when eviction is needed.
   */
  public abstract void setLimit(int maximum);

  /**
   * This method is an artifact when eviction controllers used to called capacity controllers and
   * were configured in the cache.xml file as <code>Declarable</code>
   * 
   * @since GemFire 4.1.1
   */
  public abstract Properties getProperties();

  /**
   * Releases resources obtained by this <code>LRUAlgorithm</code>
   */
  public void close() {
    if (this.stats != null) {
      if (bucketRegion != null) {
        this.stats.incEvictions(bucketRegion.getEvictions() * -1);
        this.stats.decrementCounter(bucketRegion.getCounter());
        bucketRegion.close();
      } else {
        this.stats.close();
      }
    }
  }

  /**
   * Returns a copy of this LRU-based eviction controller. This method is a artifact when capacity
   * controllers were used on a <code>Region</code>
   */
  @Override
  public Object clone() throws CloneNotSupportedException {
    synchronized (this) {
      LRUAlgorithm clone = (LRUAlgorithm) super.clone();
      clone.stats = null;
      synchronized (clone) {
        clone.helper = clone.createLRUHelper();
      }
      return clone;
    }
  }

  /**
   * Return true if the specified capacity controller is compatible with this
   */
  @Override
  public boolean equals(Object cc) {
    if (cc == null)
      return false;
    if (!getClass().isAssignableFrom(cc.getClass()))
      return false;
    LRUAlgorithm other = (LRUAlgorithm) cc;
    if (!other.evictionAction.equals(this.evictionAction))
      return false;
    return true;
  }

  /**
   * Note that we just need to make sure that equal objects return equal hashcodes; nothing really
   * elaborate is done here.
   */
  @Override
  public int hashCode() {
    return this.evictionAction.hashCode();
  }

  /**
   * Force subclasses to have a reasonable <code>toString</code>
   *
   * @since GemFire 4.0
   */
  @Override
  public abstract String toString();

  /**
   * A partial implementation of the <code>EnableLRU</code> interface that contains code common to
   * all <code>LRUAlgorithm</code>s.
   */
  protected abstract class AbstractEnableLRU implements EnableLRU {

    /** The region whose capacity is controller by this eviction controller */
    private volatile transient String regionName;

    public long limit() {
      if (stats == null) {
        throw new InternalGemFireException(
            LocalizedStrings.LRUAlgorithm_LRU_STATS_IN_EVICTION_CONTROLLER_INSTANCE_SHOULD_NOT_BE_NULL
                .toLocalizedString());
      }
      if (bucketRegion != null) {
        return bucketRegion.getLimit();
      }
      return stats.getLimit();
    }

    public String getRegionName() {
      return this.regionName;
    }

    public void setRegionName(Object region) {
      String fullPathName;
      if (region instanceof Region) {
        fullPathName = ((Region) region).getFullPath();
      } else if (region instanceof PlaceHolderDiskRegion) {
        PlaceHolderDiskRegion phdr = (PlaceHolderDiskRegion) region;
        if (phdr.isBucket()) {
          fullPathName = phdr.getPrName();
        } else {
          fullPathName = phdr.getName();
        }
      } else {
        throw new IllegalStateException("expected Region or PlaceHolderDiskRegion");
      }
      if (this.regionName != null && !this.regionName.equals(fullPathName)) {
        throw new IllegalArgumentException(
            LocalizedStrings.LRUAlgorithm_LRU_EVICTION_CONTROLLER_0_ALREADY_CONTROLS_THE_CAPACITY_OF_1_IT_CANNOT_ALSO_CONTROL_THE_CAPACITY_OF_REGION_2
                .toLocalizedString(
                    new Object[] {LRUAlgorithm.this, this.regionName, fullPathName}));
      }
      this.regionName = fullPathName; // store the name not the region since
      // region is not fully constructed yet
    }

    protected void setStats(LRUStatistics stats) {
      LRUAlgorithm.this.stats = stats;
    }

    public LRUStatistics initStats(Object region, StatisticsFactory sf) {
      setRegionName(region);
      final LRUStatistics stats = new LRUStatistics(sf, getRegionName(), this);
      stats.setLimit(LRUAlgorithm.this.getLimit());
      stats.setDestroysLimit(1000);
      setStats(stats);
      return stats;
    }

    public LRUStatistics getStats() {
      return LRUAlgorithm.this.stats;
    }

    public EvictionAction getEvictionAction() {
      return LRUAlgorithm.this.evictionAction;
    }

    public void afterEviction() {
      // Do nothing
    }

  }

}
