/* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Represents an ASN.1 <code>ANY</code> value. An ANY is just an arbitrary
 * ASN.1 value. It can be thought of as the simplest implementation of the
 * <code>ASN1Value</code> interface. Although they can be created
 * from scratch (from raw BER), instances of <code>ANY</code> are usually
 * found after decoding
 * with a template that has an <code>ANY</code> field.
 *
 * <p>
 * An <code>ANY</code> supports extracting the BER encoding, or decoding
 * with a different template.
 */
public class ANY implements ASN1Value {

    // The complete encoding of header + contents
    private byte[] encoded;
    private Tag tag;

    /**
     * Creates an ANY value, which is just a generic ASN.1 value.
     * This method is provided for efficiency if the tag is already known,
     * so that we don't have to parse the encoding for it.
     * 
     * @param tag The tag of this value. It must be the same as the actual tag
     *            contained in the encoding.
     * @param encoded The complete BER encoding of this value, including
     *            tag, form, length, and contents.
     */
    public ANY(Tag tag, byte[] encoded) {
        this.encoded = encoded;
        this.tag = tag;
    }

    /**
     * Creates an ANY value, which is just a generic ASN.1 value.
     * 
     * @param encoded The complete BER encoding of this value, including
     *            tag, form, length, and contents.
     * @throws InvalidBERException If there is an invalid BER encoding.
     */
    public ANY(byte[] encoded) throws InvalidBERException {
        try {
            this.encoded = encoded;

            ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
            ASN1Header head = new ASN1Header(bis);
            this.tag = head.getTag();
        } catch (IOException e) {
            throw new org.mozilla.jss.util.AssertionException(
                    "IOException while creating ANY: " + e);
        }
    }

    /**
     * Returns the tag of this value.
     */
    @Override
    public Tag getTag() {
        return tag;
    }

    /**
     * Returns the complete encoding of header and contents, as passed into
     * the constructor or read from a BER input stream.
     *
     * @return Encoded header and contents.
     */
    public byte[] getEncoded() {
        return encoded;
    }

    /**
     * Returns the ASN.1 header from the encoding.
     *
     * @return ASN.1 header.
     * @throws InvalidBERException If there is an invalid BER encoding.
     * @throws IOException If other error occurred.
     */
    public ASN1Header getHeader() throws InvalidBERException, IOException {
        if (header == null) {
            ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
            header = new ASN1Header(bis);
        }
        return header;
    }

    private ASN1Header header = null;

    /**
     * Strips out the header and returns just the contents octets of the
     * encoding.
     */
    private byte[] contents = null;

    public byte[] getContents() throws InvalidBERException {
        try {
            if (contents == null) {
                ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
                header = new ASN1Header(bis);
                contents = new byte[bis.available()];
                if ((contents.length != header.getContentLength()) &&
                        (header.getContentLength() != -1)) {
                    throw new InvalidBERException("Length of contents was not the " +
                            "same as the header predicted");
                }
                ASN1Util.readFully(contents, bis);
            }

            return contents;

        } catch (IOException e) {
            throw new RuntimeException("Unable to read byte array: " + e.getMessage(), e);
        }
    }

    @Override
    public void encode(OutputStream ostream) throws IOException {
        ostream.write(encoded);
    }

    /**
     * Decodes this ANY using the given template. This is useful if you
     * originally decoded something as an ANY because you didn't know
     * what it was, but now you know what it is supposed to be.
     *
     * @param template The template to use to decode this ANY.
     * @return The output of the given template when it is fed the
     *         encoding of this ANY.
     * @throws InvalidBERException If there is an invalid BER encoding.
     */
    public ASN1Value decodeWith(ASN1Template template)
            throws InvalidBERException {
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
            return template.decode(bis);
        } catch (IOException e) {
            throw new RuntimeException("Unable to read byte array: " + e.getMessage(), e);
        }
    }

    /**
     * Decodes this ANY using the given template. This is useful if you
     * originally decoded something as an ANY because you didn't know
     * what it was, but now you know what it is supposed to be.
     *
     * @param implicitTag The implicit tag for the encoding.
     * @param template The template to use to decode this ANY.
     * @return The output of the given template when it is fed the
     *         encoding of this ANY.
     * @throws InvalidBERException If there is an invalid BER encoding.
     * @throws IOException If other error occurred.
     */
    public ASN1Value decodeWith(Tag implicitTag, ASN1Template template)
            throws IOException, InvalidBERException {
        ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
        return template.decode(implicitTag, bis);
    }

    /**
     * @param implicitTag <b>This parameter is ignored</b>, because
     *            ANY values cannot have implicit tags.
     * @throws IOException If an error occurred.
     */
    @Override
    public void encode(Tag implicitTag, OutputStream ostream)
            throws IOException {
        if (!implicitTag.equals(tag)) {
            throw new RuntimeException("No implicit tags allowed for ANY");
        }
        ostream.write(encoded);
    }

    /**
     * Extracts the contents from the ANY and encodes them with
     * the provided tag.
     *
     * @param alternateTag Alternate tag.
     * @param ostream Output stream.
     * @throws InvalidBERException If there is an invalid BER encoding.
     * @throws IOException If other error occurred.
     */
    public void encodeWithAlternateTag(Tag alternateTag, OutputStream ostream)
            throws IOException, InvalidBERException {
        byte[] contents = getContents();
        ASN1Header oldHead = getHeader();
        assert (contents.length == oldHead.getContentLength());

        ASN1Header newHead = new ASN1Header(alternateTag, oldHead.getForm(),
                contents.length);
        newHead.encode(ostream);
        ostream.write(contents);
    }

    /**
     * Returns a singleton instance of a decoding template.
     *
     * @return Decoding template.
     */
    public static Template getTemplate() {
        return templateInstance;
    }

    private static Template templateInstance = new Template();

    /**
     * A class for decoding <code>ANY</code> values from BER.
     */
    public static class Template implements ASN1Template {

        @Override
        public boolean tagMatch(Tag tag) {
            return true; // wheeeeee...it's ANY!

        }

        @Override
        public ASN1Value decode(InputStream istream)
                throws IOException, InvalidBERException {
            try {

                ASN1Header head = ASN1Header.lookAhead(istream);

                if (head.getContentLength() == -1) {
                    // indefinite length encoding
                    ByteArrayOutputStream recording = new ByteArrayOutputStream();

                    // eat the header off the input stream
                    head = new ASN1Header(istream);

                    // write the header to the recording stream
                    recording.write(head.encode());

                    // write all objects from the input stream to the recording
                    // stream, until we hit an END-OF-CONTENTS tag
                    ANY any;
                    ANY.Template anyt = new ANY.Template();
                    int count = 0;
                    do {
                        any = (ANY) anyt.decode(istream);
                        recording.write(any.getEncoded());
                    } while (!any.getTag().equals(Tag.EOC));

                    return new ANY(head.getTag(), recording.toByteArray());

                } else {
                    // definite length encoding
                    byte[] data = new byte[(int) head.getTotalLength()];

                    ASN1Util.readFully(data, istream);
                    return new ANY(head.getTag(), data);
                }

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

        @Override
        public ASN1Value decode(Tag implicitTag, InputStream istream)
                throws IOException, InvalidBERException {
            throw new InvalidBERException("Implicit tag on ANY");
        }
    } // End of Template

}
