package org.ejbca.cvc.internal.tlv;

import java.nio.ByteBuffer;

/**
 * The tag portion of a TLV.
 * <p>
 * A tag consists of 3 components:
 * <ul>
 * <li>class: the context in which the tag is defined</li>
 * <li>constructed: whether the value consists of TLVs itself (i.e.
 * SEQUENCE)</li>
 * <li>tagNumber: the tag number itself - values in the range 0-31 inclusive are
 * encoded in the first byte, multi-byte tag encoding is used to encode larger
 * tag numbers</li>
 * </ul>
 * 
 * @author fichtelmannm
 *
 */
public class Tag {
    public static final byte CONSTRUCTED_MASK = 0x20;
    public static final byte TAG_NO_MASK = 0x1F;

    private final int tagNumber;
    private final TagClass tagClass;
    private final boolean constructed;

    private int size = -1;

    /**
     * Create a tag with the provided information.
     * 
     * @param tagNumber
     *            the actual tag number
     * @param tagClass
     *            the tag class
     * @param constructed
     *            if the value inside this tag is constructed (i.e. SEQUENCE/SET)
     */
    public Tag(int tagNumber, TagClass tagClass, boolean constructed) {
	assertMaximumNotExceeded(tagNumber);
	this.tagNumber = tagNumber;
	this.tagClass = tagClass;
	this.constructed = constructed;
    }

    private static void assertMaximumNotExceeded(int tagNumber) {
	if ((tagNumber >> (7 * 4)) > 0) {
	    throw new IllegalArgumentException("tagNumber out of valid range");
	}
    }

    /**
     * The tag number.
     * 
     * @return the tag number.
     */
    public int getTagNumber() {
	return tagNumber;
    }

    /**
     * The tag class.
     * 
     * @return the tag class.
     */
    public TagClass getTagClass() {
	return tagClass;
    }

    /**
     * Whether the constructed bit is set.
     * 
     * @return <code>true</code> if the constructed bit is set.
     */
    public boolean isConstructed() {
	return constructed;
    }

    /**
     * Calculate the size that is needed to encode this tag.
     * 
     * @return the number of bytes needed to encode this tag.
     */
    public int size() {
	if (size == -1) {
	    if (tagNumber < 0x1F)
		size = 1;
	    else if (tagNumber < 0x80)
		size = 2;
	    else if (tagNumber < 0x4000)
		size = 3;
	    else if (tagNumber < 0x200000)
		size = 4;
	    else
		size = 5;
	}
	return size;
    }

    /**
     * Encode this tag unto the provided {@link ByteBuffer} starting at the current
     * position and advancing the position by {@link #size()}.
     * 
     * @param data
     *            the {@link ByteBuffer} to write this tag to.
     */
    public void encodeTo(ByteBuffer data) {
	byte first = tagClass.getValue();
	if (constructed) {
	    first |= CONSTRUCTED_MASK;
	}

	if (tagNumber < 31) {
	    first |= tagNumber;
	    data.put(first);
	} else {
	    first |= TAG_NO_MASK;
	    data.put(first);
	    if (tagNumber <= 0x7F) {
		data.put((byte) (tagNumber & 0x7F));
	    } else {
		putMultiByteTag(data, tagNumber);
	    }
	}

    }

    /**
     * Parse a tag from the provided {@link ByteBuffer}. Starts at the current
     * position and advances the position after the last tag byte.
     * 
     * @param data
     *            the {@link ByteBuffer} to read from.
     * @return the parsed tag.
     */
    public static Tag parse(ByteBuffer data) {
	byte first = data.get();
	TagClass tagClass = TagClass.fromTag(first);
	boolean constructed = (first & CONSTRUCTED_MASK) == CONSTRUCTED_MASK;

	int tagNo = first & TAG_NO_MASK;
	if (tagNo == TAG_NO_MASK) {
	    tagNo = parseMultiByteTag(data);
	}

	return new Tag(tagNo, tagClass, constructed);
    }

    private static void putMultiByteTag(ByteBuffer data, int tagNumber) {
	byte[] stack = new byte[5];
	int position = stack.length;

	stack[--position] = (byte) (tagNumber & 0x7F);
	do {
	    tagNumber >>= 7;
	    stack[--position] = (byte) (tagNumber & 0x7F);
	} while (tagNumber > 0x7F);

	data.put(stack, position, stack.length - position);
    }

    private static int parseMultiByteTag(ByteBuffer data) {
	int tagNo = 0;
	byte b;
	int processed = 0;

	do {
	    tagNo <<= 7;
	    b = data.get();
	    tagNo |= b & 0x7F;
	    processed++;
	} while ((b & 0x80) != 0 && processed < 4);
	if ((b & 0x80) != 0 && processed == 4) {
	    throw new IllegalStateException("illegal tag length (>5 bytes)");
	}

	return tagNo;
    }
}

@@ -748,36 +748,8 @@
 d;%0A%0A
-    private int size = -1;%0A%0A
     
@@ -2084,31 +2084,8 @@
 %09if 
-(size == -1) %7B%0A%09    if 
 (tag
@@ -2104,24 +2104,23 @@
 F)%0A%09
-%09size =
+    return
  1;%0A%09
-    
 else
@@ -2147,24 +2147,23 @@
 0)%0A%09
-%09size =
+    return
  2;%0A%09
-    
 else
@@ -2192,24 +2192,23 @@
 0)%0A%09
-%09size =
+    return
  3;%0A%09
-    
 else
@@ -2239,56 +2239,41 @@
 0)%0A%09
-%09size =
+    return
  4;%0A%09
-    
 else%0A%09
-%09size = 5;%0A%09%7D%0A%09
+    
 return 
-size
+5
 ;%0A  

remove internal state since size calculation is not expensive