/*
 *  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.HashMap;
import java.util.HashSet;
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.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
import org.apache.directory.api.ldap.model.exception.LdapSchemaViolationException;
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.SchemaObject;
import org.apache.directory.api.ldap.model.schema.SchemaObjectWrapper;
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.i18n.I18n;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * An abstract registry synchronizer with some reused functionality.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public abstract class AbstractRegistrySynchronizer implements RegistrySynchronizer
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( AbstractRegistrySynchronizer.class );

    /** The global SchemaManager */
    protected final SchemaManager schemaManager;

    /** The m-oid AttributeType */
    protected final AttributeType moidAT;

    /** The Schema objetc factory */
    protected final SchemaEntityFactory factory;

    /** A map associating a SchemaObject type with its path on the partition*/
    private static final Map<String, String> OBJECT_TYPE_TO_PATH = new HashMap<>();

    static
    {
        // Removed the starting 'ou=' from the paths
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.ATTRIBUTE_TYPE, SchemaConstants.ATTRIBUTE_TYPES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.COMPARATOR, SchemaConstants.COMPARATORS_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH
            .put( SchemaConstants.DIT_CONTENT_RULE, SchemaConstants.DIT_CONTENT_RULES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.DIT_STRUCTURE_RULE,
            SchemaConstants.DIT_STRUCTURE_RULES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.MATCHING_RULE, SchemaConstants.MATCHING_RULES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.MATCHING_RULE_USE,
            SchemaConstants.MATCHING_RULE_USE_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.NAME_FORM, SchemaConstants.NAME_FORMS_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.NORMALIZER, SchemaConstants.NORMALIZERS_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.OBJECT_CLASS, SchemaConstants.OBJECT_CLASSES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.SYNTAX, SchemaConstants.SYNTAXES_PATH.substring( 3 ) );
        OBJECT_TYPE_TO_PATH.put( SchemaConstants.SYNTAX_CHECKER, SchemaConstants.SYNTAX_CHECKERS_PATH.substring( 3 ) );
    }


    protected AbstractRegistrySynchronizer( SchemaManager schemaManager )
    {
        this.schemaManager = schemaManager;
        moidAT = schemaManager.getAttributeType( MetaSchemaConstants.M_OID_AT );
        factory = new SchemaEntityFactory();
    }


    /**
     * Tells if the schema the Dn references is loaded or not
     *
     * @param dn The SchemaObject's Dn
     * @return true if the schema is loaded
     * @throws LdapException If The Dn is not a SchemaObject Dn
     */
    protected boolean isSchemaLoaded( Dn dn ) throws LdapException
    {
        return schemaManager.isSchemaLoaded( getSchemaName( dn ) );
    }


    /**
     * Tells if the schemaName is loaded or not
     *
     * @param schemaName The schema we want to check
     * @return true if the schema is loaded
     */
    protected boolean isSchemaLoaded( String schemaName )
    {
        return schemaManager.isSchemaLoaded( schemaName );
    }


    /**
     * Tells if a schema is loaded and enabled
     *
     * @param schemaName The schema we want to check
     * @return true if the schema is loaded and enabled, false otherwise
     */
    protected boolean isSchemaEnabled( String schemaName )
    {
        Schema schema = schemaManager.getLoadedSchema( schemaName );

        return ( schema != null ) && schema.isEnabled();
    }


    /**
     * Exctract the schema name from the Dn. It is supposed to be the
     * second Rdn in the dn :
     * <pre>
     * ou=schema, cn=MySchema, ...
     * </pre>
     * Here, the schemaName is MySchema
     *
     * @param dn The Dn we want to get the schema name from
     * @return The schema name
     * @throws LdapException If we got an error
     */
    protected String getSchemaName( Dn dn ) throws LdapException
    {
        int size = dn.size();

        if ( size < 2 )
        {
            throw new LdapInvalidDnException( I18n.err( I18n.ERR_276 ) );
        }

        Rdn rdn = dn.getRdn( size - 2 );

        return Strings.trim( rdn.getAva().getValue().getNormalized() );
    }


    protected void checkOidIsUnique( Entry entry ) throws LdapException
    {
        String oid = getOid( entry );

        if ( schemaManager.getGlobalOidRegistry().contains( oid ) )
        {
            throw new LdapOtherException( I18n.err( I18n.ERR_335, oid ) );
        }
    }


    /**
     * Check that a SchemaObject exists in the global OidRegsitry, and if so,
     * return it.
     * 
     * @param entry The Entry we want to verify the existence of
     * @return The found SchemaObject
     * @throws LdapException  If the check failed
     */
    protected SchemaObject checkOidExists( Entry entry ) throws LdapException
    {
        String oid = getOid( entry );

        if ( schemaManager.getGlobalOidRegistry().contains( oid ) )
        {
            return schemaManager.getGlobalOidRegistry().getSchemaObject( oid );
        }
        else
        {
            throw new LdapSchemaViolationException( ResultCodeEnum.OTHER,
                I18n.err( I18n.ERR_336, oid ) );
        }
    }


    /**
     * Checks that the parent Dn is a valid Dn
     * 
     * @param newParent The parent DN to check
     * @param schemaManager The SchemaManager instance
     * @param objectType The ObjectType to check
     * @throws LdapException  If the deletion failed
     */
    protected void checkParent( Dn newParent, SchemaManager schemaManager, String objectType ) throws LdapException
    {
        if ( newParent.size() != 3 )
        {
            throw new LdapInvalidDnException( ResultCodeEnum.NAMING_VIOLATION, I18n.err( I18n.ERR_337 ) );
        }

        Rdn rdn = newParent.getRdn();

        if ( !schemaManager.getAttributeTypeRegistry().getOidByName( rdn.getNormType() )
            .equals( SchemaConstants.OU_AT_OID ) )
        {
            throw new LdapInvalidDnException( ResultCodeEnum.NAMING_VIOLATION,
                I18n.err( I18n.ERR_338, objectType ) );
        }

        if ( !rdn.getValue().equalsIgnoreCase( OBJECT_TYPE_TO_PATH.get( objectType ) ) )
        {
            throw new LdapInvalidDnException( ResultCodeEnum.NAMING_VIOLATION,
                I18n.err( I18n.ERR_339, objectType, OBJECT_TYPE_TO_PATH.get( objectType ) ) );
        }
    }


    protected void checkOidIsUnique( SchemaObject schemaObject ) throws LdapException
    {
        String oid = schemaObject.getOid();

        if ( schemaManager.getGlobalOidRegistry().contains( oid ) )
        {
            throw new LdapSchemaViolationException( ResultCodeEnum.OTHER,
                I18n.err( I18n.ERR_335, oid ) );
        }
    }


    protected void checkOidIsUnique( String oid ) throws LdapException
    {
        if ( schemaManager.getGlobalOidRegistry().contains( oid ) )
        {
            throw new LdapSchemaViolationException( ResultCodeEnum.OTHER,
                I18n.err( I18n.ERR_335, oid ) );
        }
    }


    /**
     * Add a new SchemaObject to the schema content, assuming that
     * it has an associated schema and that this schema is loaded
     * 
     * @param schemaObject The SchemaObject to add
     * @param schemaName The Schema we want the SchemaObject to be added in
     * @throws LdapException  If the addition failed
     */
    protected void addToSchema( SchemaObject schemaObject, String schemaName ) throws LdapException
    {
        if ( isSchemaLoaded( schemaName ) )
        {
            // Get the set of all the SchemaObjects associated with this schema
            Set<SchemaObjectWrapper> schemaObjects = schemaManager.getRegistries().getObjectBySchemaName()
                .get( schemaName );

            if ( schemaObjects == null )
            {
                // TODO : this should never happen...
                schemaObjects = schemaManager.getRegistries().addSchema( schemaName );
            }

            SchemaObjectWrapper schemaObjectWrapper = new SchemaObjectWrapper( schemaObject );

            if ( schemaObjects.contains( schemaObjectWrapper ) )
            {
                String msg = I18n.err( I18n.ERR_341, schemaObject.getName(), schemaName );
                LOG.warn( msg );

                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
            }

            schemaObjects.add( schemaObjectWrapper );
            LOG.debug( "The SchemaObject {} has been added to the schema {}", schemaObject, schemaName );
        }
        else
        {
            String msg = I18n.err( I18n.ERR_342, schemaObject.getName(), schemaName );
            LOG.warn( msg );

            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
        }
    }


    /**
     * Delete a SchemaObject from the schema registry, assuming that
     * it has an associated schema and that this schema is loaded
     * 
     * @param schemaObject The SchemaObject to delete
     * @param schemaName The Schema we want the SchemaObject to be deleted from
     * @throws LdapException  If the deletion failed
     */
    protected void deleteFromSchema( SchemaObject schemaObject, String schemaName ) throws LdapException
    {
        if ( isSchemaLoaded( schemaName ) )
        {
            Set<SchemaObjectWrapper> schemaObjects = schemaManager.getRegistries().getObjectBySchemaName()
                .get( schemaName );

            SchemaObjectWrapper schemaObjectWrapper = new SchemaObjectWrapper( schemaObject );

            if ( !schemaObjects.contains( schemaObjectWrapper ) )
            {
                String msg = I18n.err( I18n.ERR_343, schemaObject.getName(), schemaName );
                LOG.warn( msg );

                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
            }

            schemaObjects.remove( schemaObjectWrapper );
            LOG.debug( "The SchemaObject {} has been removed from the schema {}", schemaObject, schemaName );
        }
        else
        {
            String msg = I18n.err( I18n.ERR_342, schemaObject.getName(), schemaName );
            LOG.warn( msg );

            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
        }
    }


    protected Set<String> getOids( Set<Entry> results )
    {
        Set<String> oids = new HashSet<>( results.size() );

        for ( Entry result : results )
        {
            Dn dn = result.getDn();
            oids.add( dn.getRdn().getValue() );
        }

        return oids;
    }


    protected String getOid( Entry entry ) throws LdapException
    {
        Attribute oid = entry.get( moidAT );

        if ( oid == null )
        {
            return null;
        }

        return oid.getString();
    }


    /**
     * Unregister a SchemaObject's OID from the associated oidRegistry
     *
     * @param obj The SchemaObject to unregister
     * @throws LdapException If the unregistering failed
     */
    protected void unregisterOids( SchemaObject obj ) throws LdapException
    {
        schemaManager.getGlobalOidRegistry().unregister( obj.getOid() );
    }


    /**
     * Register a SchemaObject's OID in the associated oidRegistry
     *
     * @param obj The SchemaObject to register
     * @throws LdapException If the registering failed
     */
    protected void registerOids( SchemaObject obj ) throws LdapException
    {
        schemaManager.getGlobalOidRegistry().register( obj );
    }


    /**
     * Get a String containing the SchemaObjects referencing the
     * given ShcemaObject
     *
     * @param schemaObject The SchemaObject we want the referencing SchemaObjects for
     * @return A String containing all the SchemaObjects referencing the give SchemaObject
     */
    protected String getReferenced( SchemaObject schemaObject )
    {
        StringBuilder sb = new StringBuilder();

        Set<SchemaObjectWrapper> useds = schemaManager.getRegistries().getUsedBy( schemaObject );

        for ( SchemaObjectWrapper used : useds )
        {
            sb.append( used );
            sb.append( '\n' );
        }

        return sb.toString();
    }
}
