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


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());
                    if (millis > 999) {
                        pParsePosition.setErrorIndex(offset);
                        return null;
                    }
                    for (int i = digits.length();  i < 3;  i++) {
                        millis *= 10;
                    }
	            } 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;
    }
}
