/*
 *  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.normalization;


import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.core.entry.ClonedServerEntry;
import org.apache.directory.server.core.filtering.BaseEntryFilteringCursor;
import org.apache.directory.server.core.filtering.EntryFilteringCursor;
import org.apache.directory.server.core.interceptor.BaseInterceptor;
import org.apache.directory.server.core.interceptor.NextInterceptor;
import org.apache.directory.server.core.interceptor.context.AddContextPartitionOperationContext;
import org.apache.directory.server.core.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.interceptor.context.BindOperationContext;
import org.apache.directory.server.core.interceptor.context.CompareOperationContext;
import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
import org.apache.directory.server.core.interceptor.context.GetMatchedNameOperationContext;
import org.apache.directory.server.core.interceptor.context.GetSuffixOperationContext;
import org.apache.directory.server.core.interceptor.context.ListOperationContext;
import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.interceptor.context.RemoveContextPartitionOperationContext;
import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
import org.apache.directory.server.core.partition.DefaultPartitionNexus;
import org.apache.directory.shared.ldap.cursor.EmptyCursor;
import org.apache.directory.shared.ldap.entry.StringValue;
import org.apache.directory.shared.ldap.entry.ServerEntry;
import org.apache.directory.shared.ldap.entry.Value;
import org.apache.directory.shared.ldap.filter.ExprNode;
import org.apache.directory.shared.ldap.name.AVA;
import org.apache.directory.shared.ldap.name.DN;
import org.apache.directory.shared.ldap.name.NameComponentNormalizer;
import org.apache.directory.shared.ldap.name.RDN;
import org.apache.directory.shared.ldap.schema.AttributeType;
import org.apache.directory.shared.ldap.schema.SchemaManager;
import org.apache.directory.shared.ldap.schema.normalizers.ConcreteNameComponentNormalizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A name normalization service.  This service makes sure all relative and distinguished
 * names are normalized before calls are made against the respective interface methods
 * on {@link DefaultPartitionNexus}.
 * 
 * The Filters are also normalized.
 * 
 * If the RDN AttributeTypes are not present in the entry for an Add request,
 * they will be added.
 *
 * @org.apache.xbean.XBean
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 * @version $Rev$
 */
public class NormalizationInterceptor extends BaseInterceptor
{
    /** logger used by this class */
    private static final Logger LOG = LoggerFactory.getLogger( NormalizationInterceptor.class );

    /** a filter node value normalizer and undefined node remover */
    private FilterNormalizingVisitor normVisitor;

    /** The attributeType registry */
    private SchemaManager schemaManager;

    /**
     * Initialize the registries, normalizers. 
     */
    public void init( DirectoryService directoryService ) throws Exception
    {
        LOG.debug( "Initialiazing the NormalizationInterceptor" );
        
        schemaManager = directoryService.getSchemaManager();
        NameComponentNormalizer ncn = new ConcreteNameComponentNormalizer( schemaManager );
        normVisitor = new FilterNormalizingVisitor( ncn, schemaManager );
    }

    /**
     * The destroy method does nothing
     */
    public void destroy()
    {
    }

    // ------------------------------------------------------------------------
    // Normalize all Name based arguments for ContextPartition interface operations
    // ------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    public void add( NextInterceptor nextInterceptor, AddOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        opContext.getEntry().getDn().normalize( schemaManager.getNormalizerMapping() );
        addRdnAttributesToEntry( opContext.getDn(), opContext.getEntry() );
        nextInterceptor.add( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void delete( NextInterceptor nextInterceptor, DeleteOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        nextInterceptor.delete( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        nextInterceptor.modify( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext ) throws Exception
    {
        // Normalize the new RDN and the DN
        opContext.getNewRdn().normalize( schemaManager.getNormalizerMapping() );
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        opContext.getNewDn().normalize( schemaManager.getNormalizerMapping() );

        // Push to the next interceptor
        nextInterceptor.rename( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        opContext.getParent().normalize( schemaManager.getNormalizerMapping());
        nextInterceptor.move( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext )
        throws Exception
    {
        DN rdn = new DN();
        rdn.add( opContext.getNewRdn() );
        rdn.normalize( schemaManager.getNormalizerMapping() );
        opContext.setNewRdn( rdn.getRdn() );

        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        opContext.getParent().normalize( schemaManager.getNormalizerMapping() );
        nextInterceptor.moveAndRename( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );

        ExprNode filter = opContext.getFilter();
        
        // Normalize the filter
        ExprNode result = ( ExprNode ) filter.accept( normVisitor );

        if ( result == null )
        {
            LOG.warn( "undefined filter based on undefined attributeType not evaluted at all.  Returning empty enumeration." );
            return new BaseEntryFilteringCursor( new EmptyCursor<ServerEntry>(), opContext );
        }
        else
        {
            opContext.setFilter( result );
            
            // TODO Normalize the returned Attributes, storing the UP attributes to format the returned values.
            return nextInterceptor.search( opContext );
        }
    }


    /**
     * {@inheritDoc}
     */
    public boolean hasEntry( NextInterceptor nextInterceptor, EntryOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        return nextInterceptor.hasEntry( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        return nextInterceptor.list( opContext );
    }

    
    /**
     * {@inheritDoc}
     */
    private String[] normalizeAttrsId( String[] attrIds ) throws Exception
    {
        if ( attrIds == null )
        {
            return attrIds;
        }
        
        String[] normalizedAttrIds = new String[attrIds.length];
        int pos = 0;
        
        for ( String id:attrIds )
        {
            String oid = schemaManager.lookupAttributeTypeRegistry( id ).getOid();
            normalizedAttrIds[pos++] = oid;
        }
        
        return normalizedAttrIds;
    }

    
    /**
     * {@inheritDoc}
     */
    public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        
        if ( opContext.getAttrsId() != null )
        {
            // We have to normalize the requested IDs
            opContext.setAttrsId( normalizeAttrsId( opContext.getAttrsIdArray() ) );
        }
        
        return nextInterceptor.lookup( opContext );
    }


    // ------------------------------------------------------------------------
    // Normalize all Name based arguments for other interface operations
    // ------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    public DN getMatchedName ( NextInterceptor nextInterceptor, GetMatchedNameOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        return nextInterceptor.getMatchedName( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public DN getSuffix ( NextInterceptor nextInterceptor, GetSuffixOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        return nextInterceptor.getSuffix( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public boolean compare( NextInterceptor next, CompareOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        
        AttributeType at = opContext.getSession().getDirectoryService().getSchemaManager().lookupAttributeTypeRegistry( opContext.getOid() );
        
        if ( at.getSyntax().isHumanReadable() && ( opContext.getValue().isBinary() ) )
        {
            String value = opContext.getValue().getString();
            opContext.setValue( new StringValue( value ) );
        }
        
        return next.compare( opContext );
    }
    
    
    /**
     * {@inheritDoc}
     */
    public void bind( NextInterceptor next, BindOperationContext opContext )  throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        next.bind( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void addContextPartition( NextInterceptor next, AddContextPartitionOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        next.addContextPartition( opContext );
    }


    /**
     * {@inheritDoc}
     */
    public void removeContextPartition( NextInterceptor next, RemoveContextPartitionOperationContext opContext ) throws Exception
    {
        opContext.getDn().normalize( schemaManager.getNormalizerMapping() );
        next.removeContextPartition( opContext );
    }


    /**
     * Adds missing RDN's attributes and values to the entry.
     *
     * @param dn the DN
     * @param entry the entry
     */
    private void addRdnAttributesToEntry( DN dn, ServerEntry entry ) throws Exception
    {
        if ( dn == null || entry == null )
        {
            return;
        }

        RDN rdn = dn.getRdn();

        // Loop on all the AVAs
        for ( AVA ava : rdn )
        {
            Value<?> value = ava.getNormValue();
            Value<?> upValue = ava.getUpValue();
            String upId = ava.getUpType();

            // Check that the entry contains this AVA
            if ( !entry.contains( upId, value ) )
            {
                String message = "The RDN '" + upId + "=" + upValue + "' is not present in the entry";
                LOG.warn( message );

                // We don't have this attribute : add it.
                // Two cases : 
                // 1) The attribute does not exist
                if ( !entry.containsAttribute( upId ) )
                {
                    entry.add( upId, upValue );
                }
                // 2) The attribute exists
                else
                {
                    AttributeType at = schemaManager.lookupAttributeTypeRegistry( upId );

                    // 2.1 if the attribute is single valued, replace the value
                    if ( at.isSingleValued() )
                    {
                        entry.removeAttributes( upId );
                        entry.add( upId, upValue );
                    }
                    // 2.2 the attribute is multi-valued : add the missing value
                    else
                    {
                        entry.add( upId, upValue );
                    }
                }
            }
        }
    }

}
