/*
 *   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
 *
 *     http://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.server.core.api.schema.registries.synchronizers;


import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.schema.registries.Schema;
import org.apache.directory.api.ldap.schema.loader.SchemaEntityFactory;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.api.entry.ServerEntryUtils;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.i18n.I18n;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * This class handle modifications made on a global schema. Modifications made
 * on SchemaObjects are handled by the specific shcemaObject synchronizers.
 *
 * TODO poorly implemented - revisit the SchemaChangeHandler for this puppy
 * and do it right.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class SchemaSynchronizer implements RegistrySynchronizer
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( SchemaSynchronizer.class );

    private final SchemaEntityFactory factory;

    private final SchemaManager schemaManager;

    /** The m-disable AttributeType */
    private final AttributeType disabledAT;

    /** The CN attributeType */
    private final AttributeType cnAT;

    /** The m-dependencies AttributeType */
    private final AttributeType dependenciesAT;

    /** A static Dn referencing ou=schema */
    private final Dn ouSchemaDn;


    /**
     * Creates and initializes a new instance of Schema synchronizer
     *
     * @param schemaManager The server schemaManager
     * @throws Exception If something went wrong
     */
    public SchemaSynchronizer( SchemaManager schemaManager ) throws Exception
    {
        this.schemaManager = schemaManager;
        disabledAT = schemaManager.lookupAttributeTypeRegistry( MetaSchemaConstants.M_DISABLED_AT );
        factory = new SchemaEntityFactory();
        cnAT = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.CN_AT );
        dependenciesAT = schemaManager.lookupAttributeTypeRegistry( MetaSchemaConstants.M_DEPENDENCIES_AT );

        ouSchemaDn = new Dn( schemaManager, SchemaConstants.OU_SCHEMA );
    }


    /**
     * The only modification done on a schema element is on the m-disabled
     * attributeType
     *
     * Depending in the existence of this attribute in the previous entry, we will
     * have to update the entry or not.
     */
    @Override
    public boolean modify( ModifyOperationContext modifyContext, Entry targetEntry, boolean cascade )
        throws LdapException
    {
        Entry entry = modifyContext.getEntry();
        List<Modification> mods = modifyContext.getModItems();
        boolean hasModification = SCHEMA_UNCHANGED;

        // Check if the entry has a m-disabled attribute
        Attribute disabledInEntry = entry.get( disabledAT );
        Modification disabledModification = ServerEntryUtils.getModificationItem( mods, disabledAT );

        // The attribute might be present, but that does not mean we will change it.
        // If it's absent, and if we have it in the previous entry, that mean we want
        // to enable the schema
        if ( disabledModification != null )
        {
            // We are trying to modify the m-disabled attribute.
            ModificationOperation modification = disabledModification.getOperation();
            Attribute attribute = disabledModification.getAttribute();

            hasModification = modifyDisable( modifyContext, modification, attribute, disabledInEntry );
        }
        else if ( disabledInEntry != null )
        {
            hasModification = modifyDisable( modifyContext, ModificationOperation.REMOVE_ATTRIBUTE, null,
                disabledInEntry );
        }

        return hasModification;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( Dn oriChildName, Dn newParentName, Rdn newRn, boolean deleteOldRn, Entry entry,
        boolean cascaded ) throws LdapException
    {
        // Not implemented yet
    }


    /**
     * Handles the addition of a metaSchema object to the schema partition.
     *
     * @param entry the attributes of the new metaSchema object
     * @throws LdapException If the add failed
     */
    @Override
    public void add( Entry entry ) throws LdapException
    {
        Dn dn = entry.getDn();
        Dn parentDn = dn.getParent();

        if ( !parentDn.equals( ouSchemaDn ) )
        {
            throw new LdapInvalidDnException( ResultCodeEnum.NAMING_VIOLATION, I18n.err( I18n.ERR_380,
                ouSchemaDn.getName(),
                parentDn.getName() ) );
        }

        // check if the new schema is enabled or disabled
        boolean isEnabled = false;
        Attribute disabled = entry.get( disabledAT );

        if ( disabled == null )
        {
            // If the attribute is absent, then the schema is enabled by default
            isEnabled = true;
        }
        else if ( !disabled.contains( "TRUE" ) )
        {
            isEnabled = true;
        }

        // check to see that all dependencies are resolved and loaded if this
        // schema is enabled, otherwise check that the dependency schemas exist
        checkForDependencies( isEnabled, entry );

        /*
         * There's a slight problem that may result when adding a metaSchema
         * object if the addition of the physical entry fails.  If the schema
         * is enabled when added in the condition tested below, that schema
         * is added to the global registries.  We need to add this so subsequent
         * schema entity additions are loaded into the registries as they are
         * added to the schema partition.  However if the metaSchema object
         * addition fails then we're left with this schema object looking like
         * it is enabled in the registries object's schema hash.  The effects
         * of this are unpredictable.
         *
         * This whole problem is due to the inability of these handlers to
         * react to a failed operation.  To fix this we would need some way
         * for these handlers to respond to failed operations and revert their
         * effects on the registries.
         *
         * TODO: might want to add a set of failedOnXXX methods to the adapter
         * where on failure the schema service calls the schema manager and it
         * calls the appropriate methods on the respective handler.  This way
         * the schema manager can rollback registry changes when LDAP operations
         * fail.
         */

        if ( isEnabled )
        {
            Schema schema = factory.getSchema( entry );
            schemaManager.load( schema );
        }
    }


    /**
     * Called to react to the deletion of a metaSchema object.  This method
     * simply removes the schema from the loaded schema map of the global
     * registries.
     *
     * @param entry the attributes of the metaSchema object
     * @param cascade If we have to process recursively
     * @throws LdapException If the delete failed
     */
    @Override
    public void delete( Entry entry, boolean cascade ) throws LdapException
    {
        Attribute cn = entry.get( cnAT );
        String schemaName = cn.getString();

        // Before allowing a schema object to be deleted we must check
        // to make sure it's not depended upon by another schema
        Set<String> dependents = schemaManager.listDependentSchemaNames( schemaName );

        if ( ( dependents != null ) && !dependents.isEmpty() )
        {
            String msg = I18n.err( I18n.ERR_381, dependents );
            LOG.warn( msg );
            throw new LdapUnwillingToPerformException(
                ResultCodeEnum.UNWILLING_TO_PERFORM,
                msg );
        }

        // no need to check if schema is enabled or disabled here
        // if not in the loaded set there will be no negative effect
        schemaManager.unload( schemaName );
    }


    /**
     * Responds to the rdn (commonName) of the metaSchema object being
     * changed.  Changes all the schema entities associated with the
     * renamed schema so they now map to a new schema name.
     *
     * @param entry the entry of the metaSchema object before the rename
     * @param newRdn the new commonName of the metaSchema object
     * @param cascade If we have to process recursively
     * @throws LdapException If the rename failed
     */
    @Override
    public void rename( Entry entry, Rdn newRdn, boolean cascade ) throws LdapException
    {
        String rdnAttribute = newRdn.getNormType();
        String rdnAttributeOid = schemaManager.getAttributeTypeRegistry().getOidByName( rdnAttribute );

        if ( !rdnAttributeOid.equals( cnAT.getOid() ) )
        {
            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
                I18n.err( I18n.ERR_382, rdnAttribute ) );
        }

        /*
         * This operation has to do the following:
         *
         * [1] check and make sure there are no dependent schemas on the
         *     one being renamed - if so an exception should result
         *
         * [2] make non-schema object registries modify the mapping
         *     for their entities: non-schema object registries contain
         *     objects that are not SchemaObjects and hence do not carry
         *     their schema within the object as a property
         *
         * [3] make schema object registries do the same but the way
         *     they do them will be different since these objects will
         *     need to be replaced or will require a setter for the
         *     schema name
         */

        // step [1]
        /*
        String schemaName = getSchemaName( entry.getDn() );
        Set<String> dependents = schemaManager.listDependentSchemaNames( schemaName );
        if ( ! dependents.isEmpty() )
        {
            throw new LdapUnwillingToPerformException(
                "Cannot allow a rename on " + schemaName + " schema while it has depentents.",
                ResultCodeEnum.UNWILLING_TO_PERFORM );
        }

        // check if the new schema is enabled or disabled
        boolean isEnabled = false;
        EntryAttribute disabled = entry.get( disabledAT );

        if ( disabled == null )
        {
            isEnabled = true;
        }
        else if ( ! disabled.get().equals( "TRUE" ) )
        {
            isEnabled = true;
        }

        if ( ! isEnabled )
        {
            return;
        }

        // do steps 2 and 3 if the schema has been enabled and is loaded

        // step [2]
        String newSchemaName = ( String ) newRdn.getUpValue();
        registries.getComparatorRegistry().renameSchema( schemaName, newSchemaName );
        registries.getNormalizerRegistry().renameSchema( schemaName, newSchemaName );
        registries.getSyntaxCheckerRegistry().renameSchema( schemaName, newSchemaName );

        // step [3]
        renameSchema( registries.getAttributeTypeRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getDitContentRuleRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getDitStructureRuleRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getMatchingRuleRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getMatchingRuleUseRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getNameFormRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getObjectClassRegistry(), schemaName, newSchemaName );
        renameSchema( registries.getLdapSyntaxRegistry(), schemaName, newSchemaName );
        */
    }


    /**
     * Moves are not allowed for metaSchema objects so this always throws an
     * UNWILLING_TO_PERFORM LdapException.
     * 
     * @param oriChildName The original child name
     * @param newParentName The new parent name
     * @param newRdn the new commonName of the metaSchema object
     * @param deleteOldRdn If we need to delete the old Rdn 
     * @param entry the entry of the metaSchema object before the rename
     * @param cascade If we have to process recursively
     * @throws LdapUnwillingToPerformException If the rename failed
     */
    public void moveAndRename( Dn oriChildName, Dn newParentName, String newRdn, boolean deleteOldRdn,
        Entry entry, boolean cascade ) throws LdapUnwillingToPerformException
    {
        throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
            I18n.err( I18n.ERR_383 ) );
    }


    /**
     * Moves are not allowed for metaSchema objects so this always throws an
     * UNWILLING_TO_PERFORM LdapException.
     */
    @Override
    public void move( Dn oriChildName, Dn newParentName,
        Entry entry, boolean cascade ) throws LdapUnwillingToPerformException
    {
        throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
            I18n.err( I18n.ERR_383 ) );
    }


    // -----------------------------------------------------------------------
    // private utility methods
    // -----------------------------------------------------------------------

    /**
     * Modify the Disable flag (the flag can be set to true or false).
     *
     * We can ADD, REMOVE or MODIFY this flag. The following matrix expose what will be the consequences
     * of this operation, depending on the current state
     *
     * <pre>
     *                 +-------------------+--------------------+--------------------+
     *     op/state    |       TRUE        |       FALSE        |       ABSENT       |
     * +-------+-------+----------------------------------------+--------------------+
     * | ADD   | TRUE  | do nothing        | do nothing         | disable the schema |
     * |       +-------+-------------------+--------------------+--------------------+
     * |       | FALSE | do nothing        | do nothing         | do nothing         |
     * +-------+-------+-------------------+--------------------+--------------------+
     * |REMOVE | N/A   | enable the schema | do nothing         | do nothing         |
     * +-------+-------+-------------------+--------------------+--------------------+
     * |MODIFY | TRUE  | do nothing        | disable the schema | disable the schema |
     * |       +-------+-------------------+--------------------+--------------------+
     * |       | FALSE | enable the schema | do nothing         |  do nothing        |
     * +-------+-------+-------------------+--------------------+--------------------+
     * </pre>
     */
    private boolean modifyDisable( ModifyOperationContext modifyContext, ModificationOperation modOp,
        Attribute disabledInMods, Attribute disabledInEntry ) throws LdapException
    {
        Dn name = modifyContext.getDn();

        switch ( modOp )
        {
        /*
         * If the user is adding a new m-disabled attribute to an enabled schema,
         * we check that the value is "TRUE" and disable that schema if so.
         */
            case ADD_ATTRIBUTE:
                if ( disabledInEntry == null && "TRUE".equalsIgnoreCase( disabledInMods.getString() ) )
                {
                    return disableSchema( getSchemaName( name ) );
                }

                break;

            /*
             * If the user is removing the m-disabled attribute we check if the schema is currently
             * disabled.  If so we enable the schema.
             */
            case REMOVE_ATTRIBUTE:
                if ( ( disabledInEntry != null ) && ( "TRUE".equalsIgnoreCase( disabledInEntry.getString() ) ) )
                {
                    return enableSchema( getSchemaName( name ) );
                }

                break;

            /*
             * If the user is replacing the m-disabled attribute we check if the schema is
             * currently disabled and enable it if the new state has it as enabled.  If the
             * schema is not disabled we disable it if the mods set m-disabled to true.
             */
            case REPLACE_ATTRIBUTE:

                boolean isCurrentlyDisabled = false;

                if ( disabledInEntry != null )
                {
                    isCurrentlyDisabled = "TRUE".equalsIgnoreCase( disabledInEntry.getString() );
                }

                boolean isNewStateDisabled = false;

                if ( disabledInMods != null )
                {
                    Value val = disabledInMods.get();

                    if ( val == null )
                    {
                        isNewStateDisabled = false;
                    }
                    else
                    {
                        isNewStateDisabled = "TRUE".equalsIgnoreCase( val.getString() );
                    }
                }

                if ( isCurrentlyDisabled && !isNewStateDisabled )
                {
                    return enableSchema( getSchemaName( name ) );
                }

                if ( !isCurrentlyDisabled && isNewStateDisabled )
                {
                    return disableSchema( getSchemaName( name ) );
                }

                break;

            default:
                throw new IllegalArgumentException( I18n.err( I18n.ERR_384, modOp ) );
        }

        return SCHEMA_UNCHANGED;
    }


    private String getSchemaName( Dn schema )
    {
        return schema.getRdn().getValue();
    }


    private boolean disableSchema( String schemaName ) throws LdapException
    {
        Schema schema = schemaManager.getLoadedSchema( schemaName );

        if ( schema == null )
        {
            // This is not possible. We can't enable a schema which is not loaded.
            String msg = I18n.err( I18n.ERR_85, schemaName );
            LOG.error( msg );
            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
        }

        return schemaManager.disable( schemaName );

    }


    /**
     * Enabling a schema consist on switching all of its schema element to enable.
     * We have to do it on a temporary registries.
     */
    private boolean enableSchema( String schemaName ) throws LdapException
    {
        Schema schema = schemaManager.getLoadedSchema( schemaName );

        if ( schema == null )
        {
            // We have to load the schema before enabling it.
            schemaManager.loadDisabled( schemaName );
        }

        return schemaManager.enable( schemaName );
    }


    /**
     * Checks to make sure the dependencies either exist for disabled metaSchemas,
     * or exist and are loaded (enabled) for enabled metaSchemas.
     *
     * @param isEnabled whether or not the new metaSchema is enabled
     * @param entry the Attributes for the new metaSchema object
     * @throws NamingException if the dependencies do not resolve or are not
     * loaded (enabled)
     */
    private void checkForDependencies( boolean isEnabled, Entry entry ) throws LdapException
    {
        Attribute dependencies = entry.get( this.dependenciesAT );

        if ( dependencies == null )
        {
            return;
        }

        if ( isEnabled )
        {
            // check to make sure all the dependencies are also enabled
            Map<String, Schema> loaded = schemaManager.getRegistries().getLoadedSchemas();

            for ( Value value : dependencies )
            {
                String dependency = value.getString();

                if ( !loaded.containsKey( dependency ) )
                {
                    throw new LdapUnwillingToPerformException(
                        ResultCodeEnum.UNWILLING_TO_PERFORM,
                        "Unwilling to perform operation on enabled schema with disabled or missing dependencies: "
                            + dependency );
                }
            }
        }
        else
        {
            for ( Value value : dependencies )
            {
                String dependency = value.getString();

                if ( schemaManager.getLoadedSchema( Strings.toLowerCaseAscii( dependency ) ) == null )
                {
                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
                        I18n.err( I18n.ERR_385, dependency ) );
                }
            }
        }
    }
}
