/*
 *  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.ui.editors.entry;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.directory.studio.ldapbrowser.common.BrowserCommonActivator;
import org.apache.directory.studio.ldapbrowser.common.BrowserCommonConstants;
import org.apache.directory.studio.ldapbrowser.common.widgets.browser.BrowserLabelProvider;
import org.apache.directory.studio.ldapbrowser.common.widgets.entryeditor.EntryEditorWidget;
import org.apache.directory.studio.ldapbrowser.core.model.IAttribute;
import org.apache.directory.studio.ldapbrowser.core.model.IEntry;
import org.apache.directory.studio.ldapbrowser.core.model.IValue;
import org.apache.directory.studio.ldapbrowser.core.utils.Utils;
import org.apache.directory.studio.ldifeditor.LdifEditorActivator;
import org.apache.directory.studio.ldifeditor.LdifEditorConstants;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;


/**
 * This class implements the Outline Page for the Entry Editor. 
 * It used to display LDAP entries.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class EntryEditorOutlinePage extends ContentOutlinePage
{
    /** The editor it is attached to */
    private EntryEditor entryEditor;

    /** This listener updates the viewer if an property (e.g. is operational attributes visible) has been changed */
    protected IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener()
    {
        public void propertyChange( PropertyChangeEvent event )
        {
            refresh();
        }
    };

    private Composite noOutlineComposite;

    private Composite composite;

    private Composite fakeComposite;


    /**
     * Creates a new instance of EntryEditorOutlinePage.
     *
     * @param entryEditor
     *      the editor the Outline page is attached to
     */
    public EntryEditorOutlinePage( EntryEditor entryEditor )
    {
        this.entryEditor = entryEditor;
        BrowserCommonActivator.getDefault().getPreferenceStore().addPropertyChangeListener( propertyChangeListener );
    }


    public Control getControl()
    {
        return composite;
    }


    /**
     * {@inheritDoc}
     */
    public void createControl( Composite parent )
    {
        // Creating the composite and fake composite
        this.composite = new Composite( parent, SWT.NONE );
        composite.setLayout( new FillLayout() );
        this.fakeComposite = new Composite( parent, SWT.NONE );

        // Creating the No Outline composite
        noOutlineComposite = new Composite( composite, SWT.NONE );
        noOutlineComposite.setLayout( new FillLayout() );

        Label label = new Label( noOutlineComposite, SWT.WRAP );
        label.setText( Messages.getString( "EntryEditorOutlinePage.NoOutline" ) ); //$NON-NLS-1$

        // Creating the Outline tree viewer
        super.createControl( parent );

        final TreeViewer treeViewer = getTreeViewer();
        treeViewer.setLabelProvider( new EntryEditorOutlineLabelProvider() );
        treeViewer.setContentProvider( new EntryEditorOutlineContentProvider() );

        treeViewer.addSelectionChangedListener( new ISelectionChangedListener()
        {
            public void selectionChanged( SelectionChangedEvent event )
            {
                if ( !event.getSelection().isEmpty() && event.getSelection() instanceof IStructuredSelection )
                {
                    if ( getEntryEditorWidgetTreeViewerInput() != null )
                    {
                        List<Object> selectionList = new ArrayList<Object>();

                        for ( Object element : ( ( IStructuredSelection ) event.getSelection() ).toArray() )
                        {
                            if ( element instanceof IValue )
                            {
                                // select the value
                                IValue value = ( IValue ) element;
                                selectionList.add( value );
                            }
                            else if ( element instanceof IAttribute )
                            {
                                // select attribute and all values
                                IAttribute attribute = ( IAttribute ) element;
                                selectionList.add( attribute );
                                selectionList.addAll( Arrays.asList( attribute.getValues() ) );
                            }
                            else if ( element instanceof EntryWrapper )
                            {
                                // select all attributes and values
                                IEntry entry = ( ( EntryWrapper ) element ).entry;
                                for ( IAttribute attribute : entry.getAttributes() )
                                {
                                    selectionList.add( attribute );
                                    selectionList.addAll( Arrays.asList( attribute.getValues() ) );
                                }
                            }
                        }

                        IStructuredSelection selection = new StructuredSelection( selectionList );
                        TreeViewer entryEditorWidgetTreeViewer = getEntryEditorWidgetTreeViewer();
                        if ( entryEditorWidgetTreeViewer != null )
                        {
                            entryEditorWidgetTreeViewer.setSelection( selection );
                        }
                    }
                }
            }
        } );

        treeViewer.addDoubleClickListener( new IDoubleClickListener()
        {
            public void doubleClick( DoubleClickEvent event )
            {
                if ( event.getSelection() instanceof IStructuredSelection )
                {
                    Object obj = ( ( IStructuredSelection ) event.getSelection() ).getFirstElement();
                    if ( treeViewer.getExpandedState( obj ) )
                    {
                        treeViewer.collapseToLevel( obj, 1 );
                    }
                    else if ( ( ( ITreeContentProvider ) treeViewer.getContentProvider() ).hasChildren( obj ) )
                    {
                        treeViewer.expandToLevel( obj, 1 );
                    }
                }
            }
        } );

        this.refresh();
    }


    /**
     * Refreshes this viewer starting with the given element.
     *
     * @param element
     *      the element
     */
    public void refresh( Object element )
    {
        final TreeViewer treeViewer = getTreeViewer();
        if ( treeViewer != null && treeViewer.getTree() != null && !treeViewer.getTree().isDisposed() )
        {
            TreeViewer entryEditorWidgetTreeViewer = getEntryEditorWidgetTreeViewer();
            if ( entryEditorWidgetTreeViewer != null )
            {
                treeViewer.setFilters( entryEditorWidgetTreeViewer.getFilters() );
            }
            treeViewer.refresh( element );
        }
    }


    /**
     * Refreshes this viewer completely with information freshly obtained from this viewer's model.
     */
    public void refresh()
    {
        TreeViewer treeViewer = getTreeViewer();

        if ( treeViewer != null && composite != null )
        {
            if ( hasAnOutline() )
            {
                Control treeViewerControl = treeViewer.getControl();

                if ( ( treeViewerControl != null ) && ( !treeViewerControl.isDisposed() ) )
                {
                    treeViewerControl.setParent( composite );
                }

                noOutlineComposite.setParent( fakeComposite );
            }
            else
            {
                Control treeViewerControl = treeViewer.getControl();

                if ( ( treeViewerControl != null ) && ( !treeViewerControl.isDisposed() ) )
                {
                    treeViewerControl.setParent( fakeComposite );
                }

                noOutlineComposite.setParent( composite );
            }

            composite.layout();

            if ( treeViewer.getTree() != null && !treeViewer.getTree().isDisposed() )
            {
                TreeViewer entryEditorWidgetTreeViewer = getEntryEditorWidgetTreeViewer();
                if ( entryEditorWidgetTreeViewer != null )
                {
                    treeViewer.setFilters( entryEditorWidgetTreeViewer.getFilters() );
                }
                if ( !treeViewer.getTree().isEnabled() )
                {
                    treeViewer.getTree().setEnabled( true );
                }

                IEntry entry = getEntryEditorWidgetTreeViewerInput();
                if ( entry == null )
                {
                    treeViewer.setInput( null );
                    treeViewer.getTree().setEnabled( false );
                }
                else
                {
                    treeViewer.setInput( entry );
                    treeViewer.expandToLevel( 2 );
                }

                treeViewer.refresh();
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    public void dispose()
    {
        super.dispose();
        if ( entryEditor != null )
        {
            BrowserCommonActivator.getDefault().getPreferenceStore().removePropertyChangeListener(
                propertyChangeListener );
            entryEditor = null;
        }
    }

    /**
     * This class implements the ContentProvider used for the Entry Editor Outline View.
     *
     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
     */
    private static class EntryEditorOutlineContentProvider implements ITreeContentProvider
    {
        /**
         * {@inheritDoc}
         */
        public Object[] getChildren( Object element )
        {
            // entry -> entry wrapper
            // the entry is the input and is not visible, 
            // so we use an wrapper around to make it visible as root element 
            if ( element instanceof IEntry )
            {
                IEntry entry = ( IEntry ) element;
                return new EntryWrapper[]
                    { new EntryWrapper( entry ) };
            }

            // entry wrapper -> attribute
            if ( element instanceof EntryWrapper )
            {
                EntryWrapper entryWrapper = ( EntryWrapper ) element;
                return entryWrapper.entry.getAttributes();
            }

            // attribute -> values
            else if ( element instanceof IAttribute )
            {
                IAttribute attribute = ( IAttribute ) element;
                return attribute.getValues();
            }

            else
            {
                return new Object[0];
            }
        }


        /**
         * {@inheritDoc}
         */
        public Object getParent( Object element )
        {
            return null;
        }


        /**
         * {@inheritDoc}
         */
        public boolean hasChildren( Object element )
        {
            return getChildren( element ) != null && getChildren( element ).length > 0;
        }


        /**
         * {@inheritDoc}
         */
        public Object[] getElements( Object inputElement )
        {
            return getChildren( inputElement );
        }


        /**
         * {@inheritDoc}
         */
        public void dispose()
        {
        }


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

    /**
     * This class implements the LabelProvider used for the Entry Editor Outline View
     *
     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
     */
    private static class EntryEditorOutlineLabelProvider extends LabelProvider
    {
        /**
         * {@inheritDoc}
         */
        public String getText( Object element )
        {
            // Entry
            if ( element instanceof EntryWrapper )
            {
                IEntry entry = ( ( EntryWrapper ) element ).entry;

                // Checking the Root DSE
                if ( entry.getDn() != null && "".equals( entry.getDn().toString() ) ) //$NON-NLS-1$
                {
                    // Root DSE
                    return "Root DSE"; //$NON-NLS-1$
                }
                else
                {
                    // Any other case
                    return entry.getDn().getName();
                }
            }

            // Attribute
            else if ( element instanceof IAttribute )
            {
                IAttribute attribute = ( IAttribute ) element;
                return attribute.getDescription() + " (" + attribute.getValueSize() + ")"; //$NON-NLS-1$  //$NON-NLS-2$
            }

            // Value
            else if ( element instanceof IValue )
            {
                IValue value = ( IValue ) element;
                return Utils.getShortenedString( value.getStringValue(), 20 );
            }

            else
            {
                return ""; //$NON-NLS-1$
            }
        }


        /**
         * {@inheritDoc}
         */
        public Image getImage( Object element )
        {
            // Entry
            if ( element instanceof EntryWrapper )
            {
                IEntry entry = ( ( EntryWrapper ) element ).entry;

                // Checking the Root DSE
                if ( entry.getDn() != null && "".equals( entry.getDn().toString() ) ) //$NON-NLS-1$
                {
                    // Root DSE
                    return BrowserCommonActivator.getDefault().getImage( BrowserCommonConstants.IMG_ENTRY_ROOT );
                }
                else
                {
                    // Any other case
                    return BrowserLabelProvider.getImageByObjectClass( entry );
                }
            }

            // Attribute
            else if ( element instanceof IAttribute )
            {
                return LdifEditorActivator.getDefault().getImage( LdifEditorConstants.IMG_LDIF_ATTRIBUTE );
            }

            // Value
            else if ( element instanceof IValue )
            {
                return LdifEditorActivator.getDefault().getImage( LdifEditorConstants.IMG_LDIF_VALUE );
            }

            else
            {
                return null;
            }
        }
    }

    /**
     * Wrapper around an entry.
     *
     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
     */
    private static class EntryWrapper
    {
        IEntry entry;


        public EntryWrapper( IEntry entry )
        {
            super();
            this.entry = entry;
        }


        @Override
        public int hashCode()
        {
            final int prime = 31;
            int result = 1;
            result = prime * result + ( ( entry == null ) ? 0 : entry.hashCode() );
            return result;
        }


        @Override
        public boolean equals( Object obj )
        {
            if ( this == obj )
                return true;
            if ( obj == null )
                return false;
            if ( getClass() != obj.getClass() )
                return false;
            EntryWrapper other = ( EntryWrapper ) obj;
            if ( entry == null )
            {
                if ( other.entry != null )
                    return false;
            }
            else if ( !entry.equals( other.entry ) )
                return false;
            return true;
        }
    }


    /**
     * Indicates if the entry has an outline.
     *
     * @return <code>true</code> if the entry editor has an outline,
     *         <code>false</code> if not.
     */
    private boolean hasAnOutline()
    {
        return getEntryEditorWidgetTreeViewerInput() != null;
    }


    /**
     * Gets the entryEditor.getMainWidget().getViewer() or null.
     */
    private TreeViewer getEntryEditorWidgetTreeViewer()
    {
        if ( entryEditor != null )
        {
            EntryEditorWidget mainWidget = entryEditor.getMainWidget();
            if ( mainWidget != null )
            {
                TreeViewer viewer = mainWidget.getViewer();
                return viewer;
            }
        }
        return null;
    }


    /**
     * Gets the entryEditor.getMainWidget().getViewer().getInput() if input is an IEntry or null.
     */
    private IEntry getEntryEditorWidgetTreeViewerInput()
    {
        TreeViewer viewer = getEntryEditorWidgetTreeViewer();
        if ( viewer != null )
        {
            Object o = viewer.getInput();
            if (  o instanceof IEntry )
            {
                return ( IEntry ) o;
            }
        }
        return null;
    }
}
