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


import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.asn1.ber.AbstractContainer;
import org.apache.directory.api.asn1.ber.tlv.TLV;
import org.apache.directory.api.ldap.codec.LdapMessageGrammar;
import org.apache.directory.api.ldap.codec.LdapStatesEnum;
import org.apache.directory.api.ldap.codec.search.ConnectorFilter;
import org.apache.directory.api.ldap.codec.search.Filter;
import org.apache.directory.api.ldap.codec.search.PresentFilter;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.ExtendedResponse;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.Message;
import org.apache.directory.api.ldap.model.message.ResultResponse;
import org.apache.directory.api.ldap.model.name.DnFactory;


/**
 * The LdapMessage container stores all the messages decoded by the Asn1Decoder.
 * When dealing with an encoding PDU, we will obtain a LdapMessage in the
 * container.
 *
 * @param <E> The decorated message
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class LdapMessageContainer<E extends Message> extends AbstractContainer
{
    /** The Message being decoded */
    private E message;

    /** checks if attribute is binary */
    private BinaryAttributeDetector binaryAttributeDetector;

    /** The message ID */
    private int messageId;

    /** The current control */
    private Control currentControl;
    
    /** The current control factory, if any */
    private ControlFactory<?> controlFactory;
    
    /** The DN Factory used to speed up Ldap Message DN decoding */
    private DnFactory dnFactory;
    
    /** The current Intermediate response factory */
    private IntermediateOperationFactory intermediateFactory;
    
    /** The current Extended operation factory */
    private ExtendedOperationFactory extendedFactory;

    /** The codec service */
    private final LdapApiService codec;
    
    /** The current LdapResult for a response */
    private LdapResult ldapResult;
    
    /** The current attribute being decoded */
    private Attribute currentAttribute;

    /** A local storage for the MODIFY operation */
    private Modification currentModification;
    
    /** The SearchRequest TLV id */
    private int tlvId;

    /** A temporary storage for a terminal Filter */
    private Filter terminalFilter;

    /** The current filter. This is used while decoding a PDU */
    private Filter currentFilter;

    /** The global filter. This is used while decoding a PDU */
    private Filter topFilter;


    /**
     * Creates a new LdapMessageContainer object. We will store ten grammars,
     * it's enough ...
     * 
     * @param codec The LDAP service instance
     */
    public LdapMessageContainer( LdapApiService codec )
    {
        this( codec, new DefaultConfigurableBinaryAttributeDetector() );
    }


    /**
     * Creates a new LdapMessageContainer object. 
     *
     * @param codec The LDAP service instance
     * @param binaryAttributeDetector checks if an attribute is binary
     */
    public LdapMessageContainer( LdapApiService codec, BinaryAttributeDetector binaryAttributeDetector )
    {
        super();
        this.codec = codec;
        setGrammar( LdapMessageGrammar.getInstance() );
        this.binaryAttributeDetector = binaryAttributeDetector;
        setTransition( LdapStatesEnum.START_STATE );
    }


    /**
     * Gets the {@link LdapApiService} associated with this Container.
     *
     * @return The LDAP service instance
     */
    public LdapApiService getLdapCodecService()
    {
        return codec;
    }


    /**
     * @return Returns the ldapMessage.
     */
    public E getMessage()
    {
        return message;
    }


    /**
     * Set a Message Object into the container. It will be completed by the
     * ldapDecoder.
     *
     * @param message The message to set.
     */
    public void setMessage( E message )
    {
        this.message = message;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void clean()
    {
        super.clean();

        messageId = -1;
        tlvId = -1;
        message = null;
        ldapResult = null;
        currentControl = null;
        currentAttribute = null;
        currentFilter = null;
        terminalFilter = null;
        topFilter = null;
        controlFactory = null;
        intermediateFactory = null;
        extendedFactory = null;
        setDecodedBytes( 0 );
    }


    /**
     * @return Returns true if the attribute is binary.
     * @param id checks if an attribute id is binary
     */
    public boolean isBinary( String id )
    {
        return binaryAttributeDetector.isBinary( id );
    }


    /**
     * @return The message ID
     */
    public int getMessageId()
    {
        return messageId;
    }


    /**
     * Set the message ID
     * @param messageId the id of the message
     */
    public void setMessageId( int messageId )
    {
        this.messageId = messageId;
    }


    /**
     * @return the current control being created
     */
    public Control getCurrentControl()
    {
        return currentControl;
    }


    /**
     * Store a newly created control
     * @param currentControl The control to store
     */
    public void setCurrentControl( Control currentControl )
    {
        this.currentControl = currentControl;
    }


    /**
     * Sets the binary attribute detector
     * 
     * @param binaryAttributeDetector the binary attribute detector
     */
    public void setBinaryAttributeDetector( BinaryAttributeDetector binaryAttributeDetector )
    {
        this.binaryAttributeDetector = binaryAttributeDetector;
    }


    /**
     * @return the binary attribute detector
     */
    public BinaryAttributeDetector getBinaryAttributeDetector()
    {
        return binaryAttributeDetector;
    }


    /**
     * @return the ldapResult
     */
    public LdapResult getLdapResult()
    {
        return ldapResult;
    }


    /**
     * @param ldapResult the ldapResult to set
     */
    public void setLdapResult( LdapResult ldapResult )
    {
        this.ldapResult = ldapResult;
    }


    /**
     * @return the controlFactory
     */
    public ControlFactory<?> getControlFactory()
    {
        return controlFactory;
    }


    /**
     * @param controlFactory the controlFactory to set
     */
    public void setControlFactory( ControlFactory<?> controlFactory )
    {
        this.controlFactory = controlFactory;
    }


    /**
     * @return the currentAttribute
     */
    public Attribute getCurrentAttribute()
    {
        return currentAttribute;
    }


    /**
     * @param currentAttribute the currentAttribute to set
     */
    public void setCurrentAttribute( Attribute currentAttribute )
    {
        this.currentAttribute = currentAttribute;
    }


    /**
     * @return the currentModification
     */
    public Modification getCurrentModification()
    {
        return currentModification;
    }


    /**
     * @param currentModification the currentModification to set
     */
    public void setCurrentModification( Modification currentModification )
    {
        this.currentModification = currentModification;
    }


    /**
     * Set the SearchRequest PDU TLV's Id
     * @param tlvId The TLV id
     */
    public void setTlvId( int tlvId )
    {
        this.tlvId = tlvId;
    }


    /**
     * @return the terminalFilter
     */
    public Filter getTerminalFilter()
    {
        return terminalFilter;
    }


    /**
     * @param terminalFilter the terminalFilter to set
     */
    public void setTerminalFilter( Filter terminalFilter )
    {
        this.terminalFilter = terminalFilter;
    }


    /**
     * @return the currentFilter
     */
    public Filter getCurrentFilter()
    {
        return currentFilter;
    }


    /**
     * @param currentFilter the currentFilter to set
     */
    public void setCurrentFilter( Filter currentFilter )
    {
        this.currentFilter = currentFilter;
    }


    /**
     * Add a current filter. We have two cases :
     * - there is no previous current filter : the filter
     * is the top level filter
     * - there is a previous current filter : the filter is added
     * to the currentFilter set, and the current filter is changed
     *
     * In any case, the previous current filter will always be a
     * ConnectorFilter when this method is called.
     *
     * @param localFilter The filter to set.
     * @throws DecoderException If the filter is invalid
     */
    public void addCurrentFilter( Filter localFilter ) throws DecoderException
    {
        if ( currentFilter != null )
        {
            // Ok, we have a parent. The new Filter will be added to
            // this parent, and will become the currentFilter if it's a connector.
            ( ( ConnectorFilter ) currentFilter ).addFilter( localFilter );
            localFilter.setParent( currentFilter, currentFilter.getTlvId() );

            if ( localFilter instanceof ConnectorFilter )
            {
                currentFilter = localFilter;
            }
        }
        else
        {
            // No parent. This Filter will become the root.
            currentFilter = localFilter;
            currentFilter.setParent( null, tlvId );
            topFilter = localFilter;
        }
    }


    /**
     * This method is used to clear the filter's stack for terminated elements. An element
     * is considered as terminated either if :
     *  - it's a final element (ie an element which cannot contains a Filter)
     *  - its current length equals its expected length.
     */
    public void unstackFilters()
    {
        TLV tlv = getCurrentTLV();
        TLV localParent = tlv.getParent();
        Filter localFilter = terminalFilter;

        // The parent has been completed, so fold it
        while ( ( localParent != null ) && ( localParent.getExpectedLength() == 0 ) )
        {
            int parentTlvId = localFilter.getParent() != null ? localFilter.getParent().getTlvId() : localFilter
                .getParentTlvId();

            if ( localParent.getId() != parentTlvId )
            {
                localParent = localParent.getParent();
            }
            else
            {
                Filter filterParent = localFilter.getParent();

                // We have a special case with PresentFilter, which has not been
                // pushed on the stack, so we need to get its parent's parent
                if ( localFilter instanceof PresentFilter )
                {
                    if ( filterParent == null )
                    {
                        // We don't have parent, get out
                        break;
                    }

                    filterParent = filterParent.getParent();
                }
                else
                {
                    filterParent = filterParent.getParent();
                }

                if ( filterParent != null )
                {
                    // The parent is a filter ; it will become the new currentFilter
                    // and we will loop again.
                    localFilter = currentFilter;
                    currentFilter = filterParent;
                    localParent = localParent.getParent();
                }
                else
                {
                    // We can stop the recursion, we have reached the searchResult Object
                    break;
                }
            }
        }
    }
    
    
    /**
     * Copy the LdapResult element from a opaque response to a newly created 
     * extendedResponse
     *  
     * @param resultResponse The original response
     * @param extendedResponse The newly created ExtendedResponse
     */
    public static void copyLdapResult( ResultResponse resultResponse, ExtendedResponse extendedResponse )
    {
        extendedResponse.getLdapResult().setDiagnosticMessage( resultResponse.getLdapResult().getDiagnosticMessage() );
        extendedResponse.getLdapResult().setMatchedDn( resultResponse.getLdapResult().getMatchedDn() );
        extendedResponse.getLdapResult().setReferral( resultResponse.getLdapResult().getReferral() );
        extendedResponse.getLdapResult().setResultCode( resultResponse.getLdapResult().getResultCode() );
    }


    /**
     * @return the topFilter
     */
    public Filter getTopFilter()
    {
        return topFilter;
    }


    /**
     * @param topFilter the topFilter to set
     */
    public void setTopFilter( Filter topFilter )
    {
        this.topFilter = topFilter;
    }


    /**
     * @return the tlvId
     */
    public int getTlvId()
    {
        return tlvId;
    }


    /**
     * @return the intermediateFactory
     */
    public IntermediateOperationFactory getIntermediateFactory()
    {
        return intermediateFactory;
    }


    /**
     * @param intermediateFactory the intermediateFactory to set
     */
    public void setIntermediateFactory( IntermediateOperationFactory intermediateFactory )
    {
        this.intermediateFactory = intermediateFactory;
    }


    /**
     * @return the extendedFactory
     */
    public ExtendedOperationFactory getExtendedFactory()
    {
        return extendedFactory;
    }


    /**
     * @param extendedFactory the extendedFactory to set
     */
    public void setExtendedFactory( ExtendedOperationFactory extendedFactory )
    {
        this.extendedFactory = extendedFactory;
    }


    /**
     * @return the dnFactory
     */
    public DnFactory getDnFactory()
    {
        return dnFactory;
    }


    /**
     * @param dnFactory the dnFactory to set
     */
    public void setDnFactory( DnFactory dnFactory )
    {
        this.dnFactory = dnFactory;
    }
}
