/*
 *  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.common.widgets.browser;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.directory.studio.connection.core.jobs.OpenConnectionsRunnable;
import org.apache.directory.studio.connection.core.jobs.StudioConnectionRunnableWithProgress;
import org.apache.directory.studio.ldapbrowser.core.jobs.InitializeChildrenRunnable;
import org.apache.directory.studio.ldapbrowser.core.jobs.SearchRunnable;
import org.apache.directory.studio.ldapbrowser.core.jobs.StudioBrowserJob;
import org.apache.directory.studio.ldapbrowser.core.model.IBookmark;
import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
import org.apache.directory.studio.ldapbrowser.core.model.IContinuation;
import org.apache.directory.studio.ldapbrowser.core.model.IContinuation.State;
import org.apache.directory.studio.ldapbrowser.core.model.IEntry;
import org.apache.directory.studio.ldapbrowser.core.model.IQuickSearch;
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.DirectoryMetadataEntry;
import org.apache.directory.studio.ldapbrowser.core.model.impl.SearchContinuation;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;


/**
 * The BrowserContentProvider implements the content provider for
 * the browser widget. It accepts an IConnection as input.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class BrowserContentProvider implements ITreeContentProvider
{
    /** The viewer. */
    private TreeViewer viewer;

    /** The preferences */
    protected BrowserPreferences preferences;

    /** The sorter */
    protected BrowserSorter sorter;

    /** This map contains the pages for entries with many children (if folding is activated) */
    private Map<IEntry, BrowserEntryPage[]> entryToEntryPagesMap;

    /** This map contains the pages for searches with many results (if folding is activated) */
    private Map<ISearch, BrowserSearchResultPage[]> searchToSearchResultPagesMap;

    /** This map contains the top-level categories for each connection */
    private Map<IBrowserConnection, BrowserCategory[]> connectionToCategoriesMap;

    /** The page listener. */
    private ISelectionChangedListener pageListener = new ISelectionChangedListener()
    {
        public void selectionChanged( SelectionChangedEvent event )
        {
            IStructuredSelection selection = ( IStructuredSelection ) event.getSelection();
            if ( selection.size() == 1 && selection.getFirstElement() instanceof StudioConnectionRunnableWithProgress )
            {
                StudioConnectionRunnableWithProgress runnable = ( StudioConnectionRunnableWithProgress ) selection
                    .getFirstElement();
                new StudioBrowserJob( runnable ).execute();
            }
        }
    };


    /**
     * Creates a new instance of BrowserContentProvider.
     *
     * @param viewer the viewer
     * @param preferences the preferences
     * @param sorter the sorter
     */
    public BrowserContentProvider( BrowserWidget widget, BrowserPreferences preferences, BrowserSorter sorter )
    {
        this.viewer = widget.getViewer();
        this.preferences = preferences;
        this.sorter = sorter;
        this.entryToEntryPagesMap = new HashMap<IEntry, BrowserEntryPage[]>();
        this.searchToSearchResultPagesMap = new HashMap<ISearch, BrowserSearchResultPage[]>();
        this.connectionToCategoriesMap = new HashMap<IBrowserConnection, BrowserCategory[]>();

        viewer.addSelectionChangedListener( pageListener );
    }


    /**
     * {@inheritDoc}
     */
    public void inputChanged( Viewer v, Object oldInput, Object newInput )
    {
    }


    /**
     * {@inheritDoc}
     */
    public void dispose()
    {
        if ( entryToEntryPagesMap != null )
        {
            entryToEntryPagesMap.clear();
            entryToEntryPagesMap = null;
        }
        if ( searchToSearchResultPagesMap != null )
        {
            searchToSearchResultPagesMap.clear();
            searchToSearchResultPagesMap = null;
        }
        if ( connectionToCategoriesMap != null )
        {
            connectionToCategoriesMap.clear();
            connectionToCategoriesMap = null;
        }
        viewer.removeSelectionChangedListener( pageListener );
    }


    /**
     * {@inheritDoc}
     */
    public Object[] getElements( Object parent )
    {
        if ( parent instanceof IBrowserConnection )
        {
            IBrowserConnection connection = ( IBrowserConnection ) parent;
            if ( !connectionToCategoriesMap.containsKey( connection ) )
            {
                BrowserCategory[] categories = new BrowserCategory[3];
                categories[0] = new BrowserCategory( BrowserCategory.TYPE_DIT, connection );
                categories[1] = new BrowserCategory( BrowserCategory.TYPE_SEARCHES, connection );
                categories[2] = new BrowserCategory( BrowserCategory.TYPE_BOOKMARKS, connection );
                connectionToCategoriesMap.put( connection, categories );
            }

            BrowserCategory[] categories = connectionToCategoriesMap.get( connection );

            List<BrowserCategory> catList = new ArrayList<BrowserCategory>( 3 );
            if ( preferences.isShowDIT() )
            {
                catList.add( categories[0] );
            }
            if ( preferences.isShowSearches() )
            {
                catList.add( categories[1] );
            }
            if ( preferences.isShowBookmarks() )
            {
                catList.add( categories[2] );
            }

            return catList.toArray( new BrowserCategory[0] );
        }
        else if ( parent instanceof IEntry[] )
        {
            return ( IEntry[] ) parent;
        }
        else
        {
            return getChildren( parent );
        }
    }


    /**
     * {@inheritDoc}
     */
    public Object getParent( final Object child )
    {
        if ( child instanceof BrowserCategory )
        {
            return ( ( BrowserCategory ) child ).getParent();
        }
        else if ( child instanceof BrowserEntryPage )
        {
            return ( ( BrowserEntryPage ) child ).getParent();
        }
        else if ( child instanceof IEntry )
        {
            IEntry parentEntry = ( ( IEntry ) child ).getParententry();
            if ( parentEntry == null )
            {
                if ( connectionToCategoriesMap.get( ( ( IEntry ) child ).getBrowserConnection() ) != null )
                {
                    return connectionToCategoriesMap.get( ( ( IEntry ) child ).getBrowserConnection() )[0];
                }
                else
                {
                    return null;
                }
            }
            else if ( parentEntry.getChildrenCount() <= preferences.getFoldingSize() || !preferences.isUseFolding() )
            {
                return parentEntry;
            }
            else
            {
                BrowserEntryPage[] entryPages = getEntryPages( parentEntry );
                BrowserEntryPage ep = null;
                for ( int i = 0; i < entryPages.length && ep == null; i++ )
                {
                    ep = entryPages[i].getParentOf( ( IEntry ) child );
                }
                return ep;
            }
        }
        else if ( child instanceof BrowserSearchResultPage )
        {
            return ( ( BrowserSearchResultPage ) child ).getParent();
        }
        else if ( child instanceof IQuickSearch )
        {
            IQuickSearch quickSearch = ( ( IQuickSearch ) child );
            IEntry entry = quickSearch.getBrowserConnection().getEntryFromCache( quickSearch.getSearchBase() );
            return entry;
        }
        else if ( child instanceof ISearch )
        {
            ISearch search = ( ( ISearch ) child );
            if ( connectionToCategoriesMap.get( search.getBrowserConnection() ) != null )
            {
                return connectionToCategoriesMap.get( search.getBrowserConnection() )[1];
            }
            else
            {
                return null;
            }
        }
        else if ( child instanceof ISearchResult )
        {
            ISearch parentSearch = ( ( ISearchResult ) child ).getSearch();

            if ( parentSearch == null || parentSearch.getSearchResults().length <= preferences.getFoldingSize()
                || !preferences.isUseFolding() )
            {
                return parentSearch;
            }
            else
            {
                BrowserSearchResultPage[] srPages = getSearchResultPages( parentSearch );
                BrowserSearchResultPage srp = null;
                for ( int i = 0; i < srPages.length && srp == null; i++ )
                {
                    srp = srPages[i].getParentOf( ( ISearchResult ) child );
                }
                return srp;
            }
        }
        else if ( child instanceof IBookmark )
        {
            IBookmark bookmark = ( ( IBookmark ) child );
            if ( connectionToCategoriesMap.get( bookmark.getBrowserConnection() ) != null )
            {
                return connectionToCategoriesMap.get( bookmark.getBrowserConnection() )[2];
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    public Object[] getChildren( Object parent )
    {
        if ( parent instanceof BrowserEntryPage )
        {
            BrowserEntryPage entryPage = ( BrowserEntryPage ) parent;
            Object[] objects = entryPage.getChildren();
            if ( objects == null )
            {
                return new String[]
                    { Messages.getString( "BrowserContentProvider.FetchingEntries" ) }; //$NON-NLS-1$
            }
            else if ( objects instanceof IEntry[] )
            {
                IEntry[] entries = ( IEntry[] ) objects;
                return entries;
            }
            else
            {
                return objects;
            }
        }
        else if ( parent instanceof IRootDSE )
        {
            final IRootDSE rootDSE = ( IRootDSE ) parent;

            if ( !rootDSE.isChildrenInitialized() )
            {
                new StudioBrowserJob( new InitializeChildrenRunnable( false, rootDSE ) ).execute();
                return new String[]
                    { Messages.getString( "BrowserContentProvider.FetchingEntries" ) }; //$NON-NLS-1$
            }

            // get base entries
            List<IEntry> entryList = new ArrayList<IEntry>();
            entryList.addAll( Arrays.asList( rootDSE.getChildren() ) );

            // remove non-visible entries
            for ( Iterator<IEntry> it = entryList.iterator(); it.hasNext(); )
            {
                Object o = it.next();
                if ( !preferences.isShowDirectoryMetaEntries() && ( o instanceof DirectoryMetadataEntry ) )
                {
                    it.remove();
                }
            }

            return entryList.toArray();
        }
        else if ( parent instanceof IEntry )
        {
            final IEntry parentEntry = ( IEntry ) parent;

            if ( parentEntry instanceof IContinuation )
            {
                IContinuation continuation = ( IContinuation ) parentEntry;
                if ( continuation.getState() == State.UNRESOLVED )
                {
                    continuation.resolve();
                }
                if ( continuation.getState() == State.CANCELED )
                {
                    return new Object[0];
                }
            }

            List<Object> objects = new ArrayList<Object>();

            IQuickSearch quickSearch = getQuickSearchForEntry( parentEntry );
            if ( quickSearch != null )
            {
                objects.add( quickSearch );
            }

            if ( !parentEntry.isChildrenInitialized() )
            {
                new StudioBrowserJob( new InitializeChildrenRunnable( false, parentEntry ) ).execute();
                return new String[]
                    { Messages.getString( "BrowserContentProvider.FetchingEntries" ) }; //$NON-NLS-1$
            }
            else if ( parentEntry.getChildrenCount() <= preferences.getFoldingSize() || !preferences.isUseFolding() )
            {
                if ( entryToEntryPagesMap.containsKey( parentEntry ) )
                {
                    entryToEntryPagesMap.remove( parentEntry );
                }

                IEntry[] results = parentEntry.getChildren();

                if ( parentEntry.getTopPageChildrenRunnable() != null )
                {
                    objects.add( parentEntry.getTopPageChildrenRunnable() );
                }

                objects.addAll( Arrays.asList( results ) );

                if ( parentEntry.getNextPageChildrenRunnable() != null )
                {
                    objects.add( parentEntry.getNextPageChildrenRunnable() );
                }

                return objects.toArray();
            }
            else
            {
                BrowserEntryPage[] entryPages = getEntryPages( parentEntry );

                objects.addAll( Arrays.asList( entryPages ) );

                return objects.toArray();
            }
        }
        else if ( parent instanceof BrowserSearchResultPage )
        {
            BrowserSearchResultPage srPage = ( BrowserSearchResultPage ) parent;
            Object[] objects = srPage.getChildren();
            if ( objects == null )
            {
                return new String[]
                    { Messages.getString( "BrowserContentProvider.FetchingSearchResults" ) }; //$NON-NLS-1$
            }
            else if ( objects instanceof ISearchResult[] )
            {
                ISearchResult[] srs = ( ISearchResult[] ) objects;
                return srs;
            }
            else
            {
                return objects;
            }
        }
        else if ( parent instanceof ISearch )
        {
            ISearch search = ( ISearch ) parent;
            if ( search instanceof IContinuation )
            {
                IContinuation continuation = ( IContinuation ) search;
                if ( continuation.getState() == State.UNRESOLVED )
                {
                    continuation.resolve();
                }
                if ( continuation.getState() == State.CANCELED )
                {
                    return new Object[0];
                }
            }

            if ( search.getSearchResults() == null || search.getSearchContinuations() == null )
            {
                new StudioBrowserJob( new SearchRunnable( new ISearch[]
                    { search } ) ).execute();
                return new String[]
                    { Messages.getString( "BrowserContentProvider.PerformingSearch" ) }; //$NON-NLS-1$
            }
            else if ( search.getSearchResults().length + search.getSearchContinuations().length == 0 )
            {
                return new String[]
                    { Messages.getString( "BrowserContentProvider.NoResults" ) }; //$NON-NLS-1$
            }
            else if ( search.getSearchResults().length <= preferences.getFoldingSize() || !preferences.isUseFolding() )
            {
                if ( searchToSearchResultPagesMap.containsKey( search ) )
                {
                    searchToSearchResultPagesMap.remove( search );
                }

                ISearchResult[] results = search.getSearchResults();
                SearchContinuation[] scs = search.getSearchContinuations();
                List<Object> objects = new ArrayList<Object>();

                if ( search.getTopSearchRunnable() != null )
                {
                    objects.add( search.getTopSearchRunnable() );
                }

                objects.addAll( Arrays.asList( results ) );

                if ( scs != null )
                {
                    objects.addAll( Arrays.asList( scs ) );
                }

                if ( search.getNextSearchRunnable() != null )
                {
                    objects.add( search.getNextSearchRunnable() );
                }

                return objects.toArray();
            }
            else
            {
                BrowserSearchResultPage[] srPages = getSearchResultPages( search );
                return srPages;
            }
        }
        else if ( parent instanceof BrowserCategory )
        {
            BrowserCategory category = ( BrowserCategory ) parent;
            IBrowserConnection browserConnection = category.getParent();

            switch ( category.getType() )
            {
                case BrowserCategory.TYPE_DIT:
                {
                    // open connection when expanding DIT
                    if ( browserConnection.getConnection() != null
                        && !browserConnection.getConnection().getConnectionWrapper().isConnected() )
                    {
                        new StudioBrowserJob( new OpenConnectionsRunnable( browserConnection.getConnection() ) )
                            .execute();
                        return new String[]
                            { Messages.getString( "BrowserContentProvider.OpeningConnection" ) }; //$NON-NLS-1$
                    }

                    return new Object[]
                        { browserConnection.getRootDSE() };
                }

                case BrowserCategory.TYPE_SEARCHES:
                {
                    return browserConnection.getSearchManager().getSearches().toArray();
                }

                case BrowserCategory.TYPE_BOOKMARKS:
                {
                    return browserConnection.getBookmarkManager().getBookmarks();
                }
            }

            return new Object[0];
        }
        else
        {
            return new Object[0];
        }
    }


    /**
     * {@inheritDoc}
     */
    public boolean hasChildren( Object parent )
    {
        if ( parent instanceof IEntry )
        {
            IEntry parentEntry = ( IEntry ) parent;
            return parentEntry.hasChildren() || getQuickSearchForEntry( parentEntry ) != null;
        }
        else if ( parent instanceof SearchContinuation )
        {
            return true;
        }
        else if ( parent instanceof BrowserEntryPage )
        {
            return true;
        }
        else if ( parent instanceof BrowserSearchResultPage )
        {
            return true;
        }
        else if ( parent instanceof ISearchResult )
        {
            return false;
        }
        else if ( parent instanceof ISearch )
        {
            return true;
        }
        else if ( parent instanceof BrowserCategory )
        {
            return true;
        }
        else
        {
            return false;
        }
    }


    private IQuickSearch getQuickSearchForEntry( IEntry parentEntry )
    {
        IQuickSearch quickSearch = parentEntry.getBrowserConnection().getQuickSearch();
        if ( quickSearch != null
            && parentEntry.getDn().equals( quickSearch.getSearchBase() ) )
        {
            return quickSearch;
        }

        return null;
    }


    private BrowserEntryPage[] getEntryPages( final IEntry parentEntry )
    {
        BrowserEntryPage[] entryPages;
        if ( !entryToEntryPagesMap.containsKey( parentEntry ) )
        {
            entryPages = getEntryPages( parentEntry, 0, parentEntry.getChildrenCount() - 1 );
            entryToEntryPagesMap.put( parentEntry, entryPages );
        }
        else
        {
            entryPages = entryToEntryPagesMap.get( parentEntry );
            if ( parentEntry.getChildrenCount() - 1 != entryPages[entryPages.length - 1].getLast() )
            {
                entryPages = getEntryPages( parentEntry, 0, parentEntry.getChildrenCount() - 1 );
                entryToEntryPagesMap.put( parentEntry, entryPages );
            }
        }
        return entryPages;
    }


    /**
     * Creates and returns the entry pages for the given entry. The number of pages
     * depends on the number of entries and the paging size. 
     *
     * @param entry the parent entry
     * @param first the index of the first child entry
     * @param last the index of the last child entry
     * @return the created entry pages
     */
    private BrowserEntryPage[] getEntryPages( IEntry entry, int first, int last )
    {
        int pagingSize = preferences.getFoldingSize();

        int diff = last - first;
        int factor = diff > 0 ? ( int ) ( Math.log( diff ) / Math.log( pagingSize ) ) : 0;

        int groupFirst = first;
        int groupLast = first;
        BrowserEntryPage[] pages = new BrowserEntryPage[( int ) ( diff / Math.pow( pagingSize, factor ) ) + 1];
        for ( int i = 0; i < pages.length; i++ )
        {
            groupFirst = ( int ) ( i * Math.pow( pagingSize, factor ) ) + first;
            groupLast = ( int ) ( ( i + 1 ) * Math.pow( pagingSize, factor ) ) + first - 1;
            groupLast = groupLast > last ? last : groupLast;
            BrowserEntryPage[] subpages = ( factor > 1 ) ? getEntryPages( entry, groupFirst, groupLast ) : null;
            pages[i] = new BrowserEntryPage( entry, groupFirst, groupLast, subpages, sorter );
        }

        return pages;
    }


    private BrowserSearchResultPage[] getSearchResultPages( ISearch search )
    {
        BrowserSearchResultPage[] srPages;
        if ( !searchToSearchResultPagesMap.containsKey( search ) )
        {
            srPages = getSearchResultPages( search, 0, search.getSearchResults().length - 1 );
            searchToSearchResultPagesMap.put( search, srPages );
        }
        else
        {
            srPages = searchToSearchResultPagesMap.get( search );
            if ( search.getSearchResults().length - 1 != srPages[srPages.length - 1].getLast() )
            {
                srPages = getSearchResultPages( search, 0, search.getSearchResults().length - 1 );
                searchToSearchResultPagesMap.put( search, srPages );
            }
        }
        return srPages;
    }


    /**
     * Creates and returns the search result pages for the given search. The number of pages
     * depends on the number of search results and the paging size. 
     *
     * @param search the parent search
     * @param first the index of the first search result
     * @param last the index of the last child search result
     * @return the created search result pages
     */
    private BrowserSearchResultPage[] getSearchResultPages( ISearch search, int first, int last )
    {
        int pagingSize = preferences.getFoldingSize();

        int diff = last - first;
        int factor = diff > 0 ? ( int ) ( Math.log( diff ) / Math.log( pagingSize ) ) : 0;

        int groupFirst = first;
        int groupLast = first;
        BrowserSearchResultPage[] pages = new BrowserSearchResultPage[( int ) ( diff / Math.pow( pagingSize, factor ) ) + 1];
        for ( int i = 0; i < pages.length; i++ )
        {
            groupFirst = ( int ) ( i * Math.pow( pagingSize, factor ) ) + first;
            groupLast = ( int ) ( ( i + 1 ) * Math.pow( pagingSize, factor ) ) + first - 1;
            groupLast = groupLast > last ? last : groupLast;
            BrowserSearchResultPage[] subpages = ( factor > 1 ) ? getSearchResultPages( search, groupFirst, groupLast )
                : null;
            pages[i] = new BrowserSearchResultPage( search, groupFirst, groupLast, subpages, sorter );
        }

        return pages;
    }
}
