package tlv;

import javax.xml.bind.DatatypeConverter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteOrder;
import java.util.ArrayList;

/**
 * Поддержка UPOS TLV (частично не совпадает с BER-TLV)
 */

public class TlvItem {

    private Tag tag;
    private int length;
    private byte[] value;

    public TlvItem(Tag tag, int length, byte[] value) {
        this.tag = tag;
        this.length = length;
        this.value = value;
    }

    public TlvItem(Tag tag, byte[] value) {
        this.tag = tag;
        this.length = value.length;
        this.value = value;
    }

    public TlvItem(Tag tag, String value) {
        this.tag = tag;
        this.length = value.length();
        this.value = value.getBytes();
    }

    public Tag getTag() {
        return tag;
    }

    public int getLength() {
        return length;
    }

    public byte[] getValue() {
        return value;
    }

    private byte[] getTagBytes() {
        byte[] result;
        int len = 1;

        if (tag.getTag() > 0xFFFFFF) {
            len = 4;
        } else if (tag.getTag() > 0xFFFF) {
            len = 3;
        } else if (tag.getTag() > 0xFF) {
            len = 2;
        }
        result = new byte[len];
        for (int i = 0; i < len; ++i) {
            result[i] = (byte) ((tag.getTag() >> (8 * (len - i - 1))) & 0xFF);
        }
        return result;
    }

    private byte[] getLengthBytes() {
        byte[] result;
        int len = 1;

        if (length > 0xFFFF) {
            len = 4;
        } else if (length > 0xFF) {
            len = 3;
        } else if (length > 0x7F) {
            len = 2;
        }
        result = new byte[len];
        switch (len) {
            case 1:
                result[0] = (byte) length;
                break;
            case 2:
            case 3:
            case 4:
                result[0] = (byte) (0x80 | (len - 1));
                for (int i = 0; i < len - 1; ++i) {
                    result[i + 1] = (byte) ((length >> 8 * (len - i - 2)) & 0xFF);
                }
                break;
        }
        return result;
    }

    private static int parseTagId(byte[] tagBytes) {
        int len = 1;

        // поддерживаются теги, длина идентификатора которых не более 4 байт
        if ((tagBytes[0] & 0x1F) == 0x1F) {
            ++len;
            if ((tagBytes[1] & 0x80) == 0x80) {
                ++len;
                if ((tagBytes[2] & 0x80) == 0x80) {
                    ++len;
                }
            }
        }
        return len;
    }

    private static int getTagId(byte[] tagBytes, int len) {
        int tagId = tagBytes[0] & 0xFF;

        for (int i = 1; i < len; ++i) {
            tagId <<= 8;
            tagId |= (tagBytes[i] & 0xFF);
        }
        return tagId;
    }

    private static int parseTagLen(byte[] lenBytes) {
        int len = 1;

        // поддерживаются теги, длина длины которых не более 4 байт
        if ((lenBytes[0] & 0x80) == 0x80) {
            len = (lenBytes[0] & 0x0F) + 1;
        }
        return len;
    }

    private static int getTagLen(byte[] lenBytes, int len) {
        int result = lenBytes[0];
        switch (len) {
            case 2:
            case 3:
            case 4:
                result = lenBytes[1];
                for (int i = 2; i < len; ++i) {
                    result <<= 8;
                    result |= (lenBytes[i] & 0xFF);
                }
                break;
        }
        return result;
    }

    public static ArrayList<TlvItem> parseBinaryBuffer(byte[] data) {
        ArrayList<TlvItem> listTlv = new ArrayList<>();

        for (int currPos = 0; currPos < data.length; ) {
            byte[] buf = new byte[4];

            if (data.length - currPos >= 4) {
                System.arraycopy(data, currPos, buf, 0, 4);
            } else {
                java.util.Arrays.fill(buf, (byte) 0);
                System.arraycopy(data, currPos, buf, 0, data.length - currPos);
            }

            int tagIdLen = parseTagId(buf);
            int tagId = getTagId(buf, tagIdLen);
            currPos += tagIdLen;

            if (data.length - currPos >= 4) {
                System.arraycopy(data, currPos, buf, 0, 4);
            } else {
                java.util.Arrays.fill(buf, (byte) 0);
                System.arraycopy(data, currPos, buf, 0, data.length - currPos);
            }

            int tagLenLen = parseTagLen(buf);
            int tagLen = getTagLen(buf, tagLenLen);
            currPos += tagLenLen;
            buf = new byte[tagLen];
            System.arraycopy(data, currPos, buf, 0, tagLen);
            currPos += tagLen;

            try {
                TlvItem tlvItem = null;
                tlvItem = new TlvItem(Tag.findTag(tagId), tagLen, buf);
                listTlv.add(tlvItem);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return listTlv;
    }

    public byte[] getFullTLV() throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();

        // tag
        buf.write(getTagBytes());
        // length
        buf.write(getLengthBytes());
        // value
        buf.write(value);
        return buf.toByteArray();
    }

    public void print() {
        java.nio.ByteBuffer byte_buffer;
        String valueStr = "";

        try {
            switch (tag.getDataType()) {
                case StringCp866:
                    valueStr = new String(value, "cp866");
                    break;
                case StringCp1251:
                    valueStr = new String(value, "cp1251");
                    break;
                case RawData:
                    valueStr = DatatypeConverter.printHexBinary(value);
                    break;
                case LittleEndianDWORD:
                    byte_buffer = java.nio.ByteBuffer.allocate(Long.BYTES);
                    byte_buffer.put(value, 0, 4);
                    byte_buffer.order(ByteOrder.LITTLE_ENDIAN);
                    byte_buffer.flip();

                    valueStr = String.format("0x%X", (int) byte_buffer.getInt());
                    break;
                case BigEndianDWORD:
                    byte_buffer = java.nio.ByteBuffer.allocate(Long.BYTES);
                    byte_buffer.put(value, 0, 4);
                    byte_buffer.order(ByteOrder.BIG_ENDIAN);
                    byte_buffer.flip();

                    valueStr = String.format("0x%X", (int) byte_buffer.getInt());
                    break;
                case Complex:
                    //TODO: Printing of complex tags
                    valueStr = DatatypeConverter.printHexBinary(value);
                    break;
            }

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        System.out.println(tag.getName() + " = " + valueStr);
    }

    public static TlvItem findItem(String tagName, ArrayList<TlvItem> container) {
        for (TlvItem item : container) {
            if (item.getTag().getName().equals(tagName))
                return item;
        }
        return null;
    }
}
