/*
 * 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 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2010-2015 ForgeRock AS.
 */
package org.opends.server.backends.jeb;

import static com.sleepycat.je.EnvironmentConfig.*;

import static org.opends.messages.BackendMessages.*;
import static org.opends.messages.ConfigMessages.*;

import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.admin.BooleanPropertyDefinition;
import org.opends.server.admin.DurationPropertyDefinition;
import org.opends.server.admin.PropertyDefinition;
import org.opends.server.admin.std.meta.JEBackendCfgDefn;
import org.opends.server.admin.std.server.BackendCfg;
import org.opends.server.admin.std.server.JEBackendCfg;
import org.opends.server.config.ConfigConstants;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.MemoryQuota;
import org.opends.server.types.DN;

import com.sleepycat.je.Durability;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.dbi.MemoryBudget;
import org.opends.server.util.Platform;

/** This class maps JE properties to configuration attributes. */
public class ConfigurableEnvironment
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * The name of the attribute which configures the database cache size as a
   * percentage of Java VM heap size.
   */
  public static final String ATTR_DATABASE_CACHE_PERCENT =
       ConfigConstants.NAME_PREFIX_CFG + "db-cache-percent";

  /**
   * The name of the attribute which configures the database cache size as an
   * approximate number of bytes.
   */
  public static final String ATTR_DATABASE_CACHE_SIZE =
       ConfigConstants.NAME_PREFIX_CFG + "db-cache-size";

  /**
   * The name of the attribute which configures whether data updated by a
   * database transaction is forced to disk.
   */
  public static final String ATTR_DATABASE_TXN_NO_SYNC =
       ConfigConstants.NAME_PREFIX_CFG + "db-txn-no-sync";

  /**
   * The name of the attribute which configures whether data updated by a
   * database transaction is written from the Java VM to the O/S.
   */
  public static final String ATTR_DATABASE_TXN_WRITE_NO_SYNC =
       ConfigConstants.NAME_PREFIX_CFG + "db-txn-write-no-sync";

  /**
   * The name of the attribute which configures whether the database background
   * cleaner thread runs.
   */
  public static final String ATTR_DATABASE_RUN_CLEANER =
       ConfigConstants.NAME_PREFIX_CFG + "db-run-cleaner";

  /**
   * The name of the attribute which configures the minimum percentage of log
   * space that must be used in log files.
   */
  public static final String ATTR_CLEANER_MIN_UTILIZATION =
       ConfigConstants.NAME_PREFIX_CFG + "db-cleaner-min-utilization";

  /**
   * The name of the attribute which configures the maximum size of each
   * individual JE log file, in bytes.
   */
  public static final String ATTR_DATABASE_LOG_FILE_MAX =
       ConfigConstants.NAME_PREFIX_CFG + "db-log-file-max";

  /** The name of the attribute which configures the database cache eviction algorithm. */
  public static final String ATTR_EVICTOR_LRU_ONLY =
       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-lru-only";

  /**
   * The name of the attribute which configures the number of nodes in one scan
   * of the database cache evictor.
   */
  public static final String ATTR_EVICTOR_NODES_PER_SCAN =
       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-nodes-per-scan";

  /**
   * The name of the attribute which configures the minimum number of threads
   * of the database cache evictor pool.
   */
  public static final String ATTR_EVICTOR_CORE_THREADS =
       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-core-threads";
  /**
   * The name of the attribute which configures the maximum number of threads
   * of the database cache evictor pool.
   */
  public static final String ATTR_EVICTOR_MAX_THREADS =
       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-max-threads";

  /**
   * The name of the attribute which configures the time excess threads
   * of the database cache evictor pool are kept alive.
   */
  public static final String ATTR_EVICTOR_KEEP_ALIVE =
       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-keep-alive";

  /**
   * The name of the attribute which configures whether the logging file
   * handler will be on or off.
   */
  public static final String ATTR_LOGGING_FILE_HANDLER_ON =
       ConfigConstants.NAME_PREFIX_CFG + "db-logging-file-handler-on";

  /** The name of the attribute which configures the trace logging message level. */
  public static final String ATTR_LOGGING_LEVEL =
       ConfigConstants.NAME_PREFIX_CFG + "db-logging-level";

  /**
   * The name of the attribute which configures how many bytes are written to
   * the log before the checkpointer runs.
   */
  public static final String ATTR_CHECKPOINTER_BYTES_INTERVAL =
       ConfigConstants.NAME_PREFIX_CFG + "db-checkpointer-bytes-interval";

  /**
   * The name of the attribute which configures the amount of time between
   * runs of the checkpointer.
   */
  public static final String ATTR_CHECKPOINTER_WAKEUP_INTERVAL =
       ConfigConstants.NAME_PREFIX_CFG +
       "db-checkpointer-wakeup-interval";

  /** The name of the attribute which configures the number of lock tables. */
  public static final String ATTR_NUM_LOCK_TABLES =
       ConfigConstants.NAME_PREFIX_CFG + "db-num-lock-tables";

  /**
   * The name of the attribute which configures the number threads
   * allocated by the cleaner for log file processing.
   */
  public static final String ATTR_NUM_CLEANER_THREADS =
       ConfigConstants.NAME_PREFIX_CFG + "db-num-cleaner-threads";

  /** The name of the attribute which configures the size of the file handle cache. */
  public static final String ATTR_LOG_FILECACHE_SIZE =
       ConfigConstants.NAME_PREFIX_CFG + "db-log-filecache-size";

  /** The name of the attribute which may specify any native JE properties. */
  public static final String ATTR_JE_PROPERTY =
       ConfigConstants.NAME_PREFIX_CFG + "je-property";

  /** A map of JE property names to the corresponding configuration attribute. */
  private static HashMap<String, String> attrMap = new HashMap<>();

  /**
   * A map of configuration attribute names to the corresponding configuration object getter method.
   */
  private static Map<String, Method> jebMethodMap = new HashMap<>();
  /** A map of configuration attribute names to the corresponding configuration PropertyDefinition. */
  private static Map<String, PropertyDefinition<?>> jebDefnMap = new HashMap<>();

  /** Pulled from resource/admin/ABBREVIATIONS.xsl. db is mose common. */
  private static final List<String> ABBREVIATIONS = Arrays.asList(new String[]
          {"aci", "ip", "ssl", "dn", "rdn", "jmx", "smtp", "http",
           "https", "ldap", "ldaps", "ldif", "jdbc", "tcp", "tls",
           "pkcs11", "sasl", "gssapi", "md5", "je", "dse", "fifo",
           "vlv", "uuid", "md5", "sha1", "sha256", "sha384", "sha512",
           "tls", "db"});

  /** E.g. db-cache-percent -> DBCachePercent */
  private static String propNametoCamlCase(String hyphenated)
  {
    String[] components = hyphenated.split("\\-");
    StringBuilder buffer = new StringBuilder();
    for (String component: components) {
      if (ABBREVIATIONS.contains(component)) {
        buffer.append(component.toUpperCase());
      } else {
        buffer.append(component.substring(0, 1).toUpperCase()).append(component.substring(1));
      }
    }
    return buffer.toString();
  }

  /**
   * Register a JE property and its corresponding configuration attribute.
   *
   * @param propertyName The name of the JE property to be registered.
   * @param attrName     The name of the configuration attribute associated
   *                     with the property.
   * @throws Exception   If there is an error in the attribute name.
   */
  private static void registerProp(String propertyName, String attrName)
       throws Exception
  {
    // Strip off NAME_PREFIX_CFG.
    String baseName = attrName.substring(7);
    String methodBaseName = propNametoCamlCase(baseName);

    registerJebProp(attrName, methodBaseName);
    attrMap.put(propertyName, attrName);
  }

  private static void registerJebProp(String attrName, String methodBaseName) throws Exception
  {
    Class<JEBackendCfg> configClass = JEBackendCfg.class;
    JEBackendCfgDefn defn = JEBackendCfgDefn.getInstance();
    Class<? extends JEBackendCfgDefn> defClass = defn.getClass();

    String propName = "get" + methodBaseName + "PropertyDefinition";
    PropertyDefinition<?> propDefn = (PropertyDefinition<?>) defClass.getMethod(propName).invoke(defn);

    String methodPrefix = propDefn instanceof BooleanPropertyDefinition ? "is" : "get";
    String methodName = methodPrefix + methodBaseName;

    jebDefnMap.put(attrName, propDefn);
    jebMethodMap.put(attrName, configClass.getMethod(methodName));
  }

  /**
   * Get the name of the configuration attribute associated with a JE property.
   * @param jeProperty The name of the JE property.
   * @return The name of the associated configuration attribute.
   */
  public static String getAttributeForProperty(String jeProperty)
  {
    return attrMap.get(jeProperty);
  }

  /**
   * Get the value of a JE property that is mapped to a configuration attribute.
   * @param cfg The configuration containing the property values.
   * @param attrName The configuration attribute type name.
   * @return The string value of the JE property.
   */
  private static String getPropertyValue(BackendCfg cfg, String attrName, ByteString backendId)
  {
    try
    {
      PropertyDefinition<?> propDefn = jebDefnMap.get(attrName);
      Method method = jebMethodMap.get(attrName);

      if (propDefn instanceof DurationPropertyDefinition)
      {
        Long value = (Long)method.invoke(cfg);

        // JE durations are in microseconds so we must convert.
        DurationPropertyDefinition durationPropDefn =
             (DurationPropertyDefinition)propDefn;
        value = 1000*durationPropDefn.getBaseUnit().toMilliSeconds(value);

        return String.valueOf(value);
      }
      else
      {
        Object value = method.invoke(cfg);

        if (value != null)
        {
          return String.valueOf(value);
        }

        if (attrName.equals(ATTR_NUM_CLEANER_THREADS))
        {
          // Automatically choose based on the number of processors. We will use
          // similar heuristics to those used to define the default number of
          // worker threads.
          value = Platform.computeNumberOfThreads(8, 1.0f);

          logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_CLEANER_THREADS,
              backendId, (Number) value);
        }
        else if (attrName.equals(ATTR_NUM_LOCK_TABLES))
        {
          // Automatically choose based on the number of processors. We'll assume that the user has also allowed
          // automatic configuration of cleaners and workers.
          BigInteger tmp = BigInteger.valueOf(Platform.computeNumberOfThreads(1, 2));
          value = tmp.nextProbablePrime();

          logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_LOCK_TABLES, backendId, (Number) value);
        }

        return String.valueOf(value);
      }
    }
    catch (Exception e)
    {
      logger.traceException(e);
      return "";
    }
  }

  static
  {
    // Register the parameters that have JE property names.
    try
    {
      registerProp("je.maxMemoryPercent", ATTR_DATABASE_CACHE_PERCENT);
      registerProp("je.maxMemory", ATTR_DATABASE_CACHE_SIZE);
      registerProp("je.cleaner.minUtilization", ATTR_CLEANER_MIN_UTILIZATION);
      registerProp("je.env.runCleaner", ATTR_DATABASE_RUN_CLEANER);
      registerProp("je.evictor.lruOnly", ATTR_EVICTOR_LRU_ONLY);
      registerProp("je.evictor.nodesPerScan", ATTR_EVICTOR_NODES_PER_SCAN);
      registerProp("je.evictor.coreThreads", ATTR_EVICTOR_CORE_THREADS);
      registerProp("je.evictor.maxThreads", ATTR_EVICTOR_MAX_THREADS);
      registerProp("je.evictor.keepAlive", ATTR_EVICTOR_KEEP_ALIVE);
      registerProp("je.log.fileMax", ATTR_DATABASE_LOG_FILE_MAX);
      registerProp("je.checkpointer.bytesInterval",
                   ATTR_CHECKPOINTER_BYTES_INTERVAL);
      registerProp("je.checkpointer.wakeupInterval",
                   ATTR_CHECKPOINTER_WAKEUP_INTERVAL);
      registerProp("je.lock.nLockTables", ATTR_NUM_LOCK_TABLES);
      registerProp("je.cleaner.threads", ATTR_NUM_CLEANER_THREADS);
      registerProp("je.log.fileCacheSize", ATTR_LOG_FILECACHE_SIZE);
    }
    catch (Exception e)
    {
      logger.traceException(e);
    }
  }

  /**
   * Create a JE environment configuration with default values.
   *
   * @return A JE environment config containing default values.
   */
  public static EnvironmentConfig defaultConfig()
  {
    EnvironmentConfig envConfig = new EnvironmentConfig();

    envConfig.setTransactional(true);
    envConfig.setAllowCreate(true);

    // "je.env.sharedLatches" is "true" by default since JE #12136 (3.3.62?)

    // This parameter was set to false while diagnosing a Berkeley DB JE bug.
    // Normally cleansed log files are deleted, but if this is set false
    // they are instead renamed from .jdb to .del.
    envConfig.setConfigParam(CLEANER_EXPUNGE, "true");

    // Under heavy write load the check point can fall behind causing
    // uncontrolled DB growth over time. This parameter makes the out of
    // the box configuration more robust at the cost of a slight
    // reduction in maximum write throughput. Experiments have shown
    // that response time predictability is not impacted negatively.
    envConfig.setConfigParam(CHECKPOINTER_HIGH_PRIORITY, "true");

    // If the JVM is reasonably large then we can safely default to
    // bigger read buffers. This will result in more scalable checkpointer
    // and cleaner performance.
    if (Runtime.getRuntime().maxMemory() > 256 * 1024 * 1024)
    {
      envConfig.setConfigParam(CLEANER_LOOK_AHEAD_CACHE_SIZE,
          String.valueOf(2 * 1024 * 1024));
      envConfig.setConfigParam(LOG_ITERATOR_READ_SIZE,
          String.valueOf(2 * 1024 * 1024));
      envConfig.setConfigParam(LOG_FAULT_READ_SIZE, String.valueOf(4 * 1024));
    }

    // Disable lock timeouts, meaning that no lock wait
    // timelimit is enforced and a deadlocked operation
    // will block indefinitely.
    envConfig.setLockTimeout(0, TimeUnit.MICROSECONDS);

    return envConfig;
  }

  /**
   * Parse a configuration associated with a JE environment and create an
   * environment config from it.
   *
   * @param cfg The configuration to be parsed.
   * @return An environment config instance corresponding to the config entry.
   * @throws ConfigException If there is an error in the provided configuration
   * entry.
   */
  public static EnvironmentConfig parseConfigEntry(JEBackendCfg cfg) throws ConfigException
  {
    validateDbCacheSize(cfg.getDBCacheSize());

    EnvironmentConfig envConfig = defaultConfig();
    setDurability(envConfig, cfg.isDBTxnNoSync(), cfg.isDBTxnWriteNoSync());
    setJEProperties(cfg, envConfig, cfg.dn().rdn().getAttributeValue(0));
    setDBLoggingLevel(envConfig, cfg.getDBLoggingLevel(), cfg.dn(), cfg.isDBLoggingFileHandlerOn());

    // See if there are any native JE properties specified in the config
    // and if so try to parse, evaluate and set them.
    return setJEProperties(envConfig, cfg.getJEProperty(), attrMap);
  }

  private static void validateDbCacheSize(long dbCacheSize) throws ConfigException
  {
    if (dbCacheSize != 0)
    {
      if (MemoryBudget.getRuntimeMaxMemory() < dbCacheSize)
      {
        throw new ConfigException(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
            dbCacheSize, MemoryBudget.getRuntimeMaxMemory()));
      }
      if (dbCacheSize < MemoryBudget.MIN_MAX_MEMORY_SIZE)
      {
        throw new ConfigException(ERR_CONFIG_JEB_CACHE_SIZE_TOO_SMALL.get(
            dbCacheSize, MemoryBudget.MIN_MAX_MEMORY_SIZE));
      }
      MemoryQuota memoryQuota = DirectoryServer.getInstance().getServerContext().getMemoryQuota();
      if (!memoryQuota.acquireMemory(dbCacheSize))
      {
        logger.warn(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
            dbCacheSize, memoryQuota.getMaxMemory()));
      }
    }
  }

  private static void setDurability(EnvironmentConfig envConfig, boolean dbTxnNoSync, boolean dbTxnWriteNoSync)
      throws ConfigException
  {
    if (dbTxnNoSync && dbTxnWriteNoSync)
    {
      throw new ConfigException(ERR_CONFIG_JEB_DURABILITY_CONFLICT.get());
    }
    if (dbTxnNoSync)
    {
      envConfig.setDurability(Durability.COMMIT_NO_SYNC);
    }
    else if (dbTxnWriteNoSync)
    {
      envConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC);
    }
  }

  private static void setJEProperties(BackendCfg cfg, EnvironmentConfig envConfig, ByteString backendId)
  {
    for (Map.Entry<String, String> mapEntry : attrMap.entrySet())
    {
      String jeProperty = mapEntry.getKey();
      String attrName = mapEntry.getValue();

      String value = getPropertyValue(cfg, attrName, backendId);
      envConfig.setConfigParam(jeProperty, value);
    }
  }

  private static void setDBLoggingLevel(EnvironmentConfig envConfig, String loggingLevel, DN dn,
      boolean loggingFileHandlerOn) throws ConfigException
  {
    Logger parent = Logger.getLogger("com.sleepycat.je");
    try
    {
      parent.setLevel(Level.parse(loggingLevel));
    }
    catch (Exception e)
    {
      throw new ConfigException(ERR_JEB_INVALID_LOGGING_LEVEL.get(loggingLevel, dn));
    }

    final Level level = loggingFileHandlerOn ? Level.ALL : Level.OFF;
    envConfig.setConfigParam(FILE_LOGGING_LEVEL, level.getName());
  }

  /**
   * Parse, validate and set native JE environment properties for
   * a given environment config.
   *
   * @param  envConfig The JE environment config for which to set
   *                   the properties.
   * @param  jeProperties The JE environment properties to parse,
   *                      validate and set.
   * @param  configAttrMap Component supported JE properties to
   *                       their configuration attributes map.
   * @return An environment config instance with given properties
   *         set.
   * @throws ConfigException If there is an error while parsing,
   *         validating and setting any of the properties provided.
   */
  public static EnvironmentConfig setJEProperties(EnvironmentConfig envConfig,
    SortedSet<String> jeProperties, HashMap<String, String> configAttrMap)
    throws ConfigException
  {
    if (jeProperties.isEmpty()) {
      // return default config.
      return envConfig;
    }

    // Set to catch duplicate properties.
    HashSet<String> uniqueJEProperties = new HashSet<>();

    // Iterate through the config values associated with a JE property.
    for (String jeEntry : jeProperties)
    {
      StringTokenizer st = new StringTokenizer(jeEntry, "=");
      if (st.countTokens() != 2)
      {
        throw new ConfigException(ERR_CONFIG_JE_PROPERTY_INVALID_FORM.get(jeEntry));
      }

      String jePropertyName = st.nextToken();
      String jePropertyValue = st.nextToken();
      // Check if it is a duplicate.
      if (uniqueJEProperties.contains(jePropertyName)) {
        throw new ConfigException(ERR_CONFIG_JE_DUPLICATE_PROPERTY.get(jePropertyName));
      }

      // Set JE property.
      try {
        envConfig.setConfigParam(jePropertyName, jePropertyValue);
        // If this property shadows an existing config attribute.
        if (configAttrMap.containsKey(jePropertyName)) {
          LocalizableMessage message = ERR_CONFIG_JE_PROPERTY_SHADOWS_CONFIG.get(
            jePropertyName, attrMap.get(jePropertyName));
          throw new ConfigException(message);
        }
        // Add this property to unique set.
        uniqueJEProperties.add(jePropertyName);
      } catch(IllegalArgumentException e) {
        logger.traceException(e);
        LocalizableMessage message = ERR_CONFIG_JE_PROPERTY_INVALID.get(jeEntry, e.getMessage());
        throw new ConfigException(message, e.getCause());
      }
    }

    return envConfig;
  }
}
