/*
 *  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.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.naming.directory.SearchControls;

import org.apache.directory.api.ldap.model.message.Control;
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.Controls;
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.StudioControl;
import org.apache.directory.studio.connection.core.io.StudioLdapException;
import org.apache.directory.studio.connection.core.io.api.StudioSearchResultEnumeration;
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.BulkModificationEvent;
import org.apache.directory.studio.ldapbrowser.core.events.EventRegistry;
import org.apache.directory.studio.ldapbrowser.core.events.SearchUpdateEvent;
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.ISearch;
import org.apache.directory.studio.ldapbrowser.core.model.ISearchResult;
import org.apache.directory.studio.ldapbrowser.core.utils.JNDIUtils;


/**
 * Runnable to delete entries.
 * 
 * Deletes the entry recursively in a optimistic way:
 * <ol>
 * <li>Delete the entry
 * <li>If that fails with error code 66 then perform a one-level search
 *     and start from 1. for each entry. 
 * </ol>
 *
 * TODO: delete subentries?
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class DeleteEntriesRunnable implements StudioConnectionBulkRunnableWithProgress
{
    /** The entries to delete. */
    private Collection<IEntry> entriesToDelete;

    /** The deleted entries. */
    private Set<IEntry> deletedEntriesSet;

    /** The searches to update. */
    private Set<ISearch> searchesToUpdateSet;

    /** The use tree delete control flag. */
    private boolean useTreeDeleteControl;


    /**
     * Creates a new instance of DeleteEntriesRunnable. 
     * 
     * @param entriesToDelete the entries to delete
     */
    public DeleteEntriesRunnable( final Collection<IEntry> entriesToDelete, boolean useTreeDeleteControl )
    {
        this.entriesToDelete = entriesToDelete;
        this.useTreeDeleteControl = useTreeDeleteControl;

        this.deletedEntriesSet = new HashSet<IEntry>();
        this.searchesToUpdateSet = new HashSet<ISearch>();
    }


    /**
     * {@inheritDoc}
     */
    public Connection[] getConnections()
    {
        Connection[] connections = new Connection[entriesToDelete.size()];
        int i = 0;
        for ( IEntry entry : entriesToDelete )
        {
            connections[i] = entry.getBrowserConnection().getConnection();
            i++;
        }
        return connections;
    }


    /**
     * {@inheritDoc}
     */
    public String getName()
    {
        return entriesToDelete.size() == 1 ? BrowserCoreMessages.jobs__delete_entries_name_1
            : BrowserCoreMessages.jobs__delete_entries_name_n;
    }


    /**
     * {@inheritDoc}
     */
    public Object[] getLockedObjects()
    {
        List<IEntry> l = new ArrayList<IEntry>();
        l.addAll( entriesToDelete );
        return l.toArray();
    }


    /**
     * {@inheritDoc}
     */
    public String getErrorMessage()
    {
        return entriesToDelete.size() == 1 ? BrowserCoreMessages.jobs__delete_entries_error_1
            : BrowserCoreMessages.jobs__delete_entries_error_n;
    }


    /**
     * {@inheritDoc}
     */
    public void run( StudioProgressMonitor monitor )
    {
        monitor.beginTask(
            entriesToDelete.size() == 1 ? BrowserCoreMessages.bind( BrowserCoreMessages.jobs__delete_entries_task_1,
                new String[]
                    { entriesToDelete.iterator().next().getDn().getName() } ) : BrowserCoreMessages.bind(
                BrowserCoreMessages.jobs__delete_entries_task_n, new String[]
                    { Integer.toString( entriesToDelete.size() ) } ), 2 + entriesToDelete.size() );
        monitor.reportProgress( " " ); //$NON-NLS-1$
        monitor.worked( 1 );

        int num = 0;
        StudioProgressMonitor dummyMonitor = new StudioProgressMonitor( monitor );
        for ( Iterator<IEntry> iterator = entriesToDelete.iterator(); !monitor.isCanceled()
            && !monitor.errorsReported() && iterator.hasNext(); )
        {
            IEntry entryToDelete = iterator.next();
            IBrowserConnection browserConnection = entryToDelete.getBrowserConnection();

            // delete from directory
            int errorStatusSize1 = monitor.getErrorStatus( "" ).getChildren().length; //$NON-NLS-1$
            num = optimisticDeleteEntryRecursive( browserConnection, entryToDelete.getDn(), entryToDelete.isReferral(),
                useTreeDeleteControl, num, dummyMonitor, monitor );
            int errorStatusSize2 = monitor.getErrorStatus( "" ).getChildren().length; //$NON-NLS-1$

            if ( !monitor.isCanceled() )
            {
                if ( errorStatusSize1 == errorStatusSize2 )
                {
                    // delete
                    deletedEntriesSet.add( entryToDelete );
                    //entryToDelete.setChildrenInitialized( false );

                    // delete from parent entry
                    entryToDelete.getParententry().setChildrenInitialized( false );
                    entryToDelete.getParententry().deleteChild( entryToDelete );

                    // delete from searches
                    List<ISearch> searches = browserConnection.getSearchManager().getSearches();
                    for ( ISearch search : searches )
                    {
                        if ( search.getSearchResults() != null )
                        {
                            ISearchResult[] searchResults = search.getSearchResults();
                            List<ISearchResult> searchResultList = new ArrayList<ISearchResult>();
                            searchResultList.addAll( Arrays.asList( searchResults ) );
                            for ( Iterator<ISearchResult> it = searchResultList.iterator(); it.hasNext(); )
                            {
                                ISearchResult result = it.next();
                                if ( entryToDelete.equals( result.getEntry() ) )
                                {
                                    it.remove();
                                    searchesToUpdateSet.add( search );
                                }
                            }
                            if ( searchesToUpdateSet.contains( search ) )
                            {
                                search.setSearchResults( searchResultList.toArray( new ISearchResult[searchResultList
                                    .size()] ) );
                            }
                        }
                    }

                    // delete from cache
                    browserConnection.uncacheEntryRecursive( entryToDelete );
                }
            }
            else
            {
                entryToDelete.setChildrenInitialized( false );
            }

            monitor.worked( 1 );
        }
    }


    /**
     * {@inheritDoc}
     */
    public void runNotification( StudioProgressMonitor monitor )
    {
        // don't fire an EntryDeletedEvent for each deleted entry
        // that would cause massive UI updates
        // instead we unset children information and fire a BulkModificationEvent
        IBrowserConnection browserConnection = entriesToDelete.iterator().next().getBrowserConnection();
        EventRegistry.fireEntryUpdated( new BulkModificationEvent( browserConnection ), this );

        for ( ISearch search : searchesToUpdateSet )
        {
            EventRegistry.fireSearchUpdated( new SearchUpdateEvent( search,
                SearchUpdateEvent.EventDetail.SEARCH_PERFORMED ), this );
        }
    }


    /**
     * Deletes the entry recursively in a optimistic way:
     * <ol>
     * <li>Deletes the entry
     * <li>If that fails then perform a one-level search and call the 
     * method for each found entry
     * </ol>
     * 
     * @param browserConnection the browser connection
     * @param dn the Dn to delete
     * @param useManageDsaItControl true to use the ManageDsaIT control
     * @param useTreeDeleteControl true to use the tree delete control
     * @param numberOfDeletedEntries the number of deleted entries
     * @param dummyMonitor the dummy monitor
     * @param monitor the progress monitor
     * 
     * @return the cumulative number of deleted entries
     */
    static int optimisticDeleteEntryRecursive( IBrowserConnection browserConnection, Dn dn,
        boolean useManageDsaItControl, boolean useTreeDeleteControl, int numberOfDeletedEntries,
        StudioProgressMonitor dummyMonitor, StudioProgressMonitor monitor )
    {
        // try to delete entry
        dummyMonitor.reset();
        deleteEntry( browserConnection, dn, useManageDsaItControl, useTreeDeleteControl, dummyMonitor );

        if ( !dummyMonitor.errorsReported() )
        {
            numberOfDeletedEntries++;
            monitor.reportProgress( BrowserCoreMessages.bind( BrowserCoreMessages.model__deleted_n_entries,
                new String[]
                    { "" + numberOfDeletedEntries } ) ); //$NON-NLS-1$
        }
        else if ( StudioLdapException.isContextNotEmptyException( dummyMonitor.getException() ) )
        {
            // do not follow referrals or dereference aliases when deleting entries
            AliasDereferencingMethod aliasDereferencingMethod = AliasDereferencingMethod.NEVER;
            ReferralHandlingMethod referralsHandlingMethod = ReferralHandlingMethod.IGNORE;

            // perform one-level search and delete recursively
            int numberInBatch;
            dummyMonitor.reset();
            do
            {
                numberInBatch = 0;

                SearchControls searchControls = new SearchControls();
                searchControls.setCountLimit( 1000 );
                searchControls.setReturningAttributes( new String[0] );
                searchControls.setSearchScope( SearchControls.ONELEVEL_SCOPE );
                StudioSearchResultEnumeration result = browserConnection
                    .getConnection()
                    .getConnectionWrapper()
                    .search( dn.getName(), ISearch.FILTER_TRUE, searchControls, aliasDereferencingMethod,
                        referralsHandlingMethod, null, dummyMonitor, null );

                try
                {
                    // delete all child entries
                    while ( !dummyMonitor.isCanceled() && !dummyMonitor.errorsReported() && result.hasMore() )
                    {
                        Dn childDn = result.next().getDn();

                        numberOfDeletedEntries = optimisticDeleteEntryRecursive( browserConnection, childDn, false,
                            false, numberOfDeletedEntries, dummyMonitor, monitor );
                        numberInBatch++;
                    }
                }
                catch ( Exception e )
                {
                    int ldapStatusCode = JNDIUtils.getLdapStatusCode( e );
                    if ( ldapStatusCode == 3 || ldapStatusCode == 4 || ldapStatusCode == 11 )
                    {
                        // continue with search
                    }
                    else
                    {
                        dummyMonitor.reportError( e );
                        break;
                    }
                }
            }
            while ( numberInBatch > 0 && !monitor.isCanceled() && !dummyMonitor.errorsReported() );

            // try to delete the entry again 
            if ( !dummyMonitor.errorsReported() )
            {
                deleteEntry( browserConnection, dn, false, false, dummyMonitor );
            }
            if ( !dummyMonitor.errorsReported() )
            {
                numberOfDeletedEntries++;
                monitor.reportProgress( BrowserCoreMessages.bind( BrowserCoreMessages.model__deleted_n_entries,
                    new String[]
                        { "" + numberOfDeletedEntries } ) ); //$NON-NLS-1$
            }
        }
        else
        {
            Exception exception = dummyMonitor.getException();
            // we have another exception
            // report it to the dummy monitor if we are in the recursion
            dummyMonitor.reportError( exception );
            // also report it to the real monitor
            monitor.reportError( exception );
        }

        return numberOfDeletedEntries;
    }


    static void deleteEntry( IBrowserConnection browserConnection, Dn dn, boolean useManageDsaItControl,
        boolean useTreeDeleteControl, StudioProgressMonitor monitor )
    {
        // controls
        List<Control> controlList = new ArrayList<Control>();
        if ( useTreeDeleteControl
            && browserConnection.getRootDSE().isControlSupported( StudioControl.TREEDELETE_CONTROL.getOid() ) )
        {
            controlList.add( Controls.TREEDELETE_CONTROL );
        }
        if ( useManageDsaItControl
            && browserConnection.getRootDSE().isControlSupported( StudioControl.MANAGEDSAIT_CONTROL.getOid() ) )
        {
            controlList.add( Controls.MANAGEDSAIT_CONTROL );
        }
        Control[] controls = controlList.toArray( new Control[controlList.size()] );

        // delete entry
        if ( browserConnection.getConnection() != null )
        {
            browserConnection.getConnection().getConnectionWrapper()
                .deleteEntry( dn, controls, monitor, null );
        }
    }
}
