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

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.schema.ObjectClassType;

import static org.forgerock.util.Reject.*;
import static org.opends.server.util.ServerConstants.*;

/**
 * This class defines a data structure for storing and interacting
 * with an objectclass, which contains a collection of attributes that
 * must and/or may be present in an entry with that objectclass.
 * <p>
 * Any methods which accesses the set of names associated with this
 * object class, will retrieve the primary name as the first name,
 * regardless of whether or not it was contained in the original set
 * of <code>names</code> passed to the constructor.
 * <p>
 * Where ordered sets of names, attribute types, or extra properties
 * are provided, the ordering will be preserved when the associated
 * fields are accessed via their getters or via the
 * {@link #toString()} methods.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
     mayInstantiate=false,
     mayExtend=false,
     mayInvoke=true)
public final class ObjectClass
       extends CommonSchemaElements
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The set of optional attribute types for this objectclass. */
  private final Set<AttributeType> optionalAttributes;

  /**
   * The set of optional attribute types for this objectclass and its
   * superclasses.
   */
  private final Set<AttributeType> optionalAttributesChain;

  /** The set of required attribute types for this objectclass. */
  private final Set<AttributeType> requiredAttributes;

  /**
   * The set of required attribute types for this objectclass and its
   * superclasses.
   */
  private final Set<AttributeType> requiredAttributesChain;

  /**
   * The set of required and optional attributes for this objectclass
   * and its superclasses.
   */
  private final Set<AttributeType> requiredAndOptionalChain;

  /** The reference to one or more superior objectclasses. */
  private final Set<ObjectClass> superiorClasses;

  /** The objectclass type for this objectclass. */
  private final ObjectClassType objectClassType;

  /**
   * Indicates whether or not this object class is allowed to
   * contain any attribute.
   */
  private final boolean isExtensibleObject;

  /** The definition string used to create this objectclass. */
  private final String definition;

  /** True once this object class has been removed from the schema. */
  private volatile boolean isDirty;



  /**
   * Creates a new objectclass definition with the provided
   * information.
   * <p>
   * If no <code>primaryName</code> is specified, but a set of
   * <code>names</code> is specified, then the first name retrieved
   * from the set of <code>names</code> will be used as the primary
   * name.
   *
   * @param definition
   *          The definition string used to create this objectclass.
   *          It must not be {@code null}.
   * @param primaryName
   *          The primary name for this objectclass, or
   *          {@code null} if there is no primary name.
   * @param names
   *          The set of names that may be used to reference this
   *          objectclass.
   * @param oid
   *          The OID for this objectclass.  It must not be
   *          {@code null}.
   * @param description
   *          The description for this objectclass, or {@code null} if
   *          there is no description.
   * @param superiorClasses
   *          The superior classes for this objectclass, or
   *          {@code null} if there is no superior object class.
   * @param requiredAttributes
   *          The set of required attribute types for this
   *          objectclass.
   * @param optionalAttributes
   *          The set of optional attribute types for this
   *          objectclass.
   * @param objectClassType
   *          The objectclass type for this objectclass, or
   *          {@code null} to default to structural.
   * @param isObsolete
   *          Indicates whether this objectclass is declared
   *          "obsolete".
   * @param extraProperties
   *          A set of extra properties for this objectclass.
   */
  public ObjectClass(String definition, String primaryName,
                     Collection<String> names, String oid,
                     String description,
                     Set<ObjectClass> superiorClasses,
                     Set<AttributeType> requiredAttributes,
                     Set<AttributeType> optionalAttributes,
                     ObjectClassType objectClassType,
                     boolean isObsolete,
                     Map<String, List<String>> extraProperties)
  {
    super(primaryName, names, oid, description, isObsolete,
        extraProperties);


    ifNull(definition, oid);

    // Construct unmodifiable views of the superior classes.
    if (superiorClasses != null) {
      this.superiorClasses =  Collections
          .unmodifiableSet(new LinkedHashSet<ObjectClass>(
              superiorClasses));
    } else {
      this.superiorClasses = Collections.emptySet();
    }

    int schemaFilePos = definition.indexOf(SCHEMA_PROPERTY_FILENAME);
    if (schemaFilePos > 0)
    {
      String defStr;
      try
      {
        int firstQuotePos = definition.indexOf('\'', schemaFilePos);
        int secondQuotePos = definition.indexOf('\'',
                                                firstQuotePos+1);

        defStr = definition.substring(0, schemaFilePos).trim() + " " +
                 definition.substring(secondQuotePos+1).trim();
      }
      catch (Exception e)
      {
        logger.traceException(e);

        defStr = definition;
      }

      this.definition = defStr;
    }
    else
    {
      this.definition = definition;
    }

    // Set flag indicating whether or not this object class allows any attributes
    this.isExtensibleObject = hasName(OC_EXTENSIBLE_OBJECT_LC)
        || oid.equals(OID_EXTENSIBLE_OBJECT);

    // Construct unmodifiable views of the required attributes.
    if (requiredAttributes != null) {
      this.requiredAttributes = Collections
          .unmodifiableSet(new LinkedHashSet<AttributeType>(
              requiredAttributes));
    } else {
      this.requiredAttributes = Collections.emptySet();
    }

    if (this.superiorClasses.isEmpty()) {
      this.requiredAttributesChain = this.requiredAttributes;
    } else {
      Set<AttributeType> tmp = new HashSet<>(this.requiredAttributes);
      for(ObjectClass oc: this.superiorClasses)
      {
        tmp.addAll(oc.getRequiredAttributeChain());
      }
      this.requiredAttributesChain = Collections.unmodifiableSet(tmp);
    }

    // Construct unmodifiable views of the optional attributes.
    if (optionalAttributes != null) {
      this.optionalAttributes = Collections
          .unmodifiableSet(new LinkedHashSet<AttributeType>(
              optionalAttributes));
    } else {
      this.optionalAttributes = Collections.emptySet();
    }

    if (this.superiorClasses.isEmpty()) {
      this.optionalAttributesChain = this.optionalAttributes;
    } else {
      Set<AttributeType> tmp = new HashSet<>(this.optionalAttributes);
      for(ObjectClass oc : this.superiorClasses)
      {
        tmp.addAll(oc.getOptionalAttributeChain());
      }
      this.optionalAttributesChain = Collections.unmodifiableSet(tmp);
    }

    // Construct unmodifiable views of the required and optional attribute chains.
    int size = requiredAttributesChain.size() + optionalAttributesChain.size();
    HashSet<AttributeType> reqAndOptSet = new HashSet<>(size);
    reqAndOptSet.addAll(requiredAttributesChain);
    reqAndOptSet.addAll(optionalAttributesChain);
    requiredAndOptionalChain =
         Collections.<AttributeType>unmodifiableSet(reqAndOptSet);

    // Object class type defaults to structural.
    if (objectClassType != null) {
      this.objectClassType = objectClassType;
    } else {
      this.objectClassType = ObjectClassType.STRUCTURAL;
    }
  }



  /**
   * Retrieves an unmodifiable view of the set of direct superior
   * classes for this objectclass.
   *
   * @return An unmodifiable view of the set of  direct superior
   *                classes for this objectclass,
   */
  public Set<ObjectClass> getSuperiorClasses() {
    return superiorClasses;
  }



  /**
   * Indicates whether this objectclass is a descendant of the
   * provided class.
   *
   * @param objectClass
   *          The objectClass for which to make the determination.
   * @return <code>true</code> if this objectclass is a descendant
   *         of the provided class, or <code>false</code> if not.
   */
  public boolean isDescendantOf(ObjectClass objectClass) {

    for(ObjectClass oc : superiorClasses) {
      if(oc.equals(objectClass) || oc.isDescendantOf(objectClass)) {
        return true;
      }
    }
    return false;
  }



  /**
   * Retrieves an unmodifiable view of the set of required attributes
   * for this objectclass. Note that this set will not automatically
   * include any required attributes for superior objectclasses.
   *
   * @return Returns an unmodifiable view of the set of required
   *         attributes for this objectclass.
   */
  public Set<AttributeType> getRequiredAttributes() {

    return requiredAttributes;
  }



  /**
   * Retrieves an unmodifiable view of the set of all required
   * attributes for this objectclass and any superior objectclasses
   * that it might have.
   *
   * @return Returns an unmodifiable view of the set of all required
   *         attributes for this objectclass and any superior
   *         objectclasses that it might have.
   */
  public Set<AttributeType> getRequiredAttributeChain() {

    return requiredAttributesChain;
  }



  /**
   * Indicates whether the provided attribute type is included in the
   * required attribute list for this or any of its superior
   * objectclasses.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @return <code>true</code> if the provided attribute type is
   *         required by this objectclass or any of its superior
   *         classes, or <code>false</code> if not.
   */
  public boolean isRequired(AttributeType attributeType) {

    return requiredAttributesChain.contains(attributeType);
  }



  /**
   * Retrieves an unmodifiable view of the set of optional attributes
   * for this objectclass. Note that this list will not automatically
   * include any optional attributes for superior objectclasses.
   *
   * @return Returns an unmodifiable view of the set of optional
   *         attributes for this objectclass.
   */
  public Set<AttributeType> getOptionalAttributes() {

    return optionalAttributes;
  }



  /**
   * Retrieves an unmodifiable view of the set of optional attributes
   * for this objectclass and any superior objectclasses that it might
   * have.
   *
   * @return Returns an unmodifiable view of the set of optional
   *         attributes for this objectclass and any superior
   *         objectclasses that it might have.
   */
  public Set<AttributeType> getOptionalAttributeChain() {

    return optionalAttributesChain;
  }



  /**
   * Indicates whether the provided attribute type is included in the
   * optional attribute list for this or any of its superior
   * objectclasses.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @return <code>true</code> if the provided attribute type is
   *         optional for this objectclass or any of its superior
   *         classes, or <code>false</code> if not.
   */
  public boolean isOptional(AttributeType attributeType) {

    return optionalAttributesChain.contains(attributeType)
        || (isExtensibleObject && !requiredAttributesChain.contains(attributeType));
        // FIXME -- Do we need to do other checks here, like whether the
        // attribute type is actually defined in the schema?
        // What about DIT content rules?
  }



  /**
   * Indicates whether the provided attribute type is in the list of
   * required or optional attributes for this objectclass or any of
   * its superior classes.
   *
   * @param attributeType
   *          The attribute type for which to make the determination.
   * @return <code>true</code> if the provided attribute type is
   *         required or allowed for this objectclass or any of its
   *         superior classes, or <code>false</code> if it is not.
   */
  public boolean isRequiredOrOptional(AttributeType attributeType) {

    // FIXME -- Do we need to do any other checks here, like whether
    // the attribute type is actually defined in the schema?
    return isExtensibleObject || requiredAndOptionalChain.contains(attributeType);
  }



  /**
   * Retrieves the objectclass type for this objectclass.
   *
   * @return The objectclass type for this objectclass.
   */
  public ObjectClassType getObjectClassType() {

    return objectClassType;
  }



  /**
   * Indicates whether this objectclass is the extensibleObject
   * objectclass.
   *
   * @return <code>true</code> if this objectclass is the
   *         extensibleObject objectclass, or <code>false</code> if
   *         it is not.
   */
  public boolean isExtensibleObject() {

    return isExtensibleObject;
  }

  /** {@inheritDoc} */
  @Override
  public String toString()
  {
    return definition;
  }



  /**
   * Marks this object class as dirty, indicating that it has been removed or
   * replaced in the schema.
   *
   * @return A reference to this object class.
   */
  public ObjectClass setDirty()
  {
    isDirty = true;
    return this;
  }



  /**
   * Returns {@code true} if this object class has been removed or replaced in
   * the schema.
   *
   * @return {@code true} if this object class has been removed or replaced in
   *         the schema.
   */
  public boolean isDirty()
  {
    return isDirty;
  }
}
