/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2016 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wildfly.security.auth.server;

import static org.wildfly.common.math.HashMath.multiHashOrdered;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.StreamSupport;

import org.wildfly.common.Assert;
import org.wildfly.security.credential.PasswordCredential;
import org.wildfly.security.credential.source.CredentialSource;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.credential.AlgorithmCredential;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.util.EnumerationIterator;

/**
 * The public or private credentials retained by an identity, which can be used for authentication forwarding.  This
 * credentials set can contain zero or one credential of a given type and algorithm name.  If the credential type
 * does not support algorithm names, then the set can contain zero or one credential of that type.  The credential
 * set may be iterated; iteration order is not prescribed and may change if the implementation is changed.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public abstract class IdentityCredentials implements Iterable<Credential>, CredentialSource {
    IdentityCredentials() {
    }

    /**
     * Determine whether a credential of the given type is present in this set.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @return {@code true} if a matching credential is contained in this set, {@code false} otherwise
     */
    public final boolean contains(Class<? extends Credential> credentialType) {
        return contains(credentialType, null);
    }

    @Override
    public final SupportLevel getCredentialAcquireSupport(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
        return contains(credentialType, algorithmName, parameterSpec) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public final SupportLevel getCredentialAcquireSupport(final Class<? extends Credential> credentialType, final String algorithmName) {
        return contains(credentialType, algorithmName) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public final SupportLevel getCredentialAcquireSupport(final Class<? extends Credential> credentialType) {
        return contains(credentialType) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    /**
     * Determine whether a credential of the given type and algorithm are present in this set.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name, or {@code null} if any algorithm is acceptable or the credential type
     * does not support algorithm names
     * @param parameterSpec the parameter specification or {@code null} if any parameter specification is acceptable
     * @return {@code true} if a matching credential is contained in this set, {@code false} otherwise
     */
    public abstract boolean contains(Class<? extends Credential> credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec);

    /**
     * Determine whether a credential of the given type and algorithm are present in this set.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name, or {@code null} if any algorithm is acceptable or the credential type
     * does not support algorithm names
     * @return {@code true} if a matching credential is contained in this set, {@code false} otherwise
     */
    public final boolean contains(Class<? extends Credential> credentialType, String algorithmName) {
        Assert.checkNotNullParam("credentialType", credentialType);
        return contains(credentialType, algorithmName, null);
    }

    /**
     * Determine whether a credential of the type, algorithm, and parameters of the given credential is present in this set.
     *
     * @param credential the credential to check against (must not be {@code null})
     * @return {@code true} if a matching credential is contained in this set, {@code false} otherwise
     */
    public final boolean containsMatching(Credential credential) {
        Assert.checkNotNullParam("credential", credential);
        if (credential instanceof AlgorithmCredential) {
            final AlgorithmCredential algorithmCredential = (AlgorithmCredential) credential;
            return contains(credential.getClass(), algorithmCredential.getAlgorithm(), algorithmCredential.getParameters());
        } else {
            return contains(credential.getClass(), null, null);
        }
    }

    /**
     * Acquire a credential of the given type.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param <C> the credential type
     * @return the credential, or {@code null} if no such credential exists
     */
    @Override
    public final <C extends Credential> C getCredential(Class<C> credentialType) {
        return getCredential(credentialType, null, null);
    }

    /**
     * Acquire a credential of the given type and algorithm name.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name, or {@code null} if any algorithm is acceptable or the credential type
     * does not support algorithm names
     * @param <C> the credential type
     * @return the credential, or {@code null} if no such credential exists
     */
    @Override
    public final <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName) {
        return getCredential(credentialType, algorithmName, null);
    }

    /**
     * Acquire a credential of the given type and algorithm name.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name, or {@code null} if any algorithm is acceptable or the credential type
     * does not support algorithm names
     * @param parameterSpec the parameter specification or {@code null} if any parameter specification is acceptable
     * @param <C> the credential type
     *
     * @return the credential, or {@code null} if no such credential exists
     */
    @Override
    public abstract <C extends Credential> C getCredential(Class<C> credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec);

    /**
     * Apply the given function to the acquired credential, if it is set and of the given type.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param function the function to apply (must not be {@code null})
     * @param <C> the credential type
     * @param <R> the return type
     * @return the result of the function, or {@code null} if the criteria are not met
     */
    @Override
    public final <C extends Credential, R> R applyToCredential(Class<C> credentialType, Function<C, R> function) {
        final Credential credential = getCredential(credentialType);
        return credential == null ? null : credential.castAndApply(credentialType, function);
    }


    /**
     * Apply the given function to the acquired credential, if it is set and of the given type and algorithm.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name
     * @param function the function to apply (must not be {@code null})
     * @param <C> the credential type
     * @param <R> the return type
     * @return the result of the function, or {@code null} if the criteria are not met
     */
    @Override
    public final <C extends Credential, R> R applyToCredential(Class<C> credentialType, String algorithmName, Function<C, R> function) {
        final Credential credential = getCredential(credentialType, algorithmName);
        return credential == null ? null : credential.castAndApply(credentialType, algorithmName, function);
    }

    /**
     * Apply the given function to the acquired credential, if it is set and of the given type and algorithm.
     *
     * @param credentialType the credential type class (must not be {@code null})
     * @param algorithmName the algorithm name
     * @param function the function to apply (must not be {@code null})
     * @param <C> the credential type
     * @param <R> the return type
     * @return the result of the function, or {@code null} if the criteria are not met
     */
    @Override
    public <C extends Credential, R> R applyToCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec, final Function<C, R> function) {
        final Credential credential = getCredential(credentialType, algorithmName, parameterSpec);
        return credential == null ? null : credential.castAndApply(credentialType, algorithmName, parameterSpec, function);
    }

    /**
     * Return a copy of this credential set, but with the given credential added to it.
     *
     * @param credential the credential to append (must not be {@code null})
     * @return the new credential set (not {@code null})
     */
    public abstract IdentityCredentials withCredential(Credential credential);

    /**
     * Return a copy of this credential set with the given credential set added to it.
     *
     * @param other the credential set to append (must not be {@code null})
     * @return the new credential set (not {@code null})
     */
    public abstract IdentityCredentials with(IdentityCredentials other);

    /**
     * Return a copy of this credential set without any credentials with a type, algorithm name, and parameters matching that of the
     * given credential.  If the credential type, algorithm name, and parameters are not found in this set, return this instance.
     *
     * @param credential the credential to match against (must not be {@code null})
     * @return the new credential set (not {@code null})
     */
    public IdentityCredentials withoutMatching(Credential credential) {
        Assert.checkNotNullParam("credential", credential);
        return without(credential::matches);
    }

    /**
     * Return a copy of this credential set without any credentials of the given type.  If the credential type is not
     * found in this set, return this instance.
     *
     * @param credentialType the credential type to remove (must not be {@code null})
     * @return the new credential set (not {@code null})
     */
    public final IdentityCredentials without(Class<? extends Credential> credentialType) {
        Assert.checkNotNullParam("credentialType", credentialType);
        return without(credentialType::isInstance);
    }

    /**
     * Return a copy of this credential set without any credentials of the given type and algorithm name.  If the
     * credential type and algorithm name is not found in this set, return this instance.
     *
     * @param credentialType the credential type to remove (must not be {@code null})
     * @param algorithmName the algorithm name to remove, or {@code null} to match any algorithm name
     * @return the new credential set (not {@code null})
     */
    public final IdentityCredentials without(final Class<? extends Credential> credentialType, final String algorithmName) {
        Assert.checkNotNullParam("credentialType", credentialType);
        return without(credentialType, algorithmName, null);
    }

    /**
     * Return a copy of this credential set without any credentials of the given type, algorithm name and
     * parameter spec.  If the credential type and algorithm name is not found in this set, return this instance.
     *
     * @param credentialType the credential type to remove (must not be {@code null})
     * @param algorithmName the algorithm name to remove, or {@code null} to match any algorithm name
     * @param parameterSpec the parameter spec to remove, or {@code null} to match any parameter spec
     * @return the new credential set (not {@code null})
     */
    public IdentityCredentials without(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
        Assert.checkNotNullParam("credentialType", credentialType);
        return without(c -> c.matches(credentialType, algorithmName, parameterSpec));
    }

    /**
     * Return a copy of this credential set without any credentials that match the predicate.  If no credentials match
     * the predicate, return this instance.
     *
     * @param predicate the predicate to test (must not be {@code null})
     * @return the new credential set (not {@code null})
     */
    public abstract IdentityCredentials without(Predicate<? super Credential> predicate);

    /**
     * Return a copy of this credential set without any credentials of the given type that match the predicate.  If no credentials match
     * the predicate, return this instance.
     *
     * @param credentialType the credential type class
     * @param predicate the predicate to test (must not be {@code null})
     * @param <C> the credential type
     * @return the new credential set (not {@code null})
     */
    public final <C extends Credential> IdentityCredentials without(Class<C> credentialType, Predicate<? super C> predicate) {
        return without(c -> credentialType.isInstance(c) && predicate.test(credentialType.cast(c)));
    }

    /**
     * Get a {@link Spliterator} for this credential set.
     *
     * @return the spliterator (not {@code null})
     */
    public Spliterator<Credential> spliterator() {
        return Spliterators.spliterator(iterator(), size(), Spliterator.IMMUTABLE | Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED | Spliterator.SIZED);
    }

    /**
     * Test whether some of the credentials in this set can verify an evidence of given class and algorithm name.
     *
     * @param evidenceClass the class of the evidence (must not be {@code null})
     * @param algorithmName the algorithm name (may be {@code null} if the type of evidence does not support algorithm names)
     * @return {@code true} if the evidence can be verified
     */
    public boolean canVerify(Class<? extends Evidence> evidenceClass, String algorithmName) {
        return StreamSupport.stream(spliterator(), false)
                .filter(credential -> credential.canVerify(evidenceClass, algorithmName))
                .count() != 0;
    }

    /**
     * Test whether some of the credentials in this set can verify an evidence.
     *
     * @param evidence the evidence (must not be {@code null})
     * @return {@code true} if the evidence can be verified
     */
    public boolean canVerify(Evidence evidence) {
        return StreamSupport.stream(spliterator(), false)
                .filter(credential -> credential.canVerify(evidence))
                .count() != 0;
    }

    /**
     * Verify the given evidence.
     *
     * @param evidence the evidence to verify (must not be {@code null})
     * @return {@code true} if the evidence is verified, {@code false} otherwise
     */
    public boolean verify(Evidence evidence) {
        return verify(evidence, StandardCharsets.UTF_8);
    }

    /**
     * Verify the given evidence.
     *
     * @param providerSupplier the provider supplier to use for verification purposes (must not be {@code null})
     * @param evidence the evidence to verify (must not be {@code null})
     * @return {@code true} if the evidence is verified, {@code false} otherwise
     */
    public boolean verify(Supplier<Provider[]> providerSupplier, Evidence evidence) {
        return verify(providerSupplier, evidence, StandardCharsets.UTF_8);
    }

    /**
     * Verify the given evidence.
     *
     * @param evidence the evidence to verify (must not be {@code null})
     * @oaram hashCharset the name of the character set (must not be {@code null})
     * @return {@code true} if the evidence is verified, {@code false} otherwise
     * @deprecated use {@link #verify(Supplier, Evidence, Charset)} instead
     */
    @Deprecated
    public boolean verify(Evidence evidence, Charset hashCharset) {
        return StreamSupport.stream(spliterator(), false)
                .filter(credential -> credential.canVerify(evidence))
                .filter(credential ->
                        credential instanceof PasswordCredential ?
                                ((PasswordCredential)credential).verify(evidence, hashCharset) : credential.verify(evidence))
                .count() != 0;
    }

    /**
     * Verify the given evidence.
     *
     * @param providerSupplier the provider supplier to use for verification purposes (must not be {@code null})
     * @param evidence the evidence to verify (must not be {@code null})
     * @oaram hashCharset the name of the character set (must not be {@code null})
     * @return {@code true} if the evidence is verified, {@code false} otherwise
     */
    public boolean verify(Supplier<Provider[]> providerSupplier, Evidence evidence, Charset hashCharset) {
        return StreamSupport.stream(spliterator(), false)
                .filter(credential -> credential.canVerify(evidence))
                .filter(credential ->
                        credential instanceof PasswordCredential ?
                                ((PasswordCredential)credential).verify(providerSupplier, evidence, hashCharset) : credential.verify(providerSupplier, evidence))
                .count() != 0;
    }

    /**
     * Get the size of this credential set.
     *
     * @return the size of this credential set
     */
    public abstract int size();

    /**
     * The empty credentials object.
     */
    public static final IdentityCredentials NONE = new IdentityCredentials() {
        public boolean contains(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return false;
        }

        public <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return null;
        }

        public IdentityCredentials with(final IdentityCredentials other) {
            Assert.checkNotNullParam("other", other);
            return other;
        }

        public IdentityCredentials withCredential(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            return new One(credential);
        }

        public CredentialSource with(final CredentialSource other) {
            Assert.checkNotNullParam("other", other);
            return other;
        }

        public Iterator<Credential> iterator() {
            return Collections.emptyIterator();
        }

        public <C extends Credential, R> R applyToCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec, final Function<C, R> function) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return null;
        }

        public IdentityCredentials withoutMatching(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            return this;
        }

        public IdentityCredentials without(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return this;
        }

        public IdentityCredentials without(final Predicate<? super Credential> predicate) {
            Assert.checkNotNullParam("predicate", predicate);
            return this;
        }

        public Spliterator<Credential> spliterator() {
            return Spliterators.emptySpliterator();
        }

        public void forEach(final Consumer<? super Credential> action) {
            Assert.checkNotNullParam("action", action);
        }

        public int size() {
            return 0;
        }

        public int hashCode() {
            return 0;
        }

        public boolean equals(Object o) {
            return o == this;
        }
    };

    static class One extends IdentityCredentials {
        private final Credential credential;

        One(final Credential credential) {
            this.credential = credential;
        }

        public boolean contains(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential.matches(credentialType, algorithmName, parameterSpec);
        }

        public <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential.matches(credentialType, algorithmName, parameterSpec) ? credentialType.cast(credential.clone()) : null;
        }

        public IdentityCredentials withCredential(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            if (this.credential.matches(credential)) {
                return new One(credential);
            } else {
                return new Two(this.credential, credential);
            }
        }

        public IdentityCredentials with(final IdentityCredentials other) {
            Assert.checkNotNullParam("other", other);
            if (other == NONE) {
                return this;
            } else if (other instanceof One) {
                return withCredential(((One) other).credential);
            } else {
                return other.with(this);
            }
        }

        public CredentialSource with(final CredentialSource other) {
            Assert.checkNotNullParam("other", other);
            if (other instanceof IdentityCredentials) {
                return with((IdentityCredentials) other);
            } else {
                return super.with(other);
            }
        }

        public Iterator<Credential> iterator() {
            return EnumerationIterator.over(credential);
        }

        public <C extends Credential, R> R applyToCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec, final Function<C, R> function) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential.castAndApply(credentialType, algorithmName, parameterSpec, function);
        }

        public IdentityCredentials withoutMatching(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            return this.credential.matches(credential) ? NONE : this;
        }

        public IdentityCredentials without(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential.matches(credentialType, algorithmName, parameterSpec) ? NONE : this;
        }

        public IdentityCredentials without(final Predicate<? super Credential> predicate) {
            Assert.checkNotNullParam("predicate", predicate);
            return predicate.test(credential) ? NONE : this;
        }

        public void forEach(final Consumer<? super Credential> action) {
            Assert.checkNotNullParam("action", action);
            action.accept(credential);
        }

        public int size() {
            return 1;
        }

        public int hashCode() {
            return typeHash(credential);
        }

        public boolean equals(final Object obj) {
            return obj instanceof One && ((One) obj).credential.equals(credential);
        }
    }

    static class Two extends IdentityCredentials {
        private final Credential credential1;
        private final Credential credential2;

        Two(final Credential credential1, final Credential credential2) {
            this.credential1 = credential1;
            this.credential2 = credential2;
        }

        public boolean contains(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential1.matches(credentialType, algorithmName, parameterSpec) || credential2.matches(credentialType, algorithmName, parameterSpec);
        }

        public <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential1.matches(credentialType, algorithmName, parameterSpec) ? credentialType.cast(credential1.clone()) :
                   credential2.matches(credentialType, algorithmName, parameterSpec) ? credentialType.cast(credential2.clone()) : null;
        }

        public IdentityCredentials withCredential(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            if (credential.matches(credential1)) {
                return new Two(credential2, credential);
            } else if (credential.matches(credential2)) {
                return new Two(credential1, credential);
            } else {
                return new Many(credential1, credential2, credential);
            }
        }

        public IdentityCredentials with(final IdentityCredentials other) {
            Assert.checkNotNullParam("other", other);
            if (other == NONE) {
                return this;
            } else if (other instanceof One) {
                return withCredential(((One) other).credential);
            } else if (other instanceof Two) {
                final Two otherTwo = (Two) other;
                return withCredential(otherTwo.credential1).withCredential(otherTwo.credential2);
            } else if (other instanceof Many) {
                Many otherMany = (Many) other;
                if (otherMany.containsMatching(credential1)) {
                    if (otherMany.containsMatching(credential2)) {
                        return otherMany;
                    } else {
                        return new Many(credential2, otherMany);
                    }
                } else if (otherMany.containsMatching(credential2)) {
                    return new Many(credential1, otherMany);
                } else {
                    return new Many(credential1, credential2, otherMany);
                }
            } else {
                throw Assert.unreachableCode();
            }
        }

        public CredentialSource with(final CredentialSource other) {
            Assert.checkNotNullParam("other", other);
            if (other instanceof IdentityCredentials) {
                return with((IdentityCredentials) other);
            } else {
                return super.with(other);
            }
        }

        public Iterator<Credential> iterator() {
            return EnumerationIterator.over(credential1);
        }

        public <C extends Credential, R> R applyToCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec, final Function<C, R> function) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential1.castAndApply(credentialType, algorithmName, parameterSpec, function);
        }

        public IdentityCredentials withoutMatching(final Credential credential) {
            Assert.checkNotNullParam("credential", credential);
            return this.credential1.matches(credential) ? NONE : this;
        }

        public IdentityCredentials without(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            Assert.checkNotNullParam("credentialType", credentialType);
            return credential1.matches(credentialType, algorithmName, parameterSpec) ? NONE : this;
        }

        public IdentityCredentials without(final Predicate<? super Credential> predicate) {
            Assert.checkNotNullParam("predicate", predicate);
            return predicate.test(credential1) ? predicate.test(credential2) ? NONE : new One(credential2) : predicate.test(credential2) ? new One(credential1) : this;
        }

        public void forEach(final Consumer<? super Credential> action) {
            Assert.checkNotNullParam("action", action);
            action.accept(credential1);
            action.accept(credential2);
        }

        public int size() {
            return 2;
        }

        public int hashCode() {
            return typeHash(credential1) ^ typeHash(credential2);
        }

        public boolean equals(final Object obj) {
            if (! (obj instanceof Two)) {
                return false;
            }
            final Two two = (Two) obj;
            return credential1.equals(two.credential1) && credential2.equals(two.credential2) || credential1.equals(two.credential2) && credential2.equals(two.credential1);
        }
    }

    /**
     * A (hopefully) unique hash code for the kind of credential.
     *
     * @param credential the credential
     * @return the type hash
     */
    static int typeHash(Credential credential) {
        int ch = credential.getClass().hashCode();
        if (credential instanceof AlgorithmCredential) {
            final AlgorithmCredential algorithmCredential = (AlgorithmCredential) credential;
            return multiHashOrdered(multiHashOrdered(ch, 42979, Objects.hashCode(algorithmCredential.getAlgorithm())), 62861, Objects.hashCode(algorithmCredential.getParameters()));
        } else {
            return ch;
        }
    }

    static class Many extends IdentityCredentials {
        private final LinkedHashMap<Key, Credential> map;
        private final int hashCode;

        Many(final Credential c1, final Many subsequent) {
            LinkedHashMap<Key, Credential> map = new LinkedHashMap<>(subsequent.map.size() + 1);
            addCredential(c1, map);
            map.putAll(subsequent.map);
            this.map = map;
            int hc = 0;
            for (Credential credential : map.values()) {
                hc ^= typeHash(credential);
            }
            hashCode = hc;
            assert size() > 2;
        }

        Many(final Credential c1, final Credential c2, final Many subsequent) {
            LinkedHashMap<Key, Credential> map = new LinkedHashMap<>(subsequent.map.size() + 2);
            addCredential(c1, map);
            addCredential(c2, map);
            map.putAll(subsequent.map);
            this.map = map;
            int hc = 0;
            for (Credential credential : map.values()) {
                hc ^= typeHash(credential);
            }
            hashCode = hc;
            assert size() > 2;
        }

        Many(final LinkedHashMap<Key, Credential> map) {
            this.map = map;
            int hc = 0;
            for (Credential credential : map.values()) {
                hc ^= typeHash(credential);
            }
            hashCode = hc;
            assert size() > 2;
        }

        Many(final Credential credential1, final Credential credential2, final Credential credential3) {
            LinkedHashMap<Key, Credential> map = new LinkedHashMap<>(3);
            addCredential(credential1, map);
            addCredential(credential2, map);
            addCredential(credential3, map);
            this.map = map;
            int hc = 0;
            for (Credential credential : map.values()) {
                hc ^= typeHash(credential);
            }
            hashCode = hc;
            assert size() > 2;
        }

        public boolean contains(final Class<? extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            return map.containsKey(new Key(credentialType, algorithmName, parameterSpec));
        }

        public <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) {
            return credentialType.cast(map.get(new Key(credentialType, algorithmName, parameterSpec)).clone());
        }

        public IdentityCredentials withoutMatching(final Credential credential) {
            final Key key = Key.of(credential);
            if (map.containsKey(key)) {
                final LinkedHashMap<Key, Credential> clone = new LinkedHashMap<>(map);
                clone.remove(key);
                if (clone.size() == 2) {
                    final Iterator<Credential> iterator = clone.values().iterator();
                    return new Two(iterator.next(), iterator.next());
                } else {
                    return new Many(clone);
                }
            } else {
                return this;
            }
        }

        public void forEach(final Consumer<? super Credential> action) {
            map.values().forEach(action);
        }

        public int size() {
            return map.size();
        }

        public IdentityCredentials withCredential(final Credential credential) {
            final LinkedHashMap<Key, Credential> clone = new LinkedHashMap<>(map);
            addCredential(credential, clone);
            return new Many(clone);
        }

        public IdentityCredentials with(final IdentityCredentials other) {
            final LinkedHashMap<Key, Credential> clone = new LinkedHashMap<>(map);
            for (Credential credential : other) {
                addCredential(credential, clone);
            }
            return new Many(clone);
        }

        private void addCredential(Credential credential, LinkedHashMap<Key, Credential> map) {
            final Key key = Key.of(credential);
            // do this as two steps so it's added to the end
            map.remove(key);
            map.put(key, credential);
            // add an alternate entry without algorithm and parameter spec to allow for loose matches
            if (key.getAlgorithm() != null) {
                Key altKey = new Key(key.getClazz(), null, null);
                map.remove(altKey);
                map.put(altKey, credential);
            }
            // add an alternate entry without parameter spec to allow for loose matches
            if (key.getParameterSpec() != null) {
                Key altKey = new Key(key.getClazz(), key.getAlgorithm(), null);
                map.remove(altKey);
                map.put(altKey, credential);
            }
        }

        public CredentialSource with(final CredentialSource other) {
            return other instanceof IdentityCredentials ? with((IdentityCredentials) other) : super.with(other);
        }

        public Iterator<Credential> iterator() {
            return Collections.unmodifiableCollection(map.values()).iterator();
        }

        public <C extends Credential, R> R applyToCredential(final Class<C> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec, final Function<C, R> function) {
            final Key key = new Key(credentialType, algorithmName, parameterSpec);
            final Credential credential = map.get(key);
            if (credential != null) {
                return function.apply(credentialType.cast(credential));
            }
            return null;
        }

        public IdentityCredentials without(final Predicate<? super Credential> predicate) {
            final LinkedHashMap<Key, Credential> clone = new LinkedHashMap<>(map);
            final Collection<Credential> values = clone.values();
            values.removeIf(predicate);
            final Iterator<Credential> iterator = values.iterator();
            if (iterator.hasNext()) {
                final Credential c1 = iterator.next();
                if (iterator.hasNext()) {
                    final Credential c2 = iterator.next();
                    if (iterator.hasNext()) {
                        return new Many(clone);
                    } else {
                        return new Two(c1, c2);
                    }
                } else {
                    return new One(c1);
                }
            } else {
                return NONE;
            }
        }

        public int hashCode() {
            return hashCode;
        }

        public boolean equals(final Object obj) {
            if (! (obj instanceof Many)) {
                return false;
            }
            Many many = (Many) obj;
            // check is potentially expensive so start here
            if (hashCode != many.hashCode) {
                return false;
            }
            if (map.size() != many.map.size()) {
                return false;
            }
            // now the O(n) part
            for (Map.Entry<Key, Credential> entry : map.entrySet()) {
                if (! Objects.equals(many.map.get(entry.getKey()), entry.getValue())) {
                    return false;
                }
            }
            return true;
        }
    }

    static final class Key {
        private final Class<? extends Credential> clazz;
        private final String algorithm;
        private final AlgorithmParameterSpec parameterSpec;
        private final int hashCode;

        Key(final Class<? extends Credential> clazz, final String algorithm, final AlgorithmParameterSpec parameterSpec) {
            this.clazz = clazz;
            this.algorithm = algorithm;
            this.parameterSpec = parameterSpec;
            hashCode = multiHashOrdered(multiHashOrdered(clazz.hashCode(), 42979, Objects.hashCode(algorithm)), 62861, Objects.hashCode(parameterSpec));
        }

        static Key of(Credential c) {
            if (c instanceof AlgorithmCredential) {
                final AlgorithmCredential ac = (AlgorithmCredential) c;
                return new Key(ac.getClass(), ac.getAlgorithm(), ac.getParameters());
            } else {
                return new Key(c.getClass(), null, null);
            }
        }

        public int hashCode() {
            return hashCode;
        }

        public boolean equals(final Object obj) {
            return obj instanceof Key && equals((Key) obj);
        }

        private boolean equals(final Key key) {
            return clazz == key.clazz && Objects.equals(algorithm, key.algorithm) && Objects.equals(parameterSpec, key.parameterSpec);
        }

        Class<? extends Credential> getClazz() {
            return clazz;
        }

        String getAlgorithm() {
            return algorithm;
        }

        AlgorithmParameterSpec getParameterSpec() {
            return parameterSpec;
        }
    }
}
