/*
 * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE file for licensing information.
 */
package eu.emi.security.authn.x509.impl;

import java.math.BigInteger;
import java.security.PublicKey;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static eu.emi.security.authn.x509.impl.FormatMode.*;


/**
 * Utility allowing for converting certificates to various text representations.
 * 
 * @author K. Benedyczak
 */
public class X509Formatter
{
	static 
	{
		CertificateUtils.configureSecProvider();
	}

	private final FormatMode mode;
	private static final String[] USAGES = {
		"digitalSignature",
		"nonRepudiation",
		"keyEncipherment",
		"dataEncipherment",
		"keyAgreement",
		"keyCertSign",
		"CRLSign",
		"encipherOnly",
		"decipherOnly"};
	
	private static final String EKU = "1.3.6.1.5.5.7.3.";
	private static final Map<String, String> EXT_USAGES = new HashMap<String, String>(16);

	static {
		EXT_USAGES.put("2.5.29.37.0", "anyExtendedKeyUsage");
		EXT_USAGES.put(EKU+"1", "serverAuth");
		EXT_USAGES.put(EKU+"2", "clientAuth");
		EXT_USAGES.put(EKU+"3", "codeSigning");
		EXT_USAGES.put(EKU+"4", "emailProtection");
		EXT_USAGES.put(EKU+"5", "ipsecEndSystem");
		EXT_USAGES.put(EKU+"6", "ipsecTunnel");
		EXT_USAGES.put(EKU+"7", "ipsecUser");
		EXT_USAGES.put(EKU+"8", "timeStamping");
		EXT_USAGES.put(EKU+"9", "OCSPSigning");
		EXT_USAGES.put(EKU+"10", "dvcs");
		EXT_USAGES.put(EKU+"11", "sbgpCertAAServerAuth");
		EXT_USAGES.put(EKU+"12", "scvp_responder");
		EXT_USAGES.put(EKU+"13", "eapOverPPP");
		EXT_USAGES.put(EKU+"14", "eapOverLAN");
		EXT_USAGES.put(EKU+"15", "scvpServer");
		EXT_USAGES.put(EKU+"16", "scvpClient");
		EXT_USAGES.put(EKU+"17", "ipsecIKE");
		EXT_USAGES.put(EKU+"18", "capwapAC");
		EXT_USAGES.put(EKU+"19", "capwapWTP");
		EXT_USAGES.put("1.3.6.1.4.1.311.20.2.2", "smartcardlogon");
	};
	
	/**
	 * Creates a new X509Formatter object
	 * @param mode the formatting mode that will be used by this object.
	 */
	public X509Formatter(FormatMode mode)
	{
		this.mode = mode;
	}
	
	/**
	 * Produces a human readable text representation of the provided certificate. 
	 * @param cert input certificate
	 * @return the text representation
	 */
	public String format(X509Certificate cert)
	{
		String sep = "\n";
		if (mode.equals(COMPACT_ONE_LINE) || mode.equals(MEDIUM_ONE_LINE))
			sep = ", ";
		
		StringBuilder sb = new StringBuilder(256);
		String subject = X500NameUtils.getReadableForm(
				cert.getSubjectX500Principal());
		String issuer = X500NameUtils.getReadableForm(
				cert.getIssuerX500Principal());
		int version = cert.getVersion();
		sb.append(cert.getType()).append(" v").append(version);
		sb.append(" certificate").append(sep);
		sb.append("Subject: ").append(subject).append(sep);
		sb.append("Issuer: ").append(issuer);
		if (mode.equals(COMPACT) || mode.equals(COMPACT_ONE_LINE))
			return sb.toString();
		
		sb.append(sep);
		sb.append("Valid from: " + cert.getNotBefore()).append(sep);
		sb.append("Valid to: " + cert.getNotAfter());
		
		if (mode.equals(MEDIUM) || mode.equals(MEDIUM_ONE_LINE))
			return sb.toString();
		
		sb.append(sep);
		
		Collection<List<?>> issuerAltNames;
		Collection<List<?>> subjAltNames;
		List<String> extKeyUsage;
		try
		{
			issuerAltNames = cert.getIssuerAlternativeNames();
			subjAltNames = cert.getSubjectAlternativeNames();
			extKeyUsage = cert.getExtendedKeyUsage();
		} catch (CertificateParsingException e)
		{
			throw new IllegalArgumentException(
					"The certificate can not be sucessfuly parsed", e);
		}
		if (issuerAltNames != null)
			appendAltNames(sb, "Issuer alternative names", sep, issuerAltNames);
		if (subjAltNames != null)
			appendAltNames(sb, "Subject alternative names", sep, subjAltNames);

		boolean isCA = cert.getBasicConstraints() == Integer.MAX_VALUE;
		sb.append("CA: ").append(isCA).append(sep);

		
		
		PublicKey pubKey = cert.getPublicKey();

		String bits = "";
		if (pubKey instanceof RSAPublicKey)
			bits = " " + ((RSAPublicKey)pubKey).getModulus().bitLength() + "bit";
		if (pubKey instanceof DSAPublicKey)
			bits = " " + ((DSAPublicKey)pubKey).getParams().getG().bitLength() + "bit";
		
		String sigAlg = cert.getSigAlgName();
		sb.append("Signature alg: ").append(sigAlg).append(sep);
		sb.append("Public key type: ").append(pubKey.getAlgorithm()).append(bits).append(sep);
		
		
		boolean []keyUsage = cert.getKeyUsage();
		if (keyUsage != null)
		{
			sb.append("Allowed usage:");
			for (int i=0; i<keyUsage.length; i++)
				if (keyUsage[i])
					sb.append(" ").append(USAGES[i]);
			sb.append(sep);
		}
		if (extKeyUsage != null)
		{
			sb.append("Allowed extended usage:");
			for (String oid: extKeyUsage)
			{
				String val = EXT_USAGES.get(oid);
				if (val == null)
					val = oid;
				sb.append(" ").append(val);
			}
			sb.append(sep);
		}
		BigInteger serial = cert.getSerialNumber();
		sb.append("Serial number: ").append(serial);
		return sb.toString();
	}

	private void appendAltNames(StringBuilder sb, String info, String sep, 
			Collection<List<?>> altNames)
	{
		sb.append(info).append(": ").append(sep);
		for (List<?> altNamesL: altNames)
		{
			sb.append("  ");
			Integer i = (Integer) altNamesL.get(0);
			Object rVal = altNamesL.get(1);
			String val;
			if (i == 0 || i == 3 || i == 5)
				val = Arrays.toString((byte[])rVal);
			else
				val = (String) rVal;
			switch (i)
			{
			case 1:
				sb.append("email: ").append(val).append(sep);
				break;
			case 2:
				sb.append("DNS: ").append(val).append(sep);
				break;
			case 4:
				sb.append("DN: ").append(val).append(sep);
				break;
			case 6:
				sb.append("URI: ").append(val).append(sep);
				break;
			case 7:
				sb.append("IP: ").append(val).append(sep);
				break;
			case 8:
				sb.append("OID: ").append(val).append(sep);
				break;
			case 0:
				sb.append("other: ").append(val).append(sep);
				break;
			case 3:
				sb.append("X.400: ").append(val).append(sep);
				break;
			case 5:
				sb.append("EDI party: ").append(val).append(sep);
				break;
			}
		}
	}
	
	/**
	 * Produces a human readable text representation of the provided certificate chain. 
	 * @param certChain input certificates
	 * @return the text representation
	 */
	public String format(X509Certificate[] certChain)
	{
		return format(certChain, true);
	}
	
	/**
	 * Produces a human readable text representation of the provided certificate chain. 
	 * @param certChain input certificates
	 * @param preamble whether to print a first line with an information on 
	 * the number of elements.
	 * @return the text representation
	 */
	public String format(X509Certificate[] certChain, boolean preamble)
	{
		StringBuilder sb = new StringBuilder();
		if (preamble)
			sb.append("Certificate chain, ").append(certChain.length).
					append(" elements:\n");
		for (int i=0; i<certChain.length; i++)
		{
			sb.append("-----Certificate ").append(i+1).append("-----\n");
			sb.append(format(certChain[i])).append("\n");
			if (i+1<certChain.length)
				sb.append("\n");
		}
		return sb.toString();
	}
}
