// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// 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 for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package org.mozilla.jss.netscape.security.x509;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.CertificateException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.mozilla.jss.asn1.ASN1Util;
import org.mozilla.jss.asn1.InvalidBERException;
import org.mozilla.jss.asn1.SEQUENCE;
import org.mozilla.jss.netscape.security.util.BitArray;
import org.mozilla.jss.netscape.security.util.DerOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An extension that tells applications where to find the CRL for
 * this certificate.
 *
 * <pre>
 * cRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
 *
 * DistributionPoint ::= SEQUENCE {
 *      distributionPoint       [0]     DistributionPointName OPTIONAL,
 *      reasons                 [1]     ReasonFlags OPTIONAL,
 *      cRLIssuer               [2]     GeneralNames OPTIONAL }
 *
 * DistributionPointName ::= CHOICE {
 *      fullName                [0]     GeneralNames,
 *      nameRelativeToCRLIssuer [1]     RelativeDistinguishedName }
 *
 * ReasonFlags ::= BIT STRING {
 *      unused                  (0),
 *      keyCompromise           (1),
 *      cACompromise            (2),
 *      affiliationChanged      (3),
 *      superseded              (4),
 *      cessationOfOperation    (5),
 *      certificateHold         (6) }
 * </pre>
 */
public class CRLDistributionPointsExtension extends Extension
        implements CertAttrSet {

    /**
     *
     */
    private static final long serialVersionUID = 8551761833349709229L;

    private static final Logger logger = LoggerFactory.getLogger(CRLDistributionPointsExtension.class);
    // vector of CRLDistributionPoint
    private SEQUENCE distributionPoints = new SEQUENCE();

    // Cached DER-encoding to improve performance.
    private byte[] cachedEncoding = null;

    /**
     * This constructor is called by the CertificateExtensions class to decode
     * an extension whose OID indicates it is a CRLDistributionsPoints
     * extension.
     */
    public CRLDistributionPointsExtension(Boolean critical, Object value)
    //throws IOException
    {
        try {

            this.extensionId = PKIXExtensions.CRLDistributionPoints_Id;
            this.critical = critical.booleanValue();
            this.extensionValue = ((byte[]) value).clone();

            // decode the value
            try {
                SEQUENCE.OF_Template seqOfCRLDP =
                        new SEQUENCE.OF_Template(CRLDistributionPoint.getTemplate());

                distributionPoints =
                        (SEQUENCE) ASN1Util.decode(seqOfCRLDP, extensionValue);
            } catch (InvalidBERException e) {
                throw new IOException("Invalid BER-encoding: " + e, e);
            }
        } catch (IOException e) {
            logger.error("Error decoding CRLDistributionPoints", e);
            //throw e;
        }
    }

    /**
     * The Object Identifier for this extension.
     */
    public static final String OID = "2.5.29.31";

    /**
     * Creates a new CRLDistributionPoints extension, with the given
     * distribution point as the first element.
     */
    public CRLDistributionPointsExtension(CRLDistributionPoint dp) {
        this.extensionId = PKIXExtensions.CRLDistributionPoints_Id;
        this.critical = false;
        distributionPoints.addElement(dp);
    }

    /**
     * Adds an additional distribution point to the end of the sequence.
     */
    public void addPoint(CRLDistributionPoint dp) {
        distributionPoints.addElement(dp);
        cachedEncoding = null;
    }

    /**
     * Returns the number of distribution points in the sequence.
     */
    public int getNumPoints() {
        return distributionPoints.size();
    }

    /**
     * Returns the DistributionPoint at the given index in the sequence.
     */
    public CRLDistributionPoint getPointAt(int index) {
        return (CRLDistributionPoint) distributionPoints.elementAt(index);
    }

    /**
     * Sets the criticality of this extension. PKIX dictates that this
     * extension SHOULD NOT be critical, so applications can make it critical
     * if they have a very good reason. By default, the extension is not
     * critical.
     */
    @Override
    public void setCritical(boolean critical) {
        this.critical = critical;
    }

    /**
     * Encodes this extension to the given DerOutputStream.
     * This method re-encodes each time it is called, so it is not very
     * efficient.
     */
    @Override
    public void encode(DerOutputStream out) throws IOException {
        extensionValue = ASN1Util.encode(distributionPoints);
        super.encode(out);
    }

    /**
     * Should be called if any change is made to this data structure
     * so that the cached DER encoding can be discarded.
     */
    public void flushCachedEncoding() {
        cachedEncoding = null;
    }

    /////////////////////////////////////////////////////////////
    // CertAttrSet interface
    // This interface is not really appropriate for this extension
    // because it is so complicated. Therefore, we only provide a
    // minimal implementation.
    /////////////////////////////////////////////////////////////
    public static final String NAME = "CRLDistributionPoints";

    @Override
    public String toString() {
        return NAME;
    }

    /**
     * DER-encodes this extension to the given OutputStream.
     */
    @Override
    public void encode(OutputStream ostream)
            throws CertificateException, IOException {
        if (cachedEncoding == null) {
            // only re-encode if necessary
            DerOutputStream tmp = new DerOutputStream();
            encode(tmp);
            cachedEncoding = tmp.toByteArray();
        }
        ostream.write(cachedEncoding);
    }

    @Override
    public void decode(InputStream in)
            throws CertificateException, IOException {
        throw new IOException("Not supported");
    }

    @Override
    public void set(String name, Object obj)
            throws CertificateException, IOException {
        throw new IOException("Attribute name not recognized by " +
                "CertAttrSet:CRLDistributionPointsExtension");
    }

    @Override
    public Object get(String name)
            throws CertificateException, IOException {
        throw new IOException("Attribute name not recognized by " +
                "CertAttrSet:CRLDistributionPointsExtension");
    }

    @Override
    public void delete(String name)
            throws CertificateException, IOException {
        throw new IOException("Attribute name not recognized by " +
                "CertAttrSet:CRLDistributionPointsExtension");
    }

    /*
     *	TODO use an empty collection to generate these
     */
    @Override
    public Enumeration<String> getAttributeNames() {
        return (new Vector<String>()).elements();
    }

    @Override
    public String getName() {
        return NAME;
    }

    /**
     * Test driver.
     */
    public static void main(String args[]) {
        BufferedOutputStream bos = null;
        try {

            if (args.length != 1) {
                System.out.println("Usage: CRLDistributionPointsExtentions " +
                        "<outfile>");
                System.exit(-1);
            }

            bos = new BufferedOutputStream(
                    new FileOutputStream(args[0]));

            // URI only
            CRLDistributionPoint cdp = new CRLDistributionPoint();
            URIName uri = new URIName("http://www.mycrl.com/go/here");
            GeneralNames generalNames = new GeneralNames();
            generalNames.addElement(uri);
            cdp.setFullName(generalNames);
            CRLDistributionPointsExtension crldpExt =
                    new CRLDistributionPointsExtension(cdp);

            // DN only
            cdp = new CRLDistributionPoint();
            X500Name dn = new X500Name("CN=Otis Smith,E=otis@fedoraproject.org" +
                    ",OU=Certificate Server,O=Fedora,C=US");
            generalNames = new GeneralNames();
            generalNames.addElement(dn);
            cdp.setFullName(generalNames);
            crldpExt.addPoint(cdp);

            // DN + reason
            BitArray ba = new BitArray(5, new byte[] { (byte) 0x28 });
            cdp = new CRLDistributionPoint();
            cdp.setFullName(generalNames);
            cdp.setReasons(ba);
            crldpExt.addPoint(cdp);

            // relative DN + reason + crlIssuer
            cdp = new CRLDistributionPoint();
            RDN rdn = new RDN("OU=foobar dept");
            cdp.setRelativeName(rdn);
            cdp.setReasons(ba);
            cdp.setCRLIssuer(generalNames);
            crldpExt.addPoint(cdp);

            crldpExt.setCritical(true);
            crldpExt.encode(bos);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Represents a reason that a cert may be revoked. These reasons are
     * expressed in a ReasonFlags bit string.
     */
    public static class Reason {

        private String name;
        private byte bitMask;

        private Reason() {
        }

        private Reason(String name, byte bitMask) {
            this.name = name;
            this.bitMask = bitMask;
            map.put(name, this);
            list.addElement(this);
        }

        private static Hashtable<String, Reason> map = new Hashtable<>();
        private static Vector<Reason> list = new Vector<>();

        public static Reason fromString(String name) {
            return map.get(name);
        }

        public String getName() {
            return name;
        }

        public byte getBitMask() {
            return bitMask;
        }

        /**
         * Given a bit array representing reason flags, extracts the reasons
         * and returns them as an array.
         *
         * @param bitFlags A bit vector containing reason flags.
         * @return An array of reasons contained in the bit vector.
         *         May be zero-length but will not be null.
         */
        public static Reason[] bitArrayToReasonArray(byte bitFlags) {
            return bitArrayToReasonArray(new byte[] { bitFlags });
        }

        /**
         * Given a bit array representing reason flags, extracts the reasons
         * and returns them as an array. Currently, only the first byte
         * of the bitflags are examined.
         *
         * @param bitFlags A bit vector containing reason flags. The format
         *            is big-endian (MSB first). Only the first byte is examined.
         * @return An array of reasons contained in the bit vector.
         *         May be zero-length but will not be null.
         */
        public static Reason[] bitArrayToReasonArray(byte[] bitFlags) {
            byte first = bitFlags[0];
            int size = list.size();
            Vector<Reason> result = new Vector<>();
            for (int i = 0; i < size; i++) {
                Reason r = list.elementAt(i);
                byte b = r.getBitMask();
                if ((first & b) != 0) {
                    result.addElement(r);
                }
            }
            size = result.size();
            Reason[] retval = new Reason[size];
            for (int i = 0; i < size; i++) {
                retval[i] = result.elementAt(i);
            }
            return retval;
        }

        public static final Reason UNUSED =
                new Reason("unused", (byte) 0x80);
        public static final Reason KEY_COMPROMISE =
                new Reason("keyCompromise", (byte) 0x40);
        public static final Reason CA_COMPROMISE =
                new Reason("cACompromise", (byte) 0x20);
        public static final Reason AFFILIATION_CHANGED =
                new Reason("affiliationChanged", (byte) 0x10);
        public static final Reason SUPERSEDED =
                new Reason("superseded", (byte) 0x08);
        public static final Reason CESSATION_OF_OPERATION =
                new Reason("cessationOfOperation", (byte) 0x04);
        public static final Reason CERTIFICATE_HOLD =
                new Reason("certificateHold", (byte) 0x02);
    }

}
