/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 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.ssl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;

/**
 * An opaque predicate which can be used to match SSL/TLS cipher suites.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public abstract class CipherSuitePredicate {

    // constant predicates

    private static final CipherSuitePredicate DEFAULTS_PREDICATE = matchAny(
        matchEncryption(Encryption.RC2, Encryption.RC4, Encryption.NULL),
        matchAuthentication(Authentication.NULL)
    );
    private static final CipherSuitePredicate ANON_DH_PREDICATE = matchAll(
        matchKeyExchange(KeyAgreement.DHd, KeyAgreement.DHr, KeyAgreement.DHE),
        matchAuthentication(Authentication.NULL)
    );
    private static final CipherSuitePredicate OPENSSL_COMPLEMENT_OF_ALL = matchEncryption(Encryption.NULL);
    private static final CipherSuitePredicate OPENSSL_DEFAULT_DELETES = matchAny(
        matchAuthentication(Authentication.NULL),
        matchEncryption(Encryption.NULL),
        matchExport(),
        matchLevel(SecurityLevel.LOW),
        matchProtocol(Protocol.SSLv2)
    );
    private static final CipherSuitePredicate OPENSSL_COMPLEMENT_OF_DEFAULT = matchAll(
        matchAuthentication(Authentication.NULL),
        matchNot(matchEncryption(Encryption.NULL))
    );
    private static final CipherSuitePredicate OPENSSL_ALL = matchNot(OPENSSL_COMPLEMENT_OF_ALL);

    CipherSuitePredicate() {
    }

    static CipherSuitePredicate optimize(CipherSuitePredicate predicate) {
        return predicate.isAlwaysFalse() ? matchFalse() : predicate.isAlwaysTrue() ? matchTrue() : predicate;
    }

    // general logical

    /**
     * Match all possible cipher suites.
     *
     * @return the {@code true} predicate
     */
    public static CipherSuitePredicate matchTrue() {
        return BooleanCipherSuitePredicate.TRUE;
    }

    /**
     * Match no possible cipher suites.
     *
     * @return the {@code false} predicate
     */
    public static CipherSuitePredicate matchFalse() {
        return BooleanCipherSuitePredicate.FALSE;
    }

    /**
     * Match all of the given predicates.
     *
     * @param predicates the predicates
     * @return a predicate that is {@code true} when all nested predicates are {@code true}
     */
    public static CipherSuitePredicate matchAll(CipherSuitePredicate... predicates) {
        return optimize(new AndCipherSuitePredicate(predicates));
    }

    /**
     * Match any of the given predicates.
     *
     * @param predicates the predicates
     * @return a predicate that is {@code true} when any nested predicate is {@code true}
     */
    public static CipherSuitePredicate matchAny(CipherSuitePredicate... predicates) {
        return optimize(new OrCipherSuitePredicate(predicates));
    }

    /**
     * Invert the given predicate.
     *
     * @param predicate the predicate
     * @return a predicate which is {@code true} when the nested predicate is {@code false}, and vice-versa
     */
    public static CipherSuitePredicate matchNot(CipherSuitePredicate predicate) {
        return optimize(new NotCipherSuitePredicate(predicate));
    }

    static <T> T[] withoutNulls(T[] orig) {
        if (orig == null) return null;
        for (T item : orig) {
            if (item == null) {
                ArrayList<T> list = null;
                for (T item2 : orig) {
                    if (item2 != null) {
                        if (list == null) {
                            list = new ArrayList<T>(orig.length - 1);
                        }
                        list.add(item2);
                    }
                }
                return list == null ? null : list.toArray(Arrays.copyOf(orig, list.size()));
            }
        }
        // no nulls
        return orig;
    }

    // specific criteria

    /**
     * Return a predicate which matches the given encryption scheme.
     *
     * @param encryption the encryption scheme
     * @return the predicate
     */
    public static CipherSuitePredicate matchEncryption(Encryption encryption) {
        return encryption == null ? matchFalse() : new EncryptionCipherSuitePredicate(EnumSet.of(encryption));
    }

    /**
     * Return a predicate which matches any of the given encryption schemes.
     *
     * @param encryptions the encryption schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchEncryption(Encryption... encryptions) {
        encryptions = withoutNulls(encryptions);
        return encryptions == null || encryptions.length == 0 ? matchFalse() : encryptions.length == Encryption.fullSize ? matchTrue() : new EncryptionCipherSuitePredicate(EnumSet.of(encryptions[0], encryptions));
    }

    /**
     * Return a predicate which matches any of the given encryption schemes.
     *
     * @param encryptions the encryption schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchEncryption(EnumSet<Encryption> encryptions) {
        return encryptions == null || encryptions.isEmpty() ? matchFalse() : Encryption.isFull(encryptions) ? matchTrue() : new EncryptionCipherSuitePredicate(encryptions);
    }

    /**
     * Return a predicate which matches the given authentication scheme.
     *
     * @param authentication the authentication scheme
     * @return the predicate
     */
    public static CipherSuitePredicate matchAuthentication(Authentication authentication) {
        return authentication == null ? matchFalse() : new AuthenticationCipherSuitePredicate(true, EnumSet.of(authentication));
    }

    /**
     * Return a predicate which matches any of the given authentication schemes.
     *
     * @param authentications the authentication schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchAuthentication(Authentication... authentications) {
        authentications = withoutNulls(authentications);
        return authentications == null || authentications.length == 0 ? matchFalse() : authentications.length == Authentication.fullSize ? matchTrue() : new AuthenticationCipherSuitePredicate(true, EnumSet.of(authentications[0], authentications));
    }

    /**
     * Return a predicate which matches any of the given authentication schemes.
     *
     * @param authentications the authentication schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchAuthentication(EnumSet<Authentication> authentications) {
        return authentications == null || authentications.isEmpty() ? matchFalse() : Authentication.isFull(authentications) ? matchTrue() : optimize(new AuthenticationCipherSuitePredicate(true, authentications));
    }

    /**
     * Return a predicate which matches the given key exchange scheme.
     *
     * @param keyAgreement the key exchange scheme
     * @return the predicate
     */
    public static CipherSuitePredicate matchKeyAgreement(KeyAgreement keyAgreement) {
        return keyAgreement == null ? matchFalse() : new KeyAgreementCipherSuitePredicate(EnumSet.of(keyAgreement));
    }

    /**
     * Return a predicate which matches any of the given key exchange schemes.
     *
     * @param keyAgreements the key exchange schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchKeyExchange(KeyAgreement... keyAgreements) {
        keyAgreements = withoutNulls(keyAgreements);
        return keyAgreements == null || keyAgreements.length == 0 ? matchFalse() : keyAgreements.length == KeyAgreement.fullSize ? matchTrue() : new KeyAgreementCipherSuitePredicate(EnumSet.of(keyAgreements[0], keyAgreements));
    }

    /**
     * Return a predicate which matches any of the given key exchange schemes.
     *
     * @param keyAgreements the key exchange schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchKeyExchange(EnumSet<KeyAgreement> keyAgreements) {
        return keyAgreements == null || keyAgreements.isEmpty() ? matchFalse() : KeyAgreement.isFull(keyAgreements) ? matchTrue() : new KeyAgreementCipherSuitePredicate(keyAgreements);
    }

    /**
     * Return a predicate which matches the given digest scheme.
     *
     * @param digest the digest scheme
     * @return the predicate
     */
    public static CipherSuitePredicate matchDigest(Digest digest) {
        return digest == null ? matchFalse() : new DigestCipherSuitePredicate(EnumSet.of(digest));
    }

    /**
     * Return a predicate which matches any of the given digest schemes.
     *
     * @param digests the digest schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchDigest(Digest... digests) {
        digests = withoutNulls(digests);
        return digests == null || digests.length == 0 ? matchFalse() : digests.length == Digest.fullSize ? matchTrue() : new DigestCipherSuitePredicate(EnumSet.of(digests[0], digests));
    }

    /**
     * Return a predicate which matches any of the given digest schemes.
     *
     * @param digests the digest schemes
     * @return the predicate
     */
    public static CipherSuitePredicate matchDigest(EnumSet<Digest> digests) {
        return digests == null || digests.isEmpty() ? matchFalse() : Digest.isFull(digests) ? matchTrue() : new DigestCipherSuitePredicate(digests);
    }

    /**
     * Return a predicate which matches the given protocol.
     *
     * @param protocol the protocol
     * @return the predicate
     */
    public static CipherSuitePredicate matchProtocol(Protocol protocol) {
        return protocol == null ? matchFalse() : new ProtocolCipherSuitePredicate(EnumSet.of(protocol));
    }

    /**
     * Return a predicate which matches any of the given protocols.
     *
     * @param protocols the protocols
     * @return the predicate
     */
    public static CipherSuitePredicate matchProtocol(Protocol... protocols) {
        protocols = withoutNulls(protocols);
        return protocols == null || protocols.length == 0 ? matchFalse() : protocols.length == Protocol.fullSize ? matchTrue() : new ProtocolCipherSuitePredicate(EnumSet.of(protocols[0], protocols));
    }

    /**
     * Return a predicate which matches any of the given protocols.
     *
     * @param protocols the protocols
     * @return the predicate
     */
    public static CipherSuitePredicate matchProtocol(EnumSet<Protocol> protocols) {
        return protocols == null || protocols.isEmpty() ? matchFalse() : Protocol.isFull(protocols) ? matchTrue() : new ProtocolCipherSuitePredicate(protocols);
    }

    /**
     * Return a predicate which matches the given security level.
     *
     * @param level the security level
     * @return the predicate
     */
    public static CipherSuitePredicate matchLevel(SecurityLevel level) {
        return level == null ? matchFalse() : new LevelCipherSuitePredicate(EnumSet.of(level));
    }

    /**
     * Return a predicate which matches any of the given security levels.
     *
     * @param levels the security levels
     * @return the predicate
     */
    public static CipherSuitePredicate matchLevel(SecurityLevel... levels) {
        levels = withoutNulls(levels);
        return levels == null || levels.length == 0 ? matchFalse() : levels.length == SecurityLevel.fullSize ? matchTrue() : new LevelCipherSuitePredicate(EnumSet.of(levels[0], levels));
    }

    /**
     * Return a predicate which matches any of the given security levels.
     *
     * @param levels the security levels
     * @return the predicate
     */
    public static CipherSuitePredicate matchLevel(EnumSet<SecurityLevel> levels) {
        return levels == null || levels.isEmpty() ? matchFalse() : SecurityLevel.isFull(levels) ? matchTrue() : new LevelCipherSuitePredicate(levels);
    }

    /**
     * Return a predicate which matches all security levels less than the given level.
     *
     * @param level the security level to compare against
     * @return the predicate
     */
    public static CipherSuitePredicate matchLevelLessThan(final SecurityLevel level) {
        return level == null || level == SecurityLevel.NONE ? matchFalse() : new CipherSuitePredicate() {
            void toString(final StringBuilder b) {
                b.append("security level is less than ").append(level);
            }

            boolean test(final MechanismDatabase.Entry entry) {
                return entry.getLevel().compareTo(level) < 0;
            }
        };
    }

    /**
     * Return a predicate which matches all FIPS cipher suites.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchFips() {
        return FipsCipherSuitePredicate.TRUE;
    }

    /**
     * Return a predicate which matches all non-FIPS cipher suites.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchNonFips() {
        return FipsCipherSuitePredicate.FALSE;
    }

    /**
     * Return a predicate which matches all exportable cipher suites.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchExport() {
        return ExportCipherSuitePredicate.TRUE;
    }

    /**
     * Return a predicate which matches all non-exportable cipher suites.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchNonExport() {
        return ExportCipherSuitePredicate.FALSE;
    }

    /**
     * Return a predicate which matches a cipher suite with the given name.  The cipher suite name must be a
     * standard or OpenSSL-style mechanism name identifying a single mechanism.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchName(final String name) {
        return name == null ? matchFalse() : new CipherSuitePredicate() {
            void toString(final StringBuilder b) {
                b.append("cipher name is \"").append(name).append("\"");
            }

            boolean test(final MechanismDatabase.Entry entry) {
                return entry.getOpenSslNames().contains(name) || entry.getAliases().contains(name) || entry.getName().equals(name);
            }
        };
    }

    /* -- Our defaults -- */

    /**
     * Return a predicate which matches all cipher suites that would be fully deleted in the default selector
     * configuration.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchDefaultDeletes() {
        return DEFAULTS_PREDICATE;
    }

    /* -- OpenSSL specials -- */

    /**
     * Match all anonymous ciphers which use Diffie-Hellman key exchange.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchAnonDH() {
        return ANON_DH_PREDICATE;
    }

    /**
     * Match all cipher suites except for anonymous and encryptionless suites, which must be explicitly enabled.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchOpenSslAll() {
        return OPENSSL_ALL;
    }

    /**
     * Match all cipher suites included by {@link #matchOpenSslAll()} but are disabled by default (generally,
     * anonymous Diffie-Hellman suites including elliptic curve suites).
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchOpenSslComplementOfAll() {
        return OPENSSL_COMPLEMENT_OF_ALL;
    }

    /**
     * Match all of the cipher suites which are automatically deleted by OpenSSL when using the special {@code DEFAULT}
     * rule.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchOpenSslDefaultDeletes() {
        return OPENSSL_DEFAULT_DELETES;
    }

    /**
     * Match all of the cipher suites which are added by OpenSSL when using the special {@code COMPLEMENTOFDEFAULT}
     * rule.
     *
     * @return the predicate
     */
    public static CipherSuitePredicate matchOpenSslComplementOfDefault() {
        return OPENSSL_COMPLEMENT_OF_DEFAULT;
    }

    abstract void toString(StringBuilder b);

    /**
     * Get the string representation of this predicate.
     *
     * @return the string representation of this predicate
     */
    public final String toString() {
        StringBuilder b = new StringBuilder();
        toString(b);
        return b.toString();
    }

    abstract boolean test(MechanismDatabase.Entry entry);

    boolean isAlwaysTrue() {
        return false;
    }
    boolean isAlwaysFalse() {
        return false;
    }
}
