/*
 * Copyright 2006, Danger, Inc.  All Rights Reserved.
 * This file is subject to the Danger, Inc. Sample Code License,
 * which is provided in the file SAMPLE_CODE_LICENSE.
 * Copies are also available from http://developer.danger.com/
 */

package com.danger.terminal;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.Enumeration;
import danger.util.ByteArray;


/**
 * General class for packing a set of TLVs into a byte array, or unpacking
 * them back out.
 *
 * A TLV is a (type, length, value).
 */

/*
 * TLVSet format (v3):
 * 
 * <byte = 03> <series of TLVs...>
 *
 * TLV = <short type> <short len> <data bytes...>
 *
 * type = ffffTTTT TTTTTTTT
 *     upper 4 bits: format (0 = string, 1 = byte[], ...)
 *     lower 12 bits: user-defined key (type)
 */
 
public class TLVSet
{
    public
    TLVSet ()
    {
        mLenBuffer = new byte[2];
        mOut = new ByteArrayOutputStream();
        mOut.write(3);
        mData = new Hashtable();
    }

    public
    TLVSet (byte[] in)
        throws TLVException
    {
        mBytes = in;
        init(in, 0, in.length);
    }

    public
    TLVSet (byte[] in, int offset, int length)
        throws TLVException
    {
        mBytes = in;
        init(in, offset, length);
    }

    protected void
    init (byte[] in, int offset, int length)
        throws TLVException
    {
        mVersion = (int)in[0];
        if ((mVersion < 2) || (mVersion > 3)) {
            throw new TLVException("TLVSet doesn't understand v" + mVersion + " byte streams.");
        }

        offset++;
        length--;
        mData = new Hashtable();

        while (length > 0) {
            TLV tlv = new TLV();
            int key = 0;

            if (mVersion == 2) {
                tlv.type = TYPE_STRING;
                key = in[offset++];
                length--;
            } else {
                key = ByteArray.readShort(in, offset);
                tlv.type = (key >> 12) & 15;
                key &= 0xfff;
                offset += 2;
                length -= 2;
            }

            tlv.length = ByteArray.readShort(in, offset);
            tlv.offset = offset + 2;
            offset += tlv.length + 2;
            length -= tlv.length + 2;

            mData.put(new Integer(key), tlv);
        }

        mBytes = in;
    }

    public byte[]
    getBytes ()
    {
        return mOut.toByteArray();
    }

    /** for debugging */
    public String
    toString ()
    {
        StringBuffer out = new StringBuffer();

        for (Enumeration e = mData.keys(); e.hasMoreElements(); ) {
            Integer key = (Integer) e.nextElement();
            TLV tlv = (TLV) mData.get(key);

            out.append("(");
            out.append(key.toString());
            out.append(": ");

            switch (tlv.type) {
            case TYPE_STRING:
                out.append("\"");
                out.append(quoteString(new String(mBytes, tlv.offset, tlv.length)));
                out.append("\"");
                break;
            case TYPE_DATA:
                for (int i = 0; i < tlv.length; i++) {
                    int x = (int)mBytes[tlv.offset + i] & 0xff;
                    if (i > 0) {
                        out.append(" ");
                    }
                    if (x < 16) {
                        out.append("0");
                    }
                    out.append(Integer.toHexString(x));
                }
                break;
            case TYPE_BOOLEAN:
                if (mBytes[tlv.offset] != 0) {
                    out.append("True");
                } else {
                    out.append("False");
                }
                break;
            case TYPE_INT:
                out.append(Integer.toString(ByteArray.readInt(mBytes, tlv.offset)));
                break;
            case TYPE_INTARRAY:
                out.append("[ ");
                for (int i = 0; i < tlv.length; i += 4) {
                    out.append(Integer.toString(ByteArray.readInt(mBytes, tlv.offset + i)));
                    out.append(" ");
                }
                out.append("]");
            }
            out.append(") ");
        }

        return out.toString();
    }


    //  ----------  accessors

    public String
    getString (int key, String defaultValue)
    {
        TLV tlv = (TLV) mData.get(new Integer(key));
        if ((tlv == null) || (tlv.type != TYPE_STRING)) {
            return defaultValue;
        }

        return new String(mBytes, tlv.offset, tlv.length);
    }

    public String
    getString (int key)
    {
        return getString(key, null);
    }

    public byte[]
    getData (int key)
    {
        TLV tlv = (TLV) mData.get(new Integer(key));
        if ((tlv == null) || (tlv.type != TYPE_DATA)) {
            return null;
        }
        byte[] data = new byte[tlv.length];
        System.arraycopy(mBytes, tlv.offset, data, 0, tlv.length);
        return data;
    }

    public boolean
    getBoolean (int key, boolean defaultValue)
    {
        TLV tlv = (TLV) mData.get(new Integer(key));
        if ((tlv == null) || (tlv.type != TYPE_BOOLEAN)) {
            return defaultValue;
        }
        return (mBytes[tlv.offset] != 0);
    }

    public int
    getInt (int key, int defaultValue)
    {
        TLV tlv = (TLV) mData.get(new Integer(key));
        if ((tlv == null) || (tlv.type != TYPE_INT)) {
            return defaultValue;
        }
        return ByteArray.readInt(mBytes, tlv.offset);
    }

    public int[]
    getIntArray (int key)
    {
        TLV tlv = (TLV) mData.get(new Integer(key));
        if ((tlv == null) || (tlv.type != TYPE_INTARRAY)) {
            return null;
        }

        int[] out = new int[tlv.length >> 2];
        int index = 0;
        for (int i = 0; i < tlv.length; i += 4) {
            out[index++] = ByteArray.readInt(mBytes, tlv.offset + i);
        }
        return out;
    }

    public void
    addString (int key, String s)
    {
        try {
            byte[] data = s.getBytes("UTF-8");
            addTLV(TYPE_STRING, key, data, 0, data.length);
        } catch (UnsupportedEncodingException x) {
            // can't happen with UTF-8
        }
    }

    public void
    addData (int key, byte[] data, int offset, int length)
    {
        addTLV(TYPE_DATA, key, data, offset, length);
    }

    public void
    addData (int key, byte[] data)
    {
        addData(key, data, 0, data.length);
    }

    public void
    addBoolean (int key, boolean b)
    {
        byte[] x = new byte[1];
        x[0] = b ? (byte)1 : (byte)0;

        addTLV(TYPE_BOOLEAN, key, x, 0, 1);
    }

    public void
    addInt (int key, int data)
    {
        byte[] x = new byte[4];
        ByteArray.writeInt(x, 0, data);
        addTLV(TYPE_INT, key, x, 0, 4);
    }

    public void
    addIntArray (int key, int[] data, int offset, int length)
    {
        byte[] x = new byte[4 * length];
        for (int i = 0; i < x.length; i += 4) {
            ByteArray.writeInt(x, i, data[offset++]);
        }

        addTLV(TYPE_INTARRAY, key, x, 0, 4 * length);
    }

    public void
    addIntArray (int key, int[] data)
    {
        addIntArray(key, data, 0, data.length);
    }


    //  ----------


    protected void
    addTLV (int type, int key, byte[] data, int offset, int length)
    {
        key = key & 0xfff;

        TLV tlv = new TLV();
        tlv.type = type;
        tlv.offset = mOut.size() + 4;
        tlv.length = length;
        mData.put(new Integer(key), tlv);
        
        ByteArray.writeShort(mLenBuffer, 0, (type << 12) | key);
        mOut.write(mLenBuffer, 0, 2);
        ByteArray.writeShort(mLenBuffer, 0, length);
        mOut.write(mLenBuffer, 0, 2);
        mOut.write(data, offset, length);
        
        mBytes = mOut.toByteArray();
    }


    //  ----------

    public static String
    quoteString (String s)
    {
        StringBuffer out = new StringBuffer();

        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '\\') {
                out.append(c);
            }
            if ((c >= 32) && (c <= 126)) {
                out.append(c);
                continue;
            }

            if (c == '\t') {
                out.append("\\t");
                continue;
            }
            if (c == '\r') {
                out.append("\\r");
                continue;
            }
            if (c == '\n') {
                out.append("\\n");
                continue;
            }
            if (c < 256) {
                out.append("\\x");
                if (c < 16) {
                    out.append("0");
                }
                out.append(Integer.toHexString(c));
                continue;
            }
            out.append("\\u");
            if (c < 4096) {
                out.append("0");
            }
            out.append(Integer.toHexString(c));
        }

        return out.toString();
    }


    protected ByteArrayOutputStream         mOut;

    // used for temporary storage when writing length bytes
    protected byte[]                        mLenBuffer;

    protected byte[]                        mBytes;
    protected int                           mVersion;
    protected Hashtable                     mData;      // map<int, TLV>: key -> TLV

    private static final int TYPE_STRING    = 0;
    private static final int TYPE_DATA      = 1;
    private static final int TYPE_BOOLEAN   = 2;
    private static final int TYPE_INT       = 3;
    private static final int TYPE_INTARRAY  = 11;


    protected static class TLV {
        public int type;
        public int length;
        public int offset;
    };
}
