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

import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.opends.server.api.CompressedSchema;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;

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

/**
 * This class provides a compressed schema implementation whose definitions are
 * persisted in a tree.
 */
final class PersistentCompressedSchema extends CompressedSchema
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The name of the tree used to store compressed attribute description definitions. */
  private static final String DB_NAME_AD = "compressed_attributes";
  /** The name of the tree used to store compressed object class set definitions. */
  private static final String DB_NAME_OC = "compressed_object_classes";

  /** The compressed attribute description schema tree. */
  private static final TreeName adTreeName = new TreeName("compressed_schema", DB_NAME_AD);
  /** The compressed object class set schema tree. */
  private static final TreeName ocTreeName = new TreeName("compressed_schema", DB_NAME_OC);

  /** The storage in which the trees are held. */
  private final Storage storage;

  private final ByteStringBuilder storeAttributeWriterBuffer = new ByteStringBuilder();
  private final ASN1Writer storeAttributeWriter = ASN1.getWriter(storeAttributeWriterBuffer);
  private final ByteStringBuilder storeObjectClassesWriterBuffer = new ByteStringBuilder();
  private final ASN1Writer storeObjectClassesWriter = ASN1.getWriter(storeObjectClassesWriterBuffer);

  /**
   * Creates a new instance of this compressed schema manager.
   *
   * @param storage
   *          A reference to the storage in which the trees will be held.
   * @param txn a non null transaction
   * @param accessMode specifies how the storage has been opened (read only or read/write)
   *
   * @throws StorageRuntimeException
   *           If a problem occurs while loading the compressed schema
   *           definitions from the tree.
   * @throws InitializationException
   *           If an error occurs while loading and processing the compressed
   *           schema definitions.
   */
  PersistentCompressedSchema(final Storage storage, WriteableTransaction txn, AccessMode accessMode)
      throws StorageRuntimeException, InitializationException
  {
    this.storage = storage;
    load(txn, accessMode.isWriteable());
  }

  @Override
  protected void storeAttribute(final byte[] encodedAttribute,
      final String attributeName, final Collection<String> attributeOptions)
      throws DirectoryException
  {
    try
    {
      storeAttributeWriterBuffer.clear();
      storeAttributeWriter.writeStartSequence();
      storeAttributeWriter.writeOctetString(attributeName);
      for (final String option : attributeOptions)
      {
        storeAttributeWriter.writeOctetString(option);
      }
      storeAttributeWriter.writeEndSequence();
      store(adTreeName, encodedAttribute, storeAttributeWriterBuffer);
    }
    catch (final IOException e)
    {
      // TODO: Shouldn't happen but should log a message
    }
  }

  @Override
  protected void storeObjectClasses(final byte[] encodedObjectClasses,
      final Collection<String> objectClassNames) throws DirectoryException
  {
    try
    {
      storeObjectClassesWriterBuffer.clear();
      storeObjectClassesWriter.writeStartSequence();
      for (final String ocName : objectClassNames)
      {
        storeObjectClassesWriter.writeOctetString(ocName);
      }
      storeObjectClassesWriter.writeEndSequence();
      store(ocTreeName, encodedObjectClasses, storeObjectClassesWriterBuffer);
    }
    catch (final IOException e)
    {
      // TODO: Shouldn't happen but should log a message
    }
  }

  private void load(WriteableTransaction txn, boolean shouldCreate)
      throws StorageRuntimeException, InitializationException
  {
    txn.openTree(adTreeName, shouldCreate);
    txn.openTree(ocTreeName, shouldCreate);

    // Cursor through the object class database and load the object class set
    // definitions. At the same time, figure out the highest token value and
    // initialize the object class counter to one greater than that.
    final Cursor<ByteString, ByteString> ocCursor = txn.openCursor(ocTreeName);
    try
    {
      while (ocCursor.next())
      {
        final byte[] encodedObjectClasses = ocCursor.getKey().toByteArray();
        final ASN1Reader reader = ASN1.getReader(ocCursor.getValue());
        reader.readStartSequence();
        final List<String> objectClassNames = new LinkedList<>();
        while (reader.hasNextElement())
        {
          objectClassNames.add(reader.readOctetStringAsString());
        }
        reader.readEndSequence();
        loadObjectClasses(encodedObjectClasses, objectClassNames);
      }
    }
    catch (final IOException e)
    {
      logger.traceException(e);
      throw new InitializationException(ERR_COMPSCHEMA_CANNOT_DECODE_OC_TOKEN.get(e.getMessage()), e);
    }
    finally
    {
      ocCursor.close();
    }

    // Cursor through the attribute description database and load the attribute
    // set definitions.
    final Cursor<ByteString, ByteString> adCursor = txn.openCursor(adTreeName);
    try
    {
      while (adCursor.next())
      {
        final byte[] encodedAttribute = adCursor.getKey().toByteArray();
        final ASN1Reader reader = ASN1.getReader(adCursor.getValue());
        reader.readStartSequence();
        final String attributeName = reader.readOctetStringAsString();
        final List<String> attributeOptions = new LinkedList<>();
        while (reader.hasNextElement())
        {
          attributeOptions.add(reader.readOctetStringAsString());
        }
        reader.readEndSequence();
        loadAttribute(encodedAttribute, attributeName, attributeOptions);
      }
    }
    catch (final IOException e)
    {
      logger.traceException(e);
      throw new InitializationException(ERR_COMPSCHEMA_CANNOT_DECODE_AD_TOKEN.get(e.getMessage()), e);
    }
    finally
    {
      adCursor.close();
    }
  }

  private boolean store(final TreeName treeName, final byte[] key, final ByteStringBuilder value)
      throws DirectoryException
  {
    final ByteString keyEntry = ByteString.wrap(key);
    try
    {
      storage.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          txn.put(treeName, keyEntry, value);
        }
      });
      return true;
    }
    catch (final Exception e)
    {
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
          ERR_COMPSCHEMA_CANNOT_STORE_EX.get(e.getMessage()), e);
    }
  }
}
