/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.sshd.common.util.security;

import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.util.Objects;

import org.apache.sshd.common.util.ExceptionUtils;
import org.apache.sshd.common.util.ValidateUtils;

/**
 * @param  <T> Type of security entity being generated by this factory
 * @author     <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public interface SecurityEntityFactory<T> {
    Class<T> getEntityType();

    T getInstance(String algorithm) throws GeneralSecurityException;

    /**
     * Uses reflection in order to wrap the {@code getInstance} method(s) as a security entity factory.
     *
     * @param  <F>                          Type of entity being generated by the factor
     * @param  entityType                   The entity type class
     * @param  registrar                    The {@code SecurityProviderRegistrar} to use - if {@code null} then default
     *                                      provider is used (if specified).
     * @param  defaultProvider              Default provider choice to use if no registrar provided. If
     *                                      {@code null}/empty then JCE default is used
     * @return                              The {@link SecurityEntityFactory} for the entity
     * @throws ReflectiveOperationException If failed to create the factory
     * @see                                 #toDefaultFactory(Class)
     * @see                                 #toNamedProviderFactory(Class, String)
     * @see                                 #toProviderInstanceFactory(Class, Provider)
     * @see                                 SecurityProviderChoice#isNamedProviderUsed()
     * @see                                 SecurityProviderChoice#getSecurityProvider()
     */
    static <F> SecurityEntityFactory<F> toFactory(
            Class<F> entityType, SecurityProviderChoice registrar, SecurityProviderChoice defaultProvider)
            throws ReflectiveOperationException {
        if (registrar == null) {
            if ((defaultProvider == null) || (defaultProvider == SecurityProviderChoice.EMPTY)) {
                return toDefaultFactory(entityType);
            } else if (defaultProvider.isNamedProviderUsed()) {
                return toNamedProviderFactory(entityType, defaultProvider.getProviderName());
            } else {
                return toProviderInstanceFactory(entityType, defaultProvider.getSecurityProvider());
            }
        } else if (registrar.isNamedProviderUsed()) {
            return toNamedProviderFactory(entityType, registrar.getProviderName());
        } else {
            return toProviderInstanceFactory(entityType, registrar.getSecurityProvider());
        }
    }

    static <F> SecurityEntityFactory<F> toDefaultFactory(Class<F> entityType)
            throws ReflectiveOperationException {
        Method m = entityType.getDeclaredMethod("getInstance", String.class);
        return new SecurityEntityFactory<F>() {
            private final String s = SecurityEntityFactory.class.getSimpleName()
                                     + "[" + entityType.getSimpleName() + "]"
                                     + "[default]";

            @Override
            public Class<F> getEntityType() {
                return entityType;
            }

            @Override
            public F getInstance(String algorithm) throws GeneralSecurityException {
                try {
                    Object value = m.invoke(null, algorithm);
                    return entityType.cast(value);
                } catch (ReflectiveOperationException t) {
                    Throwable e = ExceptionUtils.peelException(t);
                    if (e instanceof GeneralSecurityException) {
                        throw (GeneralSecurityException) e;
                    } else if (e instanceof RuntimeException) {
                        throw (RuntimeException) e;
                    } else if (e instanceof Error) {
                        throw (Error) e;
                    } else {
                        throw new GeneralSecurityException(e);
                    }
                }
            }

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

    static <F> SecurityEntityFactory<F> toNamedProviderFactory(Class<F> entityType, String name)
            throws ReflectiveOperationException {
        ValidateUtils.checkNotNullAndNotEmpty(name, "No provider name specified");
        Method m = entityType.getDeclaredMethod("getInstance", String.class, String.class);
        return new SecurityEntityFactory<F>() {
            private final String s = SecurityEntityFactory.class.getSimpleName()
                                     + "[" + entityType.getSimpleName() + "]"
                                     + "[" + name + "]";

            @Override
            public Class<F> getEntityType() {
                return entityType;
            }

            @Override
            public F getInstance(String algorithm) throws GeneralSecurityException {
                try {
                    Object value = m.invoke(null, algorithm, name);
                    return entityType.cast(value);
                } catch (ReflectiveOperationException t) {
                    Throwable e = ExceptionUtils.peelException(t);
                    if (e instanceof GeneralSecurityException) {
                        throw (GeneralSecurityException) e;
                    } else if (e instanceof RuntimeException) {
                        throw (RuntimeException) e;
                    } else if (e instanceof Error) {
                        throw (Error) e;
                    } else {
                        throw new GeneralSecurityException(e);
                    }
                }
            }

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

    static <F> SecurityEntityFactory<F> toProviderInstanceFactory(Class<F> entityType, Provider provider)
            throws ReflectiveOperationException {
        Objects.requireNonNull(provider, "No provider instance");
        Method m = entityType.getDeclaredMethod("getInstance", String.class, Provider.class);
        return new SecurityEntityFactory<F>() {
            private final String s = SecurityEntityFactory.class.getSimpleName()
                                     + "[" + entityType.getSimpleName() + "]"
                                     + "[" + Provider.class.getSimpleName() + "]"
                                     + "[" + provider.getName() + "]";

            @Override
            public Class<F> getEntityType() {
                return entityType;
            }

            @Override
            public F getInstance(String algorithm) throws GeneralSecurityException {
                try {
                    Object value = m.invoke(null, algorithm, provider);
                    return entityType.cast(value);
                } catch (ReflectiveOperationException t) {
                    Throwable e = ExceptionUtils.peelException(t);
                    if (e instanceof GeneralSecurityException) {
                        throw (GeneralSecurityException) e;
                    } else if (e instanceof RuntimeException) {
                        throw (RuntimeException) e;
                    } else if (e instanceof Error) {
                        throw (Error) e;
                    } else {
                        throw new GeneralSecurityException(e);
                    }
                }
            }

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