/*
 * 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.factory;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Network;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.annotations.AnnotationUtils;
import org.apache.directory.server.core.annotations.ApplyLdifFiles;
import org.apache.directory.server.core.annotations.ApplyLdifs;
import org.apache.directory.server.core.annotations.ContextEntry;
import org.apache.directory.server.core.annotations.CreateAuthenticator;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.annotations.LoadSchema;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.authn.AuthenticationInterceptor;
import org.apache.directory.server.core.authn.Authenticator;
import org.apache.directory.server.core.authn.DelegatingAuthenticator;
import org.apache.directory.server.core.partition.impl.btree.AbstractBTreePartition;
import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
import org.apache.directory.server.core.partition.impl.btree.mavibot.MavibotIndex;
import org.apache.directory.server.i18n.I18n;
import org.junit.runner.Description;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A Helper class used to create a DS from the annotations
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class DSAnnotationProcessor
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( DSAnnotationProcessor.class );


    private DSAnnotationProcessor()
    {
    }


    /**
     * Create the DirectoryService
     */
    public static DirectoryService createDS( CreateDS dsBuilder )
        throws Exception
    {
        LOG.debug( "Starting DS {}...", dsBuilder.name() );
        Class<?> factory = dsBuilder.factory();
        DirectoryServiceFactory dsf = ( DirectoryServiceFactory ) factory
            .newInstance();

        DirectoryService service = dsf.getDirectoryService();
        service.setAccessControlEnabled( dsBuilder.enableAccessControl() );
        service.setAllowAnonymousAccess( dsBuilder.allowAnonAccess() );
        service.getChangeLog().setEnabled( dsBuilder.enableChangeLog() );

        dsf.init( dsBuilder.name() );

        for ( Class<?> interceptorClass : dsBuilder.additionalInterceptors() )
        {
            service.addLast( ( Interceptor ) interceptorClass.newInstance() );
        }

        List<Interceptor> interceptorList = service.getInterceptors();

        if ( dsBuilder.authenticators().length != 0 )
        {
            AuthenticationInterceptor authenticationInterceptor = null;

            for ( Interceptor interceptor : interceptorList )
            {
                if ( interceptor instanceof AuthenticationInterceptor )
                {
                    authenticationInterceptor = ( AuthenticationInterceptor ) interceptor;
                    break;
                }
            }

            if ( authenticationInterceptor == null )
            {
                throw new IllegalStateException(
                    "authentication interceptor not found" );
            }

            Set<Authenticator> authenticators = new HashSet<Authenticator>();

            for ( CreateAuthenticator createAuthenticator : dsBuilder
                .authenticators() )
            {
                Authenticator auth = createAuthenticator.type().newInstance();

                if ( auth instanceof DelegatingAuthenticator )
                {
                    DelegatingAuthenticator dauth = ( DelegatingAuthenticator ) auth;
                    
                    String host = createAuthenticator.delegateHost();
                    
                    if ( Strings.isEmpty( host ) )
                    {
                        host = Network.LOOPBACK_HOSTNAME;
                    }
                    
                    dauth.setDelegateHost( host );
                    dauth.setDelegatePort( createAuthenticator.delegatePort() );
                    dauth.setDelegateSsl( createAuthenticator.delegateSsl() );
                    dauth.setDelegateTls( createAuthenticator.delegateTls() );
                    dauth.setBaseDn( service.getDnFactory().create( createAuthenticator.baseDn() ) );
                    dauth.setDelegateSslTrustManagerFQCN( createAuthenticator.delegateSslTrustManagerFQCN() );
                    dauth.setDelegateTlsTrustManagerFQCN( createAuthenticator.delegateTlsTrustManagerFQCN() );
                }

                authenticators.add( auth );
            }

            authenticationInterceptor.setAuthenticators( authenticators );
            authenticationInterceptor.init( service );
        }

        service.setInterceptors( interceptorList );

        SchemaManager schemaManager = service.getSchemaManager();

        // process the schemas
        for ( LoadSchema loadedSchema : dsBuilder.loadedSchemas() )
        {
            String schemaName = loadedSchema.name();
            Boolean enabled = loadedSchema.enabled();

            // Check if the schema is loaded or not
            boolean isLoaded = schemaManager.isSchemaLoaded( schemaName );

            if ( !isLoaded )
            {
                // We have to load the schema, if it exists
                try
                {
                    isLoaded = schemaManager.load( schemaName );
                }
                catch ( LdapUnwillingToPerformException lutpe )
                {
                    // Cannot load the schema, it does not exists
                    LOG.error( lutpe.getMessage() );
                    continue;
                }
            }

            if ( isLoaded )
            {
                if ( enabled )
                {
                    schemaManager.enable( schemaName );

                    if ( schemaManager.isDisabled( schemaName ) )
                    {
                        LOG.error( "Cannot enable " + schemaName );
                    }
                }
                else
                {
                    schemaManager.disable( schemaName );

                    if ( schemaManager.isEnabled( schemaName ) )
                    {
                        LOG.error( "Cannot disable " + schemaName );
                    }
                }
            }

            LOG.debug( "Loading schema {}, enabled= {}", schemaName, enabled );
        }

        // Process the Partition, if any.
        for ( CreatePartition createPartition : dsBuilder.partitions() )
        {
            Partition partition;

            // Determine the partition type
            if ( createPartition.type() == Partition.class )
            {
                // The annotation does not specify a specific partition type.
                // We use the partition factory to create partition and index
                // instances.
                PartitionFactory partitionFactory = dsf.getPartitionFactory();
                partition = partitionFactory.createPartition(
                    schemaManager,
                    service.getDnFactory(),
                    createPartition.name(),
                    createPartition.suffix(),
                    createPartition.cacheSize(),
                    new File( service.getInstanceLayout().getPartitionsDirectory(), createPartition.name() ) );

                partition.setCacheService( service.getCacheService() );

                CreateIndex[] indexes = createPartition.indexes();

                for ( CreateIndex createIndex : indexes )
                {
                    partitionFactory.addIndex( partition,
                        createIndex.attribute(), createIndex.cacheSize() );
                }

                partition.initialize();
            }
            else
            {
                // The annotation contains a specific partition type, we use
                // that type.
                Class<?>[] partypes = new Class[]
                    { SchemaManager.class, DnFactory.class };
                Constructor<?> constructor = createPartition.type().getConstructor( partypes );
                partition = ( Partition ) constructor.newInstance( new Object[]
                    { schemaManager, service.getDnFactory() } );
                partition.setId( createPartition.name() );
                partition.setSuffixDn( new Dn( schemaManager, createPartition.suffix() ) );

                if ( partition instanceof AbstractBTreePartition )
                {
                    AbstractBTreePartition btreePartition = ( AbstractBTreePartition ) partition;
                    btreePartition.setCacheService( service.getCacheService() );
                    btreePartition.setCacheSize( createPartition.cacheSize() );
                    btreePartition.setPartitionPath( new File( service
                        .getInstanceLayout().getPartitionsDirectory(),
                        createPartition.name() ).toURI() );

                    // Process the indexes if any
                    CreateIndex[] indexes = createPartition.indexes();

                    for ( CreateIndex createIndex : indexes )
                    {
                        if ( createIndex.type() == JdbmIndex.class )
                        {
                            // JDBM index
                            JdbmIndex index = new JdbmIndex( createIndex.attribute(), false );

                            btreePartition.addIndexedAttributes( index );
                        }
                        else if ( createIndex.type() == MavibotIndex.class )
                        {
                            // Mavibot index
                            MavibotIndex index = new MavibotIndex( createIndex.attribute(), false );

                            btreePartition.addIndexedAttributes( index );
                        }
                        else
                        {
                            // The annotation does not specify a specific index
                            // type.
                            // We use the generic index implementation.
                            JdbmIndex index = new JdbmIndex( createIndex.attribute(), false );

                            btreePartition.addIndexedAttributes( index );
                        }
                    }
                }
            }

            partition.setSchemaManager( schemaManager );

            // Inject the partition into the DirectoryService
            service.addPartition( partition );

            // Last, process the context entry
            ContextEntry contextEntry = createPartition.contextEntry();

            if ( contextEntry != null )
            {
                injectEntries( service, contextEntry.entryLdif() );
            }
        }

        return service;
    }


    /**
     * Create a DirectoryService from a Unit test annotation
     * 
     * @param description The annotations containing the info from which we will create
     *  the DS
     * @return A valid DS
     */
    public static DirectoryService getDirectoryService( Description description )
        throws Exception
    {
        CreateDS dsBuilder = description.getAnnotation( CreateDS.class );

        if ( dsBuilder != null )
        {
            return createDS( dsBuilder );
        }
        else
        {
            LOG.debug( "No {} DS.", description.getDisplayName() );
            return null;
        }
    }


    /**
     * Create a DirectoryService from an annotation. The @CreateDS annotation
     * must be associated with either the method or the encapsulating class. We
     * will first try to get the annotation from the method, and if there is
     * none, then we try at the class level.
     * 
     * @return A valid DS
     */
    public static DirectoryService getDirectoryService() throws Exception
    {
        Object instance = AnnotationUtils.getInstance( CreateDS.class );
        CreateDS dsBuilder = null;

        if ( instance != null )
        {
            dsBuilder = ( CreateDS ) instance;

            // Ok, we have found a CreateDS annotation. Process it now.
            return createDS( dsBuilder );
        }

        throw new LdapException( I18n.err( I18n.ERR_114 ) );
    }


    /**
     * injects an LDIF entry in the given DirectoryService
     * 
     * @param entry
     *            the LdifEntry to be injected
     * @param service
     *            the DirectoryService
     * @throws Exception
     */
    private static void injectEntry( LdifEntry entry, DirectoryService service )
        throws LdapException
    {
        if ( entry.isChangeAdd() || entry.isLdifContent() )
        {
            service.getAdminSession().add(
                new DefaultEntry( service.getSchemaManager(), entry
                    .getEntry() ) );
        }
        else if ( entry.isChangeModify() )
        {
            service.getAdminSession().modify( entry.getDn(),
                entry.getModifications() );
        }
        else
        {
            String message = I18n.err( I18n.ERR_117, entry.getChangeType() );
            throw new LdapException( message );
        }
    }


    /**
     * injects the LDIF entries present in a LDIF file
     * 
     * @param service
     *            the DirectoryService
     * @param ldifFiles
     *            the array of LDIF file names (only )
     * @throws Exception
     */
    public static void injectLdifFiles( Class<?> clazz,
        DirectoryService service, String[] ldifFiles ) throws Exception
    {
        if ( ( ldifFiles != null ) && ( ldifFiles.length > 0 ) )
        {
            for ( String ldifFile : ldifFiles )
            {
                InputStream is = clazz.getClassLoader().getResourceAsStream(
                    ldifFile );
                if ( is == null )
                {
                    throw new FileNotFoundException( "LDIF file '" + ldifFile
                        + "' not found." );
                }
                else
                {
                    LdifReader ldifReader = new LdifReader( is );

                    for ( LdifEntry entry : ldifReader )
                    {
                        injectEntry( entry, service );
                    }

                    ldifReader.close();
                }
            }
        }
    }


    /**
     * Inject an ldif String into the server. Dn must be relative to the root.
     * 
     * @param service
     *            the directory service to use
     * @param ldif
     *            the ldif containing entries to add to the server.
     * @throws Exception
     *             if there is a problem adding the entries from the LDIF
     */
    public static void injectEntries( DirectoryService service, String ldif )
        throws Exception
    {
        LdifReader reader = new LdifReader();
        List<LdifEntry> entries = reader.parseLdif( ldif );

        for ( LdifEntry entry : entries )
        {
            injectEntry( entry, service );
        }

        // And close the reader
        reader.close();
    }


    /**
     * Load the schemas, and enable/disable them.
     */
    public static void loadSchemas( Description desc, DirectoryService service )
    {
        if ( desc == null )
        {
            return;
        }

        /*for ( Class<?> loadSchema : dsBuilder.additionalInterceptors() )
        {
            service.addLast( ( Interceptor ) interceptorClass.newInstance() );
        }*/
        LoadSchema loadSchema = desc
            .getAnnotation( LoadSchema.class );

        if ( loadSchema != null )
        {
            System.out.println( loadSchema );
        }
    }


    /**
     * Apply the LDIF entries to the given service
     */
    public static void applyLdifs( Description desc, DirectoryService service )
        throws Exception
    {
        if ( desc == null )
        {
            return;
        }

        ApplyLdifFiles applyLdifFiles = desc
            .getAnnotation( ApplyLdifFiles.class );

        if ( applyLdifFiles != null )
        {
            LOG.debug( "Applying {} to {}", applyLdifFiles.value(),
                desc.getDisplayName() );
            injectLdifFiles( applyLdifFiles.clazz(), service, applyLdifFiles.value() );
        }

        ApplyLdifs applyLdifs = desc.getAnnotation( ApplyLdifs.class );

        if ( ( applyLdifs != null ) && ( applyLdifs.value() != null ) )
        {
            String[] ldifs = applyLdifs.value();

            String dnStart = "dn:";

            StringBuilder sb = new StringBuilder();

            for ( int i = 0; i < ldifs.length; )
            {
                String s = ldifs[i++].trim();
                if ( s.startsWith( dnStart ) )
                {
                    sb.append( s ).append( '\n' );

                    // read the rest of lines till we encounter Dn again
                    while ( i < ldifs.length )
                    {
                        s = ldifs[i++];
                        if ( !s.startsWith( dnStart ) )
                        {
                            sb.append( s ).append( '\n' );
                        }
                        else
                        {
                            break;
                        }
                    }

                    LOG.debug( "Applying {} to {}", sb, desc.getDisplayName() );
                    injectEntries( service, sb.toString() );
                    sb.setLength( 0 );

                    i--; // step up a line
                }
            }
        }
    }
}
