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

import static org.opends.messages.CoreMessages.*;
import static com.forgerock.opendj.util.StaticUtils.toLowerCase;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.opends.server.core.DirectoryServer;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.types.Attributes;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteSequenceReader;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ObjectClass;

/**
 * This class provides a utility for interacting with compressed representations
 * of schema elements. The default implementation does not persist encoded
 * attributes and object classes.
 */
@org.opends.server.types.PublicAPI(
    stability = org.opends.server.types.StabilityLevel.UNCOMMITTED,
    mayInstantiate = false,
    mayExtend = true,
    mayInvoke = false)
public class CompressedSchema
{
  /** Maps attribute description to ID. */
  private final List<Entry<AttributeType, Set<String>>> adDecodeMap = new CopyOnWriteArrayList<>();
  /** Maps ID to attribute description. */
  private final Map<Entry<AttributeType, Set<String>>, Integer> adEncodeMap = new ConcurrentHashMap<>();
  /** The map between encoded representations and object class sets. */
  private final List<Map<ObjectClass, String>> ocDecodeMap = new CopyOnWriteArrayList<>();
  /** The map between object class sets and encoded representations. */
  private final Map<Map<ObjectClass, String>, Integer> ocEncodeMap = new ConcurrentHashMap<>();

  /**
   * Decodes the contents of the provided array as an attribute at the current
   * position.
   *
   * @param reader
   *          The byte string reader containing the encoded entry.
   * @return The decoded attribute.
   * @throws DirectoryException
   *           If the attribute could not be decoded properly for some reason.
   */
  public final Attribute decodeAttribute(final ByteSequenceReader reader)
      throws DirectoryException
  {
    // First decode the encoded attribute description id.
    final int length = reader.readBERLength();
    final byte[] idBytes = new byte[length];
    reader.readBytes(idBytes);
    final int id = decodeId(idBytes);

    // Look up the attribute description.
    Entry<AttributeType, Set<String>> ad = adDecodeMap.get(id);
    if (ad == null)
    {
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
          ERR_COMPRESSEDSCHEMA_UNRECOGNIZED_AD_TOKEN.get(id));
    }

    // Before returning the attribute, make sure that the attribute type is not
    // stale.
    AttributeType attrType = ad.getKey();
    Set<String> options = ad.getValue();
    if (attrType.isDirty())
    {
      ad = loadAttribute(idBytes, attrType.getNameOrOID(), options);
      attrType = ad.getKey();
      options = ad.getValue();
    }

    // Determine the number of values for the attribute.
    final int numValues = reader.readBERLength();

    // For the common case of a single value with no options, generate
    // less garbage.
    if (numValues == 1 && options.isEmpty())
    {
      final int valueLength = reader.readBERLength();
      final ByteSequence valueBytes = reader.readByteSequence(valueLength);
      return Attributes.create(attrType, valueBytes.toByteString());
    }
    else
    {
      // Read the appropriate number of values.
      final AttributeBuilder builder = new AttributeBuilder(attrType);
      builder.setOptions(options);
      for (int i = 0; i < numValues; i++)
      {
        final int valueLength = reader.readBERLength();
        final ByteSequence valueBytes = reader.readByteSequence(valueLength);
        builder.add(valueBytes.toByteString());
      }
      return builder.toAttribute();
    }
  }



  /**
   * Decodes an object class set from the provided byte string.
   *
   * @param reader
   *          The byte string reader containing the object class set identifier.
   * @return The decoded object class set.
   * @throws DirectoryException
   *           If the provided byte string reader cannot be decoded as an object
   *           class set.
   */
  public final Map<ObjectClass, String> decodeObjectClasses(
      final ByteSequenceReader reader) throws DirectoryException
  {
    // First decode the encoded object class id.
    final int length = reader.readBERLength();
    final byte[] idBytes = new byte[length];
    reader.readBytes(idBytes);
    final int id = decodeId(idBytes);

    // Look up the object classes.
    final Map<ObjectClass, String> ocMap = ocDecodeMap.get(id);
    if (ocMap != null)
    {
      // Before returning the object classes, make sure that none of them are
      // stale.
      for (final ObjectClass oc : ocMap.keySet())
      {
        if (oc.isDirty())
        {
          // Found at least one object class which is dirty so refresh them.
          return loadObjectClasses(idBytes, ocMap.values());
        }
      }
      return ocMap;
    }
    else
    {
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
          ERR_COMPRESSEDSCHEMA_UNKNOWN_OC_TOKEN.get(id));
    }
  }



  /**
   * Encodes the information in the provided attribute to a byte array.
   *
   * @param builder
   *          The buffer to encode the attribute to.
   * @param attribute
   *          The attribute to be encoded.
   * @throws DirectoryException
   *           If a problem occurs while attempting to determine the appropriate
   *           identifier.
   */
  public final void encodeAttribute(final ByteStringBuilder builder,
      final Attribute attribute) throws DirectoryException
  {
    // Re-use or allocate a new ID.
    final AttributeType type = attribute.getAttributeType();
    final Set<String> options = attribute.getOptions();
    final Entry<AttributeType, Set<String>> ad = new SimpleImmutableEntry<>(type, options);

    // Use double checked locking to avoid lazy registration races.
    Integer id = adEncodeMap.get(ad);
    if (id == null)
    {
      synchronized (adEncodeMap)
      {
        id = adEncodeMap.get(ad);
        if (id == null)
        {
          id = adDecodeMap.size();
          adDecodeMap.add(ad);
          adEncodeMap.put(ad, id);
          storeAttribute(encodeId(id), type.getNameOrOID(), options);
        }
      }
    }

    // Encode the attribute.
    final byte[] idBytes = encodeId(id);
    builder.appendBERLength(idBytes.length);
    builder.appendBytes(idBytes);
    builder.appendBERLength(attribute.size());
    for (final ByteString v : attribute)
    {
      builder.appendBERLength(v.length());
      builder.appendBytes(v);
    }
  }



  /**
   * Encodes the provided set of object classes to a byte array. If the same set
   * had been previously encoded, then the cached value will be used. Otherwise,
   * a new value will be created.
   *
   * @param builder
   *          The buffer to encode the object classes to.
   * @param objectClasses
   *          The set of object classes for which to retrieve the corresponding
   *          byte array token.
   * @throws DirectoryException
   *           If a problem occurs while attempting to determine the appropriate
   *           identifier.
   */
  public final void encodeObjectClasses(final ByteStringBuilder builder,
      final Map<ObjectClass, String> objectClasses) throws DirectoryException
  {
    // Re-use or allocate a new ID.
    // Use double checked locking to avoid lazy registration races.
    Integer id = ocEncodeMap.get(objectClasses);
    if (id == null)
    {
      synchronized (ocEncodeMap)
      {
        id = ocEncodeMap.get(objectClasses);
        if (id == null)
        {
          id = ocDecodeMap.size();
          ocDecodeMap.add(objectClasses);
          ocEncodeMap.put(objectClasses, id);
          storeObjectClasses(encodeId(id), objectClasses.values());
        }
      }
    }

    // Encode the object classes.
    final byte[] idBytes = encodeId(id);
    builder.appendBERLength(idBytes.length);
    builder.appendBytes(idBytes);
  }



  /**
   * Returns a view of the encoded attributes in this compressed schema which
   * can be used for saving the entire content to disk. The iterator returned by
   * this method is not thread safe.
   *
   * @return A view of the encoded attributes in this compressed schema.
   */
  protected final Iterable<Entry<byte[],
                                 Entry<String,
                                       Collection<String>>>> getAllAttributes()
  {
    return new Iterable<Entry<byte[], Entry<String, Collection<String>>>>()
    {

      @Override
      public Iterator<Entry<byte[],
                            Entry<String, Collection<String>>>> iterator()
      {
        return new Iterator<Entry<byte[], Entry<String, Collection<String>>>>()
        {
          private int id = 0;



          @Override
          public boolean hasNext()
          {
            return id < adDecodeMap.size();
          }



          @Override
          public Entry<byte[], Entry<String, Collection<String>>> next()
          {
            final byte[] encodedAttribute = encodeId(id);
            final Entry<AttributeType, Set<String>> ad = adDecodeMap.get(id++);
            return new SimpleImmutableEntry<byte[],
                                            Entry<String, Collection<String>>>(
                encodedAttribute,
                new SimpleImmutableEntry<String, Collection<String>>(ad
                    .getKey().getNameOrOID(), ad.getValue()));
          }



          @Override
          public void remove()
          {
            throw new UnsupportedOperationException();
          }
        };
      }
    };
  }



  /**
   * Returns a view of the encoded object classes in this compressed schema
   * which can be used for saving the entire content to disk. The iterator
   * returned by this method is not thread safe.
   *
   * @return A view of the encoded object classes in this compressed schema.
   */
  protected final Iterable<Entry<byte[],
                                 Collection<String>>> getAllObjectClasses()
  {
    return new Iterable<Entry<byte[], Collection<String>>>()
    {

      @Override
      public Iterator<Entry<byte[], Collection<String>>> iterator()
      {
        return new Iterator<Map.Entry<byte[], Collection<String>>>()
        {
          private int id = 0;

          @Override
          public boolean hasNext()
          {
            return id < ocDecodeMap.size();
          }

          @Override
          public Entry<byte[], Collection<String>> next()
          {
            final byte[] encodedObjectClasses = encodeId(id);
            final Map<ObjectClass, String> ocMap = ocDecodeMap.get(id++);
            return new SimpleImmutableEntry<>(encodedObjectClasses, ocMap.values());
          }

          @Override
          public void remove()
          {
            throw new UnsupportedOperationException();
          }
        };
      }
    };
  }

  /**
   * Loads an encoded attribute into this compressed schema. This method may
   * called by implementations during initialization when loading content from
   * disk.
   *
   * @param encodedAttribute
   *          The encoded attribute description.
   * @param attributeName
   *          The user provided attribute type name.
   * @param attributeOptions
   *          The non-null but possibly empty set of attribute options.
   * @return The attribute type description.
   */
  protected final Entry<AttributeType, Set<String>> loadAttribute(
      final byte[] encodedAttribute, final String attributeName,
      final Collection<String> attributeOptions)
  {
    final AttributeType type = DirectoryServer.getAttributeTypeOrDefault(toLowerCase(attributeName));
    final Set<String> options = getOptions(attributeOptions);
    final Entry<AttributeType, Set<String>> ad = new SimpleImmutableEntry<>(type, options);
    final int id = decodeId(encodedAttribute);
    synchronized (adEncodeMap)
    {
      adEncodeMap.put(ad, id);
      if (id < adDecodeMap.size())
      {
        adDecodeMap.set(id, ad);
      }
      else
      {
        // Grow the decode array.
        while (id > adDecodeMap.size())
        {
          adDecodeMap.add(null);
        }
        adDecodeMap.add(ad);
      }
    }
    return ad;
  }

  private Set<String> getOptions(final Collection<String> attributeOptions)
  {
    switch (attributeOptions.size())
    {
    case 0:
      return Collections.emptySet();
    case 1:
      return Collections.singleton(attributeOptions.iterator().next());
    default:
      return new LinkedHashSet<>(attributeOptions);
    }
  }

  /**
   * Loads an encoded object class into this compressed schema. This method may
   * called by implementations during initialization when loading content from
   * disk.
   *
   * @param encodedObjectClasses
   *          The encoded object classes.
   * @param objectClassNames
   *          The user provided set of object class names.
   * @return The object class set.
   */
  protected final Map<ObjectClass, String> loadObjectClasses(
      final byte[] encodedObjectClasses,
      final Collection<String> objectClassNames)
  {
    final LinkedHashMap<ObjectClass, String> ocMap = new LinkedHashMap<>(objectClassNames.size());
    for (final String name : objectClassNames)
    {
      final String lowerName = toLowerCase(name);
      final ObjectClass oc = DirectoryServer.getObjectClass(lowerName, true);
      ocMap.put(oc, name);
    }
    final int id = decodeId(encodedObjectClasses);
    synchronized (ocEncodeMap)
    {
      ocEncodeMap.put(ocMap, id);
      if (id < ocDecodeMap.size())
      {
        ocDecodeMap.set(id, ocMap);
      }
      else
      {
        // Grow the decode array.
        while (id > ocDecodeMap.size())
        {
          ocDecodeMap.add(null);
        }
        ocDecodeMap.add(ocMap);
      }
    }
    return ocMap;
  }



  /**
   * Persists the provided encoded attribute. The default implementation is to
   * do nothing. Calls to this method are synchronized, so implementations can
   * assume that this method is not being called by other threads. Note that
   * this method is not thread-safe with respect to
   * {@link #storeObjectClasses(byte[], Collection)}.
   *
   * @param encodedAttribute
   *          The encoded attribute description.
   * @param attributeName
   *          The user provided attribute type name.
   * @param attributeOptions
   *          The non-null but possibly empty set of attribute options.
   * @throws DirectoryException
   *           If an error occurred while persisting the encoded attribute.
   */
  protected void storeAttribute(final byte[] encodedAttribute,
      final String attributeName, final Collection<String> attributeOptions)
      throws DirectoryException
  {
    // Do nothing by default.
  }



  /**
   * Persists the provided encoded object classes. The default implementation is
   * to do nothing. Calls to this method are synchronized, so implementations
   * can assume that this method is not being called by other threads. Note that
   * this method is not thread-safe with respect to
   * {@link #storeAttribute(byte[], String, Collection)}.
   *
   * @param encodedObjectClasses
   *          The encoded object classes.
   * @param objectClassNames
   *          The user provided set of object class names.
   * @throws DirectoryException
   *           If an error occurred while persisting the encoded object classes.
   */
  protected void storeObjectClasses(final byte[] encodedObjectClasses,
      final Collection<String> objectClassNames) throws DirectoryException
  {
    // Do nothing by default.
  }



  /**
   * Decodes the provided encoded schema element ID.
   *
   * @param idBytes
   *          The encoded schema element ID.
   * @return The schema element ID.
   */
  private int decodeId(final byte[] idBytes)
  {
    int id = 0;
    for (final byte b : idBytes)
    {
      id <<= 8;
      id |= b & 0xFF;
    }
    return id - 1; // Subtract 1 to compensate for old behavior.
  }



  /**
   * Encodes the provided schema element ID.
   *
   * @param id
   *          The schema element ID.
   * @return The encoded schema element ID.
   */
  private byte[] encodeId(final int id)
  {
    final int value = id + 1; // Add 1 to compensate for old behavior.
    final byte[] idBytes;
    if (value <= 0xFF)
    {
      idBytes = new byte[1];
      idBytes[0] = (byte) (value & 0xFF);
    }
    else if (value <= 0xFFFF)
    {
      idBytes = new byte[2];
      idBytes[0] = (byte) ((value >> 8) & 0xFF);
      idBytes[1] = (byte) (value & 0xFF);
    }
    else if (value <= 0xFFFFFF)
    {
      idBytes = new byte[3];
      idBytes[0] = (byte) ((value >> 16) & 0xFF);
      idBytes[1] = (byte) ((value >> 8) & 0xFF);
      idBytes[2] = (byte) (value & 0xFF);
    }
    else
    {
      idBytes = new byte[4];
      idBytes[0] = (byte) ((value >> 24) & 0xFF);
      idBytes[1] = (byte) ((value >> 16) & 0xFF);
      idBytes[2] = (byte) ((value >> 8) & 0xFF);
      idBytes[3] = (byte) (value & 0xFF);
    }
    return idBytes;
  }
}
