/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.extensions;

import java.util.Collections;
import java.util.List;
import java.util.SortedMap;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.EntryCacheCfg;
import org.opends.server.api.Backend;
import org.opends.server.api.BackendInitializationListener;
import org.opends.server.api.EntryCache;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.Attribute;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;

/**
 * This class defines the default entry cache which acts as an arbiter for
 * every entry cache implementation configured and installed within the
 * Directory Server or acts an an empty cache if no implementation specific
 * entry cache is configured.  It does not actually store any entries, so
 * all calls to the entry cache public API are routed to underlying entry
 * cache according to the current configuration order and preferences.
 */
public class DefaultEntryCache
       extends EntryCache<EntryCacheCfg>
       implements ConfigurationChangeListener<EntryCacheCfg>,
       BackendInitializationListener
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();


  /**
   * The entry cache order array reflects all currently configured and
   * active entry cache implementations in cache level specific order.
   */
  private static EntryCache<? extends EntryCacheCfg>[] cacheOrder =
    new EntryCache<?>[0];


  /**
   * Creates a new instance of this default entry cache.
   */
  public DefaultEntryCache()
  {
    super();

    // Register with backend initialization listener to clear cache
    // entries belonging to given backend that about to go offline.
    DirectoryServer.registerBackendInitializationListener(this);
  }

  /** {@inheritDoc} */
  @Override
  public void initializeEntryCache(EntryCacheCfg configEntry)
         throws ConfigException, InitializationException
  {
    // No implementation required.
  }

  /** {@inheritDoc} */
  @Override
  public void finalizeEntryCache()
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      entryCache.finalizeEntryCache();
    }
    // ReInitialize cache order array.
    cacheOrder = new EntryCache<?>[0];
  }

  /** {@inheritDoc} */
  @Override
  public boolean containsEntry(DN entryDN)
  {
    if (entryDN == null) {
      return false;
    }

    for (EntryCache<?> entryCache : cacheOrder) {
      if (entryCache.containsEntry(entryDN)) {
        return true;
      }
    }

    return false;
  }

  /** {@inheritDoc} */
  @Override
  public Entry getEntry(String backendID, long entryID)
  {
    for (EntryCache<? extends EntryCacheCfg> entryCache : cacheOrder)
    {
      Entry entry = entryCache.getEntry(backendID, entryID);
      if (entry != null)
      {
        return entry.duplicate(true);
      }
    }

    // Indicate global cache miss.
    if (cacheOrder.length != 0)
    {
      cacheMisses.getAndIncrement();
    }
    return null;
  }

  /** {@inheritDoc} */
  @Override
  public Entry getEntry(DN entryDN)
  {
    for (EntryCache<? extends EntryCacheCfg> entryCache : cacheOrder)
    {
      Entry entry = entryCache.getEntry(entryDN);
      if (entry != null)
      {
        return entry.duplicate(true);
      }
    }

    // Indicate global cache miss.
    if (cacheOrder.length != 0)
    {
      cacheMisses.getAndIncrement();
    }
    return null;
  }

  /** {@inheritDoc} */
  @Override
  public long getEntryID(DN entryDN)
  {
    for (EntryCache<?> entryCache : cacheOrder)
    {
      long entryID = entryCache.getEntryID(entryDN);
      if (entryID != -1)
      {
        return entryID;
      }
    }
    return -1;
  }

  /** {@inheritDoc} */
  @Override
  public DN getEntryDN(String backendID, long entryID)
  {
    for (EntryCache<?> entryCache : cacheOrder)
    {
      DN entryDN = entryCache.getEntryDN(backendID, entryID);
      if (entryDN != null)
      {
        return entryDN;
      }
    }
    return null;
  }

  /** {@inheritDoc} */
  @Override
  public void putEntry(Entry entry, String backendID, long entryID)
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      // The first cache in the order which can take this entry
      // gets it.
      if (entryCache.filtersAllowCaching(entry)) {
        entryCache.putEntry(entry.duplicate(false), backendID, entryID);
        break;
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public boolean putEntryIfAbsent(Entry entry, String backendID, long entryID)
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      // The first cache in the order which can take this entry
      // gets it.
      if (entryCache.filtersAllowCaching(entry)) {
        return entryCache.putEntryIfAbsent(entry.duplicate(false),
                backendID, entryID);
      }
    }

    return false;
  }

  /** {@inheritDoc} */
  @Override
  public void removeEntry(DN entryDN)
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      if (entryCache.containsEntry(entryDN)) {
        entryCache.removeEntry(entryDN);
        break;
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public void clear()
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      entryCache.clear();
    }
  }

  /** {@inheritDoc} */
  @Override
  public void clearBackend(String backendID)
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      entryCache.clearBackend(backendID);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void clearSubtree(DN baseDN)
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      entryCache.clearSubtree(baseDN);
    }
  }

  /** {@inheritDoc} */
  @Override
  public void handleLowMemory()
  {
    for (EntryCache<?> entryCache : cacheOrder) {
      entryCache.handleLowMemory();
    }
  }

  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(
      EntryCacheCfg configuration,
      List<LocalizableMessage> unacceptableReasons
      )
  {
    // No implementation required.
    return true;
  }

  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(EntryCacheCfg configuration)
  {
    // No implementation required.
    return new ConfigChangeResult();
  }

  /** {@inheritDoc} */
  @Override
  public List<Attribute> getMonitorData()
  {
    // The sum of cache hits of all active entry cache implementations.
    long entryCacheHits = 0;
    // Common for all active entry cache implementations.
    long entryCacheMisses = cacheMisses.longValue();
    // The sum of cache counts of all active entry cache implementations.
    long currentEntryCacheCount = 0;

    for (EntryCache<?> entryCache : cacheOrder) {
      // Get cache hits and counts from every active cache.
      entryCacheHits += entryCache.getCacheHits();
      currentEntryCacheCount += entryCache.getCacheCount();
    }

    try {
      return EntryCacheCommon.getGenericMonitorData(
        entryCacheHits,
        entryCacheMisses,
        null,
        null,
        currentEntryCacheCount,
        null
        );
    } catch (Exception e) {
      logger.traceException(e);
      return Collections.emptyList();
    }
  }

  /** {@inheritDoc} */
  @Override
  public Long getCacheCount()
  {
    long cacheCount = 0;
    for (EntryCache<?> entryCache : cacheOrder) {
      cacheCount += entryCache.getCacheCount();
    }
    return cacheCount;
  }

  /** {@inheritDoc} */
  @Override
  public String toVerboseString()
  {
    StringBuilder sb = new StringBuilder();
    for (EntryCache<?> entryCache : cacheOrder)
    {
      String s = entryCache.toVerboseString();
      if (s != null)
      {
        sb.append(s);
      }
    }
    String verboseString = sb.toString();
    return verboseString.length() > 0 ? verboseString : null;
  }



  /**
   * Retrieves the current cache order array.
   *
   * @return  The current cache order array.
   */
  public final static EntryCache<? extends EntryCacheCfg>[] getCacheOrder()
  {
    return DefaultEntryCache.cacheOrder;
  }



  /**
   * Sets the current cache order array.
   *
   * @param  cacheOrderMap  The current cache order array.
   */
  public final static void setCacheOrder(
    SortedMap<Integer,
    EntryCache<? extends EntryCacheCfg>> cacheOrderMap)
  {
    DefaultEntryCache.cacheOrder =
      cacheOrderMap.values().toArray(new EntryCache<?>[0]);
  }



  /**
   * Performs any processing that may be required whenever a backend
   * is initialized for use in the Directory Server.  This method will
   * be invoked after the backend has been initialized but before it
   * has been put into service.
   *
   * @param  backend  The backend that has been initialized and is
   *                  about to be put into service.
   */
  @Override
  public void performBackendPreInitializationProcessing(Backend<?> backend)
  {
    // Do nothing.
  }



  /**
   * Performs any processing that may be required whenever a backend
   * is finalized.  This method will be invoked after the backend has
   * been taken out of service but before it has been finalized.
   *
   * @param  backend  The backend that has been taken out of service
   *                  and is about to be finalized.
   */
  @Override
  public void performBackendPostFinalizationProcessing(Backend<?> backend)
  {
    // Do not clear any backends if the server is shutting down.
    if (!DirectoryServer.getInstance().isShuttingDown())
    {
      clearBackend(backend.getBackendID());
    }
  }

  @Override
  public void performBackendPostInitializationProcessing(Backend<?> backend) {
    // Nothing to do.
  }

  @Override
  public void performBackendPreFinalizationProcessing(Backend<?> backend) {
    // Nothing to do.
  }
}
