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


import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;

import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.i18n.I18n;


/**
 * An immutable representation of an object identifier that provides conversion 
 * between their <code>String</code>, and encoded <code>byte[]</code> 
 * representations.
 * 
 * <p> The encoding of OID values is performed according to 
 * <a href='http://www.itu.int/rec/T-REC-X.690/en'>itu X.690</a> section 8.19.
 * Specifically:</p>
 * 
 * <p><b>8.19.2</b> The contents octets shall be an (ordered) list of encodings
 * of subidentifiers (see 8.19.3 and 8.19.4) concatenated together. Each 
 * subidentifier is represented as a series of (one or more) octets. Bit 8 of 
 * each octet indicates whether it is the last in the series: bit 8 of the last 
 * octet is zero; bit 8 of each preceding octet is one. Bits 7 to 1 of the 
 * octets in the series collectively encode the subidentifier. Conceptually, 
 * these groups of bits are concatenated to form an unsigned binary number whose 
 * most significant bit is bit 7 of the first octet and whose least significant 
 * bit is bit 1 of the last octet. The subidentifier shall be encoded in the 
 * fewest possible octets, that is, the leading octet of the subidentifier shall 
 * not have the value 0x80. </p>
 * 
 * <p><b>8.19.3</b> The number of subidentifiers (N) shall be one less than the 
 * number of object identifier components in the object identifier value being 
 * encoded.</p>
 * 
 * <p><b>8.19.4</b> The numerical value of the first subidentifier is derived 
 * from the values of the first two object identifier components in the object 
 * identifier value being encoded, using the formula:
 * <br><code>(X*40) + Y</code><br> 
 * where X is the value of the first object identifier component and Y is the 
 * value of the second object identifier component. <i>NOTE – This packing of 
 * the first two object identifier components recognizes that only three values 
 * are allocated from the root node, and at most 39 subsequent values from nodes 
 * reached by X = 0 and X = 1.</i></p>
 * 
 * <p>For example, the OID "2.12.3456.7" would be turned into a list of 3 values:
 * <code>[((2*40)+12), 3456, 7]</code>. The first of which, 
 * <code>92</code>, would be encoded as the bytes <code>0x5C</code>, the second 
 * would be <code>[0x9B, 0x00]</code>, and the third as <code>0x07</code>
 * giving the final encoding <code>[0x5C, 0x9B, 0x00, 0x07]</code>.</p>
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class Oid
{
    /** A byte[] representation of an OID */
    private byte[] oidBytes;
    
    /** The OID as a String */
    private String oidString;
    
    private static final BigInteger JOINT_ISO_ITU_T = BigInteger.valueOf( 80 );
    
    /**
     * The OID FSA states. We have the following Finite State Automaton :
     * 
     * <pre>
     * (Start) --['0','1']--&gt; (A)
     * (start) --['2']--&gt; (F)
     * 
     * (A) --['.']--&gt; (B)
     * 
     * (B) --['0']--&gt; (D)
     * (B) --['1'..'3']--&gt; (C)
     * (B) --['4'..'9']--&gt; (E)
     * 
     * (C) --[]--&gt; (End)
     * (C) --['.']--&gt; (K)
     * (C) --['0'..'9']--&gt; (E)
     * 
     * (D) --[]--&gt; (End)
     * (D) --['.']--&gt; (K)
     * 
     * (E) --[]--&gt; (End)
     * (E) --['.']--&gt; (K)
     * 
     * (F) --['.']--&gt; (G)
     * 
     * (G) --['0']--&gt; (I)
     * (G) --['1'..'9']--&gt; (H)
     *
     * (H) --[]--&gt; (End)
     * (H) --['.']--&gt; (K)
     * (H) --['0'..'9']--&gt; (J)
     * 
     * (I) --[]--&gt; (End)
     * (I) --['.']--&gt; (K)
     *
     * (J) --[]--&gt; (End)
     * (J) --['.']--&gt; (K)
     * (J) --['0'..'9']--&gt; (J)
     * 
     * (K) --['0']--&gt; (M) 
     * (K) --['1'..'9']--&gt; (L)
     * 
     * (L) --[]--&gt; (End)
     * (L) --['.']--&gt; (K)
     * (L) --['0'..'9']--&gt; (L)
     * 
     * (M) --[]--&gt; (End)
     * (M) --['.']--&gt; (K)
     * </pre>
     */
    private enum OidFSAState 
    {
        START,
        STATE_A,
        STATE_B,
        STATE_C,
        STATE_D,
        STATE_E,
        STATE_F,
        STATE_G,
        STATE_H,
        STATE_I,
        STATE_J,
        STATE_K,
        STATE_L,
        STATE_M,
    }


    /**
     * Creates a new instance of Oid.
     *
     * @param oidString The OID as a String
     * @param oidBytes The OID as a byte[]
     */
    private Oid( String oidString, byte[] oidBytes )
    {
        this.oidString = oidString;
        this.oidBytes = new byte[oidBytes.length];
        System.arraycopy( oidBytes, 0, this.oidBytes, 0, oidBytes.length );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals( Object other )
    {
        return ( other instanceof Oid )
            && oidString.equals( ( ( Oid ) other ).oidString );
    }


    /**
     * Decodes an OID from a <code>byte[]</code>.
     * 
     * @param oidBytes The encoded<code>byte[]</code>
     * @return A new Oid
     * @throws DecoderException When the OID is not valid
     */
    public static Oid fromBytes( byte[] oidBytes ) throws DecoderException
    {
        if ( ( oidBytes == null ) || ( oidBytes.length < 1 ) )
        {
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, Arrays.toString( oidBytes ) ) );
        }

        StringBuilder builder = new StringBuilder();
        long value = 0;
        int valStart = 0;
        int valLength = 0;
        boolean firstArc = true;
        
        for ( int i = 0; i < oidBytes.length; i++ )
        {
            value |= oidBytes[i] & 0x7F;

            if ( oidBytes[i] < 0 )
            {
                // leading 1, so value continues
                value = value << 7;
                valLength++;
            }
            else
            {
                valLength++;
                
                if ( valLength > 8 )
                {
                    // Above 9 bytes, we won't be able to store the value in a long...
                    // Compute the number of necessary bytes
                    int nbBytes = valLength * 7 / 8;
                    
                    if ( valLength % 7 != 0 )
                    {
                        nbBytes++;
                    }
                    
                    byte[] result = new byte[nbBytes];
                    
                    // Now iterate on the incoming bytes
                    int pos = nbBytes - 1;
                    int valEnd = valStart + valLength - 1;
                    int j = 0;
                    
                    while ( j < valLength - 8 )
                    {
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 1] << 7 ) | ( oidBytes[valEnd - j] & 0x7F ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 2] << 6 ) | ( ( oidBytes[valEnd - j - 1] & 0x7E ) >> 1 ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 3] << 5 ) | ( ( oidBytes[valEnd - j - 2] & 0x7C ) >> 2 ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 4] << 4 ) | ( ( oidBytes[valEnd - j - 3] & 0x78 ) >> 3 ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 5] << 3 ) | ( ( oidBytes[valEnd - j - 4] & 0x70 ) >> 4 ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 6] << 2 ) | ( ( oidBytes[valEnd - j - 5] & 0x60 ) >> 5 ) );
                        result[pos--] = ( byte ) ( ( oidBytes[valEnd - j - 7] << 1 ) | ( ( oidBytes[valEnd - j - 6] & 0x40 ) >> 6 ) );
                        j += 8;
                    }
                    
                    switch ( valLength - j )
                    {
                        case 7 :
                            result[pos--] = ( byte ) ( ( oidBytes[5] << 7 ) | ( oidBytes[6] & 0x7F ) );
                            result[pos--] = ( byte ) ( ( oidBytes[4] << 6 ) | ( ( oidBytes[5] & 0x7E ) >> 1 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[3] << 5 ) | ( ( oidBytes[4] & 0x7C ) >> 2 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[2] << 4 ) | ( ( oidBytes[3] & 0x78 ) >> 3 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[1] << 3 ) | ( ( oidBytes[2] & 0x70 ) >> 4 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 2 ) | ( ( oidBytes[1] & 0x60 ) >> 5 ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x40 ) >> 6 );
                            break;
                            
                        case 6 :
                            result[pos--] = ( byte ) ( ( oidBytes[4] << 7 ) | ( oidBytes[5] & 0x7F ) );
                            result[pos--] = ( byte ) ( ( oidBytes[3] << 6 ) | ( ( oidBytes[4] & 0x7E ) >> 1 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[2] << 5 ) | ( ( oidBytes[3] & 0x7C ) >> 2 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[1] << 4 ) | ( ( oidBytes[2] & 0x78 ) >> 3 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 3 ) | ( ( oidBytes[1] & 0x70 ) >> 4 ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x60 ) >> 5 );
                            break;

                        case 5 :
                            result[pos--] = ( byte ) ( ( oidBytes[3] << 7 ) | ( oidBytes[4] & 0x7F ) );
                            result[pos--] = ( byte ) ( ( oidBytes[2] << 6 ) | ( ( oidBytes[3] & 0x7E ) >> 1 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[1] << 5 ) | ( ( oidBytes[2] & 0x7C ) >> 2 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 4 ) | ( ( oidBytes[1] & 0x78 ) >> 3 ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x70 ) >> 4 );
                            break;
                            
                        case 4 :
                            result[pos--] = ( byte ) ( ( oidBytes[2] << 7 ) | ( oidBytes[3] & 0x7F ) );
                            result[pos--] = ( byte ) ( ( oidBytes[1] << 6 ) | ( ( oidBytes[2] & 0x7E ) >> 1 ) );
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 5 ) | ( ( oidBytes[1] & 0x7C ) >> 2 ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x78 ) >> 3 );
                            break;
                            
                        case 3 :
                            result[pos--] = ( byte ) ( ( oidBytes[1] << 7 ) | ( oidBytes[2] & 0x7F ) );
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 6 ) | ( ( oidBytes[1] & 0x7E ) >> 1 ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x7C ) >> 2 );
                            break;

                        case 2 :
                            result[pos--] = ( byte ) ( ( oidBytes[0] << 7 ) | ( oidBytes[1] & 0x7F ) );
                            result[pos] = ( byte ) ( ( oidBytes[0] & 0x7E ) >> 1 );
                            break;
                            
                        case 1 :
                            result[pos] = ( byte ) ( oidBytes[0] & 0x7F );
                            break;
                            
                        default :
                            // Exist to please checkstyle...
                            break;
                    }
                    
                    BigInteger bigInteger;
                    
                    if ( ( result[0] & 0x80 ) == 0x80 )
                    {
                        byte[] newResult = new byte[result.length + 1];
                        System.arraycopy( result, 0, newResult, 1, result.length );
                        result = newResult;
                    }
                    
                    bigInteger = new BigInteger( result );
                    
                    if ( firstArc )
                    {
                        // This is a joint-iso-itu-t(2) arc
                        bigInteger = bigInteger.subtract( JOINT_ISO_ITU_T );
                        builder.append( '2' );
                    }
                    
                    builder.append( '.' ).append( bigInteger.toString() );
                }
                else
                {
                    // value completed
                    if ( firstArc )
                    {
                        // first value special processing
                        if ( value >= 80 )
                        {
                            // starts with 2
                            builder.append( '2' );
                            value = value - 80;
                        }
                        else
                        {
                            // starts with 0 or 1
                            long one = value / 40;
                            long two = value % 40;
    
                            if ( ( one < 0 ) || ( one > 2 ) || ( two < 0 ) || ( ( one < 2 ) && ( two > 39 ) ) )
                            {
                                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID,
                                    Arrays.toString( oidBytes ) ) );
                            }
    
                            if ( one < 2 )
                            {
                                builder.append( one );
                                value = two;
                            }
                        }
                        
                        firstArc = false;
                    }
    
                    // normal processing
                    builder.append( '.' ).append( value );
                }
                
                valStart = i;
                valLength = 0;
                value = 0;
            }
        }
    
        return new Oid( builder.toString(), oidBytes );
    }


    /**
     * Process state A
     * <pre>
     * (Start) --['0','1']--&gt; (A)
     * (start) --['2']--&gt; (F)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateStart( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
            case '1' :
                buffer[0] = ( byte ) ( ( c - '0' ) * 40 );
                return OidFSAState.STATE_A;
                
            case '2' :
                return OidFSAState.STATE_F;
                
            default :
                // This is an error
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "Should start with 0, 1 or 2" ) );
        }
    }
    
    
    /**
     * Process state B
     * <pre>
     * (A) --['.']--&gt; (B)
     * </pre>
     * 
     * @param oid The OID to check
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateA( String oid, int pos ) throws DecoderException
    {
        if ( oid.charAt( pos ) != '.' )
        {
            // Expecting a Dot here
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a '.' is expected" ) );
        }
        
        return OidFSAState.STATE_B;
    }
    
    
    /**
     * Process state B
     * <pre>
     * (B) --['0']--&gt; (D)
     * (B) --['1'..'3']--&gt; (C)
     * (B) --['4'..'9']--&gt; (E)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateB( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
                return OidFSAState.STATE_D;
                
            case '1' :
            case '2' :
            case '3' :
                // We may have a second digit. Atm, store the current one in the second psotion
                buffer[1] = ( byte ) ( c - '0' );
                
                return  OidFSAState.STATE_C;
                
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                buffer[0] += ( byte ) ( c - '0' );
                return OidFSAState.STATE_E;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state C
     * <pre>
     * (C) --['.']--&gt; (K)
     * (C) --['0'..'9']--&gt; (E)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateC( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                buffer[0] += ( byte ) ( buffer[1] * 10 + ( c - '0' ) );
                buffer[1] = 0;
                return OidFSAState.STATE_E;

            case '.' :
                buffer[0] += buffer[1];
                buffer[1] = 0;
                return OidFSAState.STATE_K;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state D and E
     * <pre>
     * (D) --['.']--&gt; (K)
     * (E) --['.']--&gt; (K)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateDE( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        if ( c != '.' )
        {
            // Expecting a '.' here
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a dot is expected" ) );
        }
        
        // Store the first byte into it
        buffer[0] = ( byte ) ( buffer[0] | buffer[1] );
        buffer[1] = 0;
        
        return OidFSAState.STATE_K;
    }
    
    
    /**
     * Process state F
     * <pre>
     * (F) --['.']--&gt; (G)
     * </pre>
     * 
     * @param oid The OID to check
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateF( String oid, int pos ) throws DecoderException
    {
        if ( oid.charAt( pos ) != '.' )
        {
            // Expecting a Dot here
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a '.' is expected" ) );
        }
        
        return OidFSAState.STATE_G;
    }
    
    
    /**
     * Process state G
     * <pre>
     * (G) --['0']--&gt; (I)
     * (G) --['1'..'9']--&gt; (H)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateG( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
                buffer[0] = ( byte ) 80;
                return OidFSAState.STATE_I;
                
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                // Store the read digit in the second position in the buffer
                buffer[0] = ( byte ) ( c - '0' );
                return OidFSAState.STATE_H;

            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state H
     * <pre>
     * (H) --['.']--&gt; (K)
     * (H) --['0'..'9']--&gt; (J)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateH( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                // Store the read digit in the first position in the buffer
                buffer[1] = ( byte ) ( c - '0' );
                return OidFSAState.STATE_J;

            case '.' :
                // The first 2 arcs are single digit, we can collapse them in one byte.
                buffer[0] = ( byte ) ( 80 + buffer[0] );
                
                return OidFSAState.STATE_K;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state I
     * <pre>
     * (I) --['.']--&gt; (K)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateI( String oid, byte[] buffer, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        if ( c == '.' )
        {
            // The first 2 arcs are single digit, we can collapse them in one byte.
            buffer[0] = ( byte ) ( 80 + buffer[1] );
            return OidFSAState.STATE_K;
        }
        else
        {
            // Expecting a digit here
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state J
     * <pre>
     * (J) --['.']--&gt; (K)
     * (J) --['0'..'9']--&gt; (J)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param bufferPos The current position in the buffer
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateJ( String oid, byte[] buffer, int bufferPos, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '.' :
                return OidFSAState.STATE_K;
                
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                // Store the new digit at the right position in the buffer
                buffer[bufferPos] = ( byte ) ( c - '0' );
                return OidFSAState.STATE_J;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state J
     * <pre>
     * (K) --['0']--&gt; (M)
     * (K) --['1'..'9']--&gt; (L)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param bufferPos The current position in the buffer
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateK( String oid, byte[] buffer, int bufferPos, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '0' :
                buffer[bufferPos] = 0x00;
                return OidFSAState.STATE_M;
                
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                // Store the new digit at the right position in the buffer
                return OidFSAState.STATE_L;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit is expected" ) );
        }
    }
    
    
    /**
     * Process state J
     * <pre>
     * (L) --['.']--&gt; (K)
     * (L) --['0'..'9']--&gt; (L)
     * </pre>
     * 
     * @param oid The OID to check
     * @param buffer The buffer gathering the binary OID
     * @param bufferPos The current position in the buffer
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateL( String oid, byte[] buffer, int bufferPos, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        switch ( c )
        {
            case '.' :
                return OidFSAState.STATE_K;
                
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
            case '6' :
            case '7' :
            case '8' :
            case '9' :
                // Store the new digit at the right position in the buffer
                buffer[bufferPos] = ( byte ) ( c - '0' );
                
                return OidFSAState.STATE_L;
                
            default :
                // Expecting a digit here
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a digit or a dot is expected" ) );
        }
    }

    
    /**
     * Process state J
     * <pre>
     * (M) --['.']--&gt; (K)
     * </pre>
     * 
     * @param oid The OID to check
     * @param pos The position in the OID string
     * @return The next OID decoding state
     * @throws DecoderException If the OID is invalid
     */
    private static OidFSAState processStateM( String oid, int pos ) throws DecoderException
    {
        char c = oid.charAt( pos );
        
        if ( c == '.' )
        {
            return OidFSAState.STATE_K;
        }
        else
        {
            // Expecting a '.' here
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "a '.' is expected" ) );
        }
    }

    
    /**
     * Convert a list of digits to a list of 7 bits bytes. We must start by the end, and we don't
     * know how many bytes we will need, except when we will be done with the conversion.
     * 
     * @param oid The OID to convert
     * @param buffer The buffer gathering the binary OID
     * @param start The starting position in the OID string
     * @param nbDigits Teh number of digits the OID has 
     * @param posBuffer The position in the buffer
     * @param isJointIsoItuT A flag set if we know the OID is a JointIsoItuT OID
     * @return The number of bytes required to store the OID in a PDU
     */
    private static int convert( String oid, byte[] buffer, int start, int nbDigits, int posBuffer, boolean isJointIsoItuT )
    {
        if ( nbDigits < 3 )
        {
            // Speedup when we have a number in [0..99] : it's guaranteed to be hold
            // by a single byte.
            if ( isJointIsoItuT )
            {
                // Another special case : this is an OID that starts with '2.'
                buffer[0] =  ( byte ) ( 80 + ( oid.charAt( 2 ) - '0' ) * 10 + ( oid.charAt( 3 ) - '0' ) );
                
                if ( buffer[0] < 0 )
                {
                    // Here, we need 2 bytes
                    buffer[1] = ( byte ) ( buffer[0] & 0x007F );
                    buffer[0] = ( byte ) 0x81;
                    
                    return 2;
                }
                else
                {
                    return 1;
                }
            }
            else
            {
                if ( nbDigits == 1 )
                {
                    buffer[posBuffer] = ( byte ) ( oid.charAt( start ) - '0' );
                }
                else
                {
                    buffer[posBuffer] = ( byte ) ( ( oid.charAt( start ) - '0' ) * 10 + ( oid.charAt( start + 1 ) - '0' ) );
                    
                }
                return 1;
            }

        }
        else if ( nbDigits < 19 )
        {
            // The value can be hold in a Long if it's up to 999999999999999999 
            // Convert the String to a long :
            String number = oid.substring( start, start + nbDigits );

            long value = Long.parseLong( number );

            if ( isJointIsoItuT )
            {
                value += 80L;
            }
            
            // Convert the long to a byte array
            if ( ( value & 0xFFFFFFFFFFFFFF80L ) == 0 )
            {
                // The value will be hold in one byte
                buffer[posBuffer] = ( byte ) ( value );
                
                return 1;
            }
            
            if ( ( value & 0xFFFFFFFFFFFFC000L ) == 0 )
            {
                // The value is between 0x80 and 0x3FFF : it will be hold in 2 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( value & 0x000000000000007FL );
                
                return 2;
            }
            
            if ( ( value & 0xFFFFFFFFFFE00000L ) == 0 )
            {
                // The value is between 0x4000 and 0x1FFFFF : it will be hold in 3 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( value & 0x000000000000007FL );
                
                return 3;
            }
            
            if ( ( value & 0xFFFFFFFFF0000000L ) == 0 )
            {
                // The value is between 0x200000 and 0xFFFFFFF : it will be hold in 4 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( value & 0x000000000000007FL );
                
                return 4;
            }

            if ( ( value & 0xFFFFFFF800000000L ) == 0 )
            {
                // The value is between 0x10000000 and 0x7FFFFFFFF : it will be hold in 5 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x00000007F0000000L ) >> 28 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 4] = ( byte ) ( value & 0x000000000000007FL );
                
                return 5;
            }

            if ( ( value & 0xFFFFFC0000000000L ) == 0 )
            {
                // The value is between 0x800000000 and 0x3FFFFFFFFFF : it will be hold in 6 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x000003F800000000L ) >> 35 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x00000007F0000000L ) >> 28 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 4] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 5] = ( byte ) ( value & 0x000000000000007FL );
                
                return 6;
            }

            if ( ( value & 0xFFFE000000000000L ) == 0 )
            {
                // The value is between 0x40000000000 and 0x1FFFFFFFFFFFF : it will be hold in 7 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x0001FC0000000000L ) >> 42 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x000003F800000000L ) >> 35 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x00000007F0000000L ) >> 28 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 4] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 5] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 6] = ( byte ) ( value & 0x000000000000007FL );
                
                return 7;
            }

            if ( ( value & 0xFF00000000000000L ) == 0 )
            {
                // The value is between 0x2000000000000 and 0xFF000000000000 : it will be hold in 8 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x00FE000000000000L ) >> 49 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x0001FC0000000000L ) >> 42 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x000003F800000000L ) >> 35 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( ( byte ) ( ( value & 0x00000007F0000000L ) >> 28 ) | 0x80 );
                buffer[posBuffer + 4] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 5] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 6] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 7] = ( byte ) ( value & 0x000000000000007FL );
                
                return 8;
            }
            else
            {
                // The value is between 0x100000000000000 and 0x7F00000000000000 : it will be hold in 9 bytes
                buffer[posBuffer] = ( byte ) ( ( byte ) ( ( value & 0x7F00000000000000L ) >> 56 ) | 0x80 );
                buffer[posBuffer + 1] = ( byte ) ( ( byte ) ( ( value & 0x00FE000000000000L ) >> 49 ) | 0x80 );
                buffer[posBuffer + 2] = ( byte ) ( ( byte ) ( ( value & 0x0001FC0000000000L ) >> 42 ) | 0x80 );
                buffer[posBuffer + 3] = ( byte ) ( ( byte ) ( ( value & 0x000003F800000000L ) >> 35 ) | 0x80 );
                buffer[posBuffer + 4] = ( byte ) ( ( byte ) ( ( value & 0x00000007F0000000L ) >> 28 ) | 0x80 );
                buffer[posBuffer + 5] = ( byte ) ( ( byte ) ( ( value & 0x000000000FE00000L ) >> 21 ) | 0x80 );
                buffer[posBuffer + 6] = ( byte ) ( ( byte ) ( ( value & 0x00000000001FC000L ) >> 14 ) | 0x80 );
                buffer[posBuffer + 7] = ( byte ) ( ( byte ) ( ( value & 0x0000000000003F80L ) >> 7 ) | 0x80 );
                buffer[posBuffer + 8] = ( byte ) ( value & 0x000000000000007FL );
                
                return 9;
            }
        }
        else
        {
            // The value is bigger than 9999999999999999999, we need to use a BigInteger
            // First, get the number of bytes we need to store the value in base 16
            String number = oid.substring( start, start + nbDigits );
            BigInteger bigInteger = new BigInteger( number );
            
            if ( isJointIsoItuT )
            {
                bigInteger = bigInteger.add( JOINT_ISO_ITU_T );
                posBuffer = 0;
            }
            
            byte[] bytes = bigInteger.toByteArray();
            
            // Now, convert this value to the ASN.1 OID format : we store the value
            // as 7 bits bytes 
            int nbNeededBytes = ( bytes.length * 8 ) / 7;
            
            switch ( ( bytes.length - 1 ) % 7 )
            {
                case 0 :
                    if ( ( bytes[0] & 0x0080 ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 1 :
                    if ( ( bytes[0] & 0x00C0 ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 2 :
                    if ( ( bytes[0] & 0x00E0 ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 3 : 
                    if ( ( bytes[0] & 0x00F0 ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 4 :
                    if ( ( bytes[0] & 0x00F8 ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 5 :
                    if ( ( bytes[0] & 0x00FC ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                case 6 : 
                    if ( ( bytes[0] & 0x00FE ) != 0 )
                    {
                        nbNeededBytes++;
                    }
                    
                    break;
                    
                default :
                    // Exist to please checkstyle...
                    break;
            }
            
            byte[] converted = new byte[nbNeededBytes];
            
            int posConverted = nbNeededBytes - 1;
            int posBytes = bytes.length - 1;
            int counter = 0;
            byte reminder = 0;
            
            while ( posBytes >= 0 )
            {
                byte newByte = ( byte ) ( ( bytes[posBytes] & 0x00FF ) << counter );
                converted[posConverted] = ( byte ) ( reminder | newByte | 0x0080 );
                reminder = ( byte ) ( ( bytes[posBytes] & 0x00FF ) >> ( 7 - counter ) );
                counter =  ( counter + 1 ) % 8; 
                posConverted--;
                
                if ( counter != 0 )
                {
                    posBytes--;
                }
                else
                {
                    reminder = 0;
                }
            }
            
            converted[nbNeededBytes - 1] &= 0x7F;
            
            // Copy the converted bytes in the buffer
            System.arraycopy( converted, 0, buffer, posBuffer, nbNeededBytes );
            
            return nbNeededBytes;
        }
    }
    
    
    /**
     * Returns an OID object representing <code>oidString</code>.  
     *  
     * @param oidString The string representation of the OID
     * @return A new Oid
     * @throws DecoderException  When the OID is not valid
     */
    public static Oid fromString( String oidString ) throws DecoderException
    {
        if ( ( oidString == null ) || oidString.isEmpty() )
        {
            throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "empty" ) );
        }

        // Create a buffer that is wide enough to contain all the values
        byte[] buffer = new byte[oidString.length()];

        OidFSAState state = OidFSAState.START;
        
        // A counter of chars used for an arc. In 1.2.45345, this counter will be 5 for the '45345' arc.
        int arcNbChars = 0;
        
        // The position in the buffer where we accumulate the result. 
        int bufPos = 0;
        
        // The position in the OID string where we started to read an arc
        int startArc = 0;
        
        // The number of bytes in the resulting OID byte[]
        int nbBytes;
        
        for ( int i = 0; i < oidString.length(); i++ )
        {
            switch ( state )
            {
                case START :
                    // (Start) --['0'..'1']--> (A)
                    // (start) --['2']--> (F)
                    state = processStateStart( oidString, buffer, i );
                    break;
                    
                case STATE_A :
                    // (A) --['.']--> (B)
                    state = processStateA( oidString, i );

                    
                    break;
                    
                case STATE_B :
                    // (B) --['0']--> (D)
                    // (B) --['1'..'3']--> (C)
                    // (B) --['4'..'9']--> (E)
                    state = processStateB( oidString, buffer, i );
                    
                    break;
                    
                case STATE_C :
                    // (C) --['.']--> (K)
                    // (C) --['0'..'9']--> (E)
                    state = processStateC( oidString, buffer, i );

                    // the next arc will be store at position 1 in the buffer
                    bufPos = 1;

                    break;
                    
                case STATE_D :
                    // (D) --['.']--> (K)
                    // Fallthrough
                    
                case STATE_E :
                    // (E) --['.']--> (K)
                    state = processStateDE( oidString, buffer, i );
                    
                    // the next arc will be store at position 1 in teh buffer
                    bufPos = 1;

                    break;
                    
                case STATE_F :
                    // (F) --['.']--> (G)
                    state = processStateF( oidString, i );
                    
                    break;
                    
                case STATE_G :
                    // (G) --['0']--> (I)
                    // (G) --['1'..'9']--> (H)
                    state = processStateG( oidString, buffer, i );
                    arcNbChars = 1;
                    startArc = i;

                    break;

                case STATE_H :
                    // (H) --['.']--> (K)
                    // (H) --['0'..'9']--> (J)
                    state = processStateH( oidString, buffer, i );
                    
                    if ( state == OidFSAState.STATE_J )
                    {
                        // We have already two digits
                        arcNbChars = 2;
                        bufPos = 0;
                    }
                    
                    break;

                case STATE_I :
                    // (I) --['.']--> (K)
                    state = processStateI( oidString, buffer, i );
                    
                    // Set the arc position to buffer[1], we haven't yet accumulated digits.
                    bufPos = 1;
                    
                    break;

                case STATE_J :
                    // (J) --['.']--> (K)
                    // (J) --['0'..'9']--> (J)
                    state = processStateJ( oidString, buffer, arcNbChars + bufPos, i );
                    
                    if ( state == OidFSAState.STATE_J )
                    {
                        // We can increment the number of digit for this arc
                        arcNbChars++;
                    }
                    else
                    {
                        // We are done with the first arc : convert it
                        bufPos += convert( oidString, buffer, bufPos, arcNbChars, 0, true );
                    }
                    
                    break;

                case STATE_K :
                    startArc = i;
                    state = processStateK( oidString, buffer, bufPos, i );
                    
                    if ( state == OidFSAState.STATE_M )
                    { 
                        bufPos++;
                    }
                    else
                    {
                        arcNbChars = 1;
                    }
                    
                    break;

                case STATE_L :
                    state = processStateL( oidString, buffer, arcNbChars + bufPos, i );
                    
                    if ( state == OidFSAState.STATE_L )
                    {
                        arcNbChars++;
                        break;
                    }
                    else
                    {
                        // We are done with the arc : convert it
                        bufPos += convert( oidString, buffer, startArc, arcNbChars, bufPos, false );
                    }

                    break;
                    
                case STATE_M :
                    state = processStateM( oidString, i );
                    break;
                    
                default :
                    // Exist to please checkstyle...
                    break;
            }
        }
        
        // End of the string : check that we are in a correct state for a completion
        // The only valid exit states are :
        // (C) --[]--> (End)
        // (D) --[]--> (End)
        // (E) --[]--> (End)
        // (H) --[]--> (End)
        // (I) --[]--> (End)
        // (J) --[]--> (End)
        // (L) --[]--> (End)
        // (M) --[]--> (End)
        switch ( state )
        {
            case STATE_C :
                // (C) --[]--> (End)
                // fallthrough
                
            case STATE_D :
                // (D) --[]--> (End)
                // fallthrough
                
            case STATE_E :
                // (E) --[]--> (End)
                // fallthrough

            case STATE_H :
                // (H) --[]--> (End)
                // fallthrough
                
            case STATE_I :
                // (I) --[]--> (End)
                byte[] bytes = new byte[1];
                bytes[0] = ( byte ) ( buffer[0] | buffer[1] );

                return new Oid( oidString, bytes );
                
            case STATE_J :
                // (J) --[]--> (End)
                nbBytes = convert( oidString, buffer, 2, arcNbChars, 0, true );
                bytes = new byte[nbBytes];
                System.arraycopy( buffer, 0, bytes, 0, nbBytes );
                
                return new Oid( oidString, bytes );

            case STATE_L :
                bufPos += convert( oidString, buffer, startArc, arcNbChars, bufPos, false );
                bytes = new byte[bufPos];
                System.arraycopy( buffer, 0, bytes, 0, bufPos );
                
                return new Oid( oidString, bytes );
                
            case STATE_M :
                bytes = new byte[bufPos];
                System.arraycopy( buffer, 0, bytes, 0, bufPos );
                
                return new Oid( oidString, bytes );
                
            default :
                // This should never happen...
                throw new DecoderException( I18n.err( I18n.ERR_00003_INVALID_OID, "Wrong OID" ) );
        }
    }

    
    /**
     * Returns the length of the encoded <code>byte[]</code> representation.
     * 
     * @return The length of the byte[]
     */
    public int getEncodedLength()
    {
        return oidBytes.length;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode()
    {
        return oidString.hashCode();
    }


    /**
     * Returns true if <code>oidString</code> is a valid string representation
     * of an OID.  This method simply calls {@link #fromString(String)} and 
     * returns true if no exception was thrown.  As such, it should not be used 
     * in an attempt to check if a string is a valid OID before calling 
     * {@link #fromString(String)}.
     * 
     * @param oidString The string to test
     * @return True, if <code>oidString</code> is valid
     */
    public static boolean isOid( String oidString )
    {
        try
        {
            Oid.fromString( oidString );

            return true;
        }
        catch ( DecoderException e )
        {
            return false;
        }
    }


    /**
     * Returns the <code>byte[]</code> representation of the OID. The 
     * <code>byte[]</code> that is returned is <i>copied</i> from the internal
     * value so as to preserve the immutability of an OID object.  If the 
     * output of a call to this method is intended to be written to a stream,
     * the {@link #writeBytesTo(OutputStream)} should be used instead as it will
     * avoid creating this copy. 
     * 
     * @return The encoded <code>byte[]</code> representation of the OID.
     */
    public byte[] toBytes()
    {
        return Arrays.copyOf( oidBytes, oidBytes.length );
    }


    /**
     * Returns the string representation of the OID.
     * 
     * @return The string representation of the OID
     */
    @Override
    public String toString()
    {
        return oidString;
    }


    /**
     * Writes the bytes respresenting this OID to the provided buffer.  This 
     * should be used in preference to the {@link #toBytes()} method in order
     * to prevent the creation of copies of the actual <code>byte[]</code>.
     * 
     * @param buffer The buffer to write the bytes into
     */
    public void writeBytesTo( ByteBuffer buffer )
    {
        buffer.put( oidBytes );
    }


    /**
     * Writes the bytes respresenting this OID to the provided stream.  This 
     * should be used in preference to the {@link #toBytes()} method in order
     * to prevent the creation of copies of the actual <code>byte[]</code>.
     * 
     * @param outputStream The stream to write the bytes to
     * @throws IOException When we can't write the OID into a Stream
     */
    public void writeBytesTo( OutputStream outputStream ) throws IOException
    {
        outputStream.write( oidBytes );
    }
}
