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


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.event.ConnectionEventRegistry;
import org.apache.directory.studio.connection.core.event.ConnectionUpdateAdapter;
import org.apache.directory.studio.connection.core.event.ConnectionUpdateListener;
import org.apache.directory.studio.connection.ui.ConnectionUIPlugin;
import org.apache.directory.studio.connection.ui.RunnableContextRunner;
import org.apache.directory.studio.ldapbrowser.common.BrowserCommonActivator;
import org.apache.directory.studio.ldapbrowser.core.events.EntryModificationEvent;
import org.apache.directory.studio.ldapbrowser.core.events.EntryUpdateListener;
import org.apache.directory.studio.ldapbrowser.core.events.EventRegistry;
import org.apache.directory.studio.ldapbrowser.core.events.ValueAddedEvent;
import org.apache.directory.studio.ldapbrowser.core.events.ValueDeletedEvent;
import org.apache.directory.studio.ldapbrowser.core.events.ValueModifiedEvent;
import org.apache.directory.studio.ldapbrowser.core.events.ValueMultiModificationEvent;
import org.apache.directory.studio.ldapbrowser.core.events.ValueRenamedEvent;
import org.apache.directory.studio.ldapbrowser.core.jobs.StudioBrowserJob;
import org.apache.directory.studio.ldapbrowser.core.jobs.UpdateEntryRunnable;
import org.apache.directory.studio.ldapbrowser.core.model.IAttribute;
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.IEntry;
import org.apache.directory.studio.ldapbrowser.core.model.ISearchResult;
import org.apache.directory.studio.ldapbrowser.core.model.IValue;
import org.apache.directory.studio.ldapbrowser.core.utils.CompoundModification;
import org.apache.directory.studio.ldapbrowser.core.utils.Utils;
import org.apache.directory.studio.ldapbrowser.ui.BrowserUIConstants;
import org.apache.directory.studio.ldapbrowser.ui.BrowserUIPlugin;
import org.apache.directory.studio.ldifparser.LdifFormatParameters;
import org.apache.directory.studio.ldifparser.model.LdifFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.plugin.AbstractUIPlugin;


/**
 * A EntryEditorManager is used to manage entry editors. It provides methods to get
 * the best or alternative entry editors for a given entry.
 * 
 * The available value editors are specified by the extension point
 * <code>org.apache.directory.studio.entryeditors</code>. 
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class EntryEditorManager
{
    private static final String ID_ATTR = "id"; //$NON-NLS-1$
    private static final String NAME_ATTR = "name"; //$NON-NLS-1$
    private static final String DESCRIPTION_ATTR = "description"; //$NON-NLS-1$
    private static final String ICON_ATTR = "icon"; //$NON-NLS-1$
    private static final String CLASS_ATTR = "class"; //$NON-NLS-1$
    private static final String EDITOR_ID_ATTR = "editorId"; //$NON-NLS-1$
    private static final String MULTI_WINDOW_ATTR = "multiWindow"; //$NON-NLS-1$
    private static final String PRIORITY_ATTR = "priority"; //$NON-NLS-1$

    /** The priorities separator */
    public static final String PRIORITIES_SEPARATOR = ","; //$NON-NLS-1$

    /** The list of entry editors */
    private Map<String, EntryEditorExtension> entryEditorExtensions = new HashMap<>();

    /** The shared reference copies for open-save-close editors; original entry -> reference copy */
    private Map<IEntry, IEntry> oscSharedReferenceCopies = new HashMap<>();

    /** The shared working copies for open-save-close editors; original entry -> working copy */
    private Map<IEntry, IEntry> oscSharedWorkingCopies = new HashMap<>();

    /** The shared reference copies for auto-save editors; original entry -> reference copy */
    private Map<IEntry, IEntry> autoSaveSharedReferenceCopies = new HashMap<>();

    /** The shared working copies for auto-save editors; original entry -> working copy */
    private Map<IEntry, IEntry> autoSaveSharedWorkingCopies = new HashMap<>();

    /** The comparator for entry editors */
    private Comparator<EntryEditorExtension> entryEditorComparator = new Comparator<EntryEditorExtension>()
    {
        @Override
        public int compare( EntryEditorExtension o1, EntryEditorExtension o2 )
        {
            if ( o1 == null )
            {
                return ( o2 == null ) ? 0 : -1;
            }

            if ( o2 == null )
            {
                return 1;
            }

            // Getting priorities
            int o1Priority = o1.getPriority();
            int o2Priority = o2.getPriority();

            if ( o1Priority != o2Priority )
            {
                return ( o1Priority > o2Priority ) ? -1 : 1;
            }

            // Getting names
            String o1Name = o1.getName();
            String o2Name = o2.getName();

            if ( o1Name == null )
            {
                return ( o2Name == null ) ? 0 : -1;
            }

            return o1Name.compareTo( o2Name );
        }
    };
    

    /** The listener for workbench part update */
    private IPartListener2 partListener = new IPartListener2()
    {
        @Override
        public void partActivated( IWorkbenchPartReference partRef )
        {
            cleanupCopies( partRef.getPage() );

            IEntryEditor editor = getEntryEditor( partRef );
            
            if ( editor != null )
            {
                EntryEditorInput eei = editor.getEntryEditorInput();
                IEntry originalEntry = eei.getResolvedEntry();
                IEntry oscSharedReferenceCopy = oscSharedReferenceCopies.get( originalEntry );
                IEntry oscSharedWorkingCopy = oscSharedWorkingCopies.get( originalEntry );
                
                if ( editor.isAutoSave() )
                {
                    // check if the same entry is used in an OSC editor and is dirty -> should save first?
                    if ( oscSharedReferenceCopy != null && oscSharedWorkingCopy != null )
                    {
                        LdifFile diff = Utils.computeDiff( oscSharedReferenceCopy, oscSharedWorkingCopy );
                        
                        if ( diff != null )
                        {
                            MessageDialog dialog = new MessageDialog( partRef.getPart( false ).getSite().getShell(),
                                Messages.getString( "EntryEditorManager.SaveChanges" ), null,//$NON-NLS-1$ 
                                Messages.getString( "EntryEditorManager.SaveChangesDescription" ), //$NON-NLS-1$ 
                                MessageDialog.QUESTION, new String[]
                                    { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 0 );
                            int result = dialog.open();
                            
                            if ( result == 0 )
                            {
                                saveSharedWorkingCopy( originalEntry, true, null );
                            }
                        }
                    }
                }
                else
                {
                    // check if original entry was updated
                    if ( ( oscSharedReferenceCopy != null ) && ( oscSharedWorkingCopy != null ) )
                    {
                        LdifFile refDiff = Utils.computeDiff( originalEntry, oscSharedReferenceCopy );
                        
                        if ( refDiff != null )
                        {
                            // check if we could just update the working copy
                            LdifFile workDiff = Utils.computeDiff( oscSharedReferenceCopy, oscSharedWorkingCopy );
                            
                            if ( workDiff != null )
                            {
                                askUpdateSharedWorkingCopy( partRef, originalEntry, oscSharedWorkingCopy, null );
                            }
                        }
                    }
                }
            }
        }


        @Override
        public void partOpened( IWorkbenchPartReference partRef )
        {
        }


        @Override
        public void partClosed( IWorkbenchPartReference partRef )
        {
            cleanupCopies( partRef.getPage() );
        }


        @Override
        public void partInputChanged( IWorkbenchPartReference partRef )
        {
            cleanupCopies( partRef.getPage() );
        }


        @Override
        public void partHidden( IWorkbenchPartReference partRef )
        {
        }


        @Override
        public void partDeactivated( IWorkbenchPartReference partRef )
        {
        }


        @Override
        public void partBroughtToTop( IWorkbenchPartReference partRef )
        {
        }


        @Override
        public void partVisible( IWorkbenchPartReference partRef )
        {
        }
    };

    /** The listener for entry update */
    private EntryUpdateListener entryUpdateListener = new EntryUpdateListener()
    {
        @Override
        public void entryUpdated( EntryModificationEvent event )
        {
            IEntry modifiedEntry = event.getModifiedEntry();
            IBrowserConnection browserConnection = modifiedEntry.getBrowserConnection();
            IEntry originalEntry = browserConnection.getEntryFromCache( modifiedEntry.getDn() );

            if ( modifiedEntry == originalEntry )
            {
                // an original entry has been modified, check if we could update the editors

                // if the OSC editor is not dirty we could update the working copy
                IEntry oscSharedReferenceCopy = oscSharedReferenceCopies.get( originalEntry );
                IEntry oscSharedWorkingCopy = oscSharedWorkingCopies.get( originalEntry );
                
                if ( ( oscSharedReferenceCopy != null ) && ( oscSharedWorkingCopy != null ) )
                {
                    LdifFile refDiff = Utils.computeDiff( originalEntry, oscSharedReferenceCopy );
                    
                    if ( refDiff != null )
                    {
                        // diff between original entry and reference copy
                        LdifFile workDiff = Utils.computeDiff( oscSharedReferenceCopy, oscSharedWorkingCopy );
                        
                        if ( workDiff == null )
                        {
                            // no changes on working copy, update
                            updateOscSharedReferenceCopy( originalEntry );
                            updateOscSharedWorkingCopy( originalEntry );

                            // inform all OSC editors
                            List<IEntryEditor> oscEditors = getOscEditors( oscSharedWorkingCopy );
                            
                            for ( IEntryEditor editor : oscEditors )
                            {
                                editor.workingCopyModified( event.getSource() );
                            }
                        }
                        else
                        {
                            // changes on working copy, ask before update
                            IWorkbenchPartReference reference = getActivePartRef( getOscEditors( oscSharedWorkingCopy ) );
                            
                            if ( reference != null )
                            {
                                askUpdateSharedWorkingCopy( reference, originalEntry, oscSharedWorkingCopy, event
                                    .getSource() );
                            }
                        }
                    }
                    else
                    {
                        // no diff betweeen original entry and reference copy, check if editor is dirty
                        LdifFile workDiff = Utils.computeDiff( oscSharedReferenceCopy, oscSharedWorkingCopy );
                        
                        if ( workDiff != null )
                        {
                            // changes on working copy, ask before update
                            IWorkbenchPartReference reference = getActivePartRef( getOscEditors( oscSharedWorkingCopy ) );
                            if ( reference != null )
                            {
                                askUpdateSharedWorkingCopy( reference, originalEntry, oscSharedWorkingCopy, event
                                    .getSource() );
                            }
                        }
                    }
                }

                // always update auto-save working copies, if necessary
                IEntry autoSaveSharedReferenceCopy = autoSaveSharedReferenceCopies.get( originalEntry );
                IEntry autoSaveSharedWorkingCopy = autoSaveSharedWorkingCopies.get( originalEntry );
                
                if ( ( autoSaveSharedReferenceCopy != null ) && ( autoSaveSharedWorkingCopy != null ) )
                {
                    LdifFile diff = Utils.computeDiff( originalEntry, autoSaveSharedReferenceCopy );
                    
                    if ( diff != null )
                    {
                        updateAutoSaveSharedReferenceCopy( originalEntry );
                        updateAutoSaveSharedWorkingCopy( originalEntry );
                        List<IEntryEditor> editors = getAutoSaveEditors( autoSaveSharedWorkingCopy );
                        
                        for ( IEntryEditor editor : editors )
                        {
                            editor.workingCopyModified( event.getSource() );
                        }
                    }
                }

                // check all editors: if the input does not exist any more then close the editor
                IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
                // Collecting editor references to close
                List<IEditorReference> editorReferences = new ArrayList<>();
                
                for ( IEditorReference ref : activePage.getEditorReferences() )
                {
                    IEntryEditor editor = getEntryEditor( ref );
                    if ( editor != null )
                    {
                        EntryEditorInput entryEditorInput = editor.getEntryEditorInput();

                        if ( entryEditorInput != null )
                        {
                            IEntry resolvedEntry = entryEditorInput.getResolvedEntry();

                            if ( ( editor != null ) && ( resolvedEntry != null ) )
                            {
                                IBrowserConnection bc = resolvedEntry.getBrowserConnection();
                                Dn dn = resolvedEntry.getDn();

                                if ( bc.getEntryFromCache( dn ) == null )
                                {
                                    editorReferences.add( ref );
                                }
                            }
                        }
                    }
                }

                // Closing the corresponding editor references
                if ( !editorReferences.isEmpty() )
                {
                    activePage.closeEditors( editorReferences.toArray( new IEditorReference[0] ), false );
                }
            }

            else if ( oscSharedWorkingCopies.containsKey( originalEntry )
                && ( oscSharedWorkingCopies.get( originalEntry ) == modifiedEntry ) )
            {
                // OSC working copy has been modified: inform OSC editors
                IEntry oscSharedWorkingCopy = oscSharedWorkingCopies.get( originalEntry );
                List<IEntryEditor> oscEditors = getOscEditors( oscSharedWorkingCopy );
                
                for ( IEntryEditor editor : oscEditors )
                {
                    editor.workingCopyModified( event.getSource() );
                }
            }

            else if ( autoSaveSharedWorkingCopies.containsValue( originalEntry )
                && ( autoSaveSharedWorkingCopies.get( originalEntry ) == modifiedEntry ) )
            {
                // auto-save working copy has been modified: save and inform all auto-save editors
                IEntry autoSaveSharedReferenceCopy = autoSaveSharedReferenceCopies.get( originalEntry );
                IEntry autoSaveSharedWorkingCopy = autoSaveSharedWorkingCopies.get( originalEntry );

                // sanity check: never save if event source is the EntryEditorManager
                if ( event.getSource() instanceof EntryEditorManager )
                {
                    return;
                }

                // only save if we receive a real value modification event
                if ( !( ( event instanceof ValueAddedEvent ) || 
                        ( event instanceof ValueDeletedEvent ) || 
                        ( event instanceof ValueModifiedEvent ) || 
                        ( event instanceof ValueRenamedEvent ) || 
                        ( event instanceof ValueMultiModificationEvent ) ) )
                {
                    return;
                }

                // consistency check: don't save if there is an empty value, silently return in that case
                for ( IAttribute attribute : autoSaveSharedWorkingCopy.getAttributes() )
                {
                    for ( IValue value : attribute.getValues() )
                    {
                        if ( value.isEmpty() )
                        {
                            return;
                        }
                    }
                }

                LdifFile diff = Utils.computeDiff( autoSaveSharedReferenceCopy, autoSaveSharedWorkingCopy );
                
                if ( diff != null )
                {
                    // remove entry from map, reduces number of fired events
                    autoSaveSharedReferenceCopies.remove( originalEntry );
                    autoSaveSharedWorkingCopies.remove( originalEntry );
                    UpdateEntryRunnable runnable = new UpdateEntryRunnable( originalEntry, diff
                        .toFormattedString( LdifFormatParameters.DEFAULT ) );
                    RunnableContextRunner.execute( runnable, null, true );
                    // put entry back to map
                    autoSaveSharedReferenceCopies.put( originalEntry, autoSaveSharedReferenceCopy );
                    autoSaveSharedWorkingCopies.put( originalEntry, autoSaveSharedWorkingCopy );

                    // don't care if status is ok or not: always update
                    updateAutoSaveSharedReferenceCopy( originalEntry );
                    updateAutoSaveSharedWorkingCopy( originalEntry );
                    List<IEntryEditor> editors = getAutoSaveEditors( autoSaveSharedWorkingCopy );
                    
                    for ( IEntryEditor editor : editors )
                    {
                        editor.workingCopyModified( event.getSource() );
                    }
                }
            }
        }
    };

    /** The listener for connection update */
    private ConnectionUpdateListener connectionUpdateListener = new ConnectionUpdateAdapter()
    {
        @Override
        public void connectionClosed( Connection connection )
        {
            closeEditorsBelongingToConnection( connection );
        }


        @Override
        public void connectionRemoved( Connection connection )
        {
            closeEditorsBelongingToConnection( connection );
        }
    };


    /**
     * Creates a new instance of EntryEditorManager.
     */
    public EntryEditorManager()
    {
        if ( PlatformUI.getWorkbench().getActiveWorkbenchWindow() != null )
        {
            getEditorManager();
        }
    }

    
    /**
     * Get the EditorManager instance
     */
    public void getEditorManager()
    {
        initEntryEditorExtensions();
        PlatformUI.getWorkbench().getActiveWorkbenchWindow().getPartService().addPartListener( partListener );
        EventRegistry
            .addEntryUpdateListener( entryUpdateListener, BrowserCommonActivator.getDefault().getEventRunner() );
        ConnectionEventRegistry.addConnectionUpdateListener( connectionUpdateListener, ConnectionUIPlugin.getDefault()
            .getEventRunner() );
    }


    /**
     * Initializes the entry editors extensions.
     */
    private void initEntryEditorExtensions()
    {
        entryEditorExtensions = new HashMap<>();

        IExtensionRegistry registry = Platform.getExtensionRegistry();
        IExtensionPoint extensionPoint = registry.getExtensionPoint( BrowserUIConstants.ENTRY_EDITOR_EXTENSION_POINT );
        IConfigurationElement[] members = extensionPoint.getConfigurationElements();

        // For each extension:
        for ( IConfigurationElement member : members )
        {
            EntryEditorExtension bean = new EntryEditorExtension();

            IExtension extension = member.getDeclaringExtension();
            String extendingPluginId = extension.getNamespaceIdentifier();

            bean.setId( member.getAttribute( ID_ATTR ) );
            bean.setName( member.getAttribute( NAME_ATTR ) );
            bean.setDescription( member.getAttribute( DESCRIPTION_ATTR ) );
            String iconPath = member.getAttribute( ICON_ATTR );
            ImageDescriptor icon = AbstractUIPlugin.imageDescriptorFromPlugin( extendingPluginId, iconPath );
            
            if ( icon == null )
            {
                icon = ImageDescriptor.getMissingImageDescriptor();
            }
            
            bean.setIcon( icon );
            bean.setClassName( member.getAttribute( CLASS_ATTR ) );
            bean.setEditorId( member.getAttribute( EDITOR_ID_ATTR ) );
            bean.setMultiWindow( "true".equalsIgnoreCase( member.getAttribute( MULTI_WINDOW_ATTR ) ) ); //$NON-NLS-1$
            bean.setPriority( Integer.parseInt( member.getAttribute( PRIORITY_ATTR ) ) );

            try
            {
                bean.setEditorInstance( ( IEntryEditor ) member.createExecutableExtension( CLASS_ATTR ) );
            }
            catch ( CoreException e )
            {
                // Will never happen
            }

            entryEditorExtensions.put( bean.getId(), bean );
        }
    }

    public void dispose()
    {
        IWorkbenchWindow ww = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        
        if ( ww != null )
        {
            ww.getPartService().removePartListener( partListener );
            EventRegistry.removeEntryUpdateListener( entryUpdateListener );
        }
    }


    /**
     * Gets the entry editor extensions.
     * 
     * @return the entry editor extensions
     */
    public Collection<EntryEditorExtension> getEntryEditorExtensions()
    {
        return entryEditorExtensions.values();
    }


    /**
     * Gets the entry editor extension.
     * 
     * @param id the entry editor extension id
     * 
     * @return the entry editor extension, null if none found
     */
    public EntryEditorExtension getEntryEditorExtension( String id )
    {
        return entryEditorExtensions.get( id );
    }


    /**
     * Gets the sorted entry editor extensions.
     * 
     * @return
     *      the sorted entry editor extensions
     */
    public Collection<EntryEditorExtension> getSortedEntryEditorExtensions()
    {
        boolean useUserPriority = BrowserUIPlugin.getDefault().getPluginPreferences().getBoolean(
            BrowserUIConstants.PREFERENCE_ENTRYEDITORS_USE_USER_PRIORITIES );

        if ( useUserPriority )
        {
            return getEntryEditorExtensionsSortedByUserPriority();
        }
        else
        {
            return getEntryEditorExtensionsSortedByDefaultPriority();
        }
    }


    /**
     * Gets the entry editor extensions sorted by default priority.
     *
     * @return
     *      the entry editor extensions sorted by default priority
     */
    public Collection<EntryEditorExtension> getEntryEditorExtensionsSortedByDefaultPriority()
    {
        // Getting all entry editors
        Collection<EntryEditorExtension> entryEditorExtensions = getEntryEditorExtensions();

        // Creating the sorted entry editors list
        ArrayList<EntryEditorExtension> sortedEntryEditorsList = new ArrayList<>(
            entryEditorExtensions.size() );

        // Adding the remaining entry editors
        for ( EntryEditorExtension entryEditorExtension : entryEditorExtensions )
        {
            sortedEntryEditorsList.add( entryEditorExtension );
        }

        // Sorting the remaining entry editors based on their priority
        Collections.sort( sortedEntryEditorsList, entryEditorComparator );

        return sortedEntryEditorsList;
    }


    /**
     * Gets the entry editor extensions sorted by user's priority.
     *
     * @return
     *      the entry editor extensions sorted by user's priority
     */
    public Collection<EntryEditorExtension> getEntryEditorExtensionsSortedByUserPriority()
    {
        // Getting all entry editors
        Collection<EntryEditorExtension> entryEditorExtensions = BrowserUIPlugin.getDefault().getEntryEditorManager()
            .getEntryEditorExtensions();

        // Creating the sorted entry editors list
        Collection<EntryEditorExtension> sortedEntryEditorsList = new ArrayList<>(
            entryEditorExtensions.size() );

        // Getting the user's priorities
        String userPriorities = BrowserUIPlugin.getDefault().getPluginPreferences().getString(
            BrowserUIConstants.PREFERENCE_ENTRYEDITORS_USER_PRIORITIES );
        
        if ( ( userPriorities != null ) && ( !"".equals( userPriorities ) ) ) //$NON-NLS-1$
        {
            String[] splittedUserPriorities = userPriorities.split( PRIORITIES_SEPARATOR );
            
            if ( ( splittedUserPriorities != null ) && ( splittedUserPriorities.length > 0 ) )
            {

                // Creating a map where entry editors are accessible via their ID
                Map<String, EntryEditorExtension> entryEditorsMap = new HashMap<>();
                
                for ( EntryEditorExtension entryEditorExtension : entryEditorExtensions )
                {
                    entryEditorsMap.put( entryEditorExtension.getId(), entryEditorExtension );
                }

                // Adding the entry editors according to the user's priority
                for ( String entryEditorId : splittedUserPriorities )
                {
                    // Verifying the entry editor is present in the map
                    if ( entryEditorsMap.containsKey( entryEditorId ) )
                    {
                        // Adding it to the sorted list
                        sortedEntryEditorsList.add( entryEditorsMap.get( entryEditorId ) );
                    }
                }
            }

            // If some new plugins have been added recently, their new 
            // entry editors may not be present in the string stored in 
            // the preferences.
            // We are then adding them at the end of the sorted list.

            // Creating a list of remaining entry editors
            List<EntryEditorExtension> remainingEntryEditors = new ArrayList<>();
            
            for ( EntryEditorExtension entryEditorExtension : entryEditorExtensions )
            {
                // Verifying the entry editor is present in the sorted list
                if ( !sortedEntryEditorsList.contains( entryEditorExtension ) )
                {
                    // Adding it to the remaining list
                    remainingEntryEditors.add( entryEditorExtension );
                }
            }

            // Sorting the remaining entry editors based on their priority
            Collections.sort( remainingEntryEditors, entryEditorComparator );

            // Adding the remaining entry editors
            for ( EntryEditorExtension entryEditorExtension : remainingEntryEditors )
            {
                sortedEntryEditorsList.add( entryEditorExtension );
            }
        }

        return sortedEntryEditorsList;
    }


    /**
     * Closes the open editors belonging to the given connection.
     *
     * @param connection
     *      the connection
     */
    private void closeEditorsBelongingToConnection( Connection connection )
    {
        if ( connection != null )
        {
            IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();

            // Collecting editor references to close
            List<IEditorReference> editorReferences = new ArrayList<>();
            
            for ( IEditorReference ref : activePage.getEditorReferences() )
            {
                IEntryEditor editor = getEntryEditor( ref );
                
                if ( ( editor != null ) && ( editor.getEntryEditorInput().getResolvedEntry() != null ) )
                {
                    IBrowserConnection bc = editor.getEntryEditorInput().getResolvedEntry().getBrowserConnection();
                    
                    if ( connection.equals( bc.getConnection() ) )
                    {
                        editorReferences.add( ref );
                    }
                }
            }

            // Closing the corresponding editor references
            if ( !editorReferences.isEmpty() )
            {
                activePage.closeEditors( editorReferences.toArray( new IEditorReference[0] ), false );
            }
        }
    }


    /**
     * Opens an entry editor with the given entry editor extension and one of 
     * the given entries, search results or bookmarks.
     *
     * @param extension the entry editor extension
     * @param entries an array of entries
     * @param searchResults an array of search results
     * @param bookmarks an arrays of bookmarks
     */
    public void openEntryEditor( EntryEditorExtension extension, IEntry[] entries, ISearchResult[] searchResults,
        IBookmark[] bookmarks )
    {
        OpenEntryEditorRunnable runnable = new OpenEntryEditorRunnable( extension, entries, searchResults, bookmarks );
        StudioBrowserJob job = new StudioBrowserJob( runnable );
        job.setPriority( Job.INTERACTIVE ); // Highest priority (just in case)
        job.execute();
    }


    /**
     * Opens an entry editor with one of the given entries, search results or bookmarks.
     *
     * @param entries an array of entries
     * @param searchResults an array of search results
     * @param bookmarks an arrays of bookmarks
     */
    public void openEntryEditor( IEntry[] entries, ISearchResult[] searchResults, IBookmark[] bookmarks )
    {
        openEntryEditor( null, entries, searchResults, bookmarks );
    }


    private void updateOscSharedReferenceCopy( IEntry entry )
    {
        IEntry referenceCopy = oscSharedReferenceCopies.remove( entry );
        
        if ( referenceCopy != null )
        {
            EventRegistry.suspendEventFiringInCurrentThread();
            EntryEditorUtils.ensureAttributesInitialized( entry );
            new CompoundModification().replaceAttributes( entry, referenceCopy, this );
            EventRegistry.resumeEventFiringInCurrentThread();
            oscSharedReferenceCopies.put( entry, referenceCopy );
        }
    }


    private void updateOscSharedWorkingCopy( IEntry entry )
    {
        IEntry workingCopy = oscSharedWorkingCopies.get( entry );
        
        if ( workingCopy != null )
        {
            EntryEditorUtils.ensureAttributesInitialized( entry );
            new CompoundModification().replaceAttributes( entry, workingCopy, this );
        }
    }


    private void updateAutoSaveSharedReferenceCopy( IEntry entry )
    {
        EventRegistry.suspendEventFiringInCurrentThread();
        EntryEditorUtils.ensureAttributesInitialized( entry );
        IEntry workingCopy = autoSaveSharedReferenceCopies.get( entry );
        new CompoundModification().replaceAttributes( entry, workingCopy, this );
        EventRegistry.resumeEventFiringInCurrentThread();
    }


    private void updateAutoSaveSharedWorkingCopy( IEntry entry )
    {
        EntryEditorUtils.ensureAttributesInitialized( entry );
        IEntry workingCopy = autoSaveSharedWorkingCopies.get( entry );
        new CompoundModification().replaceAttributes( entry, workingCopy, this );
    }


    private List<IEntryEditor> getOscEditors( IEntry workingCopy )
    {
        List<IEntryEditor> oscEditors = new ArrayList<>();
        IEditorReference[] editorReferences = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
            .getEditorReferences();
        
        for ( IEditorReference ref : editorReferences )
        {
            IEntryEditor editor = getEntryEditor( ref );
            
            if ( ( editor != null ) && !editor.isAutoSave()
                && ((  workingCopy == null ) || ( editor.getEntryEditorInput().getSharedWorkingCopy( editor ) == workingCopy ) ) )
            {
                oscEditors.add( editor );
            }
        }
        
        return oscEditors;
    }


    private List<IEntryEditor> getAutoSaveEditors( IEntry workingCopy )
    {
        List<IEntryEditor> autoSaveEditors = new ArrayList<>();
        IEditorReference[] editorReferences = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
            .getEditorReferences();
        
        for ( IEditorReference ref : editorReferences )
        {
            IEntryEditor editor = getEntryEditor( ref );
            
            if ( ( editor != null ) && editor.isAutoSave()
                && ( editor.getEntryEditorInput().getSharedWorkingCopy( editor ) == workingCopy ) )
            {
                autoSaveEditors.add( editor );
            }
        }
        
        return autoSaveEditors;
    }


    private IEntryEditor getEntryEditor( IWorkbenchPartReference partRef )
    {
        IWorkbenchPart part = partRef.getPart( false );
        
        if ( part instanceof IEntryEditor )
        {
            return ( IEntryEditor ) part;
        }
        
        return null;
    }


    private IWorkbenchPartReference getActivePartRef( List<IEntryEditor> editors )
    {
        for ( IEntryEditor editor : editors )
        {
            IWorkbenchPart part = ( IWorkbenchPart ) editor;
            IEditorPart activeEditor = part.getSite().getPage().getActiveEditor();
            
            if ( part == activeEditor )
            {
                return part.getSite().getPage().getReference( part );
            }
        }
        
        return null;
    }


    IEntry getSharedWorkingCopy( IEntry originalEntry, IEntryEditor editor )
    {
        cleanupCopies( PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage() );

        EntryEditorUtils.ensureAttributesInitialized( originalEntry );
        
        if ( editor.isAutoSave() )
        {
            if ( !autoSaveSharedReferenceCopies.containsKey( originalEntry ) )
            {
                autoSaveSharedReferenceCopies
                    .put( originalEntry, new CompoundModification().cloneEntry( originalEntry ) );
            }
            
            if ( !autoSaveSharedWorkingCopies.containsKey( originalEntry ) )
            {
                IEntry referenceCopy = autoSaveSharedReferenceCopies.get( originalEntry );
                autoSaveSharedWorkingCopies.put( originalEntry, new CompoundModification().cloneEntry( referenceCopy ) );
            }
            
            return autoSaveSharedWorkingCopies.get( originalEntry );
        }
        else
        {
            if ( !oscSharedReferenceCopies.containsKey( originalEntry ) )
            {
                oscSharedReferenceCopies.put( originalEntry, new CompoundModification().cloneEntry( originalEntry ) );
            }
            
            if ( !oscSharedWorkingCopies.containsKey( originalEntry ) )
            {
                IEntry referenceCopy = oscSharedReferenceCopies.get( originalEntry );
                oscSharedWorkingCopies.put( originalEntry, new CompoundModification().cloneEntry( referenceCopy ) );
            }
            
            return oscSharedWorkingCopies.get( originalEntry );
        }
    }


    boolean isSharedWorkingCopyDirty( IEntry originalEntry, IEntryEditor editor )
    {
        if ( editor.isAutoSave() )
        {
            return false;
        }
        else
        {
            IEntry referenceCopy = oscSharedReferenceCopies.get( originalEntry );
            IEntry workingCopy = oscSharedWorkingCopies.get( originalEntry );
            
            if ( ( referenceCopy != null ) && ( workingCopy != null ) )
            {
                LdifFile diff = Utils.computeDiff( referenceCopy, workingCopy );
                return diff != null;
            }
            
            return false;
        }
    }


    IStatus saveSharedWorkingCopy( IEntry originalEntry, boolean handleError, IEntryEditor editor )
    {
        if ( ( editor == null ) || !editor.isAutoSave() )
        {
            IEntry referenceCopy = oscSharedReferenceCopies.get( originalEntry );
            IEntry workingCopy = oscSharedWorkingCopies.get( originalEntry );
            
            if ( ( referenceCopy != null ) && ( workingCopy != null ) )
            {
                // consistency check: don't save if there is an empty value, throw an exception as the user pressed 'save'
                for ( IAttribute attribute : workingCopy.getAttributes() )
                {
                    for ( IValue value : attribute.getValues() )
                    {
                        if ( value.isEmpty() )
                        {
                            throw new RuntimeException( NLS.bind( Messages
                                .getString( "EntryEditorManager.EmptyValueInAttribute" ), attribute.getDescription() ) ); //$NON-NLS-1$
                        }
                    }
                }

                LdifFile diff = Utils.computeDiff( referenceCopy, workingCopy );
                
                if ( diff != null )
                {
                    // remove entry from map, reduces number of fired events
                    oscSharedReferenceCopies.remove( originalEntry );
                    oscSharedWorkingCopies.remove( originalEntry );
                    // save by executing the LDIF
                    UpdateEntryRunnable runnable = new UpdateEntryRunnable( originalEntry, diff
                        .toFormattedString( LdifFormatParameters.DEFAULT ) );
                    IStatus status = RunnableContextRunner.execute( runnable, null, handleError );
                    // put entry back to map
                    oscSharedReferenceCopies.put( originalEntry, referenceCopy );
                    oscSharedWorkingCopies.put( originalEntry, workingCopy );
                    
                    if ( status.isOK() )
                    {
                        updateOscSharedReferenceCopy( originalEntry );
                        updateOscSharedWorkingCopy( originalEntry );
                    }
                    
                    return status;
                }
            }
        }
        
        return null;
    }


    void resetSharedWorkingCopy( IEntry originalEntry, IEntryEditor editor )
    {
        if ( ( editor == null ) || !editor.isAutoSave() )
        {
            IEntry referenceCopy = oscSharedReferenceCopies.get( originalEntry );
            IEntry workingCopy = oscSharedWorkingCopies.get( originalEntry );
            
            if ((  referenceCopy != null ) && ( workingCopy != null ) )
            {
                updateOscSharedReferenceCopy( originalEntry );
                updateOscSharedWorkingCopy( originalEntry );
            }
        }
    }


    private void askUpdateSharedWorkingCopy( IWorkbenchPartReference partRef, IEntry originalEntry,
        IEntry oscSharedWorkingCopy, Object source )
    {
        MessageDialog dialog = new MessageDialog( partRef.getPart( false ).getSite().getShell(), Messages
            .getString( "EntryEditorManager.EntryChanged" ), null, Messages //$NON-NLS-1$
            .getString( "EntryEditorManager.EntryChangedDescription" ), MessageDialog.QUESTION, new String[] //$NON-NLS-1$
            { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 0 );
        int result = dialog.open();
        
        if ( result == 0 )
        {
            // update reference copy and working copy
            updateOscSharedReferenceCopy( originalEntry );
            updateOscSharedWorkingCopy( originalEntry );

            // inform all OSC editors
            List<IEntryEditor> oscEditors = getOscEditors( oscSharedWorkingCopy );
            
            for ( IEntryEditor oscEditor : oscEditors )
            {
                oscEditor.workingCopyModified( source );
            }
        }
    }


    private void cleanupCopies( IWorkbenchPage page )
    {
        // cleanup unused copies (OSC + auto-save)
        Set<IEntry> oscEntries = new HashSet<>();
        Set<IEntry> autoSaveEntries = new HashSet<>();
        IEditorReference[] editorReferences = page.getEditorReferences();
        
        for ( IEditorReference ref : editorReferences )
        {
            IEntryEditor editor = getEntryEditor( ref );
            
            if ( editor != null )
            {
                EntryEditorInput input = editor.getEntryEditorInput();
                
                if ( ( input != null ) && ( input.getResolvedEntry() != null ) )
                {
                    IEntry entry = input.getResolvedEntry();
                    
                    if ( editor.isAutoSave() )
                    {
                        autoSaveEntries.add( entry );
                    }
                    else
                    {
                        oscEntries.add( entry );
                    }
                }
            }
        }
        
        for ( Iterator<IEntry> it = oscSharedReferenceCopies.keySet().iterator(); it.hasNext(); )
        {
            IEntry entry = it.next();
            
            if ( !oscEntries.contains( entry ) )
            {
                it.remove();
                oscSharedWorkingCopies.remove( entry );
            }
        }
        
        for ( Iterator<IEntry> it = oscSharedWorkingCopies.keySet().iterator(); it.hasNext(); )
        {
            IEntry entry = it.next();
            
            if ( !oscEntries.contains( entry ) )
            {
                it.remove();
            }
        }
        
        for ( Iterator<IEntry> it = autoSaveSharedReferenceCopies.keySet().iterator(); it.hasNext(); )
        {
            IEntry entry = it.next();
            
            if ( !autoSaveEntries.contains( entry ) )
            {
                it.remove();
            }
        }
        
        for ( Iterator<IEntry> it = autoSaveSharedWorkingCopies.keySet().iterator(); it.hasNext(); )
        {
            IEntry entry = it.next();
            
            if ( !autoSaveEntries.contains( entry ) )
            {
                it.remove();
            }
        }
    }
}
