/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.directory.api.ldap.model.schema.registries.helper;

import java.util.List;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapSchemaException;
import org.apache.directory.api.ldap.model.exception.LdapSchemaExceptionCodes;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.ObjectClass;
import org.apache.directory.api.ldap.model.schema.ObjectClassTypeEnum;
import org.apache.directory.api.ldap.model.schema.SchemaErrorHandler;
import org.apache.directory.api.ldap.model.schema.registries.AttributeTypeRegistry;
import org.apache.directory.api.ldap.model.schema.registries.ObjectClassRegistry;
import org.apache.directory.api.ldap.model.schema.registries.Registries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An helper class used to store all the methods associated with an ObjectClass
 * in relation with the Registries and SchemaManager.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class ObjectClassHelper
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( ObjectClassHelper.class );

    private ObjectClassHelper()
    {
    }


    /**
     * Inject the ObjectClass into the registries, updating the references to
     * other SchemaObject
     *
     * @param objectClass The ObjectClass to add to the Registries
     * @param errorHandler Error handler
     * @param registries The Registries
     */
    public static void addToRegistries( ObjectClass objectClass, SchemaErrorHandler errorHandler, Registries registries )
    {
        if ( registries != null )
        {
            try
            {
                objectClass.unlock();
                
                // The superiors
                buildSuperiors( objectClass, errorHandler, registries );
    
                // The MAY AttributeTypes
                buildMay( objectClass, errorHandler, registries );
    
                // The MUST AttributeTypes
                buildMust( objectClass, errorHandler, registries );
    
                /**
                 * Add the OC references (using and usedBy) :
                 * OC -> AT (MAY and MUST)
                 * OC -> OC (SUPERIORS)
                 */
                for ( AttributeType mayAttributeType : objectClass.getMayAttributeTypes() )
                {
                    registries.addReference( objectClass, mayAttributeType );
                }
    
                for ( AttributeType mustAttributeType : objectClass.getMustAttributeTypes() )
                {
                    registries.addReference( objectClass, mustAttributeType );
                }
    
                for ( ObjectClass superiorObjectClass : objectClass.getSuperiors() )
                {
                    registries.addReference( objectClass, superiorObjectClass );
                }
            }
            finally
            {
                objectClass.lock();
            }
        }
    }


    /**
     * Build the references to this ObjectClass SUPERIORS, checking that the type
     * hierarchy is correct.
     * 
     * @param objectClass The oOjectClass to process
     * @param errorHandler The error handler
     * @param registries The Registries instance
     */
    private static void buildSuperiors( ObjectClass objectClass, SchemaErrorHandler errorHandler, Registries registries )
    {
        ObjectClassRegistry ocRegistry = registries.getObjectClassRegistry();
        List<String> superiorOids = objectClass.getSuperiorOids();

        if ( superiorOids != null )
        {
            objectClass.getSuperiors().clear();

            for ( String superiorName : superiorOids )
            {
                try
                {
                    ObjectClass superior = ocRegistry.lookup( ocRegistry.getOidByName( superiorName ) );

                    // Before adding the superior, check that the ObjectClass type is consistent
                    switch ( objectClass.getType() )
                    {
                        case ABSTRACT:
                            if ( superior.getType() != ObjectClassTypeEnum.ABSTRACT )
                            {
                                // An ABSTRACT OC can only inherit from ABSTRACT OCs
                                String msg = I18n.err( I18n.ERR_13766_ABSTRACT_OC_CANNOT_INHERIT_FROM_OC, 
                                    objectClass.getOid(), superior.getObjectType(), superior );

                                LdapSchemaException ldapSchemaException = new LdapSchemaException(
                                    LdapSchemaExceptionCodes.OC_ABSTRACT_MUST_INHERIT_FROM_ABSTRACT_OC, msg );
                                ldapSchemaException.setSourceObject( objectClass );
                                errorHandler.handle( LOG, msg, ldapSchemaException );

                                continue;
                            }

                            break;

                        case AUXILIARY:
                            if ( superior.getType() == ObjectClassTypeEnum.STRUCTURAL )
                            {
                                // An AUXILIARY OC cannot inherit from STRUCTURAL OCs
                                String msg = I18n.err( I18n.ERR_13767_AUX_OC_CANNOT_INHERIT_FROM_STRUCT_OC, objectClass.getOid(), superior );

                                LdapSchemaException ldapSchemaException = new LdapSchemaException(
                                    LdapSchemaExceptionCodes.OC_AUXILIARY_CANNOT_INHERIT_FROM_STRUCTURAL_OC, msg );
                                ldapSchemaException.setSourceObject( objectClass );
                                errorHandler.handle( LOG, msg, ldapSchemaException );

                                continue;
                            }

                            break;

                        case STRUCTURAL:
                            if ( superior.getType() == ObjectClassTypeEnum.AUXILIARY )
                            {
                                // A STRUCTURAL OC cannot inherit from AUXILIARY OCs
                                String msg = I18n.err( I18n.ERR_13768_STRUCT_OC_CANNOT_INHERIT_FROM_AUX_OC, objectClass.getOid(), superior );

                                LdapSchemaException ldapSchemaException = new LdapSchemaException(
                                    LdapSchemaExceptionCodes.OC_STRUCTURAL_CANNOT_INHERIT_FROM_AUXILIARY_OC, msg );
                                ldapSchemaException.setSourceObject( objectClass );
                                errorHandler.handle( LOG, msg, ldapSchemaException );

                                continue;
                            }

                            break;

                        default:
                            throw new IllegalArgumentException( I18n.err( I18n.ERR_13717_UNEXPECTED_OBJECT_CLASS_TYPE_ENUM, 
                                objectClass.getType() ) );
                    }

                    objectClass.getSuperiors().add( superior );
                }
                catch ( LdapException ne )
                {
                    // Cannot find the OC
                    String msg = I18n.err( I18n.ERR_13769_CANNOT_REGISTER_SUPERIOR_MISSING, 
                        objectClass.getOid(), superiorName );

                    LdapSchemaException ldapSchemaException = new LdapSchemaException(
                        LdapSchemaExceptionCodes.OC_NONEXISTENT_SUPERIOR, msg, ne );
                    ldapSchemaException.setSourceObject( objectClass );
                    ldapSchemaException.setRelatedId( superiorName );
                    errorHandler.handle( LOG, msg, ldapSchemaException );

                    return;
                }
            }
        }
    }


    /**
     * Build and check the MUST AT for this ObjectClass.
     * 
     * @param objectClass The oOjectClass to process
     * @param errorHandler The error handler
     * @param registries The Registries instance
     */
    private static void buildMust( ObjectClass objectClass, SchemaErrorHandler errorHandler, Registries registries )
    {
        AttributeTypeRegistry atRegistry = registries.getAttributeTypeRegistry();
        List<String> mustAttributeTypeOids = objectClass.getMustAttributeTypeOids();

        if ( mustAttributeTypeOids != null )
        {
            objectClass.getMustAttributeTypes().clear();

            for ( String mustAttributeTypeName : mustAttributeTypeOids )
            {
                try
                {
                    AttributeType attributeType = atRegistry.lookup( mustAttributeTypeName );

                    if ( attributeType.isCollective() )
                    {
                        // Collective Attributes are not allowed in MAY or MUST
                        String msg = I18n.err( I18n.ERR_13778_COLLECTIVE_NOT_ALLOWED_IN_MUST, mustAttributeTypeName,
                            objectClass.getOid() );

                        LdapSchemaException ldapSchemaException = new LdapSchemaException(
                            LdapSchemaExceptionCodes.OC_COLLECTIVE_NOT_ALLOWED_IN_MUST, msg );
                        ldapSchemaException.setSourceObject( objectClass );
                        ldapSchemaException.setRelatedId( mustAttributeTypeName );
                        errorHandler.handle( LOG, msg, ldapSchemaException );

                        continue;
                    }

                    if ( objectClass.getMustAttributeTypes().contains( attributeType ) )
                    {
                        // Already registered : this is an error
                        String msg = I18n.err( I18n.ERR_13772_CANNOT_REGISTER_DUPLICATE_AT_IN_MUST, 
                            objectClass.getOid(), mustAttributeTypeName );

                        LdapSchemaException ldapSchemaException = new LdapSchemaException(
                            LdapSchemaExceptionCodes.OC_DUPLICATE_AT_IN_MUST, msg );
                        ldapSchemaException.setSourceObject( objectClass );
                        ldapSchemaException.setRelatedId( mustAttributeTypeName );
                        errorHandler.handle( LOG, msg, ldapSchemaException );

                        continue;
                    }

                    // Check that the MUST AT is not also present in the MAY AT
                    if ( objectClass.getMayAttributeTypes().contains( attributeType ) )
                    {
                        // Already registered : this is an error
                        String msg = I18n.err( I18n.ERR_13773_CANNOT_REGISTER_DUPLICATE_AT_IN_MAY_AND_MUST, 
                            objectClass.getOid(), mustAttributeTypeName );

                        LdapSchemaException ldapSchemaException = new LdapSchemaException(
                            LdapSchemaExceptionCodes.OC_DUPLICATE_AT_IN_MAY_AND_MUST,
                            msg );
                        ldapSchemaException.setSourceObject( objectClass );
                        ldapSchemaException.setRelatedId( mustAttributeTypeName );
                        errorHandler.handle( LOG, msg, ldapSchemaException );

                        continue;
                    }

                    objectClass.getMustAttributeTypes().add( attributeType );
                }
                catch ( LdapException ne )
                {
                    // Cannot find the AT
                    String msg = I18n.err( I18n.ERR_13774_CANNOT_REGISTER_AT_IN_MUST_DOES_NOT_EXIST, 
                        objectClass.getOid(), mustAttributeTypeName );

                    LdapSchemaException ldapSchemaException = new LdapSchemaException(
                        LdapSchemaExceptionCodes.OC_NONEXISTENT_MUST_AT, msg, ne );
                    ldapSchemaException.setSourceObject( objectClass );
                    ldapSchemaException.setRelatedId( mustAttributeTypeName );
                    errorHandler.handle( LOG, msg, ldapSchemaException );
                }
            }
        }
    }
    
    
    /**
     * Build and check the MAY AT for this ObjectClass
     * 
     * @param objectClass The oOjectClass to process
     * @param errorHandler The error handler
     * @param registries The Registries instance
     */
    private static void buildMay( ObjectClass objectClass, SchemaErrorHandler errorHandler, Registries registries )
    {
        AttributeTypeRegistry atRegistry = registries.getAttributeTypeRegistry();
        List<String> mayAttributeTypeOids = objectClass.getMayAttributeTypeOids();

        if ( mayAttributeTypeOids != null )
        {
            objectClass.getMayAttributeTypes().clear();

            for ( String mayAttributeTypeName : mayAttributeTypeOids )
            {
                try
                {
                    AttributeType attributeType = atRegistry.lookup( mayAttributeTypeName );

                    if ( attributeType.isCollective() )
                    {
                        // Collective Attributes are not allowed in MAY or MUST
                        String msg = I18n.err( I18n.ERR_13779_COLLECTIVE_NOT_ALLOWED_IN_MAY, mayAttributeTypeName, objectClass.getOid() );

                        LdapSchemaException ldapSchemaException = new LdapSchemaException(
                            LdapSchemaExceptionCodes.OC_COLLECTIVE_NOT_ALLOWED_IN_MAY, msg );
                        ldapSchemaException.setSourceObject( objectClass );
                        ldapSchemaException.setRelatedId( mayAttributeTypeName );
                        errorHandler.handle( LOG, msg, ldapSchemaException );

                        continue;
                    }

                    if ( objectClass.getMayAttributeTypes().contains( attributeType ) )
                    {
                        // Already registered : this is an error
                        String msg = I18n.err( 
                            I18n.ERR_13770_CANNOT_REGISTER_DUPLICATE_AT_IN_MAY, objectClass.getOid(), mayAttributeTypeName );

                        LdapSchemaException ldapSchemaException = new LdapSchemaException(
                            LdapSchemaExceptionCodes.OC_DUPLICATE_AT_IN_MAY, msg );
                        ldapSchemaException.setSourceObject( objectClass );
                        ldapSchemaException.setRelatedId( mayAttributeTypeName );
                        errorHandler.handle( LOG, msg, ldapSchemaException );

                        continue;
                    }

                    objectClass.getMayAttributeTypes().add( attributeType );
                }
                catch ( LdapException ne )
                {
                    // Cannot find the AT
                    String msg = I18n.err( I18n.ERR_13771_CANNOT_REGISTER_AT_IN_MAY_DOES_NOT_EXIST, objectClass.getOid(), mayAttributeTypeName );

                    LdapSchemaException ldapSchemaException = new LdapSchemaException(
                        LdapSchemaExceptionCodes.OC_NONEXISTENT_MAY_AT, msg, ne );
                    ldapSchemaException.setSourceObject( objectClass );
                    ldapSchemaException.setRelatedId( mayAttributeTypeName );
                    errorHandler.handle( LOG, msg, ldapSchemaException );
                }
            }
        }
    }
    
    
    /**
     * Remove the ObjectClass from the registries, updating the references to
     * other SchemaObject.
     *
     * If one of the referenced SchemaObject does not exist (SUPERIORS, MAY, MUST),
     * an exception is thrown.
     *
     * @param objectClass The ObjectClass to remove fro the registries
     * @param errorHandler Error handler
     * @param registries The Registries
     * @throws LdapException If the ObjectClass is not valid
     */
    public static void removeFromRegistries( ObjectClass objectClass, SchemaErrorHandler errorHandler, Registries registries ) throws LdapException
    {
        if ( registries != null )
        {
            ObjectClassRegistry objectClassRegistry = registries.getObjectClassRegistry();

            // Unregister this ObjectClass into the Descendant map
            objectClassRegistry.unregisterDescendants( objectClass, objectClass.getSuperiors() );

            /**
             * Remove the OC references (using and usedBy) :
             * OC -> AT (for MAY and MUST)
             * OC -> OC
             */
            if ( objectClass.getMayAttributeTypes() != null )
            {
                for ( AttributeType may : objectClass.getMayAttributeTypes() )
                {
                    registries.delReference( objectClass, may );
                }
            }

            if ( objectClass.getMustAttributeTypes() != null )
            {
                for ( AttributeType must : objectClass.getMustAttributeTypes() )
                {
                    registries.delReference( objectClass, must );
                }
            }

            if ( objectClass.getSuperiors() != null )
            {
                for ( ObjectClass superior : objectClass.getSuperiors() )
                {
                    registries.delReference( objectClass, superior );
                }
            }
        }
    }
}
