package org.apereo.cas.adaptors.x509.authentication.principal;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.cryptacular.x509.dn.Attribute;
import org.cryptacular.x509.dn.AttributeType;
import org.cryptacular.x509.dn.NameReader;
import org.cryptacular.x509.dn.RDN;
import org.cryptacular.x509.dn.RDNSequence;
import org.cryptacular.x509.dn.StandardAttributeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Credential to principal resolver that extracts one or more attribute values
 * from the certificate subject DN and combines them with intervening delimiters.
 *
 * @author Marvin S. Addison
 * @since 3.4.4
 */
public class X509SubjectPrincipalResolver extends AbstractX509PrincipalResolver {
    private static final Logger LOGGER = LoggerFactory.getLogger(X509SubjectPrincipalResolver.class);
    
    /**
     * Pattern used to extract attribute names from descriptor.
     */
    private static final Pattern ATTR_PATTERN = Pattern.compile("\\$(\\w+)");

    /**
     * Descriptor representing an abstract format of the principal to be resolved.
     */
    private final String descriptor;

    /**
     * Sets the descriptor that describes for format of the principal ID to
     * create from X.509 subject DN attributes.  The descriptor is made up of
     * common X.509 attribute names prefixed by "$", which are replaced by
     * attribute values extracted from DN attribute values.
     * <p>
     * EXAMPLE:
     * </p>
     * {@code
     * {@code
     * &lt;bean class="X509SubjectPrincipalResolver"
     * p:descriptor="$UID@$DC.$DC" /&gt;
     * }
     * }
     * <p>
     * The above bean when applied to a certificate with the DN
     * <p>
     * <b>DC=edu, DC=vt/UID=jacky, CN=Jascarnella Ellagwonto</b></p>
     * <p>
     * produces the principal <strong>jacky@vt.edu</strong>.</p>
     *
     * @param descriptor Descriptor string where attribute names are prefixed with "$"
     *          to identify replacement by real attribute values from the subject DN.
     *          Valid attributes include common X.509 DN attributes such as the following:
     *          <ul>
     *          <li>C</li>
     *          <li>CN</li>
     *          <li>DC</li>
     *          <li>EMAILADDRESS</li>
     *          <li>L</li>
     *          <li>O</li>
     *          <li>OU</li>
     *          <li>SERIALNUMBER</li>
     *          <li>ST</li>
     *          <li>UID</li>
     *          <li>UNIQUEIDENTIFIER</li>
     *          </ul>
     *          For a complete list of supported attributes, see
     *          {@link org.cryptacular.x509.dn.StandardAttributeType}.
     */

    public X509SubjectPrincipalResolver(final String descriptor) {
        this.descriptor = descriptor;
    }

    /**
     * Replaces placeholders in the descriptor with values extracted from attribute
     * values in relative distinguished name components of the DN.
     *
     * @param certificate X.509 certificate credential.
     * @return Resolved principal ID.
     * @see AbstractX509PrincipalResolver#resolvePrincipalInternal(java.security.cert.X509Certificate)
     */
    @Override
    protected String resolvePrincipalInternal(final X509Certificate certificate) {
        LOGGER.debug("Resolving principal for [{}]", certificate);
        final StringBuffer sb = new StringBuffer();
        final Matcher m = ATTR_PATTERN.matcher(this.descriptor);
        final Map<String, AttributeContext> attrMap = new HashMap<>();
        final RDNSequence rdnSequence = new NameReader(certificate).readSubject();
        String name;
        String[] values;
        AttributeContext context;
        while (m.find()) {
            name = m.group(1);
            if (!attrMap.containsKey(name)) {
                values = getAttributeValues(rdnSequence,
                        StandardAttributeType.fromName(name));
                attrMap.put(name, new AttributeContext(values));
            }
            context = attrMap.get(name);
            m.appendReplacement(sb, context.nextValue());
        }
        m.appendTail(sb);
        return sb.toString();
    }

    /**
     * Gets the values of the given attribute contained in the DN.
     * <p>
     * <p><strong>NOTE:</strong> no escaping is done on special characters in the
     * values, which could be different from what would appear in the string
     * representation of the DN.</p>
     *
     * @param rdnSequence list of relative distinguished names
     *                    that contains the attributes comprising the DN.
     * @param attribute   Attribute whose values will be retrieved.
     * @return The attribute values for the given attribute in the order they
     * appear would appear in the string representation of the DN or an empty
     * array if the given attribute does not exist.
     */
    private static String[] getAttributeValues(final RDNSequence rdnSequence, final AttributeType attribute) {
        // Iterates sequence in reverse order as specified in section 2.1 of RFC 2253
        final List<String> values = new ArrayList<>();
        for (final RDN rdn : rdnSequence.backward()) {
            for (final Attribute attr : rdn.getAttributes()) {
                if (attr.getType().equals(attribute)) {
                    values.add(attr.getValue());
                }
            }
        }
        return values.toArray(new String[values.size()]);
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .appendSuper(super.toString())
                .append("descriptor", descriptor)
                .toString();
    }


    private static class AttributeContext {
        private int currentIndex;
        private final String[] values;

        /**
         * Instantiates a new attribute context.
         *
         * @param values the values
         */
        AttributeContext(final String[] values) {
            this.values = values;
        }


        /**
         * Retrieve the next value, by incrementing the current index.
         *
         * @return the string
         * @throws IllegalStateException if no values are remaining.
         */
        public String nextValue() {
            if (this.currentIndex == this.values.length) {
                throw new IllegalStateException("No values remaining for attribute");
            }
            return this.values[this.currentIndex++];
        }
    }
    
    
}
