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


import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.util.Chars;
import org.apache.directory.api.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Represents 'Change Sequence Number' in LDUP specification.
 * 
 * A CSN is a composition of a timestamp, a replica ID and a 
 * operation sequence number.
 * 
 * It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09.
 * 
 * The CSN syntax is :
 * <pre>
 * &lt;CSN&gt;            ::= &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replicaId&gt; # &lt;modifierNumber&gt;
 * &lt;timestamp&gt;      ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ
 * &lt;changeCount&gt;    ::= [000000-ffffff] 
 * &lt;replicaId&gt;      ::= [000-fff]
 * &lt;modifierNumber&gt; ::= [000000-ffffff]
 * </pre>
 *  
 * It distinguishes a change made on an object on a server,
 * and if two operations take place during the same timeStamp,
 * the operation sequence number makes those operations distinct.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class Csn implements Comparable<Csn>
{
    /** The logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( Csn.class );

    /** The timeStamp of this operation */
    private final long timestamp;

    /** The server identification */
    private final int replicaId;

    /** The operation number in a modification operation */
    private final int operationNumber;

    /** The changeCount to distinguish operations done in the same second */
    private final int changeCount;

    /** Stores the String representation of the CSN */
    private String csnStr;

    /** Stores the byte array representation of the CSN */
    private byte[] bytes;

    /** The Timestamp syntax. The last 'z' is _not_ the Time Zone */
    private final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );

    private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );

    /** Padding used to format number with a fixed size */
    private static final String[] PADDING_6 = new String[]
        { "00000", "0000", "000", "00", "0", "" };

    /** Padding used to format number with a fixed size */
    private static final String[] PADDING_3 = new String[]
        { "00", "0", "" };


    /**
     * Creates a new instance.
     * <b>This method should be used only for deserializing a CSN</b> 
     * 
     * @param timestamp GMT timestamp of modification
     * @param changeCount The operation increment
     * @param replicaId Replica ID where modification occurred (<tt>[-_A-Za-z0-9]{1,16}</tt>)
     * @param operationNumber Operation number in a modification operation
     */
    public Csn( long timestamp, int changeCount, int replicaId, int operationNumber )
    {
        this.timestamp = timestamp;
        this.replicaId = replicaId;
        this.operationNumber = operationNumber;
        this.changeCount = changeCount;
        sdf.setTimeZone( UTC_TIME_ZONE );
    }


    /**
     * Creates a new instance of SimpleCSN from a String.
     * 
     * The string format must be :
     * &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replica ID&gt; # &lt;operation number&gt;
     *
     * @param value The String containing the CSN
     */
    public Csn( String value )
    {
        sdf.setTimeZone( UTC_TIME_ZONE );
        
        if ( Strings.isEmpty( value ) )
        {
            String message = I18n.err( I18n.ERR_13015_NULL_OR_EMPTY_CSN );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        if ( value.length() != 40 )
        {
            String message = I18n.err( I18n.ERR_13016_INCORRECT_CSN_LENGTH );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        // Get the Timestamp
        int sepTS = value.indexOf( '#' );

        if ( sepTS < 0 )
        {
            String message = I18n.err( I18n.ERR_13017_CANT_FIND_SHARP_IN_CSN );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        String timestampStr = value.substring( 0, sepTS ).trim();

        if ( timestampStr.length() != 22 )
        {
            String message = I18n.err( I18n.ERR_13018_TIMESTAMP_NOT_LONG_ENOUGH );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        // Let's transform the Timestamp by removing the mulliseconds and microseconds
        String realTimestamp = timestampStr.substring( 0, 14 );

        long tempTimestamp = 0L;

        synchronized ( sdf )
        {
            try
            {
                tempTimestamp = sdf.parse( realTimestamp ).getTime();
            }
            catch ( ParseException pe )
            {
                String message = I18n.err( I18n.ERR_13019_CANNOT_PARSE_TIMESTAMP, timestampStr );
                LOG.error( message );
                throw new InvalidCSNException( message, pe );
            }
        }

        int millis = 0;

        // And add the milliseconds and microseconds now
        try
        {
            millis = Integer.parseInt( timestampStr.substring( 15, 21 ) );
        }
        catch ( NumberFormatException nfe )
        {
            String message = I18n.err( I18n.ERR_13020_INVALID_MICROSECOND );
            LOG.error( message );
            throw new InvalidCSNException( message, nfe );
        }

        tempTimestamp += ( millis / 1000 );
        timestamp = tempTimestamp;

        // Get the changeCount. It should be an hex number prefixed with '0x'
        int sepCC = value.indexOf( '#', sepTS + 1 );

        if ( sepCC < 0 )
        {
            String message = I18n.err( I18n.ERR_13014_DN_ATTR_FLAG_INVALID, value );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        String changeCountStr = value.substring( sepTS + 1, sepCC ).trim();

        try
        {
            changeCount = Integer.parseInt( changeCountStr, 16 );
        }
        catch ( NumberFormatException nfe )
        {
            String message = I18n.err( I18n.ERR_13021_INVALID_CHANGE_COUNT, changeCountStr );
            LOG.error( message );
            throw new InvalidCSNException( message, nfe );
        }

        // Get the replicaID
        int sepRI = value.indexOf( '#', sepCC + 1 );

        if ( sepRI < 0 )
        {
            String message = I18n.err( I18n.ERR_13022_MISSING_SHARP_IN_CSN, value );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        String replicaIdStr = value.substring( sepCC + 1, sepRI ).trim();

        if ( Strings.isEmpty( replicaIdStr ) )
        {
            String message = I18n.err( I18n.ERR_13023_REPLICA_ID_NULL );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        try
        {
            replicaId = Integer.parseInt( replicaIdStr, 16 );
        }
        catch ( NumberFormatException nfe )
        {
            String message = I18n.err( I18n.ERR_13024_INVALID_REPLICA_ID, replicaIdStr );
            LOG.error( message );
            throw new InvalidCSNException( message, nfe );
        }

        // Get the modification number
        if ( sepCC == value.length() )
        {
            String message = I18n.err( I18n.ERR_13025_NO_OPERATION_NUMBER );
            LOG.error( message );
            throw new InvalidCSNException( message );
        }

        String operationNumberStr = value.substring( sepRI + 1 ).trim();

        try
        {
            operationNumber = Integer.parseInt( operationNumberStr, 16 );
        }
        catch ( NumberFormatException nfe )
        {
            String message = I18n.err( I18n.ERR_13026_INVALID_OPERATION_NUMBER, operationNumberStr );
            LOG.error( message );
            throw new InvalidCSNException( message, nfe );
        }

        csnStr = value;
        bytes = Strings.getBytesUtf8( csnStr );
    }


    /**
     * Creates a new instance of SimpleCSN from the serialized data
     *
     * @param value The byte array which contains the serialized CSN
     */
    Csn( byte[] value )
    {
        csnStr = Strings.utf8ToString( value );
        Csn csn = new Csn( csnStr );
        timestamp = csn.timestamp;
        changeCount = csn.changeCount;
        replicaId = csn.replicaId;
        operationNumber = csn.operationNumber;
        bytes = Strings.getBytesUtf8( csnStr );
    }

    
    /**
     * Check if the given String is a valid CSN.
     * 
     * @param value The String to check
     * @return <code>true</code> if the String is a valid CSN
     */
    public static boolean isValid( String value )
    {
        if ( Strings.isEmpty( value ) )
        {
            return false;
        }

        char[] chars = value.toCharArray();
        
        if ( chars.length != 40 )
        {
            return false;
        }

        // Get the Timestamp
        // Check the timestamp's year
        for ( int pos = 0; pos < 4; pos++ )
        {
            if ( !Chars.isDigit( chars[pos] ) )
            {
                return false;
            }
        }
        
        // Check the timestamp month
        switch ( chars[4] )
        {
            case '0' :
                if ( !Chars.isDigit( chars[5] ) )
                {
                    return false;
                }
                
                if ( chars[5] == '0' )
                {
                    return false;
                }
                
                break;
                
            case '1' :
                if ( ( chars[5] != '0' ) && ( chars[5] != '1' ) && ( chars[5] != '2' ) )
                {
                    return false;
                }
                
                break;
                
            default :
                return false;
        }

        // Check the timestamp day
        switch ( chars[6] )
        {
            case '0' :
                if ( !Chars.isDigit( chars[7] ) )
                {
                    return false;
                }
                
                if ( chars[7] == '0' )
                {
                    return false;
                }
                
                break;
                
            case '1' :
            case '2' : // Special case for february...
                if ( !Chars.isDigit( chars[7] ) )
                {
                    return false;
                }
                
                break;
                
            case '3' :
                // Deal with 30 days months
                if ( ( chars[7] != '0' ) && ( chars[7] != '1' ) )
                {
                    return false;
                }
                
                break;
                
            default :
                return false;
        }

        // Check the timestamp hour
        switch ( chars[8] )
        {
            case '0' :
            case '1' :
                if ( !Chars.isDigit( chars[9] ) )
                {
                    return false;
                }

                break;
                
            case '2' :
                if ( ( chars[9] != '0' ) && ( chars[9] != '1' ) && ( chars[9] != '2' ) && ( chars[9] != '3' ) )
                {
                    return false;
                }
                
                break;
                
            default :
                return false;
        }

        // Check the timestamp minute
        switch ( chars[10] )
        {
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
                break;
                
            default :
                return false;
        }
        
        if ( !Chars.isDigit( chars[11] ) )
        {
            return false;
        }
        
        // Check the timestamp seconds
        switch ( chars[12] )
        {
            case '0' :
            case '1' :
            case '2' :
            case '3' :
            case '4' :
            case '5' :
                break;
                
            default :
                return false;
        }
        
        if ( !Chars.isDigit( chars[13] ) )
        {
            return false;
        }

        // Check the milliseconds
        if ( chars[14] != '.' )
        {
            return false;
        }

        for ( int i = 0; i < 6; i++ )
        {
            if ( !Chars.isDigit( chars[15 + i] ) )
            {
                return false;
            }
        }

        if ( chars[21] != 'Z' )
        {
            return false;
        }

        if ( chars[22] != '#' )
        {
            return false;
        }

        // Get the changeCount. It should be an 6 digit hex number
        if ( !Chars.isHex( ( byte ) chars[23] )
            || !Chars.isHex( ( byte ) chars[24] )
            || !Chars.isHex( ( byte ) chars[25] )
            || !Chars.isHex( ( byte ) chars[26] )
            || !Chars.isHex( ( byte ) chars[27] )
            || !Chars.isHex( ( byte ) chars[28] ) )
        {
            return false;
        }

        if ( chars[29] != '#' )
        {
            return false;
        }
        
        // Get the replicaID, which should be a 3 digits hex number
        if ( !Chars.isHex( ( byte ) chars[30] )
            || !Chars.isHex( ( byte ) chars[31] )
            || !Chars.isHex( ( byte ) chars[32] ) )
        {
            return false;
        }

        if ( chars[33] != '#' )
        {
            return false;
        }

        // Check the modification number, which should be a 6 digits hex number
        if ( !Chars.isHex( ( byte ) chars[34] )
            || !Chars.isHex( ( byte ) chars[35] )
            || !Chars.isHex( ( byte ) chars[36] )
            || !Chars.isHex( ( byte ) chars[37] )
            || !Chars.isHex( ( byte ) chars[38] )
            || !Chars.isHex( ( byte ) chars[39] ) )
        {
            return false;
        }

        return true;
    }


    /**
     * Get the CSN as a byte array. The data are stored as :
     * bytes 1 to 8  : timestamp, big-endian
     * bytes 9 to 12 : change count, big endian
     * bytes 13 to ... : ReplicaId 
     * 
     * @return A copy of the byte array representing theCSN
     */
    public byte[] getBytes()
    {
        if ( bytes == null )
        {
            bytes = Strings.getBytesUtf8( csnStr );
        }

        byte[] copy = new byte[bytes.length];
        System.arraycopy( bytes, 0, copy, 0, bytes.length );
        return copy;
    }


    /**
     * @return The timestamp
     */
    public long getTimestamp()
    {
        return timestamp;
    }


    /**
     * @return The changeCount
     */
    public int getChangeCount()
    {
        return changeCount;
    }


    /**
     * @return The replicaId
     */
    public int getReplicaId()
    {
        return replicaId;
    }


    /**
     * @return The operation number
     */
    public int getOperationNumber()
    {
        return operationNumber;
    }


    /**
     * @return The CSN as a String
     */
    @Override
    public String toString()
    {
        if ( csnStr == null )
        {
            StringBuilder buf = new StringBuilder( 40 );

            synchronized ( sdf )
            {
                buf.append( sdf.format( new Date( timestamp ) ) );
            }

            // Add the milliseconds part
            long millis = ( timestamp % 1000 ) * 1000;
            String millisStr = Long.toString( millis );

            buf.append( '.' ).append( PADDING_6[millisStr.length() - 1] ).append( millisStr ).append( "Z#" );

            String countStr = Integer.toHexString( changeCount );

            buf.append( PADDING_6[countStr.length() - 1] ).append( countStr );
            buf.append( '#' );

            String replicaIdStr = Integer.toHexString( replicaId );

            buf.append( PADDING_3[replicaIdStr.length() - 1] ).append( replicaIdStr );
            buf.append( '#' );

            String operationNumberStr = Integer.toHexString( operationNumber );

            buf.append( PADDING_6[operationNumberStr.length() - 1] ).append( operationNumberStr );

            csnStr = buf.toString();
        }

        return csnStr;
    }


    /**
     * Returns a hash code value for the object.
     * 
     * @return a hash code value for this object.
     */
    @Override
    public int hashCode()
    {
        int h = 37;

        h = h * 17 + ( int ) ( timestamp ^ ( timestamp >>> 32 ) );
        h = h * 17 + changeCount;
        h = h * 17 + replicaId;
        h = h * 17 + operationNumber;

        return h;
    }


    /**
     * Indicates whether some other object is "equal to" this one
     * 
     * @param o the reference object with which to compare.
     * @return <code>true</code> if this object is the same as the obj argument; 
     * <code>false</code> otherwise.
     */
    @Override
    public boolean equals( Object o )
    {
        if ( this == o )
        {
            return true;
        }

        if ( !( o instanceof Csn ) )
        {
            return false;
        }

        Csn that = ( Csn ) o;

        return ( timestamp == that.timestamp ) && ( changeCount == that.changeCount )
            && ( replicaId == that.replicaId ) && ( operationNumber == that.operationNumber );
    }


    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.<p>
     * 
     * @param   csn the Object to be compared.
     * @return  a negative integer, zero, or a positive integer as this object
     *      is less than, equal to, or greater than the specified object.
     */
    @Override
    public int compareTo( Csn csn )
    {
        if ( csn == null )
        {
            return 1;
        }

        // Compares the timestamp first
        if ( this.timestamp < csn.timestamp )
        {
            return -1;
        }
        else if ( this.timestamp > csn.timestamp )
        {
            return 1;
        }

        // Then the change count
        if ( this.changeCount < csn.changeCount )
        {
            return -1;
        }
        else if ( this.changeCount > csn.changeCount )
        {
            return 1;
        }

        // Then the replicaId
        int replicaIdCompareResult = getReplicaIdCompareResult( csn );

        if ( replicaIdCompareResult != 0 )
        {
            return replicaIdCompareResult;
        }

        // Last, not least, compares the operation number
        if ( this.operationNumber < csn.operationNumber )
        {
            return -1;
        }
        else if ( this.operationNumber > csn.operationNumber )
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }


    private int getReplicaIdCompareResult( Csn csn )
    {
        if ( this.replicaId < csn.replicaId )
        {
            return -1;
        }
        if ( this.replicaId > csn.replicaId )
        {
            return 1;
        }
        return 0;
    }
}
