package emv.utils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BerTlv {	
	private final Tag mTag;
	private final byte[] mValue;
	
	public BerTlv(Tag tag, byte[] value) {
		if (tag == null) {
			throw new IllegalArgumentException("The argument 'tag' can not be null");
		}
		
		if (value == null) {
			throw new IllegalArgumentException("The argument 'value' can not be null");
		}
		
		this.mTag = tag;		
		this.mValue = value;
	}

	public BerTlv(int tag, byte[] value) {
		this(new Tag(tag), value);
	}
	
	public Tag getTag() {
		return mTag;
	}
	
	public byte[] getLengthBytes() {
		return encodeLength(mValue.length);
	}
	 	
	public byte[] getValue() {
		return mValue;
	}
	
	public String getValueAsHexString() {
		final char[] hex = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
		final char[] buf = new char[mValue.length * 2];  
		
		for (int i = 0, j = 0; i < mValue.length; i++) {
			buf[j++] = hex[((mValue[i] >> 4) & 0xf)];
			buf[j++] = hex[((mValue[i]) & 0xf)];			
		}
		
		return new String(buf);
	}
		
	public int getValueAsInteger() {
		int value = 0;
		
		for (byte b: mValue) {
			value = (value << 8) + (b & 0xff);
		}
		
		return value;
	}
	
	public byte[] toByteArray() {
		byte[] tmpTag = mTag.getBytes();
		byte[] tmpLen = getLengthBytes();
		byte[] tmpVal = mValue;
		byte[] buffer = new byte[tmpTag.length + tmpLen.length + tmpVal.length];
		System.arraycopy(tmpTag, 0, buffer, 0, tmpTag.length);
		System.arraycopy(tmpLen, 0, buffer, tmpTag.length, tmpLen.length);
		System.arraycopy(tmpVal, 0, buffer, tmpTag.length + tmpLen.length, tmpVal.length);		
		return buffer;		
	}
		
	public static byte[] encodeLength(int length) {
		byte[] data = null;
		
		if (length == 0) {
			data = new byte[] { (byte) 0x00 };
		} else if (length <= 127) {
			data = new byte[] { (byte) length };
		} else {
			int numberOfBytes = 0;
			
			do {
				numberOfBytes++;
			} while ((length & (0x7FFFFF << (8 * numberOfBytes))) > 0);
				
			data = new byte[numberOfBytes + 1];
			data[0] = (byte)(0x80 + numberOfBytes);			
			for (int i = 0; i < numberOfBytes; i++) {
				data[numberOfBytes - i] = (byte)((length >> (i * 8)) & 0xff); 
			}				
		}
		
		return data;
	}
	
	public static int decodeLength(ByteBuffer data) {
		int length = (int)data.get() & 0xff;
		
		if ((length & 0x80) != 0) {
			int numberOfBytes = length & 0x7F;
			
			length = 0;
			while (numberOfBytes > 0) {
				length = (length << 8) + ((int)data.get() & 0xff);
				numberOfBytes--;
			}
		}
		
		return length;				
	}
	
	public static BerTlv create(Tag tag, byte[] value) {
		return new BerTlv(tag, value);
	}
	
	public static BerTlv create(Tag tag, List<BerTlv> values) {
		byte[][] container = new byte[values.size()][];		                                
		int totalDataLen = 0;		
		
		for (int i = 0; i < container.length; i++) {
			container[i] = values.get(i).toByteArray();			
			totalDataLen+= container[i].length;								
		}
		
		byte[] buffer = new byte[totalDataLen];
		
		for (int i = 0, off = 0; i < container.length; i++) {
			System.arraycopy(container[i], 0, buffer, off, container[i].length);
			off+= container[i].length;
		}
		
		return new BerTlv(tag, buffer);
	}
	
	public static BerTlv create(ByteBuffer buffer) {
		Tag tag = Tag.create(buffer);
		int len = decodeLength(buffer);
		byte[] val = new byte[len];
		buffer.get(val);		
		return new BerTlv(tag, val);		
	}
	
	public static BerTlv create(byte[] src, int off, int len) {
		ByteBuffer buffer = ByteBuffer.wrap(src, off, len); 
		return create(buffer);		
	}
	
	public static BerTlv create(byte[] src) {
		ByteBuffer buffer = ByteBuffer.wrap(src, 0, src.length); 
		return create(buffer);		
	}
	
	public static List<BerTlv> createList(ByteBuffer buffer) {
		final List<BerTlv> tlvList = new ArrayList<BerTlv>();
		
		while (buffer.hasRemaining()) {
			BerTlv tlv = BerTlv.create(buffer);
			tlvList.add(tlv);
		}
		
		return tlvList;
	}

	public static List<BerTlv> createList(byte[] array) {
		return createList(ByteBuffer.wrap(array));
	}
	
	public static Map<Tag, byte[]> createMap(ByteBuffer buffer) {
		final Map<Tag, byte[]> tlvMap = new HashMap<Tag, byte[]>();
		
		while (buffer.hasRemaining()) {
			BerTlv tlv = BerTlv.create(buffer);
			tlvMap.put(tlv.getTag(), tlv.getValue());
		}
		
		return tlvMap;
	}
		
	public static Map<Tag, byte[]> createMap(byte[] array) {
		return createMap(ByteBuffer.wrap(array));
	}
	
	public static byte[] listToByteArray(List<BerTlv> input) {
		final List<byte[]> dataList = new ArrayList<byte[]>();
		int totalLen = 0;
		
		for (BerTlv tlv: input) {
			byte[] tmp = tlv.toByteArray();
			dataList.add(tmp);
			totalLen += tmp.length;			
		}
		
		
		byte[] buffer = new byte[totalLen];
		totalLen = 0;
		for (byte[] data: dataList) {
			System.arraycopy(data, 0, buffer, totalLen, data.length);
			totalLen+= data.length;
		}				
		
		return buffer;
	}
	
	public static byte[] mapToByteArray(Map<Tag, byte[]> input) {
		final List<byte[]> dataList = new ArrayList<byte[]>();
		int totalLen = 0;
		
		for (Tag tag: input.keySet()) {
			byte[] tmpTag = tag.getBytes();
			byte[] tmpVal = input.get(tag);
			byte[] tmpLen = BerTlv.encodeLength(tmpVal.length);
			
			dataList.add(tmpTag);
			dataList.add(tmpLen);
			dataList.add(tmpVal);
			
			totalLen += tmpTag.length + tmpLen.length + tmpVal.length;
		}
		
		byte[] buffer = new byte[totalLen];
		totalLen = 0;
		for (byte[] data: dataList) {
			System.arraycopy(data, 0, buffer, totalLen, data.length);
			totalLen+= data.length;
		}				
		
		return buffer;
	}
	
	public static BerTlv find(ByteBuffer buffer, Tag tag) {
		while (buffer.hasRemaining()) {
			BerTlv tlv = BerTlv.create(buffer);
			
			if (tlv.getTag().equals(tag)) {
				return tlv;
			}									
		}
		
		return null;
	}
	
	public static BerTlv find(ByteBuffer buffer, int tag) {
		return find(buffer, new Tag(tag));
	}
	
	public static BerTlv find(byte[] array, Tag tag) {
		return find(ByteBuffer.wrap(array), tag);
	}
	
	public static BerTlv find(byte[] array, int tag) {
		return find(ByteBuffer.wrap(array), tag);
	}
		
	@Override
    public boolean equals(Object obj) {
        if (obj != null && (obj instanceof Tag)) {
        	BerTlv other = (BerTlv)obj;
        	        	
        	if (!mTag.equals(other.getTag())) return false;
        	
        	if (mValue.length != other.mValue.length) return false;
            
            for (int i = 0; i < mValue.length; i++) {
            	if (mValue[i] != other.mValue[i]) return false;
            }            
            
            return true;         
        }
        
        return false;
    }
    
	@Override
    public int hashCode() {
		int result = 1 + BerTlv.class.getName().hashCode();
		
		for (byte element : mTag.getBytes()) {
			result = 31 * result + element;
		}
		
		for (byte element : mValue) {
			result = 31 * result + element;
		}
		
		return result;
    }
    
    @Override
    public String toString() {
    	return "BerTlv [Tag=" + mTag.toHexValue() + ", Length=" + mValue.length + ", Value=" + getValueAsHexString() + "]";    	
    }
    
}

