/*
 *  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.kerberos.credentials.cache;


import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.apache.directory.shared.kerberos.KerberosTime;
import org.apache.directory.shared.kerberos.codec.KerberosDecoder;
import org.apache.directory.shared.kerberos.codec.types.AuthorizationType;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.apache.directory.shared.kerberos.codec.types.HostAddrType;
import org.apache.directory.shared.kerberos.components.AuthorizationData;
import org.apache.directory.shared.kerberos.components.AuthorizationDataEntry;
import org.apache.directory.shared.kerberos.components.EncryptionKey;
import org.apache.directory.shared.kerberos.components.HostAddress;
import org.apache.directory.shared.kerberos.components.HostAddresses;
import org.apache.directory.shared.kerberos.components.PrincipalName;
import org.apache.directory.shared.kerberos.flags.TicketFlags;


/**
 * Reading credentials cache according to FCC format by reference the following
 * https://www.gnu.org/software/shishi/manual/html_node/The-Credential-Cache-Binary-File-Format.html
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class CacheInputStream extends DataInputStream
{
    public CacheInputStream( InputStream in )
    {
        super( in );
    }


    public void read( CredentialsCache cache ) throws IOException
    {
        int version;
        List<Tag> tags;
        PrincipalName principal;
        Credentials cred;

        version = readVersion();
        cache.setVersion( version );

        if ( version == CredentialsCacheConstants.FCC_FVNO_4 )
        {
            tags = readTag();
        }
        else
        {
            tags = null;
        }
        cache.setTags( tags );

        principal = readPrincipal( version );
        cache.setPrimaryPrincipalName( principal );

        while ( available() > 0 )
        {
            cred = readCredentials( version );
            if ( cred != null )
            {
                cache.addCredentials( cred );
            }
        }
    }


    private int readVersion() throws IOException
    {
        return readShort();
    }


    private List<Tag> readTag() throws IOException
    {
        int len;
        int tag;
        int taglen;
        int time;
        int usec;

        len = readShort();
        List<Tag> tags = new ArrayList<>();

        while ( len > 0 )
        {
            tag = readShort();
            taglen = readShort();

            if ( tag == CredentialsCacheConstants.FCC_TAG_DELTATIME )
            {
                time = readInt();
                usec = readInt();
                tags.add( new Tag( tag, time, usec ) );
            }
            else
            {
                read( new byte[taglen], 0, taglen ); // ignore unknown tag
            }

            len = len - ( 4 + taglen );
        }

        return tags;
    }


    private PrincipalName readPrincipal( int version ) throws IOException
    {
        int type;
        int length;
        PrincipalName pname;

        if ( version == CredentialsCacheConstants.FCC_FVNO_1 )
        {
            type = CredentialsCacheConstants.NT_UNKNOWN;
        }
        else
        {
            type = readInt();
        }

        length = readInt();

        if ( version == CredentialsCacheConstants.FCC_FVNO_1 )
        {
            length--;
        }

        String realm = readCountedString();

        String[] result = new String[length];

        for ( int i = 0; i < length; i++ )
        {
            result[i] = readCountedString();
        }

        pname = new PrincipalName( result, type );

        if ( isRealm( realm ) )
        {
            pname.setRealm( realm );
        }

        return pname;
    }


    private String readCountedString() throws IOException
    {
        int namelength = readInt();
        if ( namelength > CredentialsCacheConstants.MAXNAMELENGTH )
        {
            throw new IOException( "Invalid name length in principal name." );
        }
        byte[] bytes = new byte[namelength];
        read( bytes, 0, bytes.length );

        return new String( bytes, StandardCharsets.UTF_8 );
    }


    /*
     * Domain style realm names MUST look like domain names: they consist of
     * components separated by periods (.) and they contain neither colons
     * (:) nor slashes (/). When establishing a new realm name based on an 
     * internet domain name it is recommended by convention that the characters 
     * be converted to uppercase.
     */
    private static boolean isRealm( String str )
    {
        char chr;
        for ( int i = 0; i < str.length(); i++ )
        {
            chr = str.charAt( i );
            if ( chr != '.' && chr >= 'a' )
            {
                return false;
            }
        }

        return true;
    }


    private EncryptionKey readKey( int version ) throws IOException
    {
        int keyType;
        int keyLen;
        keyType = readShort();

        if ( version == CredentialsCacheConstants.FCC_FVNO_3 )
        {
            readShort();
        }

        // It's not correct with "uint16_t keylen", instead "uint32_t keylen" in keyblock 
        keyLen = readInt();
        byte[] bytes = new byte[keyLen];
        read( bytes, 0, bytes.length );

        return new EncryptionKey( EncryptionType.getTypeByValue( keyType ), bytes );
    }


    private KerberosTime[] readKerberosTimes() throws IOException
    {
        long[] times = readTimes();
        KerberosTime[] results = new KerberosTime[times.length];
        KerberosTime ktime;

        for ( int i = 0; i < times.length; ++i )
        {
            ktime = times[i] == 0 ? null : new KerberosTime( times[i] );
            results[i] = ktime;
        }

        return results;
    }


    private long[] readTimes() throws IOException
    {
        long[] times = new long[4];
        times[0] = ( long ) readInt() * 1000;
        times[1] = ( long ) readInt() * 1000;
        times[2] = ( long ) readInt() * 1000;
        times[3] = ( long ) readInt() * 1000;

        return times;
    }


    private boolean readskey() throws IOException
    {
        return read() != 0;
    }


    private HostAddress[] readAddr() throws IOException
    {
        int numAddrs;
        int addrType;
        int addrLength;
        numAddrs = readInt();

        if ( numAddrs > 0 )
        {
            HostAddress[] addrs = new HostAddress[numAddrs];

            for ( int i = 0; i < numAddrs; i++ )
            {
                addrType = readShort();
                addrLength = readInt();

                if ( !( addrLength == 4 || addrLength == 16 ) )
                {
                    return null;
                }

                byte[] result = new byte[addrLength];

                for ( int j = 0; j < addrLength; j++ )
                {
                    result[j] = readByte();
                }

                addrs[i] = new HostAddress( HostAddrType.getTypeByOrdinal( addrType ), result );
            }
            return addrs;
        }

        return null;
    }


    private AuthorizationDataEntry[] readAuth() throws IOException
    {
        int num;
        int adtype;
        int adlength;
        num = readInt();

        if ( num > 0 )
        {
            AuthorizationDataEntry[] auData = new AuthorizationDataEntry[num];
            byte[] data = null;

            for ( int i = 0; i < num; i++ )
            {
                adtype = readShort();
                adlength = readInt();
                data = new byte[adlength];
                read( data, 0, data.length );
                auData[i] = new AuthorizationDataEntry( AuthorizationType.getTypeByValue( adtype ), data );
            }

            return auData;
        }

        return null;
    }


    private byte[] readData() throws IOException
    {
        int length;
        length = readInt();
        if ( length == 0 )
        {
            return null;
        }
        else
        {
            byte[] bytes = new byte[length];
            read( bytes, 0, length );
            return bytes;
        }
    }


    private int readFlags() throws IOException
    {
        int ticketFlags;
        ticketFlags = readInt();
        return ticketFlags;
    }


    private Credentials readCredentials( int version ) throws IOException
    {
        PrincipalName cpname = readPrincipal( version );
        PrincipalName spname = readPrincipal( version );

        EncryptionKey key = readKey( version );

        KerberosTime[] times = readKerberosTimes();
        KerberosTime authtime = times[0];
        KerberosTime starttime = times[1];
        KerberosTime endtime = times[2];
        KerberosTime renewTill = times[3];

        boolean skey = readskey();

        int flags = readFlags();
        TicketFlags tFlags = new TicketFlags( flags );
        HostAddress addr[] = readAddr();
        HostAddresses addrs = null;

        if ( addr != null )
        {
            addrs = new HostAddresses( addr );
        }

        AuthorizationDataEntry[] auDataEntries = readAuth();
        AuthorizationData auData = null;

        if ( auDataEntries != null )
        {
            auData = new AuthorizationData();

            for ( AuthorizationDataEntry ade : auDataEntries )
            {
                auData.addEntry( ade );
            }
        }

        byte[] ticketData = readData();
        byte[] ticketData2 = readData();

        if ( version != CredentialsCacheConstants.FCC_FVNO_1 &&
            spname.getNameType().getValue() == CredentialsCacheConstants.NT_UNKNOWN )
        {
            // skip krb5_ccache_conf_data/fast_avail/krbtgt/REALM@REALM in MIT KRB5
            return null;
        }

        try
        {
            return new Credentials( cpname, spname, key, authtime, starttime,
                endtime, renewTill, skey, tFlags, addrs, auData,
                ticketData != null ? KerberosDecoder.decodeTicket( ticketData ) : null,
                ticketData2 != null ? KerberosDecoder.decodeTicket( ticketData2 ) : null );
        }
        catch ( Exception e )
        {
            return null;
        }
    }
}
