/*
 * Copyright (c) 2008-2009, Motorola, Inc.
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Motorola, Inc. nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package javax.obex;

import android.security.Md5MessageDigest;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 * This class defines a set of helper methods for the implementation of Obex.
 * @hide
 */
public final class ObexHelper {

    /**
     * Defines the basic packet length used by OBEX. Every OBEX packet has the
     * same basic format:<BR>
     * Byte 0: Request or Response Code Byte 1&2: Length of the packet.
     */
    public static final int BASE_PACKET_LENGTH = 3;

    /** Prevent object construction of helper class */
    private ObexHelper() {
    }

    /**
     * The maximum packet size for OBEX packets that this client can handle. At
     * present, this must be changed for each port. TODO: The max packet size
     * should be the Max incoming MTU minus TODO: L2CAP package headers and
     * RFCOMM package headers. TODO: Retrieve the max incoming MTU from TODO:
     * LocalDevice.getProperty().
     */
    /*
     * android note set as 0xFFFE to match remote MPS
     */
    public static final int MAX_PACKET_SIZE_INT = 0xFFFE;

    public static final int OBEX_OPCODE_CONNECT = 0x80;

    public static final int OBEX_OPCODE_DISCONNECT = 0x81;

    public static final int OBEX_OPCODE_PUT = 0x02;

    public static final int OBEX_OPCODE_PUT_FINAL = 0x82;

    public static final int OBEX_OPCODE_GET = 0x03;

    public static final int OBEX_OPCODE_GET_FINAL = 0x83;

    public static final int OBEX_OPCODE_RESERVED = 0x04;

    public static final int OBEX_OPCODE_RESERVED_FINAL = 0x84;

    public static final int OBEX_OPCODE_SETPATH = 0x85;

    public static final int OBEX_OPCODE_ABORT = 0xFF;

    public static final int OBEX_AUTH_REALM_CHARSET_ASCII = 0x00;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_1 = 0x01;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_2 = 0x02;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_3 = 0x03;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_4 = 0x04;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_5 = 0x05;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_6 = 0x06;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_7 = 0x07;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_8 = 0x08;

    public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_9 = 0x09;

    public static final int OBEX_AUTH_REALM_CHARSET_UNICODE = 0xFF;

    /**
     * Updates the HeaderSet with the headers received in the byte array
     * provided. Invalid headers are ignored.
     * <P>
     * The first two bits of an OBEX Header specifies the type of object that is
     * being sent. The table below specifies the meaning of the high bits.
     * <TABLE>
     * <TR>
     * <TH>Bits 8 and 7</TH>
     * <TH>Value</TH>
     * <TH>Description</TH>
     * </TR>
     * <TR>
     * <TD>00</TD>
     * <TD>0x00</TD>
     * <TD>Null Terminated Unicode text, prefixed with 2 byte unsigned integer</TD>
     * </TR>
     * <TR>
     * <TD>01</TD>
     * <TD>0x40</TD>
     * <TD>Byte Sequence, length prefixed with 2 byte unsigned integer</TD>
     * </TR>
     * <TR>
     * <TD>10</TD>
     * <TD>0x80</TD>
     * <TD>1 byte quantity</TD>
     * </TR>
     * <TR>
     * <TD>11</TD>
     * <TD>0xC0</TD>
     * <TD>4 byte quantity - transmitted in network byte order (high byte first</TD>
     * </TR>
     * </TABLE>
     * This method uses the information in this table to determine the type of
     * Java object to create and passes that object with the full header to
     * setHeader() to update the HeaderSet object. Invalid headers will cause an
     * exception to be thrown. When it is thrown, it is ignored.
     * @param header the HeaderSet to update
     * @param headerArray the byte array containing headers
     * @return the result of the last start body or end body header provided;
     *         the first byte in the result will specify if a body or end of
     *         body is received
     * @throws IOException if an invalid header was found
     */
    public static byte[] updateHeaderSet(HeaderSet header, byte[] headerArray) throws IOException {
        int index = 0;
        int length = 0;
        int headerID;
        byte[] value = null;
        byte[] body = null;
        HeaderSet headerImpl = header;
        try {
            while (index < headerArray.length) {
                headerID = 0xFF & headerArray[index];
                switch (headerID & (0xC0)) {

                    /*
                     * 0x00 is a unicode null terminate string with the first
                     * two bytes after the header identifier being the length
                     */
                    case 0x00:
                        // Fall through
                        /*
                         * 0x40 is a byte sequence with the first
                         * two bytes after the header identifier being the length
                         */
                    case 0x40:
                        boolean trimTail = true;
                        index++;
                        length = 0xFF & headerArray[index];
                        length = length << 8;
                        index++;
                        length += 0xFF & headerArray[index];
                        length -= 3;
                        index++;
                        value = new byte[length];
                        System.arraycopy(headerArray, index, value, 0, length);
                        if (length == 0 || (length > 0 && (value[length - 1] != 0))) {
                            trimTail = false;
                        }
                        switch (headerID) {
                            case HeaderSet.TYPE:
                                try {
                                    // Remove trailing null
                                    if (trimTail == false) {
                                        headerImpl.setHeader(headerID, new String(value, 0,
                                                value.length, "ISO8859_1"));
                                    } else {
                                        headerImpl.setHeader(headerID, new String(value, 0,
                                                value.length - 1, "ISO8859_1"));
                                    }
                                } catch (UnsupportedEncodingException e) {
                                    throw e;
                                }
                                break;

                            case HeaderSet.AUTH_CHALLENGE:
                                headerImpl.mAuthChall = new byte[length];
                                System.arraycopy(headerArray, index, headerImpl.mAuthChall, 0,
                                        length);
                                break;

                            case HeaderSet.AUTH_RESPONSE:
                                headerImpl.mAuthResp = new byte[length];
                                System.arraycopy(headerArray, index, headerImpl.mAuthResp, 0,
                                        length);
                                break;

                            case HeaderSet.BODY:
                                /* Fall Through */
                            case HeaderSet.END_OF_BODY:
                                body = new byte[length + 1];
                                body[0] = (byte)headerID;
                                System.arraycopy(headerArray, index, body, 1, length);
                                break;

                            case HeaderSet.TIME_ISO_8601:
                                try {
                                    String dateString = new String(value, "ISO8859_1");
                                    Calendar temp = Calendar.getInstance();
                                    if ((dateString.length() == 16)
                                            && (dateString.charAt(15) == 'Z')) {
                                        temp.setTimeZone(TimeZone.getTimeZone("UTC"));
                                    }
                                    temp.set(Calendar.YEAR, Integer.parseInt(dateString.substring(
                                            0, 4)));
                                    temp.set(Calendar.MONTH, Integer.parseInt(dateString.substring(
                                            4, 6)));
                                    temp.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateString
                                            .substring(6, 8)));
                                    temp.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateString
                                            .substring(9, 11)));
                                    temp.set(Calendar.MINUTE, Integer.parseInt(dateString
                                            .substring(11, 13)));
                                    temp.set(Calendar.SECOND, Integer.parseInt(dateString
                                            .substring(13, 15)));
                                    headerImpl.setHeader(HeaderSet.TIME_ISO_8601, temp);
                                } catch (UnsupportedEncodingException e) {
                                    throw e;
                                }
                                break;

                            default:
                                if ((headerID & 0xC0) == 0x00) {
                                    headerImpl.setHeader(headerID, ObexHelper.convertToUnicode(
                                            value, true));
                                } else {
                                    headerImpl.setHeader(headerID, value);
                                }
                        }

                        index += length;
                        break;

                    /*
                     * 0x80 is a byte header.  The only valid byte headers are
                     * the 16 user defined byte headers.
                     */
                    case 0x80:
                        index++;
                        try {
                            headerImpl.setHeader(headerID, Byte.valueOf(headerArray[index]));
                        } catch (Exception e) {
                            // Not a valid header so ignore
                        }
                        index++;
                        break;

                    /*
                     * 0xC0 is a 4 byte unsigned integer header and with the
                     * exception of TIME_4_BYTE will be converted to a Long
                     * and added.
                     */
                    case 0xC0:
                        index++;
                        value = new byte[4];
                        System.arraycopy(headerArray, index, value, 0, 4);
                        try {
                            if (headerID != HeaderSet.TIME_4_BYTE) {
                                // Determine if it is a connection ID.  These
                                // need to be handled differently
                                if (headerID == HeaderSet.CONNECTION_ID) {
                                    headerImpl.mConnectionID = new byte[4];
                                    System.arraycopy(value, 0, headerImpl.mConnectionID, 0, 4);
                                } else {
                                    headerImpl.setHeader(headerID, Long
                                            .valueOf(convertToLong(value)));
                                }
                            } else {
                                Calendar temp = Calendar.getInstance();
                                temp.setTime(new Date(convertToLong(value) * 1000L));
                                headerImpl.setHeader(HeaderSet.TIME_4_BYTE, temp);
                            }
                        } catch (Exception e) {
                            // Not a valid header so ignore
                            throw new IOException("Header was not formatted properly");
                        }
                        index += 4;
                        break;
                }

            }
        } catch (IOException e) {
            throw new IOException("Header was not formatted properly");
        }

        return body;
    }

    /**
     * Creates the header part of OBEX packet based on the header provided.
     * TODO: Could use getHeaderList() to get the array of headers to include
     * and then use the high two bits to determine the the type of the object
     * and construct the byte array from that. This will make the size smaller.
     * @param head the header used to construct the byte array
     * @param nullOut <code>true</code> if the header should be set to
     *        <code>null</code> once it is added to the array or
     *        <code>false</code> if it should not be nulled out
     * @return the header of an OBEX packet
     */
    public static byte[] createHeader(HeaderSet head, boolean nullOut) {
        Long intHeader = null;
        String stringHeader = null;
        Calendar dateHeader = null;
        Byte byteHeader = null;
        StringBuffer buffer = null;
        byte[] value = null;
        byte[] result = null;
        byte[] lengthArray = new byte[2];
        int length;
        HeaderSet headImpl = null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        headImpl = head;

        try {
            /*
             * Determine if there is a connection ID to send.  If there is,
             * then it should be the first header in the packet.
             */
            if ((headImpl.mConnectionID != null) && (headImpl.getHeader(HeaderSet.TARGET) == null)) {

                out.write((byte)HeaderSet.CONNECTION_ID);
                out.write(headImpl.mConnectionID);
            }

            // Count Header
            intHeader = (Long)headImpl.getHeader(HeaderSet.COUNT);
            if (intHeader != null) {
                out.write((byte)HeaderSet.COUNT);
                value = ObexHelper.convertToByteArray(intHeader.longValue());
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.COUNT, null);
                }
            }

            // Name Header
            stringHeader = (String)headImpl.getHeader(HeaderSet.NAME);
            if (stringHeader != null) {
                out.write((byte)HeaderSet.NAME);
                value = ObexHelper.convertToUnicodeByteArray(stringHeader);
                length = value.length + 3;
                lengthArray[0] = (byte)(0xFF & (length >> 8));
                lengthArray[1] = (byte)(0xFF & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.NAME, null);
                }
            }

            // Type Header
            stringHeader = (String)headImpl.getHeader(HeaderSet.TYPE);
            if (stringHeader != null) {
                out.write((byte)HeaderSet.TYPE);
                try {
                    value = stringHeader.getBytes("ISO8859_1");
                } catch (UnsupportedEncodingException e) {
                    throw e;
                }

                length = value.length + 4;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                out.write(0x00);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.TYPE, null);
                }
            }

            // Length Header
            intHeader = (Long)headImpl.getHeader(HeaderSet.LENGTH);
            if (intHeader != null) {
                out.write((byte)HeaderSet.LENGTH);
                value = ObexHelper.convertToByteArray(intHeader.longValue());
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.LENGTH, null);
                }
            }

            // Time ISO Header
            dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_ISO_8601);
            if (dateHeader != null) {

                /*
                 * The ISO Header should take the form YYYYMMDDTHHMMSSZ.  The
                 * 'Z' will only be included if it is a UTC time.
                 */
                buffer = new StringBuffer();
                int temp = dateHeader.get(Calendar.YEAR);
                for (int i = temp; i < 1000; i = i * 10) {
                    buffer.append("0");
                }
                buffer.append(temp);
                temp = dateHeader.get(Calendar.MONTH);
                if (temp < 10) {
                    buffer.append("0");
                }
                buffer.append(temp);
                temp = dateHeader.get(Calendar.DAY_OF_MONTH);
                if (temp < 10) {
                    buffer.append("0");
                }
                buffer.append(temp);
                buffer.append("T");
                temp = dateHeader.get(Calendar.HOUR_OF_DAY);
                if (temp < 10) {
                    buffer.append("0");
                }
                buffer.append(temp);
                temp = dateHeader.get(Calendar.MINUTE);
                if (temp < 10) {
                    buffer.append("0");
                }
                buffer.append(temp);
                temp = dateHeader.get(Calendar.SECOND);
                if (temp < 10) {
                    buffer.append("0");
                }
                buffer.append(temp);

                if (dateHeader.getTimeZone().getID().equals("UTC")) {
                    buffer.append("Z");
                }

                try {
                    value = buffer.toString().getBytes("ISO8859_1");
                } catch (UnsupportedEncodingException e) {
                    throw e;
                }

                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(HeaderSet.TIME_ISO_8601);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.TIME_ISO_8601, null);
                }
            }

            // Time 4 Byte Header
            dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_4_BYTE);
            if (dateHeader != null) {
                out.write(HeaderSet.TIME_4_BYTE);

                /*
                 * Need to call getTime() twice.  The first call will return
                 * a java.util.Date object.  The second call returns the number
                 * of milliseconds since January 1, 1970.  We need to convert
                 * it to seconds since the TIME_4_BYTE expects the number of
                 * seconds since January 1, 1970.
                 */
                value = ObexHelper.convertToByteArray(dateHeader.getTime().getTime() / 1000L);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.TIME_4_BYTE, null);
                }
            }

            // Description Header
            stringHeader = (String)headImpl.getHeader(HeaderSet.DESCRIPTION);
            if (stringHeader != null) {
                out.write((byte)HeaderSet.DESCRIPTION);
                value = ObexHelper.convertToUnicodeByteArray(stringHeader);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.DESCRIPTION, null);
                }
            }

            // Target Header
            value = (byte[])headImpl.getHeader(HeaderSet.TARGET);
            if (value != null) {
                out.write((byte)HeaderSet.TARGET);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.TARGET, null);
                }
            }

            // HTTP Header
            value = (byte[])headImpl.getHeader(HeaderSet.HTTP);
            if (value != null) {
                out.write((byte)HeaderSet.HTTP);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.HTTP, null);
                }
            }

            // Who Header
            value = (byte[])headImpl.getHeader(HeaderSet.WHO);
            if (value != null) {
                out.write((byte)HeaderSet.WHO);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.WHO, null);
                }
            }

            // Connection ID Header
            value = (byte[])headImpl.getHeader(HeaderSet.APPLICATION_PARAMETER);
            if (value != null) {
                out.write((byte)HeaderSet.APPLICATION_PARAMETER);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.APPLICATION_PARAMETER, null);
                }
            }

            // Object Class Header
            value = (byte[])headImpl.getHeader(HeaderSet.OBJECT_CLASS);
            if (value != null) {
                out.write((byte)HeaderSet.OBJECT_CLASS);
                length = value.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(value);
                if (nullOut) {
                    headImpl.setHeader(HeaderSet.OBJECT_CLASS, null);
                }
            }

            // Check User Defined Headers
            for (int i = 0; i < 16; i++) {

                //Unicode String Header
                stringHeader = (String)headImpl.getHeader(i + 0x30);
                if (stringHeader != null) {
                    out.write((byte)i + 0x30);
                    value = ObexHelper.convertToUnicodeByteArray(stringHeader);
                    length = value.length + 3;
                    lengthArray[0] = (byte)(255 & (length >> 8));
                    lengthArray[1] = (byte)(255 & length);
                    out.write(lengthArray);
                    out.write(value);
                    if (nullOut) {
                        headImpl.setHeader(i + 0x30, null);
                    }
                }

                // Byte Sequence Header
                value = (byte[])headImpl.getHeader(i + 0x70);
                if (value != null) {
                    out.write((byte)i + 0x70);
                    length = value.length + 3;
                    lengthArray[0] = (byte)(255 & (length >> 8));
                    lengthArray[1] = (byte)(255 & length);
                    out.write(lengthArray);
                    out.write(value);
                    if (nullOut) {
                        headImpl.setHeader(i + 0x70, null);
                    }
                }

                // Byte Header
                byteHeader = (Byte)headImpl.getHeader(i + 0xB0);
                if (byteHeader != null) {
                    out.write((byte)i + 0xB0);
                    out.write(byteHeader.byteValue());
                    if (nullOut) {
                        headImpl.setHeader(i + 0xB0, null);
                    }
                }

                // Integer header
                intHeader = (Long)headImpl.getHeader(i + 0xF0);
                if (intHeader != null) {
                    out.write((byte)i + 0xF0);
                    out.write(ObexHelper.convertToByteArray(intHeader.longValue()));
                    if (nullOut) {
                        headImpl.setHeader(i + 0xF0, null);
                    }
                }
            }

            // Add the authentication challenge header
            if (headImpl.mAuthChall != null) {
                out.write((byte)HeaderSet.AUTH_CHALLENGE);
                length = headImpl.mAuthChall.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(headImpl.mAuthChall);
                if (nullOut) {
                    headImpl.mAuthChall = null;
                }
            }

            // Add the authentication response header
            if (headImpl.mAuthResp != null) {
                out.write((byte)HeaderSet.AUTH_RESPONSE);
                length = headImpl.mAuthResp.length + 3;
                lengthArray[0] = (byte)(255 & (length >> 8));
                lengthArray[1] = (byte)(255 & length);
                out.write(lengthArray);
                out.write(headImpl.mAuthResp);
                if (nullOut) {
                    headImpl.mAuthResp = null;
                }
            }

        } catch (IOException e) {
        } finally {
            result = out.toByteArray();
            try {
                out.close();
            } catch (Exception ex) {
            }
        }

        return result;

    }

    /**
     * Determines where the maximum divide is between headers. This method is
     * used by put and get operations to separate headers to a size that meets
     * the max packet size allowed.
     * @param headerArray the headers to separate
     * @param start the starting index to search
     * @param maxSize the maximum size of a packet
     * @return the index of the end of the header block to send or -1 if the
     *         header could not be divided because the header is too large
     */
    public static int findHeaderEnd(byte[] headerArray, int start, int maxSize) {

        int fullLength = 0;
        int lastLength = -1;
        int index = start;
        int length = 0;

        while ((fullLength < maxSize) && (index < headerArray.length)) {
            int headerID = (headerArray[index] < 0 ? headerArray[index] + 256 : headerArray[index]);
            lastLength = fullLength;

            switch (headerID & (0xC0)) {

                case 0x00:
                    // Fall through
                case 0x40:

                    index++;
                    length = (headerArray[index] < 0 ? headerArray[index] + 256
                            : headerArray[index]);
                    length = length << 8;
                    index++;
                    length += (headerArray[index] < 0 ? headerArray[index] + 256
                            : headerArray[index]);
                    length -= 3;
                    index++;
                    index += length;
                    fullLength += length + 3;
                    break;

                case 0x80:

                    index++;
                    index++;
                    fullLength += 2;
                    break;

                case 0xC0:

                    index += 5;
                    fullLength += 5;
                    break;

            }

        }

        /*
         * Determine if this is the last header or not
         */
        if (lastLength == 0) {
            /*
             * Since this is the last header, check to see if the size of this
             * header is less then maxSize.  If it is, return the length of the
             * header, otherwise return -1.  The length of the header is
             * returned since it would be the start of the next header
             */
            if (fullLength < maxSize) {
                return headerArray.length;
            } else {
                return -1;
            }
        } else {
            return lastLength + start;
        }
    }

    /**
     * Converts the byte array to a long.
     * @param b the byte array to convert to a long
     * @return the byte array as a long
     */
    public static long convertToLong(byte[] b) {
        long result = 0;
        long value = 0;
        long power = 0;

        for (int i = (b.length - 1); i >= 0; i--) {
            value = b[i];
            if (value < 0) {
                value += 256;
            }

            result = result | (value << power);
            power += 8;
        }

        return result;
    }

    /**
     * Converts the long to a 4 byte array. The long must be non negative.
     * @param l the long to convert
     * @return a byte array that is the same as the long
     */
    public static byte[] convertToByteArray(long l) {
        byte[] b = new byte[4];

        b[0] = (byte)(255 & (l >> 24));
        b[1] = (byte)(255 & (l >> 16));
        b[2] = (byte)(255 & (l >> 8));
        b[3] = (byte)(255 & l);

        return b;
    }

    /**
     * Converts the String to a UNICODE byte array. It will also add the ending
     * null characters to the end of the string.
     * @param s the string to convert
     * @return the unicode byte array of the string
     */
    public static byte[] convertToUnicodeByteArray(String s) {
        if (s == null) {
            return null;
        }

        char c[] = s.toCharArray();
        byte[] result = new byte[(c.length * 2) + 2];
        for (int i = 0; i < c.length; i++) {
            result[(i * 2)] = (byte)(c[i] >> 8);
            result[((i * 2) + 1)] = (byte)c[i];
        }

        // Add the UNICODE null character
        result[result.length - 2] = 0;
        result[result.length - 1] = 0;

        return result;
    }

    /**
     * Retrieves the value from the byte array for the tag value specified. The
     * array should be of the form Tag - Length - Value triplet.
     * @param tag the tag to retrieve from the byte array
     * @param triplet the byte sequence containing the tag length value form
     * @return the value of the specified tag
     */
    public static byte[] getTagValue(byte tag, byte[] triplet) {

        int index = findTag(tag, triplet);
        if (index == -1) {
            return null;
        }

        index++;
        int length = triplet[index] & 0xFF;

        byte[] result = new byte[length];
        index++;
        System.arraycopy(triplet, index, result, 0, length);

        return result;
    }

    /**
     * Finds the index that starts the tag value pair in the byte array provide.
     * @param tag the tag to look for
     * @param value the byte array to search
     * @return the starting index of the tag or -1 if the tag could not be found
     */
    public static int findTag(byte tag, byte[] value) {
        int length = 0;

        if (value == null) {
            return -1;
        }

        int index = 0;

        while ((index < value.length) && (value[index] != tag)) {
            length = value[index + 1] & 0xFF;
            index += length + 2;
        }

        if (index >= value.length) {
            return -1;
        }

        return index;
    }

    /**
     * Converts the byte array provided to a unicode string.
     * @param b the byte array to convert to a string
     * @param includesNull determine if the byte string provided contains the
     *        UNICODE null character at the end or not; if it does, it will be
     *        removed
     * @return a Unicode string
     * @throws IllegalArgumentException if the byte array has an odd length
     */
    public static String convertToUnicode(byte[] b, boolean includesNull) {
        if (b == null || b.length == 0) {
            return null;
        }
        int arrayLength = b.length;
        if (!((arrayLength % 2) == 0)) {
            throw new IllegalArgumentException("Byte array not of a valid form");
        }
        arrayLength = (arrayLength >> 1);
        if (includesNull) {
            arrayLength -= 1;
        }

        char[] c = new char[arrayLength];
        for (int i = 0; i < arrayLength; i++) {
            int upper = b[2 * i];
            int lower = b[(2 * i) + 1];
            if (upper < 0) {
                upper += 256;
            }
            if (lower < 0) {
                lower += 256;
            }
            // If upper and lower both equal 0, it should be the end of string.
            // Ignore left bytes from array to avoid potential issues
            if (upper == 0 && lower == 0) {
                return new String(c, 0, i);
            }

            c[i] = (char)((upper << 8) | lower);
        }

        return new String(c);
    }

    /**
     * Compute the MD5 hash of the byte array provided. Does not accumulate
     * input.
     * @param in the byte array to hash
     * @return the MD5 hash of the byte array
     */
    public static byte[] computeMd5Hash(byte[] in) {
        Md5MessageDigest md5 = new Md5MessageDigest();
        return md5.digest(in);
    }

    /**
     * Computes an authentication challenge header.
     * @param nonce the challenge that will be provided to the peer; the
     *        challenge must be 16 bytes long
     * @param realm a short description that describes what password to use
     * @param access if <code>true</code> then full access will be granted if
     *        successful; if <code>false</code> then read only access will be
     *        granted if successful
     * @param userID if <code>true</code>, a user ID is required in the reply;
     *        if <code>false</code>, no user ID is required
     * @throws IllegalArgumentException if the challenge is not 16 bytes long;
     *         if the realm can not be encoded in less then 255 bytes
     * @throws IOException if the encoding scheme ISO 8859-1 is not supported
     */
    public static byte[] computeAuthenticationChallenge(byte[] nonce, String realm, boolean access,
            boolean userID) throws IOException {
        byte[] authChall = null;

        if (nonce.length != 16) {
            throw new IllegalArgumentException("Nonce must be 16 bytes long");
        }

        /*
         * The authentication challenge is a byte sequence of the following form
         * byte 0: 0x00 - the tag for the challenge
         * byte 1: 0x10 - the length of the challenge; must be 16
         * byte 2-17: the authentication challenge
         * byte 18: 0x01 - the options tag; this is optional in the spec, but
         *                 we are going to include it in every message
         * byte 19: 0x01 - length of the options; must be 1
         * byte 20: the value of the options; bit 0 is set if user ID is
         *          required; bit 1 is set if access mode is read only
         * byte 21: 0x02 - the tag for authentication realm; only included if
         *                 an authentication realm is specified
         * byte 22: the length of the authentication realm; only included if
         *          the authentication realm is specified
         * byte 23: the encoding scheme of the authentication realm; we will use
         *          the ISO 8859-1 encoding scheme since it is part of the KVM
         * byte 24 & up: the realm if one is specified.
         */
        if (realm == null) {
            authChall = new byte[21];
        } else {
            if (realm.length() >= 255) {
                throw new IllegalArgumentException("Realm must be less then 255 bytes");
            }
            authChall = new byte[24 + realm.length()];
            authChall[21] = 0x02;
            authChall[22] = (byte)(realm.length() + 1);
            authChall[23] = 0x01; // ISO 8859-1 Encoding
            System.arraycopy(realm.getBytes("ISO8859_1"), 0, authChall, 24, realm.length());
        }

        // Include the nonce field in the header
        authChall[0] = 0x00;
        authChall[1] = 0x10;
        System.arraycopy(nonce, 0, authChall, 2, 16);

        // Include the options header
        authChall[18] = 0x01;
        authChall[19] = 0x01;
        authChall[20] = 0x00;

        if (!access) {
            authChall[20] = (byte)(authChall[20] | 0x02);
        }
        if (userID) {
            authChall[20] = (byte)(authChall[20] | 0x01);
        }

        return authChall;
    }
}
