/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.jss.asn1;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.StringTokenizer;
import java.util.Vector;

public class OBJECT_IDENTIFIER implements ASN1Value {

    ///////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////
    // Standard object identifiers
    ///////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////

    /**
     * The OID space for EC
     */
    public static final OBJECT_IDENTIFIER EC_PUBKEY_OID = new OBJECT_IDENTIFIER(new long[] { 1, 2, 840, 10045, 2, 1 });

    /**
     * The OID space for RSA Data Security, Inc.
     */
    public static final OBJECT_IDENTIFIER RSADSI = new OBJECT_IDENTIFIER(new long[] { 1, 2, 840, 113549 });

    /**
     * The OID space for RSA's PKCS (public key cryptography standards).
     */
    public static final OBJECT_IDENTIFIER PKCS = RSADSI.subBranch(1);

    /**
     * The OID space for RSA's PKCS #1.
     */
    public static final OBJECT_IDENTIFIER PKCS1 = PKCS.subBranch(1);

    /**
     * The OID space for RSA's PKCS #2, which has since been folded into
     * PKCS #1.
     */
    public static final OBJECT_IDENTIFIER PKCS2 = PKCS.subBranch(2);

    /**
     * The OID space for RSA's message digest algorithms.
     */
    public static final OBJECT_IDENTIFIER RSA_DIGEST = RSADSI.subBranch(2);

    /**
     * The OID space for RSA's password-based encryption standard.
     */
    public static final OBJECT_IDENTIFIER PKCS5 = PKCS.subBranch(5);

    /**
     * The OID space for RSA's Selected Attribute Types standard, PKCS #9.
     */
    public static final OBJECT_IDENTIFIER PKCS9 = PKCS.subBranch(9);

    /**
     * The OID space for RSA's personal information exchange syntax standard.
     */
    public static final OBJECT_IDENTIFIER PKCS12 = PKCS.subBranch(12);

    /**
     * The OID space for RSA's ciphers.
     */
    public static final OBJECT_IDENTIFIER RSA_CIPHER = RSADSI.subBranch(3);

    /**
     * The OID space for FIPS standardized algorithms.
     */
    public static final OBJECT_IDENTIFIER ALGORITHM = new OBJECT_IDENTIFIER(new long[] { 1, 3, 14, 3, 2 });

    /**
     * The OID space for FIPS-180-2 SHA256/SHA384/SHA512 standardized algorithms.
     */
    public static final OBJECT_IDENTIFIER HASH_ALGORITHM = new OBJECT_IDENTIFIER(
            new long[] { 2, 16, 840, 1, 101, 3, 4, 2 });

    /**
     * The OID space for NIST standardized sign algorithms.
     */
    public static final OBJECT_IDENTIFIER SIGN_ALGORITHM = new OBJECT_IDENTIFIER(
            new long[] { 2, 16, 840, 1, 101, 3, 4, 3 });

    /**
     * The OID space for PKIX.
     */
    public static final OBJECT_IDENTIFIER PKIX = new OBJECT_IDENTIFIER(new long[] { 1, 3, 6, 1, 5, 5, 7 });

    public static final OBJECT_IDENTIFIER id_cmc = PKIX.subBranch(7);

    /**
     * CMC control attributes
     */
    public static final OBJECT_IDENTIFIER id_cmc_cMCStatusInfo = id_cmc.subBranch(1);
    public static final OBJECT_IDENTIFIER id_cmc_identification = id_cmc.subBranch(2);
    public static final OBJECT_IDENTIFIER id_cmc_identityProof = id_cmc.subBranch(3);
    public static final OBJECT_IDENTIFIER id_cmc_dataReturn = id_cmc.subBranch(4);
    public static final OBJECT_IDENTIFIER id_cmc_transactionId = id_cmc.subBranch(5);
    public static final OBJECT_IDENTIFIER id_cmc_senderNonce = id_cmc.subBranch(6);
    public static final OBJECT_IDENTIFIER id_cmc_recipientNonce = id_cmc.subBranch(7);
    public static final OBJECT_IDENTIFIER id_cmc_addExtensions = id_cmc.subBranch(8);
    public static final OBJECT_IDENTIFIER id_cmc_encryptedPOP = id_cmc.subBranch(9);
    public static final OBJECT_IDENTIFIER id_cmc_decryptedPOP = id_cmc.subBranch(10);
    public static final OBJECT_IDENTIFIER id_cmc_lraPOPWitness = id_cmc.subBranch(11);
    public static final OBJECT_IDENTIFIER id_cmc_getCert = id_cmc.subBranch(15);
    public static final OBJECT_IDENTIFIER id_cmc_getCRL = id_cmc.subBranch(16);
    public static final OBJECT_IDENTIFIER id_cmc_revokeRequest = id_cmc.subBranch(17);
    public static final OBJECT_IDENTIFIER id_cmc_regInfo = id_cmc.subBranch(18);
    public static final OBJECT_IDENTIFIER id_cmc_responseInfo = id_cmc.subBranch(19);
    public static final OBJECT_IDENTIFIER id_cmc_QueryPending = id_cmc.subBranch(21);
    public static final OBJECT_IDENTIFIER id_cmc_idPOPLinkRandom = id_cmc.subBranch(22);
    public static final OBJECT_IDENTIFIER id_cmc_idPOPLinkWitness = id_cmc.subBranch(23);
    public static final OBJECT_IDENTIFIER id_cmc_idConfirmCertAcceptance = id_cmc.subBranch(24);
    // rfc 5272
    public static final OBJECT_IDENTIFIER id_cmc_statusInfoV2 = id_cmc.subBranch(25);
    public static final OBJECT_IDENTIFIER id_cmc_trustedAnchors = id_cmc.subBranch(26);
    public static final OBJECT_IDENTIFIER id_cmc_authData = id_cmc.subBranch(27);
    public static final OBJECT_IDENTIFIER id_cmc_batchRequests = id_cmc.subBranch(28);
    public static final OBJECT_IDENTIFIER id_cmc_batchResponses = id_cmc.subBranch(29);
    public static final OBJECT_IDENTIFIER id_cmc_publishCert = id_cmc.subBranch(30);
    public static final OBJECT_IDENTIFIER id_cmc_modCertTemplate = id_cmc.subBranch(31);
    public static final OBJECT_IDENTIFIER id_cmc_controlProcessed = id_cmc.subBranch(32);
    public static final OBJECT_IDENTIFIER id_cmc_popLinkWitnessV2 = id_cmc.subBranch(33);
    public static final OBJECT_IDENTIFIER id_cmc_identityProofV2 = id_cmc.subBranch(34);

    public static final OBJECT_IDENTIFIER id_cct = PKIX.subBranch(12);

    public static final OBJECT_IDENTIFIER id_cct_PKIData = id_cct.subBranch(2);

    public static final OBJECT_IDENTIFIER id_cct_PKIResponse = id_cct.subBranch(3);

    public static final Tag TAG = new Tag(Tag.Class.UNIVERSAL, 6);

    @Override
    public Tag getTag() {
        return TAG;
    }

    public static final Form FORM = Form.PRIMITIVE;

    private long[] numbers;

    /**
     * Creates an OBJECT_IDENTIFIER from an array of longs, which constitute
     * the numbers that make up the OBJECT IDENTIFIER.
     *
     * @param numbers Numbers.
     */
    public OBJECT_IDENTIFIER(long[] numbers) {
        checkLongArray(numbers);
        this.numbers = numbers;
    }

    /**
     * Checks the given array of numbers to see if it is a valid OID.
     * This is not an exhaustive test, it just looks for obvious problems.
     * It will throw an assertion if a problem is found. With DEBUG turned
     * off, it just checks for null.
     */
    private static void checkLongArray(long[] numbers) {
        if (numbers == null) {
            throw new NullPointerException();
        }
        assert (numbers.length >= 2);
        assert (numbers[0] == 0 || numbers[0] == 1 || numbers[0] == 2);
    }

    /**
     * Creates an OBJECT_IDENTIFIER from a String version. The proper format
     * for the OID string is dotted numbers, for example:
     * "<code>3.2.456.53.23.64</code>".
     *
     * Because the toString() method here provides a different format, we also
     * allow that format, for example:
     * "<code>{3 2 456 53 23 64}</code>".
     *
     * @param dottedOID OID string.
     * @exception NumberFormatException If the given string cannot be
     *                parsed into an OID.
     */
    public OBJECT_IDENTIFIER(String dottedOID) throws NumberFormatException {

        if (dottedOID == null || dottedOID.length() == 0) {
            throw new NumberFormatException("OID string is zero-length");
        }

        if (dottedOID.startsWith("{")) {
            // input string is of the format provided by OBJECT_IDENTIFIER,toString()
            // convert this first to dotted OID

            // remove the leading and trailing brackets
            dottedOID = dottedOID.substring(1, dottedOID.length() - 1);

            // convert spaces to dots
            dottedOID = dottedOID.replaceAll(" ", ".");
        }

        StringTokenizer stok = new StringTokenizer(dottedOID, ".");
        numbers = new long[stok.countTokens()];
        int i = 0;
        while (stok.hasMoreElements()) {
            numbers[i++] = Long.parseLong(stok.nextToken());
        }
        assert (i == numbers.length);
        checkLongArray(numbers);
    }

    public long[] getNumbers() {
        return numbers;
    }

    @Override
    public int hashCode() {
        int code = 1;
        for (int i = 0; i < numbers.length; i++) {
            code = (int) (code + numbers[i]) * 10;
        }
        return code;
    }

    /**
     * Creates a new OBJECT_IDENTIFIER that is a sub-branch of this one.
     * For example, if <code>OBJECT_IDENTIFIER oid</code> has the value
     * { 1 3 5 6 },
     * then calling <code>oid.subBranch(4)</code> would return a new
     * OBJECT_IDENTIFIER with the value { 1 3 5 6 4 }.
     *
     * @param num Number.
     * @return New sub-branch.
     */
    public OBJECT_IDENTIFIER subBranch(long num) {
        long[] nums = new long[numbers.length + 1];
        System.arraycopy(numbers, 0, nums, 0, numbers.length);
        nums[numbers.length] = num;
        return new OBJECT_IDENTIFIER(nums);
    }

    /**
     * Creates a new OBJECT_IDENTIFIER that is a sub-branch of this one.
     * For example, if <code>OBJECT_IDENTIFIER oid</code> has the value
     * { 1 3 5 6 },
     * then calling <code>oid.subBranch(new long[]{ 4, 3})</code>
     * would return a new
     * OBJECT_IDENTIFIER with the value { 1 3 5 6 4 3}.
     *
     * @param newNums New numbers.
     * @return New sub-branch.
     */
    public OBJECT_IDENTIFIER subBranch(long[] newNums) {
        long[] nums = new long[numbers.length + newNums.length];
        System.arraycopy(numbers, 0, nums, 0, numbers.length);
        System.arraycopy(newNums, 0, nums, numbers.length, newNums.length);
        return new OBJECT_IDENTIFIER(nums);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof OBJECT_IDENTIFIER)) {
            return false;
        }
        long[] nums = ((OBJECT_IDENTIFIER) obj).numbers;
        if (nums.length != numbers.length) {
            return false;
        }
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != numbers[i]) {
                return false;
            }
        }
        return true;
    }

    @Override
    public String toString() {
        String ret = "{" + String.valueOf(numbers[0]);
        for (int i = 1; i < numbers.length; i++) {
            ret = ret + " " + numbers[i];
        }
        ret += "}";
        return ret;
    }

    public String toDottedString() {
        String ret = String.valueOf(numbers[0]);
        for (int i = 1; i < numbers.length; i++) {
            ret = ret + "." + numbers[i];
        }
        return ret;
    }

    @Override
    public void encode(OutputStream ostream) throws IOException {
        encode(TAG, ostream);
    }

    private byte[] encodedContents = null;

    /**
     * Gets the encoding of the contents, or a cached copy.
     * Since the content encoding is the same regardless of the Tag,
     * this only needs to be computed once.
     */
    private byte[] getEncodedContents() {
        if (encodedContents == null) {
            encodedContents = computeEncodedContents();
        }
        return encodedContents;
    }

    // We cache our encoding for a given tag.  99% of the time, only
    // one tag will be used for an instance, so we will get a cache hit.
    // In the remaining 1%, we'll have to recompute the encoding.
    byte[] cachedEncoding = null;
    Tag tagForCache = null;

    /**
     * Returns the encoding for the given tag. If the encoding for
     * this tag was previously computed (and no encoding for a different
     * tag has since been computed), this method returns a cached copy.
     * Otherwise, the encoding will be recomputed.
     */
    private byte[] getEncoding(Tag tag) {
        if (!tag.equals(tagForCache)) {
            // recompute for new tag
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            ASN1Header head = getHeader(tag);
            try {
                head.encode(out);
            } catch (IOException e) {
                throw new RuntimeException("Unable to encode ASN.1 header: " + e.getMessage(), e);
            }

            out.write(getEncodedContents(), 0, getEncodedContents().length);

            tagForCache = tag;
            cachedEncoding = out.toByteArray();
        }
        return cachedEncoding;
    }

    /**
     * Compute the ASN1 header for this tag.
     */
    private ASN1Header getHeader(Tag implicitTag) {
        return new ASN1Header(implicitTag, FORM, getEncodedContents().length);
    }

    /**
     * Actually computes the encoding of this object identifier.
     */
    private byte[] computeEncodedContents() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        // handle first number
        assert (numbers.length >= 2);
        long n = numbers[0];
        assert (n == 0 || n == 1 || n == 2);
        long outb = (numbers[0] * 40) + numbers[1];
        assert (((byte) outb) == outb);
        out.write((byte) outb);

        // handle consecutive numbers
        for (int i = 2; i < numbers.length; i++) {
            n = numbers[i];
            assert (n >= 0);

            // array of output bytes, in reverse order.  10 bytes, at 7 bits
            // per byte, is 70 bits, which is more than enough to handle
            // the maximum value of a long, which takes up 63 bits.
            byte[] rev = new byte[10];
            int idx = 0; // index into reversed bytes

            // Create reversed byte list
            do {
                rev[idx++] = (byte) (n % 128);
                n = n / 128;
            } while (n > 0);
            idx--; // backup to point to last element

            // now print them in reverse order
            while (idx > 0) {
                // all but last byte have MSB==1
                out.write(rev[idx--] | 0x80);
            }
            assert (idx == 0);
            // last byte has MSB==0
            out.write(rev[0]);
        }

        return out.toByteArray();
    }

    @Override
    public void encode(Tag implicitTag, OutputStream ostream)
            throws IOException {
        ostream.write(getEncoding(implicitTag));
    }

    private static final Template templateInstance = new Template();

    public static Template getTemplate() {
        return templateInstance;
    }

    ///////////////////////////////////////////////////////////////////////
    // OBJECT_IDENTIFIER.Template
    //
    public static class Template implements ASN1Template {

        public Tag getTag() {
            return OBJECT_IDENTIFIER.TAG;
        }

        @Override
        public boolean tagMatch(Tag tag) {
            return (tag.equals(OBJECT_IDENTIFIER.TAG));
        }

        public Form getForm() {
            return OBJECT_IDENTIFIER.FORM;
        }

        public boolean formMatch(Form form) {
            return (form == OBJECT_IDENTIFIER.FORM);
        }

        @Override
        public ASN1Value decode(InputStream istream)
                throws IOException, InvalidBERException {
            return decode(getTag(), istream);
        }

        @Override
        public ASN1Value decode(Tag implicitTag, InputStream istream)
                throws IOException, InvalidBERException {
            try {
                ASN1Header head = new ASN1Header(istream);
                long remainingContent = head.getContentLength();

                // Check the information gleaned from the header
                if (!head.getTag().equals(implicitTag)) {
                    throw new InvalidBERException("Incorrect tag for " +
                            "OBJECT IDENTIFIER: " + head.getTag());
                }
                if (head.getForm() != getForm()) {
                    throw new InvalidBERException("Incorrect form for OBJECT " +
                            "IDENTIFIER");
                }
                if (remainingContent < 1) {
                    throw new InvalidBERException("Invalid 0 length for OBJECT" +
                            " IDENTIFIER");
                }

                Vector<Long> numberV = new Vector<>();

                // handle first byte, which contains first two numbers
                byte b = readByte(istream);
                remainingContent--;
                long num = b % 40;
                numberV.addElement(Long.valueOf(b % 40)); // second number
                numberV.insertElementAt(Long.valueOf(b / 40), 0); // first number

                // handle the rest of the numbers
                while (remainingContent > 0) {
                    num = 0;

                    // keep reading until MSB == 0
                    int bitcount = 0;
                    do {
                        if ((bitcount += 7) > 63) {
                            // we're about to overflow our long
                            throw new InvalidBERException("OBJECT IDENTIFIER " +
                                    "element too long; max is 63 bits");
                        }
                        b = readByte(istream);
                        remainingContent--;
                        num <<= 7;
                        num |= (b & 0x7f);
                    } while ((b & 0x80) != 0);

                    numberV.addElement(Long.valueOf(num));
                }

                // convert Vector to array
                long numbers[] = new long[numberV.size()];
                for (int i = 0; i < numbers.length; i++) {
                    numbers[i] = numberV.elementAt(i).longValue();
                }

                // create OBJECT_IDENTIFIER from array
                return new OBJECT_IDENTIFIER(numbers);

            } catch (InvalidBERException e) {
                throw new InvalidBERException(e, "OBJECT IDENTIFIER");
            }
        }

        /**
         * Reads in a byte from the stream, throws an InvalidBERException
         * if EOF is reached.
         */
        private static byte readByte(InputStream istream)
                throws InvalidBERException, IOException {
            int n = istream.read();
            if (n == -1) {
                throw new InvalidBERException("End-of-file reached while " +
                        "decoding OBJECT IDENTIFIER");
            }
            assert ((n & 0xff) == n);
            return (byte) n;
        }

    } // end of OBJECT_IDENTIFIER.Template

}
