/*
 *  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.studio.ldapbrowser.core.jobs;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.studio.common.core.jobs.StudioProgressMonitor;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.Connection.AliasDereferencingMethod;
import org.apache.directory.studio.connection.core.Connection.ReferralHandlingMethod;
import org.apache.directory.studio.connection.core.ConnectionCorePlugin;
import org.apache.directory.studio.connection.core.DetectedConnectionProperties;
import org.apache.directory.studio.connection.core.jobs.StudioConnectionBulkRunnableWithProgress;
import org.apache.directory.studio.ldapbrowser.core.BrowserCoreMessages;
import org.apache.directory.studio.ldapbrowser.core.events.AttributesInitializedEvent;
import org.apache.directory.studio.ldapbrowser.core.events.EventRegistry;
import org.apache.directory.studio.ldapbrowser.core.model.IAttribute;
import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
import org.apache.directory.studio.ldapbrowser.core.model.IEntry;
import org.apache.directory.studio.ldapbrowser.core.model.IRootDSE;
import org.apache.directory.studio.ldapbrowser.core.model.ISearch;
import org.apache.directory.studio.ldapbrowser.core.model.ISearchResult;
import org.apache.directory.studio.ldapbrowser.core.model.impl.BaseDNEntry;
import org.apache.directory.studio.ldapbrowser.core.model.impl.DirectoryMetadataEntry;
import org.apache.directory.studio.ldapbrowser.core.model.impl.Search;


/**
 * Runnable to initialize the Root DSE.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class InitializeRootDSERunnable implements StudioConnectionBulkRunnableWithProgress
{
    /** The requested attributes when reading the Root DSE. */
    public static final String[] ROOT_DSE_ATTRIBUTES =
        { 
            SchemaConstants.NAMING_CONTEXTS_AT, 
            SchemaConstants.SUBSCHEMA_SUBENTRY_AT,
            SchemaConstants.SUPPORTED_LDAP_VERSION_AT, 
            SchemaConstants.SUPPORTED_SASL_MECHANISMS_AT,
            SchemaConstants.SUPPORTED_EXTENSION_AT, 
            SchemaConstants.SUPPORTED_CONTROL_AT,
            SchemaConstants.SUPPORTED_FEATURES_AT, 
            SchemaConstants.VENDOR_NAME_AT,
            SchemaConstants.VENDOR_VERSION_AT,
            SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES };

    private IRootDSE rootDSE;


    /**
     * Creates a new instance of InitializeRootDSERunnable.
     * 
     * @param rootDSE the root DSE
     */
    private InitializeRootDSERunnable( IRootDSE rootDSE )
    {
        this.rootDSE = rootDSE;
    }


    /**
     * {@inheritDoc}
     */
    public Connection[] getConnections()
    {
        return new Connection[]
            { rootDSE.getBrowserConnection().getConnection() };
    }


    /**
     * {@inheritDoc}
     */
    public String getName()
    {
        return BrowserCoreMessages.jobs__init_entries_title_attonly;
    }


    /**
     * {@inheritDoc}
     */
    public Object[] getLockedObjects()
    {
        return new IEntry[]
            { rootDSE };
    }


    /**
     * {@inheritDoc}
     */
    public String getErrorMessage()
    {
        return BrowserCoreMessages.jobs__init_entries_error_1;
    }


    /**
     * {@inheritDoc}
     */
    public void run( StudioProgressMonitor monitor )
    {
        monitor.beginTask( " ", 3 ); //$NON-NLS-1$
        monitor.reportProgress( " " ); //$NON-NLS-1$

        monitor.setTaskName( BrowserCoreMessages.bind( BrowserCoreMessages.jobs__init_entries_task, new String[]
            { rootDSE.getDn().getName() } ) );
        monitor.worked( 1 );

        monitor.reportProgress( BrowserCoreMessages.bind( BrowserCoreMessages.jobs__init_entries_progress_att,
            new String[]
                { rootDSE.getDn().getName() } ) );

        loadRootDSE( rootDSE.getBrowserConnection(), monitor );
    }


    /**
     * {@inheritDoc}
     */
    public void runNotification( StudioProgressMonitor monitor )
    {
        EventRegistry.fireEntryUpdated( new AttributesInitializedEvent( rootDSE ), this );
    }


    /**
     * Loads the Root DSE.
     * 
     * @param browserConnection the browser connection
     * @param monitor the progress monitor
     * 
     * @throws Exception the exception
     */
    public static synchronized void loadRootDSE( IBrowserConnection browserConnection, StudioProgressMonitor monitor )
    {
        // clear old children
        InitializeChildrenRunnable.clearCaches( browserConnection.getRootDSE(), true );

        // delete old attributes
        IAttribute[] oldAttributes = browserConnection.getRootDSE().getAttributes();
        
        if ( oldAttributes != null )
        {
            for ( IAttribute oldAttribute : oldAttributes )
            {
                browserConnection.getRootDSE().deleteAttribute( oldAttribute );
            }
        }

        // load well-known Root DSE attributes and operational attributes
        ISearch search = new Search( null, browserConnection, Dn.EMPTY_DN, ISearch.FILTER_TRUE,
            ROOT_DSE_ATTRIBUTES, SearchScope.OBJECT, 0, 0, Connection.AliasDereferencingMethod.NEVER,
            Connection.ReferralHandlingMethod.IGNORE, false, null, false );
        SearchRunnable.searchAndUpdateModel( browserConnection, search, monitor );

        // Load all user attributes. This is done because the BEA "LDAP server" (so called) is stupid
        // enough not to accept searches where "+" and "*" are provided on the list of parameters.
        // We have to do two searches...
        search = new Search( null, browserConnection, Dn.EMPTY_DN, ISearch.FILTER_TRUE, new String[]
            { SchemaConstants.ALL_USER_ATTRIBUTES }, SearchScope.OBJECT, 0, 0,
            Connection.AliasDereferencingMethod.NEVER, Connection.ReferralHandlingMethod.IGNORE, false, null, false );
        SearchRunnable.searchAndUpdateModel( browserConnection, search, monitor );

        // the list of entries under the Root DSE
        Map<Dn, IEntry> rootDseEntries = new HashMap<Dn, IEntry>();

        // 1st: add base DNs, either the specified or from the namingContexts attribute
        if ( !browserConnection.isFetchBaseDNs() && browserConnection.getBaseDN() != null
            && !"".equals( browserConnection.getBaseDN().toString() ) ) //$NON-NLS-1$
        {
            // only add the specified base Dn
            Dn dn = browserConnection.getBaseDN();
            IEntry entry = browserConnection.getEntryFromCache( dn );
            
            if ( entry == null )
            {
                entry = new BaseDNEntry( dn, browserConnection );
                browserConnection.cacheEntry( entry );
            }
            rootDseEntries.put( dn, entry );
        }
        else
        {
            // get base DNs from namingContexts attribute
            Set<String> namingContextSet = new HashSet<String>();
            IAttribute attribute = browserConnection.getRootDSE().getAttribute( SchemaConstants.NAMING_CONTEXTS_AT );
            
            if ( attribute != null )
            {
                String[] values = attribute.getStringValues();
                
                for ( int i = 0; i < values.length; i++ )
                {
                    namingContextSet.add( values[i] );
                }
            }

            if ( !namingContextSet.isEmpty() )
            {
                for ( String namingContext : namingContextSet )
                {
                    if ( namingContext.length() > 0 && namingContext.charAt( namingContext.length() - 1 ) == '\u0000' )
                    {
                        namingContext = namingContext.substring( 0, namingContext.length() - 1 );
                    }

                    if ( !"".equals( namingContext ) ) //$NON-NLS-1$
                    {
                        try
                        {
                            Dn dn = new Dn( namingContext );
                            IEntry entry = browserConnection.getEntryFromCache( dn );
                            
                            if ( entry == null )
                            {
                                entry = new BaseDNEntry( dn, browserConnection );
                                browserConnection.cacheEntry( entry );
                            }
                            
                            rootDseEntries.put( dn, entry );
                        }
                        catch ( LdapInvalidDnException e )
                        {
                            monitor.reportError( BrowserCoreMessages.model__error_setting_base_dn, e );
                        }
                    }
                    else
                    {
                        // special handling of empty namingContext (Novell eDirectory): 
                        // perform a one-level search and add all result DNs to the set
                        searchRootDseEntries( browserConnection, rootDseEntries, monitor );
                    }
                }
            }
            else
            {
                // special handling of non-existing namingContexts attribute (Oracle Internet Directory)
                // perform a one-level search and add all result DNs to the set
                searchRootDseEntries( browserConnection, rootDseEntries, monitor );
            }
        }

        // 2nd: add schema sub-entry
        IEntry[] schemaEntries = getDirectoryMetadataEntries( browserConnection, SchemaConstants.SUBSCHEMA_SUBENTRY_AT );
        
        for ( IEntry entry : schemaEntries )
        {
            if ( entry instanceof DirectoryMetadataEntry )
            {
                ( ( DirectoryMetadataEntry ) entry ).setSchemaEntry( true );
            }
            
            rootDseEntries.put( entry.getDn(), entry );
        }

        // get other meta data entries
        IAttribute[] rootDseAttributes = browserConnection.getRootDSE().getAttributes();
        
        if ( rootDseAttributes != null )
        {
            for ( IAttribute attribute : rootDseAttributes )
            {
                IEntry[] metadataEntries = getDirectoryMetadataEntries( browserConnection, attribute.getDescription() );
                
                for ( IEntry entry : metadataEntries )
                {
                    rootDseEntries.put( entry.getDn(), entry );
                }
            }
        }

        // try to init entries
        StudioProgressMonitor dummyMonitor = new StudioProgressMonitor( monitor );
        
        for ( IEntry entry : rootDseEntries.values() )
        {
            initBaseEntry( entry, dummyMonitor );
        }

        // set flags
        browserConnection.getRootDSE().setHasMoreChildren( false );
        browserConnection.getRootDSE().setAttributesInitialized( true );
        browserConnection.getRootDSE().setInitOperationalAttributes( true );
        browserConnection.getRootDSE().setChildrenInitialized( true );
        browserConnection.getRootDSE().setHasChildrenHint( true );
        browserConnection.getRootDSE().setDirectoryEntry( true );

        // Set detected connection properties
        DetectedConnectionProperties detectedConnectionProperties = browserConnection.getConnection()
            .getDetectedConnectionProperties();
        IAttribute vendorNameAttribute = browserConnection.getRootDSE().getAttribute( "vendorName" ); //$NON-NLS-1$
        
        if ( ( vendorNameAttribute != null ) && ( vendorNameAttribute.getValueSize() > 0 ) )
        {
            detectedConnectionProperties.setVendorName( vendorNameAttribute.getStringValue() );
        }
        
        IAttribute vendorVersionAttribute = browserConnection.getRootDSE().getAttribute( "vendorVersion" ); //$NON-NLS-1$
        
        if ( ( vendorVersionAttribute != null ) && ( vendorVersionAttribute.getValueSize() > 0 ) )
        {
            detectedConnectionProperties.setVendorVersion( vendorVersionAttribute.getStringValue() );
        }
        
        IAttribute supportedControlAttribute = browserConnection.getRootDSE().getAttribute( "supportedControl" ); //$NON-NLS-1$
        
        if ( ( supportedControlAttribute != null ) && ( supportedControlAttribute.getValueSize() > 0 ) )
        {
            detectedConnectionProperties.setSupportedControls( Arrays.asList( supportedControlAttribute
                .getStringValues() ) );
        }
        
        IAttribute supportedExtensionAttribute = browserConnection.getRootDSE().getAttribute( "supportedExtension" ); //$NON-NLS-1$
        
        if ( ( supportedExtensionAttribute != null ) && ( supportedExtensionAttribute.getValueSize() > 0 ) )
        {
            detectedConnectionProperties.setSupportedExtensions( Arrays.asList( supportedExtensionAttribute
                .getStringValues() ) );
        }
        
        IAttribute supportedFeaturesAttribute = browserConnection.getRootDSE().getAttribute( "supportedFeatures" ); //$NON-NLS-1$
        
        if ( ( supportedFeaturesAttribute != null ) && ( supportedFeaturesAttribute.getValueSize() > 0 ) )
        {
            detectedConnectionProperties.setSupportedFeatures( Arrays.asList( supportedFeaturesAttribute
                .getStringValues() ) );
        }
        
        detectedConnectionProperties
            .setServerType( ServerTypeDetector.detectServerType( browserConnection.getRootDSE() ) );

        ConnectionCorePlugin.getDefault().getConnectionManager()
            .connectionUpdated( browserConnection.getConnection() );
    }


    private static void initBaseEntry( IEntry entry, StudioProgressMonitor monitor )
    {
        IBrowserConnection browserConnection = entry.getBrowserConnection();
        Dn dn = entry.getDn();

        // search the entry
        AliasDereferencingMethod derefAliasMethod = browserConnection.getAliasesDereferencingMethod();
        ReferralHandlingMethod handleReferralsMethod = browserConnection.getReferralsHandlingMethod();
        ISearch search = new Search( null, browserConnection, dn, ISearch.FILTER_TRUE, ISearch.NO_ATTRIBUTES,
            SearchScope.OBJECT, 1, 0, derefAliasMethod, handleReferralsMethod, true, null, false );
        SearchRunnable.searchAndUpdateModel( browserConnection, search, monitor );

        ISearchResult[] results = search.getSearchResults();
        
        if ( results != null && results.length == 1 )
        {
            // add entry to Root DSE
            ISearchResult result = results[0];
            entry = result.getEntry();
            browserConnection.getRootDSE().addChild( entry );
        }
        else
        {
            // Dn exists in the Root DSE, but doesn't exist in directory
            browserConnection.uncacheEntryRecursive( entry );
        }
    }


    private static IEntry[] getDirectoryMetadataEntries( IBrowserConnection browserConnection,
        String metadataAttributeName )
    {
        List<Dn> metadataEntryDnList = new ArrayList<Dn>();
        IAttribute attribute = browserConnection.getRootDSE().getAttribute( metadataAttributeName );
        
        if ( attribute != null )
        {
            String[] values = attribute.getStringValues();
            
            for ( String dn : values )
            {
                if ( dn != null && !"".equals( dn ) ) //$NON-NLS-1$
                {
                    try
                    {
                        metadataEntryDnList.add( new Dn( dn ) );
                    }
                    catch ( LdapInvalidDnException e )
                    {
                    }
                }
            }
        }

        IEntry[] metadataEntries = new IEntry[metadataEntryDnList.size()];
        
        for ( int i = 0; i < metadataEntryDnList.size(); i++ )
        {
            Dn dn = metadataEntryDnList.get( i );
            metadataEntries[i] = browserConnection.getEntryFromCache( dn );
            
            if ( metadataEntries[i] == null )
            {
                metadataEntries[i] = new DirectoryMetadataEntry( dn, browserConnection );
                metadataEntries[i].setDirectoryEntry( true );
                browserConnection.cacheEntry( metadataEntries[i] );
            }
        }
        
        return metadataEntries;
    }


    private static void searchRootDseEntries( IBrowserConnection browserConnection, Map<Dn, IEntry> rootDseEntries,
        StudioProgressMonitor monitor )
    {
        ISearch search = new Search( null, browserConnection, Dn.EMPTY_DN, ISearch.FILTER_TRUE,
            ISearch.NO_ATTRIBUTES, SearchScope.ONELEVEL, 0, 0, Connection.AliasDereferencingMethod.NEVER,
            Connection.ReferralHandlingMethod.IGNORE, false, null, false );
        SearchRunnable.searchAndUpdateModel( browserConnection, search, monitor );

        ISearchResult[] results = search.getSearchResults();

        if ( results != null )
        {
            for ( ISearchResult searchResult : results )
            {
                IEntry entry = searchResult.getEntry();
                rootDseEntries.put( entry.getDn(), entry );
            }
        }
    }
}
