/*
 * Copyright 2003, 2004  The Apache Software Foundation
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codehaus.xfire.util.date;

import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.TimeZone;

/**
 * <p>
 * An instance of {@link java.text.Format}, which may be used to parse and
 * format <code>xs:dateTime</code> values.
 * </p>
 */
public class XsDateTimeFormat
    extends Format
{
    private static final long serialVersionUID = 3258131340871479609L;

    final boolean parseDate;

    final boolean parseTime;

    XsDateTimeFormat(boolean pParseDate, boolean pParseTime)
    {
        parseDate = pParseDate;
        parseTime = pParseTime;
    }

    /**
     * Creates a new instance.
     */
    public XsDateTimeFormat()
    {
        this(true, true);
    }

    private int parseInt(String pString, int pOffset, StringBuffer pDigits)
    {
        int length = pString.length();
        pDigits.setLength(0);
        while (pOffset < length)
        {
            char c = pString.charAt(pOffset);
            if (Character.isDigit(c))
            {
                pDigits.append(c);
                ++pOffset;
            }
            else
            {
                break;
            }
        }
        return pOffset;
    }

    public Object parseObject(String pString, ParsePosition pParsePosition)
    {
        if (pString == null)
        {
            throw new NullPointerException("The String argument must not be null.");
        }
        if (pParsePosition == null)
        {
            throw new NullPointerException("The ParsePosition argument must not be null.");
        }
        int offset = pParsePosition.getIndex();
        int length = pString.length();

        boolean isMinus = false;
        StringBuffer digits = new StringBuffer();
        int year, month, mday;
        if (parseDate)
        {
            // Sign
            if (offset < length)
            {
                char c = pString.charAt(offset);
                if (c == '+')
                {
                    ++offset;
                }
                else if (c == '-')
                {
                    ++offset;
                    isMinus = true;
                }
            }

            offset = parseInt(pString, offset, digits);
            if (digits.length() < 4)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            year = Integer.parseInt(digits.toString());

            if (offset < length && pString.charAt(offset) == '-')
            {
                ++offset;
            }
            else
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }

            offset = parseInt(pString, offset, digits);
            if (digits.length() != 2)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            month = Integer.parseInt(digits.toString());

            if (offset < length && pString.charAt(offset) == '-')
            {
                ++offset;
            }
            else
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }

            offset = parseInt(pString, offset, digits);
            if (digits.length() != 2)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            mday = Integer.parseInt(digits.toString());

            if (parseTime)
            {
                if (offset < length && pString.charAt(offset) == 'T')
                {
                    ++offset;
                }
                else
                {
                    pParsePosition.setErrorIndex(offset);
                    return null;
                }
            }
        }
        else
        {
            year = month = mday = 0;
        }

        int hour, minute, second, millis;
        if (parseTime)
        {
            offset = parseInt(pString, offset, digits);
            if (digits.length() != 2)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            hour = Integer.parseInt(digits.toString());

            if (offset < length && pString.charAt(offset) == ':')
            {
                ++offset;
            }
            else
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }

            offset = parseInt(pString, offset, digits);
            if (digits.length() != 2)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            minute = Integer.parseInt(digits.toString());

            if (offset < length && pString.charAt(offset) == ':')
            {
                ++offset;
            }
            else
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }

            offset = parseInt(pString, offset, digits);
            if (digits.length() != 2)
            {
                pParsePosition.setErrorIndex(offset);
                return null;
            }
            second = Integer.parseInt(digits.toString());

            if (offset < length && pString.charAt(offset) == '.')
            {
                ++offset;
                offset = parseInt(pString, offset, digits);
                if (digits.length() > 0)
                {
                    millis = Integer.parseInt(digits.toString());
                }
                else
                {
                    millis = 0;
                }
            }
            else
            {
                millis = 0;
            }
        }
        else
        {
            hour = minute = second = millis = 0;
        }

        digits.setLength(0);
        digits.append("GMT");
        if (offset < length)
        {
            char c = pString.charAt(offset);
            if (c == 'Z')
            {
                // Ignore UTC, it is the default
                ++offset;
            }
            else if (c == '+' || c == '-')
            {
                digits.append(c);
                ++offset;
                for (int i = 0; i < 5; i++)
                {
                    if (offset >= length)
                    {
                        pParsePosition.setErrorIndex(offset);
                        return null;
                    }
                    c = pString.charAt(offset);
                    if ((i != 2 && Character.isDigit(c)) || (i == 2 && c == ':'))
                    {
                        digits.append(c);
                    }
                    else
                    {
                        pParsePosition.setErrorIndex(offset);
                        return null;
                    }
                    ++offset;
                }
            }
        }

        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(digits.toString()));
        cal.set(isMinus ? -year : year, parseDate ? month - 1 : month, mday, hour, minute, second);
        cal.set(Calendar.MILLISECOND, millis);
        pParsePosition.setIndex(offset);
        return cal;
    }

    private void append(StringBuffer pBuffer, int pNum, int pMinLen)
    {
        String s = Integer.toString(pNum);
        for (int i = s.length(); i < pMinLen; i++)
        {
            pBuffer.append('0');
        }
        pBuffer.append(s);
    }

    public StringBuffer format(Object pCalendar, StringBuffer pBuffer, FieldPosition pPos)
    {
        if (pCalendar == null)
        {
            throw new NullPointerException("The Calendar argument must not be null.");
        }
        if (pBuffer == null)
        {
            throw new NullPointerException("The StringBuffer argument must not be null.");
        }
        if (pPos == null)
        {
            throw new NullPointerException("The FieldPosition argument must not be null.");
        }

        Calendar cal = (Calendar) pCalendar;
        if (parseDate)
        {
            int year = cal.get(Calendar.YEAR);
            if (year < 0)
            {
                pBuffer.append('-');
                year = -year;
            }
            append(pBuffer, year, 4);
            pBuffer.append('-');
            append(pBuffer, cal.get(Calendar.MONTH) + 1, 2);
            pBuffer.append('-');
            append(pBuffer, cal.get(Calendar.DAY_OF_MONTH), 2);
            if (parseTime)
            {
                pBuffer.append('T');
            }
        }
        if (parseTime)
        {
            append(pBuffer, cal.get(Calendar.HOUR_OF_DAY), 2);
            pBuffer.append(':');
            append(pBuffer, cal.get(Calendar.MINUTE), 2);
            pBuffer.append(':');
            append(pBuffer, cal.get(Calendar.SECOND), 2);
            int millis = cal.get(Calendar.MILLISECOND);
            if (millis > 0)
            {
                pBuffer.append('.');
                append(pBuffer, millis, 3);
            }
        }
        TimeZone tz = cal.getTimeZone();
        // JDK 1.4: int offset = tz.getOffset(cal.getTimeInMillis());
        int offset = cal.get(Calendar.ZONE_OFFSET);
        if (tz.inDaylightTime(cal.getTime()))
        {
            offset += cal.get(Calendar.DST_OFFSET);
        }
        if (offset == 0)
        {
            pBuffer.append('Z');
        }
        else
        {
            if (offset < 0)
            {
                pBuffer.append('-');
                offset = -offset;
            }
            else
            {
                pBuffer.append('+');
            }
            int minutes = offset / (60 * 1000);
            int hours = minutes / 60;
            minutes -= hours * 60;
            append(pBuffer, hours, 2);
            pBuffer.append(':');
            append(pBuffer, minutes, 2);
        }
        return pBuffer;
    }
}
