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

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

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

	private ViewerComparator embeddedSorter;

	private SyncInfoSetChangeSetCollector checkedInCollector;

	private final 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 (SyncInfo info : added) {
						set.add(info);
					}
					SyncInfo[] changed = event.getChangedResources();
					for (SyncInfo info : changed) {
						set.add(info);
					}
				} finally {
					set.endInput(monitor);
				}
			}
		}
	}

	@Override
	protected void handleAddition(SyncInfo info) {
		// Nothing to do since change handling was bypassed
	}

	@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 ViewerComparator getViewerComparator() {
		if (viewerComparator == null) {
			viewerComparator = new ChangeSetModelComparator(this, ChangeSetActionGroup.getSortCriteria(getConfiguration()));
		}
		return viewerComparator;
	}

	/*
	 * Method to allow ChangeSetActionGroup to set the viewer sorter of this provider.
	 */
	public void setViewerComparator(ViewerComparator viewerSorter) {
		this.viewerComparator = 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 ViewerComparator getEmbeddedComparator() {
		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.getViewerComparator();
		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.getViewerComparator();
						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 (IDiffElement element : children) {
			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);
		}
	}
}
