/*
 *
 *
 * Copyright  1990-2009 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt).
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions.
 */
package com.sun.jsr082.obex;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.Vector;
import java.util.Stack;
import javax.microedition.io.Connection;
import javax.obex.ResponseCodes;
import javax.obex.Authenticator;
import com.sun.j2me.log.Logging;

/*
 * Obex core protocol.
 */
public abstract class ObexPacketStream implements Connection {

    /* Debug information, should be false for RR. */
    private static final boolean DEBUG = false;
    private static final boolean DEBUG2 = false;

    // OBEX operations opcodes
    static final int OPCODE_CONNECT = 0x80;
    static final int OPCODE_DISCONNECT = 0x81;
    static final int OPCODE_PUT = 0x02;
    static final int OPCODE_GET = 0x03;
    static final int OPCODE_SETPATH = 0x85;
    static final int OPCODE_CONTINUE = 0x90;
    static final int OPCODE_ABORT = 0xFF;
    static final int OPCODE_FINAL = 0x80;
    static final int OPCODE_GET_FINAL = OPCODE_GET | OPCODE_FINAL;

    static final byte[] PACKET_ABORT = { (byte) OPCODE_ABORT, 0, 0 };
    static final byte[] PACKET_CONTINUE = { (byte) OPCODE_CONTINUE, 0, 0};
    static final byte[] PACKET_DISCONNECT = {
        (byte) OPCODE_DISCONNECT, 0, 0};
    static final byte[] PACKET_SUCCESS = {
        (byte) ResponseCodes.OBEX_HTTP_OK, 0, 0};

    static final byte[] PACKET_BAD_REQUEST = {
        (byte) ResponseCodes.OBEX_HTTP_BAD_REQUEST, 0, 0};

    static final byte[] PACKET_NOT_IMPLEMENTED = {
        (byte) ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED, 0, 0};

    private static final int HEADER_BODY = 0x48;
    private static final int HEADER_EOFBODY = 0x49;
    private static final int HEADER_CONNECTION_ID = 0xCB;
    static final int HEADER_AUTH_CHALLENGE = 0x4D;
    static final int HEADER_AUTH_RESPONSE = 0x4E;

    static TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");

    private static final int TRANSPORT_READ_INTERVAL = 10;

    private ObexTransport transport;

    Authenticator authenticator;

    /*
     * Generated authentication responses. They will be send in sendPacket().
     * Stored in <CODE>byte[]</CODE> format.
     */
    Vector authResponses = new Vector();

    /*
     * Sent authentication challenges.
     * They will be used for check authentication responses.
     */
    Vector authChallenges = new Vector();

    /*
     * True when buffer contains packet for sending and some headers can
     * be added.
     */
    boolean moreHeaders = false;


    /*
     * Outgoing packet contains authentication challenges, so it should be
     * sent immediatly.
     */
    boolean challengesToSend = false;

    /*
     * True if one of sending headers cannot feet in empty packet.
     */
    boolean headerOverflow = false;

    /*
     * True if sending packet contains target header.
     */
    boolean containsTargetHeader = false;

    /*
     * Queue of outgoing headers, not feet in packet.
     */
    Vector queuedHeaders;
    QueuedHeader newHeader;
    Stack emptyHeadersPool;

    /*
     * Set when sending auth challenge,
     * reset when received valid auth response in next
     * packet.
     */
    boolean authFailed = false;

    /*
     * True if this is ClientSession, false in ServerConnectionImpl.
     */
    boolean isClient;

    /*
     * Client is connected flag. Ignored by ServerConnectionImpl.
     */
    boolean isConnected;

    int OBEX_MAXIMUM_PACKET_LENGTH;

    byte[] buffer, cache;
    int packetLength;
    int packetOffset;
    int packetType;

    int maxSendLength;

    boolean dataOpened, dataClosed;
    boolean isEof;
    int dataOffset;

    /*
     * Connection id used in <code>setConnectionID</code>,
     * <code>getConnectioID</code>.
     */

    ObexPacketStream(ObexTransport transport) {
        this.transport = transport;
        OBEX_MAXIMUM_PACKET_LENGTH = transport.getMaximumPacketSize();
        buffer = new byte[OBEX_MAXIMUM_PACKET_LENGTH];
        cache = new byte[OBEX_MAXIMUM_PACKET_LENGTH];
        maxSendLength = OBEX_MAXIMUM_PACKET_LENGTH;
        newHeader = new QueuedHeader(this);
        queuedHeaders = new Vector();
        emptyHeadersPool = new Stack();
    }


    // interface visible function
    public void close() {
        try {
            if (transport != null) {
                transport.close();
            }
        } catch (IOException e) {
            // nothing
        }
        transport = null;
    }

    public void setAuthenticator(Authenticator authenticator) {
        if (authenticator == null) {
            throw new NullPointerException("null authenticator");
        }
        this.authenticator = authenticator;
    }

    public Connection getTransport()
        throws IOException {
        if (transport == null) {
            throw new IOException("connection error");
        }
        return transport.getUnderlyingConnection();
    }

    /*
     * Sets link broken flag.
     */
    void brokenLink() {
        close();
    }

    boolean isClosed() {
        return transport == null;
    }

    void packetBegin(byte[] head) {
        if (DEBUG) {
            System.out.println("packetBegin()");
        }

        containsTargetHeader = false;
        moreHeaders = true;
        challengesToSend = false;
        System.arraycopy(head, 0, buffer, 0, head.length);
        packetLength = head.length;
        authChallenges.removeAllElements();
        dataOpened = false;
        dataClosed = false;
        dataOffset = -3; // generate aoobe when accessed
    }

    int packetAddData(byte[] data, int offset, int length) {
        if (DEBUG) {
            System.out.println("packetAddData()");
        }
        // preventing writing several data blocks, just in case
        if (dataClosed)
	    return 0;

        if (!dataOpened) {
            // let it be at least 3 bytes workload to create new Body header
            if (packetLength + 6 > maxSendLength) {
                return 0;
            }
            buffer[packetLength] = HEADER_BODY;
            dataOffset = packetLength;
            packetLength += 3;
            dataOpened = true;
        }

        int len;
        if (packetLength + length > maxSendLength) {
            len = maxSendLength - packetLength;
        } else {
            len = length;
        }
        System.arraycopy(data, offset, buffer, packetLength, len);
        packetLength += len;
        return len;
    }

    int getPacketLength() {
        return packetLength;
    }

    void restorePacketLength(int len) {
        packetLength = len;
    }

    boolean packetEOFBody() {
        if (DEBUG) {
            System.out.println("packetEOFBody()");
        }
        if (dataClosed) {
            return false;
        }
        if (dataOpened) {
            buffer[dataOffset+0] = HEADER_EOFBODY;
            return true;
        } else {
            if (packetLength + 3 > maxSendLength) {
                return false;
            }
            buffer[packetLength++] = HEADER_EOFBODY;
            buffer[packetLength++] = 0; // length
            buffer[packetLength++] = 3;
            return true;
        }
    }

    void packetMarkFinal() {
        if (DEBUG) {
            System.out.println("packetMarkFinal()");
        }
        buffer[0] |= 0x80;
    }

    void setPacketType(int type) {
        if (DEBUG) {
            System.out.println("setPacketType()");
        }
        buffer[0] = (byte) type;
    }

    /*
     * Finish packet and send it. Remove Connection ID header if packet also
     * contains TARGET header.
     */
    void packetEndStripConnID() throws IOException {

        // first header id is in 3 byte on all packet except CONNECT
        // and this function is known not to be called for connect() operation.
        if ((buffer[3] & 0xFF) == HeaderSetImpl.TARGET) {
            packetLength -= 5;

            // length of Connection ID packet is 5 bytes:
            //  1 byte header + 4 byte int value
            for (int i = 3; i < packetLength; i++) {
                buffer[i] = buffer[i + 5];
            }
        }
        packetEnd();
    }

    void packetEnd() throws IOException {
        if (DEBUG) {
            System.out.println("packetEnd()");
        }
        moreHeaders = false;

        if (transport == null) {
            throw new IOException("connection error");
        }

        if (dataOpened) {
            // closing Body header
            int len = packetLength - dataOffset;
            buffer[dataOffset+1] = (byte)(len >> 8);
            buffer[dataOffset+2] = (byte)len;
            dataOpened = false;
            dataClosed = true;
        }
        // update packet length field
        buffer[1] = (byte)(packetLength / 0x100);
        buffer[2] = (byte)(packetLength % 0x100);

        if (DEBUG) {
            int len = packetLength;
            if (!DEBUG2 && len > 20) {
                len = 20;
            }
            System.out.println("send:");
            for (int i = 0; i < len; i++) {
                System.out.print(" 0x" + Integer.toHexString(buffer[i] & 0xFF));
                int chr = buffer[i] & 0xFF;
                if (chr >= 32 && chr < 128) {
                    System.out.print("(" + (char)(buffer[i] & 0xFF) + ")");
                }
            }
            if (packetLength != len) {
                System.out.print("...");
            }
            System.out.println("");
        }
        try {
            transport.write(buffer, packetLength);
        } catch (IOException e) {
            brokenLink();
            throw e;
        }
    }

    /*
     * Connection Identifier:
     *     * must be first header in packet
     *     * 0xFFFFFFFF considered invalid - it is up to application
     *     * can't be sent on connect() request
     *     * can't be used with Target header in one packet
     */
    final void packetAddConnectionID(long id, HeaderSetImpl headers) {
        // SPEC: Illegal to send a Connection Id and a Target header
        // in the same operation.
        if (headers != null
                && headers.getHeader(HeaderSetImpl.TARGET) != null) {
            return;
        }
        if (id < 0L || id > 0xFFFFFFFFL) {
            return;
        }
        buffer[packetLength++] = (byte)HEADER_CONNECTION_ID;
        encodeInt(id);
    }

    /*
     * This method is called to handle a situation than header is too large.
     * @throws IOException
     */
    abstract void headerTooLarge() throws IOException;

    /*
     * Adds the specified headers to the packet.
     */
    final void packetAddHeaders(HeaderSetImpl headers)
        throws IOException {
        if (DEBUG) {
            System.out.println("packetAddHeaders()");
        }
        headerOverflow = false;
        newHeader.sendAllQueued();

        if (headers == null) {
            return;
        }

        int[] idList = headers.getHeaderList();

        if (!headers.challenges.isEmpty()) {
            newHeader.sendOrQueue(HEADER_AUTH_CHALLENGE,
                    headers.challenges);
        }

        if (idList == null) {
            return;
        }

        for (int i = 0; i < idList.length; i++) {
            int id = idList[i];
            Object value = headers.getHeader(id);
            newHeader.sendOrQueue(id, value);
        }
    }

    void packetAddAuthResponses() throws IOException {
        try {
            for (int i = 0; i < authResponses.size(); i++) {
                if (DEBUG) {
                    System.out.println(
                            "packetAddAuthResponses(): added response");
                }
                ObexAuth response = (ObexAuth) authResponses.elementAt(i);
                int len = response.replyAuthChallenge(buffer, packetLength,
                        authenticator);
                packetLength += len;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IOException("auth response request too large");
        }
        if (packetLength > maxSendLength) {
            throw new IOException("auth response request too large");
        }
    }

    final void sendPacket(byte[] head, long connectionId,
            HeaderSetImpl headers, boolean allHeaders) throws IOException {
        packetBegin(head);
        packetAddConnectionID(connectionId, headers);
        packetAddAuthResponses();
        packetAddHeaders(headers);
        if (allHeaders && !queuedHeaders.isEmpty()) {
            queuedHeaders.removeAllElements();
            throw new IOException("packet too large for peer");
        }
        packetEnd();
    }

    private final void encodeInt(long val) {
        buffer[packetLength++] = (byte)(val >> 24);
        buffer[packetLength++] = (byte)(val >> 16);
        buffer[packetLength++] = (byte)(val >> 8);
        buffer[packetLength++] = (byte)val;
    }

    /*
     * Reads at least <code>length</code> bytes starting from the given offset
     * into the internal buffer. More than <code>length</code> bytes may be
     * actually read. The calling function must ensure the buffer is large
     * enough to store the entire packet.
     *
     * @param offset starting offset in the destination buffer
     * @param length minimum number of bytes to read
     * @return number of bytes actually read
     * @throws IOException if an I/O error occurs
     */
    private int readLeast(int offset, int length)
	throws IOException {
        if (transport == null) {
            throw new IOException("connection error");
        }
        int read = 0;
        while (read < length) {
            int count = transport.read(cache);
    	    System.arraycopy(cache, 0, buffer, offset + read, count);
            read += count;
            if (read < length) {
                try {
                    Thread.sleep(TRANSPORT_READ_INTERVAL);
                } catch (InterruptedException e) {
                    throw new InterruptedIOException(e.getMessage());
                }
            }
        }
        return read;
    }

    final void recvPacket() throws IOException {
        authResponses.removeAllElements();
        if (transport == null) {
            throw new IOException("connection error");
        }
        try {
	    int read = readLeast(0, 3);
	    packetType = buffer[0] & 0xff;
	    packetLength = ((buffer[1] & 0xff) << 8) + (buffer[2] & 0xff);
	    if (DEBUG) {
		Logging.report(Logging.INFORMATION, 0,
		    "Expecting " + packetLength + " bytes to arrive...");
	    }
	    if (read < packetLength) {
		readLeast(read, packetLength - read);
	    }

            // dump packet:
            if (DEBUG) {
                int len = packetLength;
                if (!DEBUG2 && len > 20) {
                    len = 20;
                }

                System.out.println("recv: ");
                for (int i = 0; i < len; i++) {
                    System.out.print(" 0x"
                            + Integer.toHexString(buffer[i] & 0xFF)
				     .toUpperCase());
                }
                if (len != packetLength) System.out.print("...");
                System.out.println("");
            }
        } catch (IOException e) {
            brokenLink();
            throw e;
        }
    }

    private final void parseHeader(HeaderSetImpl headers)
            throws IOException {
        if (DEBUG) {
            System.out.println("parseHeader()");
        }
        try {
            int headerId = buffer[packetOffset++] & 0xff;
            int inputType = headerId >> 6;
            int outputType = HeaderSetImpl.internalType(headerId);
            if (outputType != HeaderSetImpl.TYPE_UNSUPPORTED) {
                inputType = outputType;
            }

            Object result = null;

            switch (inputType) {
                // ids which require special handling
                case HeaderSetImpl.TYPE_SPECIAL_TIME_ISO:
		    try {
                	result = decodeTime8601();
		    } catch (IOException e) {
			// IMPL_NOTE: Got invalid time header,
			// should probably just ignore it.
		    }
                    break;

                case HeaderSetImpl.TYPE_SPECIAL_TIME_4:
                    long date = decodeInt();
                    Calendar cal = Calendar.getInstance(utcTimeZone);
                    cal.setTime(new Date(date * 1000L));
                    result = cal;
                    packetOffset += 4;
                    break;

                case HeaderSetImpl.TYPE_SPECIAL_TYPE:
                    int len = decodeLength16(packetOffset) - 3;
                    packetOffset += 2;
                    if (buffer[packetOffset + len - 1] != 0) {
                        throw new IOException(
                                "protocol error, "
                                + "type field not null terminated");
                    }
                    result = new String(buffer, packetOffset, len - 1,
                            "ISO-8859-1");
                    packetOffset += len;
                    break;

                    // normal ids
                case HeaderSetImpl.TYPE_LONG:
                    result = new Long(decodeInt());
                    packetOffset += 4;
                    break;

                case HeaderSetImpl.TYPE_UNICODE:
                    len = decodeLength16(packetOffset) - 3;
                    packetOffset += 2;
                    if (len < 2 || buffer[packetOffset + len - 1] != 0
                            || buffer[packetOffset + len - 2] != 0) {
                        throw new IOException("protocol error, " +
                                "unicode string is not null terminated");
                    }
                    result = new String(buffer,
                                   packetOffset, len - 2, "UTF-16BE");
                    // result = new String(buffer, packetOffset, len,
                    //        "ISO-8859-1");
                    packetOffset += len;
                    break;

                case HeaderSetImpl.TYPE_BYTEARRAY:
                    len = decodeLength16(packetOffset) - 3;
                    packetOffset += 2;
                    result = new byte[len];
                    System.arraycopy(buffer, packetOffset, result, 0, len);
                    packetOffset += len;
                    break;

                case HeaderSetImpl.TYPE_BYTE:
                    result = new Byte(buffer[packetOffset++]);
                    break;

                case HeaderSetImpl.TYPE_AUTH_CHALLENGE:
                    len = decodeLength16(packetOffset);
                    ObexAuth response =
                        ObexAuth.parseAuthChallenge(buffer, packetOffset-1,
						    len);
                    if (response != null)
                        authResponses.addElement(response);
                    packetOffset += len - 1;
                    return;

                case HeaderSetImpl.TYPE_AUTH_RESPONSE:
                    len = decodeLength16(packetOffset);
                    boolean good =
                        ObexAuth.checkAuthResponse(buffer, packetOffset-1, len,
                                this, authChallenges);
                    if (good) authFailed = false;
                    packetOffset += len - 1;
                    if (DEBUG) {
                        System.out.println("checkAuthResponse() = " + good);
                    }
                    return;
            }

            if (packetOffset > packetLength) {
                throw new IOException("protocol error");
            }

            if (outputType != HeaderSetImpl.TYPE_UNSUPPORTED) {
                headers.setHeader(headerId, result);
            } else if (DEBUG) {
                System.out.println("unsupported header id = 0x"
                        + Integer.toHexString(headerId).toUpperCase());
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IOException("protocol error");
        }
    }

    /*
     * Called when requested authentication failed.
     * Implemented on server to call handler.
     */
    abstract void onAuthenticationFailure(byte[] username) throws IOException;

    void onMissingAuthResponse() throws IOException {}

    boolean shouldSendAuthResponse() {
        return (packetType == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
            && (authResponses.size() != 0);
    }

    /*
     * Parser all packet headers, BODY headers should not apear
     * and silently ignored.
     */
    final void parsePacketHeaders(HeaderSetImpl headers,
            int offset) throws IOException {
        if (DEBUG) {
            System.out.println("parsePacketHeaders()");
        }

        packetOffset = offset;
        headers.packetType = buffer[0] & 0xFF;

        parseConnectionID();

        while (packetOffset != packetLength) {
            parseHeader(headers);
        }
        parseEnd();
    }

    private final void parseConnectionID() {

        int headerId = buffer[packetOffset] & 0xFF;
        // parse connection ID
        if (packetOffset + 5 > packetLength
                || headerId != HEADER_CONNECTION_ID) {
            return;
        }
        packetOffset++;
        long id = decodeInt();
        packetOffset += 4;
        setConnectionID(id);
    }

    public abstract void setConnectionID(long id);
    public abstract long getConnectionID();

    /*
     * Begin parsing packet headers in packet possibly containing BODY
     * (data fields).
     */
    final void parsePacketDataBegin(HeaderSetImpl headers, int offset) {
        if (DEBUG) {
            System.out.println("parsePacketDataBegin()");
        }
        packetOffset = offset;
        headers.packetType = buffer[0] & 0xFF;

        parseConnectionID();
        dataOffset = packetOffset;
    }

    /*
     * Parse packet headers, put BODY field content (data) in specified output
     * array. If output is null, search for a BODY block and return 1 if it is
     * found.
     * @return number of bytes put in output.
     */
    final int parsePacketData(HeaderSetImpl headers,
            byte[] output, int outputOffset, int outputLength)
            throws IOException {
        if (DEBUG2) {
            System.out.println("parsePacketData()");
        }
        int result = 0;
        while (true) {
            int len = packetOffset - dataOffset;
            if (DEBUG2) {
                System.out.print("packetOffset = "+packetOffset+
                        " dataOffset = " +dataOffset);
                System.out.println(" len = " + len);
            }
            if (len > 0) {
                if (output == null) {
                    // special case for serching first data block
                    // without actual read
                    return 1;
                }
                if (outputLength == 0) {
		            return result;
                }
                if (len > outputLength) len = outputLength;
                System.arraycopy(buffer, dataOffset,
                        output, outputOffset, len);
                outputOffset += len;
                outputLength -= len;
                dataOffset += len;
                result += len;
                continue;
            }

            if (DEBUG) {
                System.out.println("packetOffset = " + packetOffset
                        +" packetLength = " + packetLength);
            }
            if (packetOffset == packetLength) {
                return result;
            }
            int headerId = buffer[packetOffset] & 0xff;

            if (headerId == HEADER_BODY || headerId == HEADER_EOFBODY) {
                isEof = (headerId == HEADER_EOFBODY);
                dataOffset = packetOffset + 3;
                int length = decodeLength16(packetOffset + 1);
                if (packetOffset + length > packetLength) {
                    throw new IOException("protocol error");
                }
                packetOffset += length;
                continue;
            }

            parseHeader(headers);
            dataOffset = packetOffset;
        }
    }

    final void parseEnd() throws IOException {
        if (DEBUG) {
            System.out.println("parseEnd()");
        }
        if (authFailed) {
            authFailed = false;
            onMissingAuthResponse();
        }
    }

    final int decodeLength16(int off) {
        return ((((int)buffer[off]) & 0xFF) << 8)
            + (((int)buffer[off + 1]) & 0xFF);
    }

    private final long decodeInt() {
        return ((buffer[packetOffset+0]& 0xffl) << 24)
             + ((buffer[packetOffset+1]& 0xffl) << 16)
             + ((buffer[packetOffset+2]& 0xffl) << 8)
             +  (buffer[packetOffset+3]& 0xffl);
    }

    private final Calendar decodeTime8601() throws IOException {
        int year, month, date, hour, minute, second;

        int len = decodeLength16(packetOffset) - 3;
        packetOffset += 2;

        if (len < 15 || len > 16
                || buffer[packetOffset + 8] != 0x54 // 'T'
                || (len == 16 && buffer[packetOffset + 15] != 0x5A)) { // 'Z'
	    packetOffset += len;
            throw new IOException("corrupted time header");
        }
        for (int i = 0; i < 14; i++) {
            if (i == 8) continue;
            int chr = buffer[packetOffset + i] - 0x30; // '0'
            if (chr < 0 || chr > 9) {
		packetOffset += len;
                throw new IOException("corrupted time header");
            }
        }

        year = (buffer[packetOffset+0] - 0x30) * 1000
             + (buffer[packetOffset+1] - 0x30) * 100
             + (buffer[packetOffset+2] - 0x30) * 10
             + (buffer[packetOffset+3] - 0x30);
        month = (buffer[packetOffset+4] - 0x30) * 10
	    + (buffer[packetOffset+5] - 0x30);
        date = (buffer[packetOffset+6] - 0x30) * 10
             + (buffer[packetOffset+7] - 0x30);

        hour = (buffer[packetOffset+9] - 0x30) * 10
             + (buffer[packetOffset+10] - 0x30);
        minute = (buffer[packetOffset+11] - 0x30) * 10
             + (buffer[packetOffset+12] - 0x30);
        second = (buffer[packetOffset+13] - 0x30) * 10
             + (buffer[packetOffset+14] - 0x30);

        // is check validness of time fields required?

        Calendar cal;
	// 'len' value 15 means local time,
	// 16 means UTC (in this case time string has 'Z' suffix)
        if (len == 16) {
            cal = Calendar.getInstance(utcTimeZone);
        } else {
            cal = Calendar.getInstance();
        }
        cal.set(Calendar.YEAR, year);
        cal.set(Calendar.MONTH, month - 1); // Calendar.JANUARY = 0
        cal.set(Calendar.DATE, date);

	// ISO 8601 standard uses the 24-hour clock
	// Therefore you use HOUR_OF_DAY not HOUR
        cal.set(Calendar.HOUR_OF_DAY, hour);

        cal.set(Calendar.MINUTE, minute);
        cal.set(Calendar.SECOND, second);

        // set milliseconds to zero since
        // ISO 8601 uses only second precision
	cal.set(Calendar.MILLISECOND, 0);

        packetOffset += len;
        return cal;
    }

    static int validateStatus(int status) {
        switch (status) {
            case ResponseCodes.OBEX_DATABASE_FULL:
            case ResponseCodes.OBEX_DATABASE_LOCKED:
            case ResponseCodes.OBEX_HTTP_ACCEPTED:
            case ResponseCodes.OBEX_HTTP_BAD_GATEWAY:
            case ResponseCodes.OBEX_HTTP_BAD_METHOD:
            case ResponseCodes.OBEX_HTTP_BAD_REQUEST:
            case ResponseCodes.OBEX_HTTP_CONFLICT:
            case ResponseCodes.OBEX_HTTP_CREATED:
            case ResponseCodes.OBEX_HTTP_ENTITY_TOO_LARGE:
            case ResponseCodes.OBEX_HTTP_FORBIDDEN:
            case ResponseCodes.OBEX_HTTP_GATEWAY_TIMEOUT:
            case ResponseCodes.OBEX_HTTP_GONE:
            case ResponseCodes.OBEX_HTTP_INTERNAL_ERROR:
            case ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED:
            case ResponseCodes.OBEX_HTTP_MOVED_PERM:
            case ResponseCodes.OBEX_HTTP_MOVED_TEMP:
            case ResponseCodes.OBEX_HTTP_MULT_CHOICE:
            case ResponseCodes.OBEX_HTTP_NO_CONTENT:
            case ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE:
            case ResponseCodes.OBEX_HTTP_NOT_AUTHORITATIVE:
            case ResponseCodes.OBEX_HTTP_NOT_FOUND:
            case ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED:
            case ResponseCodes.OBEX_HTTP_NOT_MODIFIED:
            case ResponseCodes.OBEX_HTTP_OK:
            case ResponseCodes.OBEX_HTTP_PARTIAL:
            case ResponseCodes.OBEX_HTTP_PAYMENT_REQUIRED:
            case ResponseCodes.OBEX_HTTP_PRECON_FAILED:
            case ResponseCodes.OBEX_HTTP_PROXY_AUTH:
            case ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE:
            case ResponseCodes.OBEX_HTTP_RESET:
            case ResponseCodes.OBEX_HTTP_SEE_OTHER:
            case ResponseCodes.OBEX_HTTP_TIMEOUT:
            case ResponseCodes.OBEX_HTTP_UNAUTHORIZED:
            case ResponseCodes.OBEX_HTTP_UNAVAILABLE:
            case ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE:
            case ResponseCodes.OBEX_HTTP_USE_PROXY:
            case ResponseCodes.OBEX_HTTP_VERSION:
                return status;
            default:
                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
        }
    }
}