/*
 *   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.directory.server.core.integ;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.directory.api.ldap.codec.api.DefaultConfigurableBinaryAttributeDetector;
import org.apache.directory.api.util.Network;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapConnectionFactory;
import org.apache.directory.ldap.client.api.LdapConnectionPool;
import org.apache.directory.ldap.client.api.LdapConnectionValidator;
import org.apache.directory.ldap.client.template.LdapConnectionTemplate;
import org.apache.directory.server.annotations.CreateLdapConnectionPool;
import org.apache.directory.server.ldap.LdapServer;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CreateLdapConnectionPoolExtension implements BeforeAllCallback, AfterAllCallback
{
    private static final Logger LOG = LoggerFactory.getLogger( CreateLdapConnectionPoolExtension.class );
    private static final String LDAP_CONNECTION_TEMPLATE = "ldapConnectionTemplate";
    private static final String LDAP_CONNECTION_FACTORY = "ldapConnectionFactory";
    private static final String LDAP_CONNECTION_POOL = "ldapConnectionPool";
    
    private LdapConnectionFactory ldapConnectionFactory;

    private void setLdapConnectionTemplate( ExtensionContext context, LdapConnectionTemplate ldapConnectionTemplate ) 
        throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
    {
        Class<?> testClass = context.getTestClass().get();

        try
        {
            Field field = testClass.getField( LDAP_CONNECTION_TEMPLATE );
            field.set( null, ldapConnectionTemplate );
        }
        catch ( NoSuchFieldException nsfe )
        {
            // Ignore
        }
    }
    
    
    private void setLdapConnectionPool( ExtensionContext context, LdapConnectionPool ldapConnectionPool ) 
        throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
    {
        Class<?> testClass = context.getTestClass().get();
        
        try
        {
            Field field = testClass.getField( LDAP_CONNECTION_POOL );
            field.set( null, ldapConnectionPool );
        }
        catch ( NoSuchFieldException nsfe )
        {
            // Ignore
        }
    }
    
    
    private void setLdapConnectionFactory( ExtensionContext context, LdapConnectionFactory ldapConnectionFactory ) 
        throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
    {
        Class<?> testClass = context.getTestClass().get();
        
        try
        {
            Field field = testClass.getField( LDAP_CONNECTION_FACTORY );
        
            field.set( null, ldapConnectionFactory );
        }
        catch ( NoSuchFieldException nsfe )
        {
            // Ignore
        }
    }
    
    
    private LdapServer getLdapServer( ExtensionContext context ) 
        throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
    {
        Class<?> testClass = context.getTestClass().get();
        Field field = testClass.getField( ApacheDSTestExtension.CLASS_LS );
        
        if ( field != null )
        {
            return ( LdapServer ) field.get( testClass );
        }
        else
        {
            return null;
        }
    }

    @Override
    public void beforeAll( ExtensionContext context ) throws Exception
    {
        AnnotatedElement annotations = context.getTestClass().get();
        CreateLdapConnectionPool createLdapConnectionPool = annotations.getAnnotation( CreateLdapConnectionPool.class );
        
        LdapConnectionTemplate ldapConnectionTemplate;
        LdapServer ldapServer = getLdapServer( context );

        if ( createLdapConnectionPool != null )
        {
            LOG.trace( "Creating connection pool to new ldap server" );

            Class<? extends PooledObjectFactory<LdapConnection>> factoryClass =
                    createLdapConnectionPool.factoryClass();
            Class<? extends LdapConnectionFactory> connectionFactoryClass =
                    createLdapConnectionPool.connectionFactoryClass();
            Class<? extends LdapConnectionValidator> validatorClass =
                    createLdapConnectionPool.validatorClass();
            LdapConnectionPool ldapConnectionPool = createLdapConnectionPool( createLdapConnectionPool, ldapServer, factoryClass, 
                        connectionFactoryClass, validatorClass );
            ldapConnectionTemplate = new LdapConnectionTemplate( ldapConnectionPool );

            setLdapConnectionTemplate( context, ldapConnectionTemplate );
            setLdapConnectionFactory( context, ldapConnectionFactory );
            setLdapConnectionPool( context, ldapConnectionPool );
        }
    }


    private LdapConnectionPool createLdapConnectionPool( 
        CreateLdapConnectionPool createLdapConnectionPool,
        LdapServer ldapServer, 
        Class<? extends PooledObjectFactory<LdapConnection>> factoryClass,
        Class<? extends LdapConnectionFactory> connectionFactoryClass,
        Class<? extends LdapConnectionValidator> validatorClass )
    {
        LdapConnectionConfig config = new LdapConnectionConfig();
        
        config.setLdapHost( Network.LOOPBACK_HOSTNAME );
        
        config.setLdapPort( ldapServer.getPort() );
        config.setName( "uid=admin,ou=system" );
        config.setCredentials( "secret" );

        if ( ( createLdapConnectionPool.additionalBinaryAttributes() != null )
            && ( createLdapConnectionPool.additionalBinaryAttributes().length > 0 ) )
        {
            DefaultConfigurableBinaryAttributeDetector binaryAttributeDetector =
                new DefaultConfigurableBinaryAttributeDetector();
            binaryAttributeDetector.addBinaryAttribute(
                createLdapConnectionPool.additionalBinaryAttributes() );
            config.setBinaryAttributeDetector( binaryAttributeDetector );
        }

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setLifo( createLdapConnectionPool.lifo() );
        poolConfig.setMaxTotal( createLdapConnectionPool.maxActive() );
        poolConfig.setMaxIdle( createLdapConnectionPool.maxIdle() );
        poolConfig.setMaxWaitMillis( createLdapConnectionPool.maxWait() );
        poolConfig.setMinEvictableIdleTimeMillis( createLdapConnectionPool
            .minEvictableIdleTimeMillis() );
        poolConfig.setMinIdle( createLdapConnectionPool.minIdle() );
        poolConfig.setNumTestsPerEvictionRun( createLdapConnectionPool
            .numTestsPerEvictionRun() );
        poolConfig.setSoftMinEvictableIdleTimeMillis( createLdapConnectionPool
            .softMinEvictableIdleTimeMillis() );
        poolConfig.setTestOnBorrow( createLdapConnectionPool.testOnBorrow() );
        poolConfig.setTestOnReturn( createLdapConnectionPool.testOnReturn() );
        poolConfig.setTestWhileIdle( createLdapConnectionPool.testWhileIdle() );
        poolConfig.setTimeBetweenEvictionRunsMillis( createLdapConnectionPool
            .timeBetweenEvictionRunsMillis() );
        poolConfig.setBlockWhenExhausted( createLdapConnectionPool
            .whenExhaustedAction() == 1 );
        
        PooledObjectFactory<LdapConnection> poolableLdapConnectionFactory;
        
        try
        {
            Constructor<? extends LdapConnectionFactory> constructor = 
                    connectionFactoryClass.getConstructor( LdapConnectionConfig.class );
            ldapConnectionFactory = constructor.newInstance( config );
        }
        catch ( Exception e )
        {
            throw new IllegalArgumentException( "invalid connectionFactoryClass " 
                    + connectionFactoryClass.getName() + ": " + e.getMessage(), e );
        }
        try
        {
            Method timeoutSetter = connectionFactoryClass.getMethod( "setTimeOut", Long.TYPE );
            
            if ( timeoutSetter != null )
            {
                timeoutSetter.invoke( ldapConnectionFactory, createLdapConnectionPool.timeout() );
            }
        }
        catch ( Exception e )
        {
            throw new IllegalArgumentException( "invalid connectionFactoryClass "
                    + connectionFactoryClass.getName() + ", missing setTimeOut(long): " 
                    + e.getMessage(), e );
        }
        
        try
        {
            Constructor<? extends PooledObjectFactory<LdapConnection>> constructor = 
                    factoryClass.getConstructor( LdapConnectionFactory.class );
            poolableLdapConnectionFactory = constructor.newInstance( ldapConnectionFactory );
        }
        catch ( Exception e )
        {
            throw new IllegalArgumentException( "invalid factoryClass " 
                    + factoryClass.getName() + ": " + e.getMessage(), e );
        }
        try
        {
            Method setValidator = factoryClass.getMethod( "setValidator", LdapConnectionValidator.class );
            
            if ( setValidator != null )
            {
                setValidator.invoke( poolableLdapConnectionFactory, validatorClass.newInstance() );
            }
        }
        catch ( Exception e )
        {
            throw new IllegalArgumentException( "invalid connectionFactoryClass "
                    + connectionFactoryClass.getName() + ", missing setTimeOut(long): " 
                    + e.getMessage(), e );
        }

        return new LdapConnectionPool( poolableLdapConnectionFactory, poolConfig );
    }


    @Override
    public void afterAll( ExtensionContext context ) throws Exception
    {
        // TODO Auto-generated method stub
        
    }
}
