/*
 * 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 2014-2015 ForgeRock AS
 */
package org.opends.server.core;

import static org.forgerock.util.Utils.*;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.util.StaticUtils.*;

import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.ClassPropertyDefinition;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.server.config.meta.SchemaProviderCfgDefn;
import org.forgerock.opendj.server.config.server.RootCfg;
import org.forgerock.opendj.server.config.server.SchemaProviderCfg;
import org.forgerock.util.Utils;
import org.opends.server.schema.SchemaProvider;
import org.opends.server.schema.SchemaUpdater;
import org.opends.server.types.InitializationException;

/**
 * Responsible for loading the server schema.
 * <p>
 * The schema is loaded in three steps :
 * <ul>
 *   <li>Start from the core schema.</li>
 *   <li>Load schema elements from the schema providers defined in configuration.</li>
 *   <li>Load all schema files located in the schema directory.</li>
 * </ul>
 */
public final class SchemaHandler
{
  private static final String CORE_SCHEMA_PROVIDER_NAME = "Core Schema";

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private ServerContext serverContext;

  private long oldestModificationTime = -1L;

  private long youngestModificationTime = -1L;

  /**
   * Creates a new instance.
   */
  public SchemaHandler()
  {
    // no implementation.
  }

  /**
   * Initialize this schema handler.
   *
   * @param serverContext
   *          The server context.
   * @throws ConfigException
   *           If a configuration problem arises in the process of performing
   *           the initialization.
   * @throws InitializationException
   *           If a problem that is not configuration-related occurs during
   *           initialization.
   */
  public void initialize(final ServerContext serverContext) throws InitializationException, ConfigException
  {
    this.serverContext = serverContext;

    final RootCfg rootConfiguration = serverContext.getServerManagementContext().getRootConfiguration();
    final SchemaUpdater schemaUpdater = serverContext.getSchemaUpdater();

    // Start from the core schema (TODO: or start with empty schema and add core schema in core schema provider ?)
    final SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema());

    // Take providers into account.
    loadSchemaFromProviders(rootConfiguration, schemaBuilder, schemaUpdater);

    // Take schema files into account (TODO : or load files using provider mechanism ?)
    completeSchemaFromFiles(schemaBuilder);

    schemaUpdater.updateSchema(schemaBuilder.toSchema());
  }

  /**
   * Load the schema from provided root configuration.
   *
   * @param rootConfiguration
   *          The root to retrieve schema provider configurations.
   * @param schemaBuilder
   *          The schema builder that providers should update.
   * @param schemaUpdater
   *          The updater that providers should use when applying a
   *          configuration change.
   */
  private void loadSchemaFromProviders(final RootCfg rootConfiguration, final SchemaBuilder schemaBuilder,
      final SchemaUpdater schemaUpdater)  throws ConfigException, InitializationException {
    for (final String name : rootConfiguration.listSchemaProviders())
    {
      final SchemaProviderCfg config = rootConfiguration.getSchemaProvider(name);
      if (config.isEnabled())
      {
        loadSchemaProvider(config.getJavaClass(), config, schemaBuilder, schemaUpdater, true);
      }
      else if (name.equals(CORE_SCHEMA_PROVIDER_NAME)) {
        // TODO : use correct message ERR_CORE_SCHEMA_NOT_ENABLED
        LocalizableMessage message = LocalizableMessage.raw("Core Schema can't be disabled");
        throw new ConfigException(message);
      }
    }
  }

  /**
   * Load the schema provider from the provided class name.
   * <p>
   * If {@code} initialize} is {@code true}, then the provider is initialized,
   * and the provided schema builder is updated with schema elements fropm the
   * provider.
   */
  private <T extends SchemaProviderCfg> SchemaProvider<T> loadSchemaProvider(final String className,
      final T config, final SchemaBuilder schemaBuilder, final SchemaUpdater schemaUpdater, final boolean initialize)
      throws InitializationException
  {
    try
    {
      final ClassPropertyDefinition propertyDef = SchemaProviderCfgDefn.getInstance().getJavaClassPropertyDefinition();
      final Class<? extends SchemaProvider> providerClass = propertyDef.loadClass(className, SchemaProvider.class);
      final SchemaProvider<T> provider = providerClass.newInstance();

      if (initialize) {
        provider.initialize(config, schemaBuilder, schemaUpdater);
      }
      else {
        final List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
        final boolean isAcceptable = provider.isConfigurationAcceptable(config, unacceptableReasons);
        if (!isAcceptable)
        {
          final String reasons = Utils.joinAsString(".  ", unacceptableReasons);
          // TODO : fix message, eg CONFIG SCHEMA PROVIDER CONFIG NOT ACCEPTABLE
          throw new InitializationException(ERR_CONFIG_ALERTHANDLER_CONFIG_NOT_ACCEPTABLE.get(config.dn(), reasons));
        }
      }
      return provider;
    }
    catch (Exception e)
      {
        // TODO : fix message
        throw new InitializationException(ERR_CONFIG_SCHEMA_SYNTAX_CANNOT_INITIALIZE.
            get(className, config.dn(), stackTraceToSingleLineString(e)), e);
      }
  }

  /**
   * Retrieves the path to the directory containing the server schema files.
   *
   * @return The path to the directory containing the server schema files.
   */
  private File getSchemaDirectoryPath() throws InitializationException
  {
    final File dir = serverContext.getEnvironment().getSchemaDirectory();
    if (dir == null)
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(null));
    }
    if (!dir.exists())
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(dir.getPath()));
    }
    if (!dir.isDirectory())
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(dir.getPath()));
    }
    return dir;
  }

  /** Returns the LDIF reader on provided LDIF file. The caller must ensure the reader is closed. */
  private EntryReader getLDIFReader(final File ldifFile, final Schema schema)
      throws InitializationException
  {
    try
    {
      final LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile));
      reader.setSchema(schema);
      return reader;
    }
    catch (Exception e)
    {
      // TODO : fix message
      throw new InitializationException(ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(ldifFile.getAbsolutePath(), e), e);
    }
  }

  /**
   * Complete the schema with schema files.
   *
   * @param schemaBuilder
   *          The schema builder to update with the content of the schema files.
   * @throws ConfigException
   *           If a configuration problem causes the schema element
   *           initialization to fail.
   * @throws InitializationException
   *           If a problem occurs while initializing the schema elements that
   *           is not related to the server configuration.
   */
  private void completeSchemaFromFiles(final SchemaBuilder schemaBuilder)
      throws ConfigException, InitializationException
  {
    final File schemaDirectory = getSchemaDirectoryPath();
    for (String schemaFile : getSchemaFileNames(schemaDirectory))
    {
      loadSchemaFile(schemaFile, schemaBuilder, Schema.getDefaultSchema());
    }
  }

  /** Returns the list of names of schema files contained in the provided directory. */
  private List<String> getSchemaFileNames(final File schemaDirectory) throws InitializationException {
    try
    {
      final File[] schemaFiles = schemaDirectory.listFiles(new SchemaFileFilter());
      final List<String> schemaFileNames = new ArrayList<>(schemaFiles.length);

      for (final File f : schemaFiles)
      {
        if (f.isFile())
        {
          schemaFileNames.add(f.getName());
        }

        final long modificationTime = f.lastModified();
        if (oldestModificationTime <= 0L
            || modificationTime < oldestModificationTime)
        {
          oldestModificationTime = modificationTime;
        }

        if (youngestModificationTime <= 0
            || modificationTime > youngestModificationTime)
        {
          youngestModificationTime = modificationTime;
        }
      }
      // If the oldest and youngest modification timestamps didn't get set
      // then set them to the current time.
      if (oldestModificationTime <= 0)
      {
        oldestModificationTime = System.currentTimeMillis();
      }

      if (youngestModificationTime <= 0)
      {
        youngestModificationTime = oldestModificationTime;
      }
      Collections.sort(schemaFileNames);
      return schemaFileNames;
    }
    catch (Exception e)
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES
          .get(schemaDirectory, getExceptionMessage(e)), e);
    }
  }

  /** Returns the schema entry from the provided reader. */
  private Entry readSchemaEntry(final EntryReader reader, final File schemaFile) throws InitializationException {
    try
    {
      Entry entry = null;
      if (reader.hasNext())
      {
        entry = reader.readEntry();
        if (reader.hasNext())
        {
          // TODO : fix message
          logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, "");
        }
        return entry;
      }
      else
      {
        // TODO : fix message - should be SCHEMA NO LDIF ENTRY
        throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(
            schemaFile, "", ""));
      }
    }
    catch (IOException e)
    {
      // TODO : fix message
      throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(
              schemaFile, "", getExceptionMessage(e)), e);
    }
    finally
    {
      closeSilently(reader);
    }
  }

  /**
   * Add the schema from the provided schema file to the provided schema
   * builder.
   *
   * @param schemaFileName
   *          The name of the schema file to be loaded
   * @param schemaBuilder
   *          The schema builder in which the contents of the schema file are to
   *          be loaded.
   * @param readSchema
   *          The schema used to read the file.
   * @throws InitializationException
   *           If a problem occurs while initializing the schema elements.
   */
  private void loadSchemaFile(final String schemaFileName, final SchemaBuilder schemaBuilder, final Schema readSchema)
         throws InitializationException
  {
    EntryReader reader = null;
    try
    {
      File schemaFile = new File(getSchemaDirectoryPath(), schemaFileName);
      reader = getLDIFReader(schemaFile, readSchema);
      final Entry entry = readSchemaEntry(reader, schemaFile);
      // TODO : there is no more file information attached to schema elements - we should add support for this
      // in order to be able to redirect schema elements in the correct file when doing backups
      schemaBuilder.addSchema(entry, true);
    }
    finally {
      Utils.closeSilently(reader);
    }
  }

  /** A file filter implementation that accepts only LDIF files. */
  private static class SchemaFileFilter implements FilenameFilter
  {
    private static final String LDIF_SUFFIX = ".ldif";

    @Override
    public boolean accept(File directory, String filename)
    {
      return filename.endsWith(LDIF_SUFFIX);
    }
  }
}
