/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.team.core.synchronize.ISyncInfoTreeChangeEvent;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.core.synchronize.SyncInfoSet;
import org.eclipse.team.core.synchronize.SyncInfoTree;
import org.eclipse.team.internal.core.subscribers.ChangeSet;
import org.eclipse.team.internal.core.subscribers.CheckedInChangeSet;
import org.eclipse.team.internal.core.subscribers.IChangeSetChangeListener;
import org.eclipse.team.internal.ui.ITeamUIImages;
import org.eclipse.team.internal.ui.TeamUIMessages;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.synchronize.actions.ChangeSetActionGroup;
import org.eclipse.team.ui.synchronize.ISynchronizeModelElement;
import org.eclipse.team.ui.synchronize.ISynchronizePageConfiguration;
import org.eclipse.team.ui.synchronize.ISynchronizeParticipant;
import org.eclipse.team.ui.synchronize.SynchronizePageActionGroup;

/**
 * Model provider for showing change sets in a sync page.
 */
public class ChangeSetModelProvider extends CompositeModelProvider {

    private ViewerSorter viewerSorter;

	// The id of the sub-provider
    private final String subProvierId;

	private Map<ISynchronizeModelElement, ISynchronizeModelProvider> rootToProvider = new HashMap<>();

	private ViewerSorter embeddedSorter;

	private SyncInfoSetChangeSetCollector checkedInCollector;

	private IChangeSetChangeListener changeSetListener = new IChangeSetChangeListener() {

        @Override
		public void setAdded(final ChangeSet set) {
            final SyncInfoTree syncInfoSet;
            // TODO: May need to be modified to work with two-way
            if (set instanceof CheckedInChangeSet) {
                syncInfoSet = checkedInCollector.getSyncInfoSet(set);
            } else {
                syncInfoSet = activeCollector.getSyncInfoSet(set);
            }
            if (syncInfoSet != null)
                createChangeSetModelElement(set, syncInfoSet);
        }

        @Override
		public void defaultSetChanged(final ChangeSet previousDefault, final ChangeSet set) {
		    refreshLabel(previousDefault);
		    refreshLabel(set);
        }

        @Override
		public void setRemoved(final ChangeSet set) {
            removeModelElementForSet(set);
        }

        @Override
		public void nameChanged(final ChangeSet set) {
            refreshLabel(set);
        }

        @Override
		public void resourcesChanged(ChangeSet set, IPath[] paths) {
            // The sub-providers listen directly to the sets for changes
            // There is no global action to be taken for such changes
        }
    };

    private ActiveChangeSetCollector activeCollector;

	/* *****************************************************************************
	 * Descriptor for this model provider
	 */
	public static class ChangeSetModelProviderDescriptor implements ISynchronizeModelProviderDescriptor {
		public static final String ID = TeamUIPlugin.ID + ".modelprovider_cvs_changelog"; //$NON-NLS-1$
		@Override
		public String getId() {
			return ID;
		}
		@Override
		public String getName() {
			return TeamUIMessages.ChangeLogModelProvider_5;
		}
		@Override
		public ImageDescriptor getImageDescriptor() {
			return TeamUIPlugin.getImageDescriptor(ITeamUIImages.IMG_CHANGE_SET);
		}
	};
	private static final ChangeSetModelProviderDescriptor descriptor = new ChangeSetModelProviderDescriptor();

    protected ChangeSetModelProvider(ISynchronizePageConfiguration configuration, SyncInfoSet set, String subProvierId) {
        super(configuration, set);
        this.subProvierId = subProvierId;
        ChangeSetCapability changeSetCapability = getChangeSetCapability();
        if (changeSetCapability.supportsCheckedInChangeSets()) {
	        checkedInCollector = changeSetCapability.createSyncInfoSetChangeSetCollector(configuration);
	        checkedInCollector.setProvider(this);
	        checkedInCollector.addListener(changeSetListener);
        }
        if (changeSetCapability.supportsActiveChangeSets()) {
            activeCollector = new ActiveChangeSetCollector(configuration, this);
            activeCollector.setChangeSetChangeListener(changeSetListener);
            configuration.addMenuGroup(ISynchronizePageConfiguration.P_CONTEXT_MENU, ChangeSetActionGroup.CHANGE_SET_GROUP);
        }
    }

    @Override
	protected void handleChanges(ISyncInfoTreeChangeEvent event, IProgressMonitor monitor) {
        boolean handled = false;
        if (checkedInCollector != null && getChangeSetCapability().enableCheckedInChangeSetsFor(getConfiguration())) {
            checkedInCollector.handleChange(event);
            handled = true;
        }
        if (activeCollector != null && getChangeSetCapability().enableActiveChangeSetsFor(getConfiguration())) {
            activeCollector.handleChange(event);
            handled = true;
        }
        if (!handled) {
            // Forward the event to the root provider
            ISynchronizeModelProvider provider = getProviderRootedAt(getModelRoot());
            if (provider != null) {
                SyncInfoSet set = provider.getSyncInfoSet();
                try {
                    set.beginInput();
                    set.removeAll(event.getRemovedResources());
                    SyncInfo[] added = event.getAddedResources();
                    for (int i = 0; i < added.length; i++) {
                        SyncInfo info = added[i];
                        set.add(info);
                    }
                    SyncInfo[] changed = event.getChangedResources();
                    for (int i = 0; i < changed.length; i++) {
                        SyncInfo info = changed[i];
                        set.add(info);
                    }
                } finally {
                    set.endInput(monitor);
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.team.internal.ui.synchronize.CompositeModelProvider#handleAddition(org.eclipse.team.core.synchronize.SyncInfo)
     */
    @Override
	protected void handleAddition(SyncInfo info) {
        // Nothing to do since change handling was bypassed
    }

    /* (non-Javadoc)
     * @see org.eclipse.team.internal.ui.synchronize.AbstractSynchronizeModelProvider#buildModelObjects(org.eclipse.team.ui.synchronize.ISynchronizeModelElement)
     */
    @Override
	protected IDiffElement[] buildModelObjects(ISynchronizeModelElement node) {
        // This method is invoked on a reset after the provider state has been cleared.
        // Resetting the collector will rebuild the model

		if (node == getModelRoot()) {

	        // First, disable the collectors
		    if (checkedInCollector != null) {
		        checkedInCollector.reset(null);
		        checkedInCollector.removeListener(changeSetListener);
		    }
		    if (activeCollector != null) {
		        activeCollector.setChangeSetChangeListener(null);
		        activeCollector.reset(null);
		    }

            // Then, re-enable the proper collection method
	        boolean handled = false;
		    if (checkedInCollector != null && getChangeSetCapability().enableCheckedInChangeSetsFor(getConfiguration())) {
		        checkedInCollector.addListener(changeSetListener);
		        checkedInCollector.reset(getSyncInfoSet());
		        handled = true;
		    }
		    if (activeCollector != null && getChangeSetCapability().enableActiveChangeSetsFor(getConfiguration())) {
		        activeCollector.setChangeSetChangeListener(changeSetListener);
	            activeCollector.reset(getSyncInfoSet());
	            handled = true;
		    }
		    if (!handled) {
		        // Forward the sync info to the root provider and trigger a build
	            ISynchronizeModelProvider provider = getProviderRootedAt(getModelRoot());
	            if (provider != null) {
	                ((SynchronizeModelProvider)provider).getSyncInfoSet().addAll(getSyncInfoSet());
	            }
		    }
		}
		return new IDiffElement[0];
    }

    @Override
	public ISynchronizeModelProviderDescriptor getDescriptor() {
        return descriptor;
    }

    @Override
	public ViewerSorter getViewerSorter() {
        if (viewerSorter == null) {
            viewerSorter = new ChangeSetModelSorter(this, ChangeSetActionGroup.getSortCriteria(getConfiguration()));
        }
        return viewerSorter;
    }

    /*
     * Method to allow ChangeSetActionGroup to set the viewer sorter of this provider.
     */
    public void setViewerSorter(ViewerSorter viewerSorter) {
        this.viewerSorter = viewerSorter;
        firePropertyChange(ISynchronizeModelProvider.P_VIEWER_SORTER, null, null);
    }

    @Override
	protected SynchronizePageActionGroup createActionGroup() {
        return new ChangeSetActionGroup(this);
    }

    private ISynchronizeModelProvider createProviderRootedAt(ISynchronizeModelElement parent, SyncInfoTree set) {
        ISynchronizeModelProvider provider = createModelProvider(parent, getSubproviderId(), set);
        addProvider(provider);
        rootToProvider.put(parent, provider);
        return provider;
    }

    private ISynchronizeModelProvider getProviderRootedAt(ISynchronizeModelElement parent) {
        return rootToProvider.get(parent);
    }

    @Override
	protected void removeProvider(ISynchronizeModelProvider provider) {
        rootToProvider.remove(provider.getModelRoot());
        super.removeProvider(provider);
    }

    /**
     * Return the id of the sub-provider used by the commit set provider.
     * @return the id of the sub-provider used by the commit set provider
     */
    public String getSubproviderId() {
        return subProvierId;
    }

    /**
     * Return the sorter associated with the sub-provider being used.
     * @return the sorter associated with the sub-provider being used
     */
    public ViewerSorter getEmbeddedSorter() {
        return embeddedSorter;
    }

    @Override
	protected void recursiveClearModelObjects(ISynchronizeModelElement node) {
        super.recursiveClearModelObjects(node);
        if (node == getModelRoot()) {
            rootToProvider.clear();
            // Throw away the embedded sorter
            embeddedSorter = null;
            createRootProvider();
        }
    }

    /*
     * Create the root subprovider which is used to display resources
     * that are not in a commit set. This provider is created even if
     * it is empty so we can have access to the appropriate sorter
     * and action group
     */
    private void createRootProvider() {
        // Recreate the sub-provider at the root and use it's viewer sorter and action group
        SyncInfoTree tree;
        if (activeCollector != null && getChangeSetCapability().enableActiveChangeSetsFor(getConfiguration())) {
            // When in outgoing mode, use the root set of the active change set collector at the root
            tree = activeCollector.getRootSet();
        } else {
            tree = new SyncInfoTree();
        }
        final ISynchronizeModelProvider provider = createProviderRootedAt(getModelRoot(), tree);
        embeddedSorter = provider.getViewerSorter();
        if (provider instanceof AbstractSynchronizeModelProvider) {
            SynchronizePageActionGroup actionGroup = ((AbstractSynchronizeModelProvider)provider).getActionGroup();
            if (actionGroup != null) {
                // This action group will be disposed when the provider is disposed
                getConfiguration().addActionContribution(actionGroup);
                provider.addPropertyChangeListener(event -> {
				    if (event.getProperty().equals(P_VIEWER_SORTER)) {
				        embeddedSorter = provider.getViewerSorter();
				        ChangeSetModelProvider.this.firePropertyChange(P_VIEWER_SORTER, null, null);
				    }
				});
            }
        }
    }

    /*
     * Find the root element for the given change set.
     * A linear search is used.
     */
    protected ISynchronizeModelElement getModelElement(ChangeSet set) {
        IDiffElement[] children = getModelRoot().getChildren();
        for (int i = 0; i < children.length; i++) {
            IDiffElement element = children[i];
            if (element instanceof ChangeSetDiffNode && ((ChangeSetDiffNode)element).getSet() == set) {
                return (ISynchronizeModelElement)element;
            }
        }
        return null;
    }

    /*
     * Return the change set capability
     */
    public ChangeSetCapability getChangeSetCapability() {
        ISynchronizeParticipant participant = getConfiguration().getParticipant();
        if (participant instanceof IChangeSetProvider) {
            IChangeSetProvider provider = (IChangeSetProvider) participant;
            return provider.getChangeSetCapability();
        }
        return null;
    }

    @Override
	public void dispose() {
        if (checkedInCollector != null) {
	        checkedInCollector.removeListener(changeSetListener);
	        checkedInCollector.dispose();
        }
        if (activeCollector != null) {
            activeCollector.setChangeSetChangeListener(null);
            activeCollector.dispose();
        }
        super.dispose();
    }


    @Override
	public void waitUntilDone(IProgressMonitor monitor) {
        super.waitUntilDone(monitor);
        if (checkedInCollector != null) {
            checkedInCollector.waitUntilDone(monitor);
            // Wait for the provider again since the change set handler may have queued UI updates
            super.waitUntilDone(monitor);
        }
    }

    void removeModelElementForSet(final ChangeSet set) {
        ISynchronizeModelElement node = getModelElement(set);
        if (node != null) {
            ISynchronizeModelProvider provider = getProviderRootedAt(node);
            removeFromViewer(new ISynchronizeModelElement[] { node });
            removeProvider(provider);
        }
    }

    public void createChangeSetModelElement(ChangeSet set, SyncInfoTree tree) {
        // Add the model element and provider for the set
        ISynchronizeModelElement node = getModelElement(set);
        ISynchronizeModelProvider provider = null;
        if (node != null) {
            provider = getProviderRootedAt(node);
        }
        if (provider == null) {
            provider = createProvider(set, tree);
	        provider.prepareInput(null);
        }
    }

    private ISynchronizeModelProvider createProvider(ChangeSet set, SyncInfoTree tree) {
        ChangeSetDiffNode node = new ChangeSetDiffNode(getModelRoot(), set);
        addToViewer(node);
        return createProviderRootedAt(node, tree);
    }

    public void refreshLabel(ChangeSet set) {
        ISynchronizeModelElement node = getModelElement(set);
        if (node != null) {
            getViewer().refresh(node);
        }
    }
}
