/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.satsa.util;

import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.TimeZone;

/**
 * Used to represent each Type, Length, Value structure in a DER buffer.
 */
public class TLV {

    /*
     * This class is used to parse DER encoded data (BER indefinite
     * length is also supported) and assemble new DER data structures
     * from elements. It is not safe to modify elements of constructed
     * DER value that was created using parsing constructor or copy()
     * method.
     */

    /** ASN context specific flag used in types (0x80). */
    public static final int CONTEXT = 0x80;
    /** ASN constructed flag used in types (0x20). */
    public static final int CONSTRUCTED = 0x20;
    /** ASN constructed flag used in types (0x20). */
    public static final int EXPLICIT = CONSTRUCTED;
    /** ANY_STRING type used as a place holder. [UNIVERSAL 0] */
    public static final int ANY_STRING_TYPE = 0x00; // our own impl
    /** ASN BOOLEAN type used in certificate parsing. [UNIVERSAL 1] */
    public static final int BOOLEAN_TYPE    = 1;
    /** ASN INTEGER type used in certificate parsing. [UNIVERSAL 2] */
    public static final int INTEGER_TYPE    = 2;
    /** ASN BIT STRING type used in certificate parsing. [UNIVERSAL 3] */
    public static final int BITSTRING_TYPE  = 3;
    /** ASN OCTET STRING type used in certificate parsing. [UNIVERSAL 4] */
    public static final int OCTETSTR_TYPE   = 4;
    /** ASN NULL type used in certificate parsing. [UNIVERSAL 5] */
    public static final int NULL_TYPE       = 5;
    /** ASN OBJECT ID type used in certificate parsing. [UNIVERSAL 6] */
    public static final int OID_TYPE        = 6;
    /** ASN ENUMERATED type. [UNIVERSAL 10] */
    public static final int ENUMERATED_TYPE        = 10;
    /** ASN UTF8String type used in certificate parsing. [UNIVERSAL 12] */
    public static final int UTF8STR_TYPE    = 12;
    /**
     *  ASN SEQUENCE type used in certificate parsing.
     * [UNIVERSAL CONSTRUCTED 16]
     */
    public static final int SEQUENCE_TYPE   = CONSTRUCTED + 16;
    /**
     * ASN SET type used in certificate parsing.
     * [UNIVERSAL CONSTRUCTED 17]
     */
    public static final int SET_TYPE        = CONSTRUCTED + 17;
    /** ASN PrintableString type used in certificate parsing. [UNIVERSAL 19] */
    public static final int PRINTSTR_TYPE   = 19;
    /** ASN TELETEX STRING type used in certificate parsing. [UNIVERSAL 20] */
    public static final int TELETEXSTR_TYPE = 20;
    /** ASN IA5 STRING type used in certificate parsing. [UNIVERSAL 22] */
    public static final int IA5STR_TYPE     = 22;
    /** ASN UCT time type used in certificate parsing. [UNIVERSAL 23] */
    public static final int UCT_TIME_TYPE   = 23;
    /**
     * ASN Generalized time type used in certificate parsing.
     * [UNIVERSAL 24]
     */
    public static final int GEN_TIME_TYPE   = 24;
    /**
     * ASN UniversalString type used in certificate parsing.
     * [UNIVERSAL 28].
     */
    public static final int UNIVSTR_TYPE    = 28;
    /** ASN BIT STRING type used in certificate parsing. [UNIVERSAL 30] */
    public static final int BMPSTR_TYPE  = 30;
    /**
     * Context specific explicit type for certificate version.
     * [CONTEXT EXPLICIT 0]
     */
    public static final int VERSION_TYPE    = CONTEXT + EXPLICIT + 0;
    /**
     * Context specific explicit type for certificate extensions.
     * [CONTEXT EXPLICIT 3]
     */
    public static final int EXTENSIONS_TYPE = CONTEXT + EXPLICIT + 3;

    /** Raw DER type. */
    public int type;
    /** Number of bytes that make up the value. */
    public int length;
    /** Offset of the value. */
    public int valueOffset;
    /** Non-null for constructed types, the first child TLV. */
    public TLV child;
    /** The next TLV in the parent sequence. */
    public TLV next;
    /** Buffer that contains the DER encoded TLV. */
    public byte[] data;

    /**
     * Constructs a TLV structure, recursing down for constructed types.
     * @param buffer DER buffer
     * @param offset where to start parsing
     * @exception IndexOutOfBoundsException if the DER is corrupt
     * @throws TLVException in case of parsing error
     */
    public TLV(byte[] buffer, int offset) throws TLVException {

        try {
            data = buffer;
            type = buffer[offset++] & 0xff;

            if ((type & 0x1f) == 0x1f) {
                // multi byte type, 7 bits per byte,
                // only last byte bit 8 as zero
                throw new TLVException("Invalid tag");
            }

            int size = buffer[offset++] & 0xff;
            boolean indefinite = (size == 128);
            if (indefinite) {
                if ((type & 0x20) == 0) {
                    throw new TLVException("Invalid length");
                }
            } else
                if (size >= 128) {
                    int sizeLen = size - 128;

                    // NOTE: for now, all sizes must fit int 3 bytes
                    if (sizeLen > 3) {
                        throw new TLVException("TLV is too large");
                    }

                    size = 0;
                    while (sizeLen > 0) {
                        size = (size << 8) + (buffer[offset++] & 0xff);
                        sizeLen--;
                    }
                }

            length = size;
            valueOffset = offset;

            if ((type & 0x20) == 0 || length == 0) {
                return;
            }

            // constructed and not empty

            TLV prev = null;
            while (true) {
                if (indefinite && data[offset] == 0 &&
                        data[offset + 1] == 0) {
                    length = offset - valueOffset;
                    return;
                }

                TLV temp = new TLV(buffer, offset);
                offset = (data[offset + 1] == (byte) 0x80 ? 2 : 0) +
                        temp.valueOffset + temp.length;

                if (prev == null) {
                    child = temp;
                } else {
                    prev.next = temp;
                }
                prev = temp;

                if (indefinite) {
                    continue;
                }
                if (offset == valueOffset + length) {
                    break;
                }
                if (offset > valueOffset + length) {
                    throw new TLVException("incorrect structure");
                }
            }
        } catch (NullPointerException npe) {
            throw new TLVException("parser error");
        } catch (IndexOutOfBoundsException iobe) {
            throw new TLVException("parser error");
        } catch (NumberFormatException nfe) {
            throw new TLVException("parser error");
        }
    }

    /**
     * Constructs a TLV structure.
     * @param tag tag of new TLV
     */
    public TLV(int tag) {
        type = tag;
    }

    /**
     * Constructs a TLV structure.
     * @param tag tag of the new TLV
     * @param bytes value of the new TLV
     */
    public TLV(int tag, byte[] bytes) {
        type = tag;
        valueOffset = 0;
        length = bytes.length;
        data = bytes;
    }

    /**
     * Constructs a TLV structure.
     * @param tag tag of the new TLV
     * @param bytes data for new TLV
     * @param offset of data
     */
    public TLV(int tag, byte[] bytes, int offset) {
        type = tag;
        valueOffset = offset;
        length = bytes.length - offset;
        data = bytes;
    }

    /**
     * Creates UTCTime TLV structure for given date.
     * @param time date
     * @return TLV value representing this date
     */
    public static TLV createUTCTime(Calendar time) {
        byte[] data = new byte[13];
        putDigits(data, 0, time.get(Calendar.YEAR));
        putDigits(data, 2, time.get(Calendar.MONTH) + 1);
        putDigits(data, 4, time.get(Calendar.DAY_OF_MONTH));
        putDigits(data, 6, time.get(Calendar.HOUR_OF_DAY));
        putDigits(data, 8, time.get(Calendar.MINUTE));
        putDigits(data, 10, time.get(Calendar.SECOND));
        data[12] = 0x5a;
        return new TLV(UCT_TIME_TYPE, data);
    }

    /**
     * Creates TLV object of type sequence.
     * @return new object
     */
    public static TLV createSequence() {
        return new TLV(SEQUENCE_TYPE);
    }

    /**
     * Creates TLV object of type integer.
     * @param data value
     * @return new object
     */
    public static TLV createInteger(byte[] data) {
        return new TLV(INTEGER_TYPE, data);
    }

    /**
     * Creates TLV object of type octet string.
     * @param data value
     * @return new object
     */
    public static TLV createOctetString(byte[] data) {
        return new TLV(OCTETSTR_TYPE, data);
    }

    /**
     * Creates TLV object of type OID.
     * @param oid OID in text form
     * @return new object
     */
    public static TLV createOID(String oid) {
        return new TLV(TLV.OID_TYPE, Utils.StringToOID(oid));
    }

    /**
     * Creates TLV object of type UTF8 string.
     * @param s string value
     * @return new object
     */
    public static TLV createUTF8String(String s) {
        return new TLV(TLV.UTF8STR_TYPE, Utils.stringToBytes(s));
    }

    /**
     * Creates TLV object of type IA5 string.
     * @param s string value
     * @throws TLVException if illegal string has been provided
     * @return new object
     */
    public static TLV createIA5String(String s)  throws TLVException {
        int len = (s == null ? 0 : s.length());
        
        if (len == 0) {
            return new TLV(TLV.IA5STR_TYPE, new byte[] {});
        }
        byte[] b = new byte[len];
        
        for (int i = 0; i < len; i++) {
            char c = s.charAt(i);
            if (c >= 0 && c <= 127) {
                b[i] = (byte)c;
            } else {
                throw new TLVException("Illegal string for IA5:" + s);
            }
        }
        return new TLV(TLV.IA5STR_TYPE, b);
    }

    /**
     * Creates TLV object of type integer.
     * @param value value
     * @return new object
     */
    public static TLV createInteger(long value) {

        int check = (value < 0) ? -1 : 0;

        int i = 1;
        while (i < 8) {
            if (value >> (i * 8) == check) {
                byte v = (byte) (value >> ((i - 1) * 8));
                if (value < 0 ? v > 0 : v < 0) {
                    i++;
                }
                break;
            }
            i++;
        }

        byte[] data = new byte[i];
        while (i > 0) {
            i--;
            data[i] = (byte) value;
            value = value >> 8;
        }
        return new TLV(TLV.INTEGER_TYPE, data);
    }

    /**
     * Creates a copy of this TLV. The value of field next of the new
     * TLV is null.
     * @return a copy of this TLV
     */
    public TLV copy() {
        try {
            return new TLV(getDERData(), 0);
        } catch (TLVException e) {}
        return null;
    }

    /**
     * Sets next element for this TLV object.
     * @param next the next object
     * @return the passed value to allow chaining
     */
    public TLV setNext(TLV next) {
        this.next = next;
        return next;
    }

    /**
     * Sets child element for this TLV object.
     * @param child the child object
     * @return the passed value to allow chaining
     */
    public TLV setChild(TLV child) {
        this.child = child;
        return child;
    }

    /**
     * Sets the (implicit) tag value for this object.
     * @param tag tag value
     * @return <code>this</code> value to allow call chaining
     */
    public TLV setTag(int tag) {
        this.type = tag;
        return this;
    }

    /**
     * Returns the value field of this TLV.
     * @return the value field of this TLV
     */
    public byte[] getValue() {

        if (data == null) {
            getDERSize();
        }
        byte[] x = new byte[length];
        getValue_(x, 0);
        return x;
    }

    /**
     * Returns DER encoded TLV.
     * @return DER encoded TLV
     */
    public byte[] getDERData() {

        byte[] x = new byte[getDERSize()];
        getDERData_(x, 0);
        return x;
    }

    /**
     * Returns DER encoded TLV.
     * @param buffer target buffer
     * @param offset offset in the buffer
     * @return value length
     */
    public int getDERData(byte[] buffer, int offset) {

        getDERSize();
        return getDERData_(buffer, offset);
    }

    /**
     * Returns the size of DER encoded TLV.
     * @return the size of DER encoded TLV
     */
    public int getDERSize() {

        if (data == null) {
            length = 0;
            TLV c = child;
            while (c != null) {
                length += c.getDERSize();
                c = c.next;
            }
        }
        return length + getTLSize();
    }

    /**
     * Returns integer value.
     * @return integer value
     * @throws TLVException if this TLV doesn't represent integer value
     */
    public int getInteger() throws TLVException {

        if (type != INTEGER_TYPE && ((type & 0xf0) != 0x80)) {
            throw new TLVException("invalid type - getInteger");
        }
        return getIntegerValue();
    }

    /**
     * Returns octet string value as integer.
     * @return integer value
     * @throws TLVException if this TLV is not octet string
     */
    public int getId() throws TLVException {

        if (type != OCTETSTR_TYPE) {
            throw new TLVException("invalid type - getId");
        }
        return getIntegerValue();
    }

    /**
     * Returns the value of enumerated type.
     * @return the value
     * @throws TLVException if TLV type is invalid
     */
    public int getEnumerated() throws TLVException {

        if (type != ENUMERATED_TYPE) {
            throw new TLVException("invalid type - getEnumerated");
        }
        return getIntegerValue();
    }

    /**
     * Returns the value of TLV as integer.
     * @return the integer value
     * @throws TLVException if the value is too long
     */
    private int getIntegerValue() throws TLVException {

        int l = data[valueOffset] < 0 ? -1 : 0;
        int check = l << 24;

        for (int i = 0; i < length; i++) {
            if ((l & 0xff000000) != check) {
                throw new TLVException("Integer value is too big");
            }
            l = (l << 8) | (data[valueOffset + i] & 0xff);
        }
        return l;
    }

    /**
     * Returns string represented by this UTF8 string.
     * @return string value
     * @throws TLVException if TLV type is invalid
     */
    public String getUTF8() throws TLVException {

        if (type != UTF8STR_TYPE && ((type & 0xf0) != 0x80)) {
            throw new TLVException("invalid type - getUTF8");
        }

        try {
            return new String(data, valueOffset, length, Utils.utf8);
        } catch (UnsupportedEncodingException e) {
            throw new TLVException("invalid encoding");
        }
    }

    /**
     * Returns true if this value represents string.
     * @return true if this value represents string
     */
    public boolean isString() {
        return (type == TELETEXSTR_TYPE ||
                type == PRINTSTR_TYPE ||
                type == UNIVSTR_TYPE ||
                type == UTF8STR_TYPE ||
                type == BMPSTR_TYPE);
    }

    /**
     * Returns time represented by this TLV.
     * @return time value
     * @throws TLVException if TLV type is invalid
     */
    public Calendar getTime() throws TLVException {

        if (type != GEN_TIME_TYPE &&
            type != UCT_TIME_TYPE) {
            throw new TLVException("invalid type - getType");
        }

        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

        int offset;
        int year;
        if (type == GEN_TIME_TYPE) {
            year = getTimeComponent(0, 4);
            offset = 4;
        } else {
            year = getTimeComponent(0, 2);
            year += (year >= 50) ? 1900 : 2000;
            offset = 2;
        }
        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, getTimeComponent(offset, 2) - 1);
        offset += 2;
        c.set(Calendar.DAY_OF_MONTH, getTimeComponent(offset, 2));
        offset += 2;
        c.set(Calendar.HOUR_OF_DAY, getTimeComponent(offset, 2));
        offset += 2;
        c.set(Calendar.MINUTE, getTimeComponent(offset, 2));
        offset += 2;
        c.set(Calendar.SECOND, getTimeComponent(offset, 2));

        return c;
    }

    /**
     * Returns decoded BCD value.
     * @param offset value offset
     * @param len value length
     * @return decoded value
     */
    private int getTimeComponent(int offset, int len) {

        int value = 0;
        while (len-- > 0) {
            value = value * 10 + (data[valueOffset + offset++] - 0x30);
        }
        return value;
    }

    /**
     * Skips optional element of DER structure with given tag.
     * @param type tag of optional value
     * @return this object if type doesn't match or the next one if it
     * does
     */
    public TLV skipOptional(int type) {

        if (this.type == type) {
            return next;
        }
        return this;
    }

    /**
     * Returns the value of flag stored in bitsring value.
     * @param index flag index
     * @return true if the flag is set
     * @throws TLVException if TLV type is invalid
     */
    public boolean checkFlag(int index) throws TLVException {

        if (type != BITSTRING_TYPE) {
            throw new TLVException("invalid type - checkFlag");
        }

        int i = (length - 1) * 8  - data[valueOffset];
        if (index >= i) {
            return false;
        }

        return ((data[valueOffset + 1 + (index/8)] << index % 8) & 0x80)
                != 0;
    }

    /**
     * Compares the value of this TLV with given value.
     * @param data the value to be compared
     * @return true if TLV object contains the same value
     */
    public boolean valueEquals(byte[] data) {
        return Utils.byteMatch(this.data, valueOffset, length,
                               data, 0, data.length);
    }

    /**
     * Places two ASCII encoded decimal digits into byte array.
     * @param data byte aray
     * @param offset the index of the first byte
     * @param value the value to be placed into the buffer
     */
    private static void putDigits(byte[] data, int offset, int value) {

        value = value % 100;
        data[offset++] = (byte) (0x30 | (value / 10));
        data[offset++] = (byte) (0x30 | (value % 10));
    }

    /**
     * IMPL_NOTE delete
     * Print the a TLV structure, recursing down for constructed types.
     * /
    public void print() {
        print(System.out, 0);
    }

    /**
     * IMPL_NOTE delete
     * Print the a TLV structure, recursing down for constructed types.
     * @param out output stream
     * /
    public void print(PrintStream out) {
        print(out, 0);
    }

    /**
     * IMPL_NOTE delete
     * Prints the a TLV structure, recursing down for constructed types.
     * @param out output stream
     * @param level what level this TLV is at
     * /
    private void print(PrintStream out, int level) {

        for (int i = 0; i < level; i++) {
            out.print("    ");
        }

        byte[] buffer;

        if (data != null) {
            buffer = data;
        } else {
            buffer = getDERData();
        }

        if (child == null) {
            out.print("Type: 0x" + Integer.toHexString(type) +
                             " length: " + length + " value: ");
            if (type == PRINTSTR_TYPE ||
                type == TELETEXSTR_TYPE ||
                type == UTF8STR_TYPE ||
                type == IA5STR_TYPE ||
                type == UNIVSTR_TYPE) {
                try {
                    out.print(new String(buffer, valueOffset, length,
                              Utils.utf8));
                } catch (UnsupportedEncodingException e) {
                    // ignore
                }
            } else if (type == OID_TYPE) {
                out.print(Utils.OIDtoString(buffer, valueOffset, length));
            } else {
                out.print(Utils.hexNumber(buffer, valueOffset, length));
            }

            out.println("");
        } else {
            if (type == SET_TYPE) {
                out.print("Set:");
            } else {
                out.print("Sequence:");
            }

            out.println("  (0x" + Integer.toHexString(type) +
                             " " + length + ")");

            child.print(out, level + 1);
        }

        if (next != null) {
            next.print(out, level);
        }
    }
/*  */

    /**
     * Places the value field of this TLV into the buffer.
     * @param buffer target buffer
     * @param offset index of the first byte
     * @return value length
     */
    private int getValue_(byte[] buffer, int offset) {

        if (data == null) {
            TLV c = child;
            while (c != null) {
                offset += c.getDERData(buffer, offset);
                c = c.next;
            }
        } else {
            System.arraycopy(data, valueOffset, buffer, offset, length);
        }
        return length;
    }

    /**
     * Places tag and length values into the buffer.
     * @param x byte buffer
     * @param i offset
     * @return value offset in the buffer
     */
    private int putHeader(byte[] x, int i) {

        x[i++] = (byte) type;

        if (length < 128) {
            x[i++] = (byte) length;
        } else
        if (length < 256) {
            x[i++] = (byte) 0x81;
            x[i++] = (byte) length;
        } else {
            x[i++] = (byte) 0x82;
            x[i++] = (byte) (length >> 8);
            x[i++] = (byte) length;
        }
        return i;
    }

    /**
     * Returns DER encoded TLV.
     * @param buffer target buffer
     * @param offset offset in the buffer
     * @return value length
     */
    private int getDERData_(byte[] buffer, int offset) {

        int initialOffset = offset;
        offset = putHeader(buffer, offset);

        if (data == null) {
            TLV c = child;
            while (c != null) {
                offset += c.getDERData_(buffer, offset);
                c = c.next;
            }
        } else {
            System.arraycopy(data, valueOffset, buffer, offset, length);
            offset += length;
        }
        return (offset - initialOffset);
    }

    /**
     * Returns the size of tag and length encoding.
     * @return the size of tag and length encoding
     */
    private int getTLSize() {

        int TLSize = 2;
        if (length >= 128) {
            int i = length;
            while (i != 0) {
                TLSize++;
                i = i >> 8;
            }
        }
        return TLSize;
    }

    /**
     * Compares this object with other TLV object.
     * @param t TLV object
     * @return true if both objects have the same type and contain
     * the same data
     */
    public boolean match(TLV t) {

        if (type != t.type) {
            return false;
        }
        if (t.data == null) {
            t = t.copy();
        }
        if (data == null) {
            t.match(this);
        }
        return Utils.byteMatch(data, valueOffset, length,
                               t.data, t.valueOffset, t.length);
    }

}
