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

import static org.apache.directory.api.util.TimeZones.GMT;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

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


/**
 * <p>This class represents the generalized time syntax as defined in 
 * RFC 4517 section 3.3.13.</p>
 * 
 * <p>The date, time and time zone information is internally backed
 * by an {@link java.util.Calendar} object</p>
 * 
 * <p>Leap seconds are not supported, as {@link java.util.Calendar}
 * does not support leap seconds.</p>
 * 
 * <pre>
 * 3.3.13.  Generalized Time
 *
 *  A value of the Generalized Time syntax is a character string
 *  representing a date and time.  The LDAP-specific encoding of a value
 *  of this syntax is a restriction of the format defined in [ISO8601],
 *  and is described by the following ABNF:
 *
 *     GeneralizedTime = century year month day hour
 *                          [ minute [ second / leap-second ] ]
 *                          [ fraction ]
 *                          g-time-zone
 *
 *     century = 2(%x30-39) ; "00" to "99"
 *     year    = 2(%x30-39) ; "00" to "99"
 *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
 *               / ( %x31 %x30-32 ) ; "10" to "12"
 *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
 *               / ( %x31-32 %x30-39 ) ; "10" to "29"
 *               / ( %x33 %x30-31 )    ; "30" to "31"
 *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
 *     minute  = %x30-35 %x30-39                        ; "00" to "59"
 *
 *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
 *     leap-second = ( %x36 %x30 )       ; "60"
 *
 *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
 *     g-time-zone     = %x5A  ; "Z"
 *                       / g-differential
 *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
 *     MINUS           = %x2D  ; minus sign ("-")
 *
 *  The &lt;DOT&gt;, &lt;COMMA&gt;, and &lt;PLUS&gt; rules are defined in [RFC4512].
 *
 *  The above ABNF allows character strings that do not represent valid
 *  dates (in the Gregorian calendar) and/or valid times (e.g., February
 *  31, 1994).  Such character strings SHOULD be considered invalid for
 *  this syntax.
 * <br>
 *  The time value represents coordinated universal time (equivalent to
 *  Greenwich Mean Time) if the "Z" form of &lt;g-time-zone&gt; is used;
 *  otherwise, the value represents a local time in the time zone
 *  indicated by &lt;g-differential&gt;.  In the latter case, coordinated
 *  universal time can be calculated by subtracting the differential from
 *  the local time.  The "Z" form of &lt;g-time-zone&gt; SHOULD be used in
 *  preference to &lt;g-differential&gt;.
 *  <br>
 *  If &lt;minute&gt; is omitted, then &lt;fraction&gt; represents a fraction of an
 *  hour; otherwise, if &lt;second&gt; and &lt;leap-second&gt; are omitted, then
 *  &lt;fraction&gt; represents a fraction of a minute; otherwise, &lt;fraction&gt;
 *  represents a fraction of a second.
 *
 *     Examples:
 *        199412161032Z
 *        199412160532-0500
 *  
 *  Both example values represent the same coordinated universal time:
 *  10:32 AM, December 16, 1994.
 *  <br>
 *  The LDAP definition for the Generalized Time syntax is:
 *  
 *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
 *  
 *  This syntax corresponds to the GeneralizedTime ASN.1 type from
 *  [ASN.1], with the constraint that local time without a differential
 *  SHALL NOT be used.
 * </pre>
 */
public class GeneralizedTime implements Comparable<GeneralizedTime>
{
    /** A Date far in the future, when Micro$oft would have vanished for a long time... */
    private static final Date INFINITE = new Date( 0x7FFFFFFFFFFFFFFFL );

    /**
     * The format of the generalized time.
     */
    public enum Format
    {
        /** Time format with minutes and seconds, excluding fraction. */
        YEAR_MONTH_DAY_HOUR_MIN_SEC,
        
        /** Time format with minutes and seconds, including fraction. */
        YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,

        /** Time format with minutes, seconds are omitted, excluding fraction. */
        YEAR_MONTH_DAY_HOUR_MIN,
        
        /** Time format with minutes seconds are omitted, including fraction of a minute. */
        YEAR_MONTH_DAY_HOUR_MIN_FRACTION,

        /** Time format, minutes and seconds are omitted, excluding fraction. */
        YEAR_MONTH_DAY_HOUR,
        
        /** Time format, minutes and seconds are omitted, including fraction of an hour. */
        YEAR_MONTH_DAY_HOUR_FRACTION;
    }

    /**
     * The fraction delimiter of the generalized time.
     */
    public enum FractionDelimiter
    {
        /** Use a dot as fraction delimiter. */
        DOT,
        /** Use a comma as fraction delimiter. */
        COMMA
    }

    /**
     * The time zone format of the generalized time.
     */
    public enum TimeZoneFormat
    {
        /** g-time-zone (Zulu) format. */
        Z,
        /** g-differential format, using hour only. */
        DIFF_HOUR,
        /** g-differential format, using hour and minute. */
        DIFF_HOUR_MINUTE
    }

    /** The user provided value */
    private String upGeneralizedTime;

    /** The user provided format */
    private Format upFormat;

    /** The user provided time zone format */
    private TimeZoneFormat upTimeZoneFormat;

    /** The user provided fraction delimiter */
    private FractionDelimiter upFractionDelimiter;

    /** the user provided fraction length */
    private int upFractionLength;

    /** The calendar */
    private Calendar calendar;


    /**
     * 
     * Creates a new instance of GeneralizedTime by setting the date to an instance of Calendar.
     * @see #GeneralizedTime(Calendar)
     * 
     * @param date the date
     */
    public GeneralizedTime( Date date )
    {
        calendar = new GregorianCalendar( GMT, Locale.ROOT );
        calendar.setTime( date );
        setUp( calendar );
    }


    /**
     * Creates a new instance of GeneralizedTime, based on the given Calendar object.
     * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
     * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
     *
     * @param calendar the calendar containing the date, time and timezone information
     */
    public GeneralizedTime( Calendar calendar )
    {
        setUp( calendar );
    }
    
    
    /**
     * Creates a new instance of GeneralizedTime by setting the time to an instance of Calendar.
     * @see #GeneralizedTime(Calendar)
     * 
     * @param timeInMillis the time in milliseconds
     */
    public GeneralizedTime( long timeInMillis )
    {
        Locale locale = Locale.getDefault();
        TimeZone timeZone = TimeZone.getTimeZone( "GMT"  );
        calendar = Calendar.getInstance( timeZone, locale );
        calendar.setTimeInMillis( timeInMillis );
        setUp( calendar );
    }


    /**
     * Creates a new instance of GeneralizedTime, based on the
     * given generalized time string.
     *
     * @param generalizedTime the generalized time
     * 
     * @throws ParseException if the given generalized time can't be parsed.
     */
    public GeneralizedTime( String generalizedTime ) throws ParseException
    {
        if ( generalizedTime == null )
        {
            throw new ParseException( I18n.err( I18n.ERR_17043_GENERALIZED_TIME_NULL ), 0 );
        }

        this.upGeneralizedTime = generalizedTime;

        calendar = new GregorianCalendar( GMT, Locale.ROOT );
        calendar.setTimeInMillis( 0 );
        calendar.setLenient( false );

        parseYear();
        parseMonth();
        parseDay();
        parseHour();

        if ( upGeneralizedTime.length() < 11 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17044_BAD_GENERALIZED_TIME ), 10 );
        }

        // pos 10: 
        // if digit => minute field
        // if . or , => fraction of hour field
        // if Z or + or - => timezone field
        // else error
        int pos = 10;
        char c = upGeneralizedTime.charAt( pos );
        
        if ( ( '0' <= c ) && ( c <= '9' ) )
        {
            parseMinute();

            if ( upGeneralizedTime.length() < 13 )
            {
                throw new ParseException( I18n.err( I18n.ERR_17045_BAD_GENERALIZED_TIME ), 12 );
            }

            // pos 12: 
            // if digit => second field
            // if . or , => fraction of minute field
            // if Z or + or - => timezone field
            // else error
            pos = 12;
            c = upGeneralizedTime.charAt( pos );
            
            if ( ( '0' <= c ) && ( c <= '9' ) )
            {
                parseSecond();

                if ( upGeneralizedTime.length() < 15 )
                {
                    throw new ParseException( I18n.err( I18n.ERR_17046_BAD_GENERALIZED_TIME ), 14 );
                }

                // pos 14: 
                // if . or , => fraction of second field
                // if Z or + or - => timezone field
                // else error
                pos = 14;
                c = upGeneralizedTime.charAt( pos );
                
                if ( ( c == '.' ) || ( c == ',' ) )
                {
                    // read fraction of second
                    parseFractionOfSecond();
                    pos += 1 + upFractionLength;

                    parseTimezone( pos );
                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
                }
                else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
                {
                    // read timezone
                    parseTimezone( pos );
                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
                }
                else
                {
                    throw new ParseException( I18n.err( I18n.ERR_17047_TIME_TOO_SHORT ), 14 );
                }
            }
            else if ( ( c == '.' ) || ( c == ',' ) )
            {
                // read fraction of minute
                parseFractionOfMinute();
                pos += 1 + upFractionLength;

                parseTimezone( pos );
                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
            }
            else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
            {
                // read timezone
                parseTimezone( pos );
                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
            }
            else
            {
                throw new ParseException( I18n.err( I18n.ERR_17048_TIME_TOO_SHORT ), 12 );
            }
        }
        else if ( ( c == '.' ) || ( c == ',' ) )
        {
            // read fraction of hour
            parseFractionOfHour();
            pos += 1 + upFractionLength;

            parseTimezone( pos );
            upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
        }
        else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
        {
            // read timezone
            parseTimezone( pos );
            upFormat = Format.YEAR_MONTH_DAY_HOUR;
        }
        else
        {
            throw new ParseException( I18n.err( I18n.ERR_17049_INVALID_GENERALIZED_TIME ), 10 );
        }

        // this calculates and verifies the calendar
        /* Not sure we should do that... */
        try
        {
            calendar.getTimeInMillis();
        }
        catch ( IllegalArgumentException iae )
        {
            throw new ParseException( I18n.err( I18n.ERR_17050_INVALID_DATE_TIME ), 0 );
        }

        calendar.setLenient( true );
    }


    /**
     * Initialize the calendar
     * 
     * @param newCalendar The caldenar to initialize
     */
    private void setUp( Calendar newCalendar )
    {
        if ( newCalendar == null )
        {
            throw new IllegalArgumentException( I18n.err( I18n.ERR_17051_CALENDAR_NULL ) );
        }

        this.calendar = newCalendar;
        upGeneralizedTime = null;
        upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
        upTimeZoneFormat = TimeZoneFormat.Z;
        upFractionDelimiter = FractionDelimiter.DOT;
        upFractionLength = 3;
    }


    /**
     * Parse the time zone
     *  
     * @param pos Position of the TZ
     * @throws ParseException If the le ngth is too short, the TZ is invalid or missing
     */
    private void parseTimezone( int pos ) throws ParseException
    {
        if ( upGeneralizedTime.length() < pos + 1 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17052_TIME_TOO_SHOR_NO_TZ ), pos );
        }

        char c = upGeneralizedTime.charAt( pos );
        
        if ( c == 'Z' )
        {
            calendar.setTimeZone( GMT );
            upTimeZoneFormat = TimeZoneFormat.Z;

            if ( upGeneralizedTime.length() > pos + 1 )
            {
                throw new ParseException( I18n.err( I18n.ERR_17053_MISSING_TZ ), pos + 1 );
            }
        }
        else if ( ( c == '+' ) || ( c == '-' ) )
        {
            StringBuilder sb = new StringBuilder( "GMT" );
            sb.append( c );

            String digits = getAllDigits( pos + 1 );
            sb.append( digits );

            if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
            {
                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
                calendar.setTimeZone( timeZone );
                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
            }
            else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
            {
                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
                calendar.setTimeZone( timeZone );
                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
            }
            else
            {
                throw new ParseException( I18n.err( I18n.ERR_17054_TZ_MUST_BE_2_OR_4_DIGITS ), pos );
            }

            if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
            {
                throw new ParseException( I18n.err( I18n.ERR_17053_MISSING_TZ ), pos + 1 + digits.length() );
            }
        }
    }


    /**
     * Parse the fraction part and convert it to milliseconds
     * 
     * @throws ParseException If there is no fraction part
     */
    private void parseFractionOfSecond() throws ParseException
    {
        parseFractionDelmiter( 14 );
        String fraction = getFraction( 14 + 1 );
        upFractionLength = fraction.length();

        double fract = Double.parseDouble( "0." + fraction );
        int millisecond = ( int ) Math.floor( fract * 1000 );

        calendar.set( GregorianCalendar.MILLISECOND, millisecond );
    }


    /**
     * Parse the fraction part and convert it to seconds, milliseconds
     * 
     * @throws ParseException If there is no fraction part
     */
    private void parseFractionOfMinute() throws ParseException
    {
        parseFractionDelmiter( 12 );
        String fraction = getFraction( 12 + 1 );
        upFractionLength = fraction.length();

        double fract = Double.parseDouble( "0." + fraction );
        int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
        int second = milliseconds / 1000;
        int millisecond = milliseconds - ( second * 1000 );

        calendar.set( Calendar.SECOND, second );
        calendar.set( Calendar.MILLISECOND, millisecond );
    }


    /**
     * Parse the fraction part and convert it to minutes, seconds, milliseconds
     * 
     * @throws ParseException If there is no fraction part
     */
    private void parseFractionOfHour() throws ParseException
    {
        parseFractionDelmiter( 10 );
        String fraction = getFraction( 10 + 1 );
        upFractionLength = fraction.length();

        double fract = Double.parseDouble( "0." + fraction );
        int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
        int minute = milliseconds / ( 1000 * 60 );
        int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
        int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );

        calendar.set( Calendar.MINUTE, minute );
        calendar.set( Calendar.SECOND, second );
        calendar.set( Calendar.MILLISECOND, millisecond );
    }


    /**
     * Find the fraction delimiter (a '.' or a ',')
     * 
     * @param fractionDelimiterPos The fraction delimiter position
     */
    private void parseFractionDelmiter( int fractionDelimiterPos )
    {
        char c = upGeneralizedTime.charAt( fractionDelimiterPos );
        upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
    }


    /**
     * Get the fraction part
     * 
     * @param startIndex The start of the fraction part
     * @return The fraction part
     * @throws ParseException If the fraction part is null
     */
    private String getFraction( int startIndex ) throws ParseException
    {
        String fraction = getAllDigits( startIndex );

        // minimum one digit
        if ( fraction.length() == 0 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17055_MISSING_FRACTION ), startIndex );
        }

        return fraction;
    }


    /**
     * Get the Generalized time digits up to the first non-digit char
     * 
     * @param startIndex Starting point to scan for digits
     * @return The digits in a string
     */
    private String getAllDigits( int startIndex )
    {
        StringBuilder sb = new StringBuilder();
        
        while ( upGeneralizedTime.length() > startIndex )
        {
            char c = upGeneralizedTime.charAt( startIndex );
            
            if ( '0' <= c && c <= '9' )
            {
                sb.append( c );
                startIndex++;
            }
            else
            {
                break;
            }
        }
        
        return sb.toString();
    }


    /**
     * Get the seconds from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 14 chars, or if the 
     * seconds' part (chars 12/13) aren't parsable as an int
     */
    private void parseSecond() throws ParseException
    {
        // read seconds
        if ( upGeneralizedTime.length() < 14 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17056_TIME_TOO_SHORT_NO_SECOND ), 12 );
        }
        try
        {
            int second = Strings.parseInt( upGeneralizedTime.substring( 12, 14 ) );
            calendar.set( Calendar.SECOND, second );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17057_SECOND_NOT_NUM ), 12 );
        }
    }


    /**
     * Get the minutes from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 12 chars, or if the 
     * minutes' part (chars 10/11) aren't parsable as an int
     */
    private void parseMinute() throws ParseException
    {
        // read minute
        if ( upGeneralizedTime.length() < 12 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17058_MISSING_MINUTE ), 10 );
        }
        try
        {
            int minute = Strings.parseInt( upGeneralizedTime.substring( 10, 12 ) );
            calendar.set( Calendar.MINUTE, minute );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17059_MIN_NOT_NUM ), 10 );
        }
    }


    /**
     * Get the hours from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 10 chars, or if the 
     * hours' part (chars 8/9) aren't parsable as an int
     */
    private void parseHour() throws ParseException
    {
        if ( upGeneralizedTime.length() < 10 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17060_TIME_TO_SHORT_MISSING_HOUR ), 8 );
        }
        try
        {
            int hour = Strings.parseInt( upGeneralizedTime.substring( 8, 10 ) );
            calendar.set( Calendar.HOUR_OF_DAY, hour );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17061_HOUR_NOT_NUM ), 8 );
        }
    }


    /**
     * Get the day from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 8 chars, or if the 
     * day's part (chars 6/7) aren't parsable as an int
     */
    private void parseDay() throws ParseException
    {
        if ( upGeneralizedTime.length() < 8 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17062_TIME_TO_SHORT_MISSING_DAY ), 6 );
        }
        try
        {
            int day = Strings.parseInt( upGeneralizedTime.substring( 6, 8 ) );
            calendar.set( Calendar.DAY_OF_MONTH, day );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17063_DAY_NOT_NUM ), 6 );
        }
    }


    /**
     * Get the month from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 6 chars, or if the 
     * month's part (chars 4/5) aren't parsable as an int
     */
    private void parseMonth() throws ParseException
    {
        if ( upGeneralizedTime.length() < 6 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17064_TIME_TO_SHORT_MISSING_MONTH ), 4 );
        }
        try
        {
            int month = Strings.parseInt( upGeneralizedTime.substring( 4, 6 ) );
            calendar.set( Calendar.MONTH, month - 1 );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17065_MONTH_NOT_NUM ), 4 );
        }
    }


    /**
     * Get the year from the instance.
     * 
     * @throws ParseException If the GeneralizedTime is shorter than 4 chars, or if the 
     * year's part (chars 0/1/2/3) aren't parsable as an int
     */
    private void parseYear() throws ParseException
    {
        if ( upGeneralizedTime.length() < 4 )
        {
            throw new ParseException( I18n.err( I18n.ERR_17066_TIME_TO_SHORT_MISSING_YEAR ), 0 );
        }
        try
        {
            int year = Strings.parseInt( upGeneralizedTime.substring( 0, 4 ) );
            calendar.set( Calendar.YEAR, year );
        }
        catch ( NumberFormatException e )
        {
            throw new ParseException( I18n.err( I18n.ERR_17067_YEAR_NOT_NUM ), 0 );
        }
    }


    /**
     * Returns the string representation of this generalized time. 
     * This method uses the same format as the user provided format.
     *
     * @return the string representation of this generalized time
     */
    public String toGeneralizedTime()
    {
        return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
    }


    /**
     * Returns the string representation of this generalized time. 
     * This method uses the same format as the user provided format.
     *
     * @return the string representation of this generalized time
     */
    public String toGeneralizedTimeWithoutFraction()
    {
        return toGeneralizedTime( getFormatWithoutFraction( upFormat ), upFractionDelimiter, upFractionLength,
            upTimeZoneFormat );
    }


    /**
     * Gets the corresponding format with fraction.
     *
     * @param f the format
     * @return the corresponding format without fraction
     */
    private Format getFormatWithoutFraction( Format f )
    {
        switch ( f )
        {
            case YEAR_MONTH_DAY_HOUR_FRACTION:
                return Format.YEAR_MONTH_DAY_HOUR;
            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
                return Format.YEAR_MONTH_DAY_HOUR_MIN;
            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
                return Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
            default:
                break;
        }

        return f;
    }


    /**
     * Returns the string representation of this generalized time.
     * 
     * @param format the target format
     * @param fractionDelimiter the target fraction delimiter, may be null
     * @param fractionLength the fraction length
     * @param timeZoneFormat the target time zone format
     * 
     * @return the string
     */
    public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
        TimeZoneFormat timeZoneFormat )
    {
        Calendar clonedCalendar = ( Calendar ) calendar.clone();

        if ( timeZoneFormat == TimeZoneFormat.Z )
        {
            clonedCalendar.setTimeZone( GMT );
        }

        // Create the result. It can contain a maximum of 23 chars
        byte[] result = new byte[23];

        // The starting point
        int pos = 0;

        // Inject the year
        int year = clonedCalendar.get( Calendar.YEAR );

        result[pos++] = ( byte ) ( ( year / 1000 ) + '0' );
        year %= 1000;

        result[pos++] = ( byte ) ( ( year / 100 ) + '0' );
        year %= 100;

        result[pos++] = ( byte ) ( ( year / 10 ) + '0' );

        result[pos++] = ( byte ) ( ( year % 10 ) + '0' );

        // Inject the month
        int month = clonedCalendar.get( Calendar.MONTH ) + 1;

        result[pos++] = ( byte ) ( ( month / 10 ) + '0' );

        result[pos++] = ( byte ) ( ( month % 10 ) + '0' );

        // Inject the day
        int day = clonedCalendar.get( Calendar.DAY_OF_MONTH );

        result[pos++] = ( byte ) ( ( day / 10 ) + '0' );

        result[pos++] = ( byte ) ( ( day % 10 ) + '0' );

        // Inject the hour
        int hour = clonedCalendar.get( Calendar.HOUR_OF_DAY );

        result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );

        result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );

        switch ( format )
        {
            case YEAR_MONTH_DAY_HOUR_MIN_SEC:
                // Inject the minutes
                int minute = clonedCalendar.get( Calendar.MINUTE );

                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );

                // Inject the seconds
                int second = clonedCalendar.get( Calendar.SECOND );

                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );

                break;

            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
                // Inject the minutes
                minute = clonedCalendar.get( Calendar.MINUTE );

                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );

                // Inject the seconds
                second = clonedCalendar.get( Calendar.SECOND );

                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );

                // Inject the fraction
                if ( fractionDelimiter == FractionDelimiter.COMMA )
                {
                    result[pos++] = ',';
                }
                else
                {
                    result[pos++] = '.';
                }

                // Inject the fraction
                int millisecond = clonedCalendar.get( Calendar.MILLISECOND );

                result[pos++] = ( byte ) ( ( millisecond / 100 ) + '0' );
                millisecond %= 100;

                result[pos++] = ( byte ) ( ( millisecond / 10 ) + '0' );

                //if ( millisecond > 0 )
                result[pos++] = ( byte ) ( ( millisecond % 10 ) + '0' );

                break;

            case YEAR_MONTH_DAY_HOUR_MIN:
                // Inject the minutes
                minute = clonedCalendar.get( Calendar.MINUTE );

                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
                break;

            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
                // Inject the minutes
                minute = clonedCalendar.get( Calendar.MINUTE );

                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );

                // sec + millis => fraction of a minute
                int fraction = 1000 * clonedCalendar.get( Calendar.SECOND )
                    + clonedCalendar.get( Calendar.MILLISECOND );
                fraction /= 60;

                if ( fraction > 0 )
                {
                    if ( fractionDelimiter == FractionDelimiter.COMMA )
                    {
                        result[pos++] = ',';
                    }
                    else
                    {
                        result[pos++] = '.';
                    }

                    // At this point, the fraction should be in [999, 1]
                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
                    fraction %= 100;

                    if ( fraction > 0 )
                    {
                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );

                        if ( fraction > 0 )
                        {
                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
                        }
                    }
                }

                break;

            case YEAR_MONTH_DAY_HOUR:
                // nothing to add
                break;

            case YEAR_MONTH_DAY_HOUR_FRACTION:
                // min + sec + millis => fraction of an hour
                fraction = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000
                    * clonedCalendar.get( Calendar.SECOND )
                    + clonedCalendar.get( Calendar.MILLISECOND );
                fraction /= 60 * 60;

                // At this point, the fraction should be in [999, 1]
                if ( fraction > 0 )
                {
                    if ( fractionDelimiter == FractionDelimiter.COMMA )
                    {
                        result[pos++] = ',';
                    }
                    else
                    {
                        result[pos++] = '.';
                    }

                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
                    fraction %= 100;

                    if ( fraction > 0 )
                    {
                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );

                        if ( fraction > 0 )
                        {
                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
                        }
                    }
                }

                break;

            default:
                throw new IllegalArgumentException( I18n.err( I18n.ERR_17069_UNEXPECTED_FORMAT, format ) );
        }

        if ( ( timeZoneFormat == TimeZoneFormat.Z ) && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
        {
            result[pos++] = 'Z';
        }
        else
        {
            // g-differential
            int offset = clonedCalendar.get( Calendar.ZONE_OFFSET ) + clonedCalendar.get( Calendar.DST_OFFSET );
            
            
            if ( offset < 0 )
            {
                result[pos++] = '-';
            }
            else
            {
                result[pos++] = '+';
            }

            offset = Math.abs( offset );
            hour = offset / ( 60 * 60 * 1000 );
            int minute = ( offset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );

            // The offset hour
            result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );

            result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );

            if ( ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE ) || ( timeZoneFormat == TimeZoneFormat.Z ) )
            {
                // The offset minute
                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );

                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
            }
        }

        return Strings.utf8ToString( result, 0, pos );
    }


    /**
     * Gets the calendar. It could be used to manipulate this 
     * {@link GeneralizedTime} settings.
     * 
     * @return the calendar
     */
    public Calendar getCalendar()
    {
        return calendar;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        return toGeneralizedTime();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + calendar.hashCode();
        return result;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals( Object obj )
    {
        if ( obj instanceof GeneralizedTime )
        {
            GeneralizedTime other = ( GeneralizedTime ) obj;
            return calendar.equals( other.calendar );
        }
        else
        {
            return false;
        }
    }


    /**
     * Compares this GeneralizedTime object with the specified GeneralizedTime object.
     * 
     * @param other the other GeneralizedTime object
     * 
     * @return a negative integer, zero, or a positive integer as this object
     *      is less than, equal to, or greater than the specified object.
     * 
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo( GeneralizedTime other )
    {
        return calendar.compareTo( other.calendar );
    }


    /**
     * Get the time
     * 
     * @return A Date representing the time as milliseconds
     */
    public long getTime()
    {
        return calendar.getTimeInMillis();
    }


    /**
     * Get the date
     * 
     * @return A Date representing the time
     */
    public Date getDate()
    {
        return calendar.getTime();
    }


    /**
     * Get the year
     * 
     * @return The year part of the date
     */
    public int getYear()
    {
        return calendar.get( Calendar.YEAR );
    }


    /**
     * Get the month
     * 
     * @return The month part of the date
     */
    public int getMonth()
    {
        return calendar.get( Calendar.MONTH );
    }


    /**
     * Get the day
     * 
     * @return The day part of the date
     */
    public int getDay()
    {
        return calendar.get( Calendar.DATE );
    }


    /**
     * Get the hours
     * 
     * @return The hours part of the date
     */
    public int getHour()
    {
        return calendar.get( Calendar.HOUR_OF_DAY );
    }


    /**
     * Get the minutes
     * 
     * @return The minutes part of the date
     */
    public int getMinutes()
    {
        return calendar.get( Calendar.MINUTE );
    }


    /**
     * Get the seconds
     * 
     * @return The seconds part of the date
     */
    public int getSeconds()
    {
        return calendar.get( Calendar.SECOND );
    }


    /**
     * Get the milliseconds
     * 
     * @return The fractional (ie, milliseconds) part of the date
     */
    public int getFraction()
    {
        return calendar.get( Calendar.MILLISECOND );
    }


    /**
     * Get a Date instance from a given String
     *
     * @param zuluTime The time as a String
     * @return A Date instance
     * @throws ParseException If the String is not a valid date
     */
    public static Date getDate( String zuluTime ) throws ParseException
    {
        try
        {
            return new GeneralizedTime( zuluTime ).calendar.getTime();
        }
        catch ( ParseException pe )
        {
            // Maybe one of the multiple Micro$oft ineptness to cope with Standards ?
            if ( "9223372036854775807".equals( zuluTime ) )
            {
                // This 0x7FFFFFFFFFFFFFFF, never ending date
                return INFINITE;
            }
            else
            {
                throw pe;
            }
        }
    }
}
