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


import java.nio.ByteBuffer;

import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswdErrorType;
import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswordException;
import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.asn1.ber.Asn1Container;
import org.apache.directory.api.asn1.ber.Asn1Decoder;
import org.apache.directory.api.asn1.ber.tlv.TLVStateEnum;
import org.apache.directory.shared.kerberos.codec.EncKdcRepPart.EncKdcRepPartContainer;
import org.apache.directory.shared.kerberos.codec.apRep.ApRepContainer;
import org.apache.directory.shared.kerberos.codec.apReq.ApReqContainer;
import org.apache.directory.shared.kerberos.codec.authenticator.AuthenticatorContainer;
import org.apache.directory.shared.kerberos.codec.authorizationData.AuthorizationDataContainer;
import org.apache.directory.shared.kerberos.codec.encApRepPart.EncApRepPartContainer;
import org.apache.directory.shared.kerberos.codec.encAsRepPart.EncAsRepPartContainer;
import org.apache.directory.shared.kerberos.codec.encKrbPrivPart.EncKrbPrivPartContainer;
import org.apache.directory.shared.kerberos.codec.encTgsRepPart.EncTgsRepPartContainer;
import org.apache.directory.shared.kerberos.codec.encTicketPart.EncTicketPartContainer;
import org.apache.directory.shared.kerberos.codec.encryptedData.EncryptedDataContainer;
import org.apache.directory.shared.kerberos.codec.encryptionKey.EncryptionKeyContainer;
import org.apache.directory.shared.kerberos.codec.krbPriv.KrbPrivContainer;
import org.apache.directory.shared.kerberos.codec.paEncTsEnc.PaEncTsEncContainer;
import org.apache.directory.shared.kerberos.codec.principalName.PrincipalNameContainer;
import org.apache.directory.shared.kerberos.codec.ticket.TicketContainer;
import org.apache.directory.shared.kerberos.components.AuthorizationData;
import org.apache.directory.shared.kerberos.components.EncKdcRepPart;
import org.apache.directory.shared.kerberos.components.EncKrbPrivPart;
import org.apache.directory.shared.kerberos.components.EncTicketPart;
import org.apache.directory.shared.kerberos.components.EncryptedData;
import org.apache.directory.shared.kerberos.components.EncryptionKey;
import org.apache.directory.shared.kerberos.components.PaEncTsEnc;
import org.apache.directory.shared.kerberos.components.PrincipalName;
import org.apache.directory.shared.kerberos.exceptions.ErrorType;
import org.apache.directory.shared.kerberos.exceptions.KerberosException;
import org.apache.directory.shared.kerberos.messages.ApRep;
import org.apache.directory.shared.kerberos.messages.ApReq;
import org.apache.directory.shared.kerberos.messages.Authenticator;
import org.apache.directory.shared.kerberos.messages.EncApRepPart;
import org.apache.directory.shared.kerberos.messages.EncAsRepPart;
import org.apache.directory.shared.kerberos.messages.EncTgsRepPart;
import org.apache.directory.shared.kerberos.messages.KrbPriv;
import org.apache.directory.shared.kerberos.messages.Ticket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class KerberosDecoder
{

    /** The logger */
    private static Logger LOG = LoggerFactory.getLogger( KerberosDecoder.class );

    /** A speedup for logger */
    private static final boolean IS_DEBUG = LOG.isDebugEnabled();


    public static Object decode( KerberosMessageContainer kerberosMessageContainer, Asn1Decoder asn1Decoder ) throws DecoderException
    {
        ByteBuffer buf = kerberosMessageContainer.getStream();
        
        if ( kerberosMessageContainer.isTCP() )
        {
            if ( buf.remaining() > 4 )
            {
                kerberosMessageContainer.setTcpLength( buf.getInt() );
                buf.mark();
            }
            else
            {
                return null;
            }
        }
        else
        {
            buf.mark();
        }

        while ( buf.hasRemaining() )
        {
            try
            {
                asn1Decoder.decode( buf, kerberosMessageContainer );
                
                if ( kerberosMessageContainer.getState() == TLVStateEnum.PDU_DECODED )
                {
                    if ( IS_DEBUG )
                    {
                        LOG.debug( "Decoded KerberosMessage : " + kerberosMessageContainer.getMessage() );
                        buf.mark();
                    }
        
                    return kerberosMessageContainer.getMessage();
                }
            }
            catch ( DecoderException de )
            {
                LOG.warn( "error while decoding", de );
                buf.clear();
                kerberosMessageContainer.clean();
                throw de;
            }
        }
        
        return null;
    }
    
    
    /**
     * Decode an EncrytedData structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncryptedData
     * @throws KerberosException If the decoding fails
     */
    public static EncryptedData decodeEncryptedData( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncryptedData Container
        Asn1Container encryptedDataContainer = new EncryptedDataContainer();

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncryptedData PDU
        try
        {
            kerberosDecoder.decode( stream, encryptedDataContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncryptedData
        EncryptedData encryptedData = ( ( EncryptedDataContainer ) encryptedDataContainer ).getEncryptedData();

        return encryptedData;
    }
    
    
    /**
     * Decode an PaEncTsEnc structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of PaEncTsEnc
     * @throws KerberosException If the decoding fails
     */
    public static PaEncTsEnc decodePaEncTsEnc( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a PaEncTsEnc Container
        Asn1Container paEncTsEncContainer = new PaEncTsEncContainer();

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the PaEncTsEnc PDU
        try
        {
            kerberosDecoder.decode( stream, paEncTsEncContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded PaEncTsEnc
        PaEncTsEnc paEncTsEnc = ( ( PaEncTsEncContainer ) paEncTsEncContainer ).getPaEncTsEnc();

        return paEncTsEnc;
    }
    
    
    /**
     * Decode an EncApRepPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncApRepPart
     * @throws KerberosException If the decoding fails
     */
    public static EncApRepPart decodeEncApRepPart( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncApRepPart Container
        Asn1Container encApRepPartContainer = new EncApRepPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncApRepPart PDU
        try
        {
            kerberosDecoder.decode( stream, encApRepPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncApRepPart
        EncApRepPart encApRepPart = ( ( EncApRepPartContainer ) encApRepPartContainer ).getEncApRepPart();

        return encApRepPart;
    }
    
    
    /**
     * Decode an EncKdcRepPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncKdcRepPart
     * @throws KerberosException If the decoding fails
     */
    public static EncKdcRepPart decodeEncKdcRepPart( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncKdcRepPart Container
        Asn1Container encKdcRepPartContainer = new EncKdcRepPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncKdcRepPart PDU
        try
        {
            kerberosDecoder.decode( stream, encKdcRepPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncKdcRepPart
        EncKdcRepPart encKdcRepPart = ( ( EncKdcRepPartContainer ) encKdcRepPartContainer ).getEncKdcRepPart();

        return encKdcRepPart;
    }
    
    
    /**
     * Decode an EncKrbPrivPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncKrbPrivPart
     * @throws KerberosException If the decoding fails
     */
    public static EncKrbPrivPart decodeEncKrbPrivPart( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncKrbPrivPart Container
        Asn1Container encKrbPrivPartContainer = new EncKrbPrivPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncKrbPrivPart PDU
        try
        {
            kerberosDecoder.decode( stream, encKrbPrivPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncKrbPrivPart
        EncKrbPrivPart encKrbPrivPart = ( ( EncKrbPrivPartContainer ) encKrbPrivPartContainer ).getEncKrbPrivPart();

        return encKrbPrivPart;
    }
    
    
    /**
     * Decode an EncTicketPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncTicketPart
     * @throws KerberosException If the decoding fails
     */
    public static EncTicketPart decodeEncTicketPart( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncTicketPart Container
        Asn1Container encTicketPartContainer = new EncTicketPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncTicketPart PDU
        try
        {
            kerberosDecoder.decode( stream, encTicketPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncTicketPart
        EncTicketPart encTicketPart = ( ( EncTicketPartContainer ) encTicketPartContainer ).getEncTicketPart();

        return encTicketPart;
    }
    
    
    /**
     * Decode an EncryptionKey structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncryptionKey
     * @throws KerberosException If the decoding fails
     */
    public static EncryptionKey decodeEncryptionKey( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncryptionKey Container
        Asn1Container encryptionKeyContainer = new EncryptionKeyContainer();

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncryptionKey PDU
        try
        {
            kerberosDecoder.decode( stream, encryptionKeyContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncryptionKey
        EncryptionKey encryptionKey = ( ( EncryptionKeyContainer ) encryptionKeyContainer ).getEncryptionKey();

        return encryptionKey;
    }
    
    
    /**
     * Decode an PrincipalName structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of PrincipalName
     * @throws KerberosException If the decoding fails
     */
    public static PrincipalName decodePrincipalName( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a PrincipalName Container
        Asn1Container principalNameContainer = new PrincipalNameContainer();

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the PrincipalName PDU
        try
        {
            kerberosDecoder.decode( stream, principalNameContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded PrincipalName
        PrincipalName principalName = ( ( PrincipalNameContainer ) principalNameContainer ).getPrincipalName();

        return principalName;
    }
    
    
    /**
     * Decode a Ticket structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of Ticket
     * @throws KerberosException If the decoding fails
     */
    public static Ticket decodeTicket( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a Ticket Container
        Asn1Container ticketContainer = new TicketContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the Ticket PDU
        try
        {
            kerberosDecoder.decode( stream, ticketContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded Ticket
        Ticket ticket = ( ( TicketContainer ) ticketContainer ).getTicket();

        return ticket;
    }
    
    
    /**
     * Decode a Authenticator structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of Authenticator
     * @throws KerberosException If the decoding fails
     */
    public static Authenticator decodeAuthenticator( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a Authenticator Container
        Asn1Container authenticatorContainer = new AuthenticatorContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the Ticket PDU
        try
        {
            kerberosDecoder.decode( stream, authenticatorContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded Authenticator
        Authenticator authenticator = ( ( AuthenticatorContainer ) authenticatorContainer ).getAuthenticator();

        return authenticator;
    }
    
    
    /**
     * Decode a AuthorizationData structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of AuthorizationData
     * @throws KerberosException If the decoding fails
     */
    public static AuthorizationData decodeAuthorizationData( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a AuthorizationData Container
        Asn1Container authorizationDataContainer = new AuthorizationDataContainer();

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the Ticket PDU
        try
        {
            kerberosDecoder.decode( stream, authorizationDataContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded AuthorizationData
        AuthorizationData authorizationData = ( ( AuthorizationDataContainer ) authorizationDataContainer ).getAuthorizationData();

        return authorizationData;
    }

    
    /**
     * Decode a AP-REP structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of ApRep
     * @throws KerberosException If the decoding fails
     */
    public static ApRep decodeApRep( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a ApRep Container
        Asn1Container apRepContainer = new ApRepContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the ApRep PDU
        try
        {
            kerberosDecoder.decode( stream, apRepContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded ApRep
        ApRep apRep = ( ( ApRepContainer ) apRepContainer ).getApRep();

        return apRep;
    }

    
    /**
     * Decode a AP-REQ structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of ApReq
     * @throws KerberosException If the decoding fails
     */
    public static ApReq decodeApReq( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a ApReq Container
        Asn1Container apReqContainer = new ApReqContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the ApReq PDU
        try
        {
            kerberosDecoder.decode( stream, apReqContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded ApReq
        ApReq apReq = ( ( ApReqContainer ) apReqContainer ).getApReq();

        return apReq;
    }

    
    /**
     * Decode a KRB-PRIV structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of KrbPriv
     * @throws KerberosException If the decoding fails
     */
    public static KrbPriv decodeKrbPriv( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a KrbPriv Container
        Asn1Container krbPrivContainer = new KrbPrivContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the KrbPriv PDU
        try
        {
            kerberosDecoder.decode( stream, krbPrivContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded KrbPriv
        KrbPriv krbPriv = ( ( KrbPrivContainer ) krbPrivContainer ).getKrbPriv();

        return krbPriv;
    }
    
    
    /**
     * Decode an EncAsRepPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncAsRepPart
     * @throws KerberosException If the decoding fails
     */
    public static EncAsRepPart decodeEncAsRepPart( byte[] data ) throws KerberosException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncAsRepPart Container
        Asn1Container encAsRepPartContainer = new EncAsRepPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncAsRepPart PDU
        try
        {
            kerberosDecoder.decode( stream, encAsRepPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new KerberosException( ErrorType.KRB_AP_ERR_BAD_INTEGRITY, de );
        }

        // get the decoded EncAsRepPart
        EncAsRepPart encAsRepPart = ( ( EncAsRepPartContainer ) encAsRepPartContainer ).getEncAsRepPart();

        return encAsRepPart;
    }

    
    /**
     * Decode an EncTgsRepPart structure
     * 
     * @param data The byte array containing the data structure to decode
     * @return An instance of EncTgsRepPart
     * @throws KerberosException If the decoding fails
     */
    public static EncTgsRepPart decodeEncTgsRepPart( byte[] data ) throws ChangePasswordException
    {
        ByteBuffer stream = ByteBuffer.allocate( data.length );
        stream.put( data );
        stream.flip();
        
        // Allocate a EncTgsRepPart Container
        Asn1Container encTgsRepPartContainer = new EncTgsRepPartContainer( stream );

        Asn1Decoder kerberosDecoder = new Asn1Decoder();

        // Decode the EncTgsRepPart PDU
        try
        {
            kerberosDecoder.decode( stream, encTgsRepPartContainer );
        }
        catch ( DecoderException de )
        {
            throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_MALFORMED, de );
        }

        // get the decoded EncTgsRepPart
        EncTgsRepPart encTgsRepPart = ( ( EncTgsRepPartContainer ) encTgsRepPartContainer ).getEncTgsRepPart();

        return encTgsRepPart;
    }
}
