package utils;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.util.Log;

public class TLVHelper
{
    public static byte[] getTLVFromResponse(byte[] bytes)
    {
        for(int i=0; i<bytes.length; ++i)
        {
            if (bytes[i++] == (byte)0xf0)
            {
                int length = 0;
                if ((bytes[i] & (byte)0x80) == 0) //short form length
                {
                    length = bytes[i++]; //i is at the start of data
                }
                else
                if((bytes[i] & (byte)0x7f) != 0) //long form length
                {
                    int numOctets = (bytes[i++] & (byte)0x7f);//i is at the start of length
                    for(int j = 0; j<numOctets; ++j, ++i)
                    {
                        length = (length<<8) | (bytes[i] & 0xff);
                    }
                }
                byte[] rcBytes=new byte[length];
                System.arraycopy(bytes, i, rcBytes, 0, length);
                return rcBytes;
            }
        }
        return null;
    }
    
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static byte[] dataDictionaryToTLVData( Map<String, Object> dictWithTagsAndData, String addHeader)
    throws Exception
    {
        ByteArrayOutputStream retValue = new ByteArrayOutputStream();
        Set<String> keys=dictWithTagsAndData.keySet();
        for(String key : keys)
        {
            Object value = dictWithTagsAndData.get(key);
            if(value instanceof List<?>)
            {
                //there is more than one value with this tag
                List<byte[]> list=(List<byte[]>)value;
                for( byte[] data : list )
                {
                    byte[] header = headerForDataWithTag(data, key);
                    retValue.write(header);
                    retValue.write(data);
                }
            }
            else
            if(value instanceof Map)
            {
                Map map=(Map)value;
                byte[] data = dataDictionaryToTLVData(map, null);
                byte[] header = headerForDataWithTag(data, key);
                retValue.write(header);
                retValue.write(data);
            }
            else
            {
                //there is only one value with this tag
                byte[] data = (byte[])value;
                byte[] header = headerForDataWithTag(data, key);
                retValue.write(header);
                retValue.write(data);
            }
        }
        
        if(addHeader != null)
        {
            byte[] header = headerForDataWithTag(retValue.toByteArray(), addHeader);
            ByteArrayOutputStream newRetValue=new ByteArrayOutputStream();
            newRetValue.write(header);
            if( retValue.size() > 0)
                newRetValue.write(retValue.toByteArray());
            newRetValue.close();
            retValue=newRetValue;
        }
        
        return retValue.toByteArray();
    }
    
    @SuppressWarnings("unchecked")
    public static Map<String, Object> tlvDataToDataDictionary(byte[] tlv)
    {
        final byte[] bytes = tlv;
        Map<String, Object> retVal=new HashMap<String, Object>();
        for (int i=0; i<tlv.length;)
        {
            String tag;
            //get the first byte of the tag
            byte tag1 = bytes[i];
            if((tag1 & (byte)0x1f) == (byte)0x1f) //this is a 2 byte tag
            {
                tag = String.format("%02x%02x", tag1, bytes[i+1]);
                i+=2;
            }
            else //this is a 1 byte tag
            {
                tag = String.format("%02x", tag1);
                ++i;
            }
            
            //get the length
            int length = 0;
            if (bytes.length == 1 && i == 1){
            	Log.d("BARRETT","breakpoint");
            }
            if((bytes[i] & (byte)0x80) == 0) //short form length
            {
                length = bytes[i++]; //i contains the number of bytes in the data part of this tag
            }
            else
            if((bytes[i] & (byte)0x7f) != 0) //long form length
            {
                int numOctets = (bytes[i++] & (byte)0x7f); //i contains the number of subsequent octets in the length
                for (int j=0; j<numOctets; ++j, ++i)
                {
                    length = (length<<8) | bytes[i];
                }
            }
            else //indefinite length
            {
                return null;
            }
            
            //get the rest of the data
            byte[] data = null;
            if (length > 0)
            {
                data=new byte[length];
                System.arraycopy(bytes, i, data, 0, length);
            }
            i += length;
            
            Object valueToAdd;
            //determine if it is primitive or constructed
            if ((tag1 & (byte)0x20) == (byte)0x20)
            {
                //it's constructed
                data=new byte[length];
                valueToAdd = tlvDataToDataDictionary(data);
            }
            else
            {
                if(data!=null)
                {
                    valueToAdd=data;
                }
                else
                {
                    valueToAdd=null;
                }
            }
            
            
            //check if it's already been added
            Object oldValue = retVal.get(tag);
            if (oldValue!=null)
            {
                if(oldValue instanceof List)
                {
                    List<byte[]> list=(List<byte[]>)oldValue;
                    list.add((byte[])valueToAdd);
                }
                else
                {
                    //there is only one,
                    //create a new array to hold all values of this tag
                    List<byte[]> list = new ArrayList<byte[]>();
                    list.add((byte[])oldValue);
                    list.add((byte[])valueToAdd);
                    retVal.put(tag, list);
                }
            }
            else
            {
                //we don't have one yet, just stash it in the dictionary
                retVal.put(tag, valueToAdd);
            }
        }
        return retVal;
    }
    
    private static byte[] headerForDataWithTag(byte[] data, String tag) throws Exception
    {
        ByteArrayOutputStream headerData=new ByteArrayOutputStream();
        headerData.write( HexUtil.hexStringToByteArray(tag) );
        
        if(data==null || data.length==0)
        {
            if("f0".equals(tag))
            {
                int zero=0;
                headerData.write((byte)zero);
            }
        }
        else
        if(data.length <= 127) //max value for short form length
        {
            int length = data.length;
            headerData.write((byte)length);
        }
        else //long form length
        {
            //calculate the number of octets for the length
            int dataLength = data.length;
            double numBits = Math.log(dataLength+1)/Math.log(2);
            double numOcts = Math.ceil(numBits/8);
            
            if (numOcts > 4)
                throw new Exception("Data is too large to encode."); // this should never happen
            
            //stash the number of octets for length in the header
            int numBytes = (int)numOcts;
            int encodedLength = numBytes | 0x80; //long form bit.
            headerData.write((byte)encodedLength);
            
            //append the length
            byte[] length = new byte[numBytes];
            //start from 4-length
            for (int i=0; i<numBytes; ++i)
            {
                int val = ((dataLength >> (i*8)) & 0xff);
                length[numBytes-i-1] = (byte)val;
            }
            headerData.write(length, 0, numBytes);
        }

        return headerData.toByteArray();
    }
}

