/*******************************************************************************
 * Copyright (c) 2006, 2017 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 * Matt McCutchen <hashproduct+eclipse@gmail.com> - Bug 128429 [Change Sets] Change Sets with / in name do not get persited
 *******************************************************************************/
package org.eclipse.team.internal.core.subscribers;

import java.util.*;

import org.eclipse.core.resources.*;
import org.eclipse.core.resources.mapping.ResourceTraversal;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.diff.*;
import org.eclipse.team.core.mapping.IChangeGroupingRequestor;
import org.eclipse.team.core.mapping.IResourceDiffTree;
import org.eclipse.team.internal.core.Messages;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.core.mapping.CompoundResourceTraversal;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

/**
 * A change set manager that contains sets that represent collections of
 * related local changes.
 */
public abstract class ActiveChangeSetManager extends ChangeSetManager implements IDiffChangeListener, IChangeGroupingRequestor {

    private static final String CTX_DEFAULT_SET = "defaultSet"; //$NON-NLS-1$

    private ActiveChangeSet defaultSet;

	/**
     * Return the Change Set whose sync info set is the
     * one given.
     * @param tree a diff tree
     * @return the change set for the given diff tree
     */
    protected ChangeSet getChangeSet(IResourceDiffTree tree) {
        ChangeSet[] sets = getSets();
        for (int i = 0; i < sets.length; i++) {
			ChangeSet changeSet = sets[i];
            if (((DiffChangeSet)changeSet).getDiffTree() == tree) {
                return changeSet;
            }
        }
        return null;
    }

    @Override
	public void add(ChangeSet set) {
        Assert.isTrue(set instanceof ActiveChangeSet);
        super.add(set);
    }

    @Override
	protected void handleSetAdded(ChangeSet set) {
    	Assert.isTrue(set instanceof ActiveChangeSet);
    	((DiffChangeSet)set).getDiffTree().addDiffChangeListener(getDiffTreeListener());
    	super.handleSetAdded(set);
    	handleAddedResources(set, ((ActiveChangeSet)set).internalGetDiffTree().getDiffs());
    }

    @Override
	protected void handleSetRemoved(ChangeSet set) {
    	((DiffChangeSet)set).getDiffTree().removeDiffChangeListener(getDiffTreeListener());
    	super.handleSetRemoved(set);
    }

    /**
     * Return the listener that is registered with the diff trees associated with
     * the sets for this manager.
     * @return the listener that is registered with the diff trees associated with
     * the sets for this manager
     */
    protected IDiffChangeListener getDiffTreeListener() {
        return this;
    }

	@Override
	public void diffsChanged(IDiffChangeEvent event, IProgressMonitor monitor) {
	    IResourceDiffTree tree = (IResourceDiffTree)event.getTree();
	    handleSyncSetChange(tree, event.getAdditions(), getAllResources(event));
	}

	@Override
	public void propertyChanged(IDiffTree tree, int property, IPath[] paths) {
		// ignore
	}

	@Override
	public boolean isModified(IFile file) throws CoreException {
		IDiff diff = getDiff(file);
		if (diff != null)
			return isModified(diff);
		return false;
	}

	/**
	 * Return whether the given diff represents a local change.
	 * @param diff the diff
	 * @return whether the given diff represents a local change
	 */
    public boolean isModified(IDiff diff) {
        if (diff != null) {
        	if (diff instanceof IThreeWayDiff) {
				IThreeWayDiff twd = (IThreeWayDiff) diff;
				int dir = twd.getDirection();
				return dir == IThreeWayDiff.OUTGOING || dir == IThreeWayDiff.CONFLICTING;
			} else {
				return diff.getKind() != IDiff.NO_CHANGE;
			}
        }
        return false;
    }

	/**
	 * Return the set with the given name.
	 * @param name the name of the set
	 * @return the set with the given name
	 */
	public ActiveChangeSet getSet(String name) {
		ChangeSet[] sets = getSets();
		for (int i = 0; i < sets.length; i++) {
			ChangeSet set = sets[i];
			if (set.getName().equals(name) && set instanceof ActiveChangeSet) {
				return (ActiveChangeSet)set;
			}
		}
		return null;
	}

    /**
     * Create a change set containing the given files if
     * they have been modified locally.
     * @param title the title of the commit set
     * @param files the files contained in the set
     * @return the created set
     * @throws CoreException
     */
    public ActiveChangeSet createSet(String title, IFile[] files) throws CoreException {
        List<IDiff> infos = new ArrayList<>();
        for (int i = 0; i < files.length; i++) {
            IFile file = files[i];
            IDiff diff = getDiff(file);
            if (diff != null) {
                infos.add(diff);
            }
        }
        return createSet(title, infos.toArray(new IDiff[infos.size()]));
    }

    /**
     * Create a commit set with the given title and files. The created
     * set is not added to the control of the commit set manager
     * so no events are fired. The set can be added using the
     * <code>add</code> method.
     * @param title the title of the commit set
     * @param diffs the files contained in the set
     * @return the created set
     */
    public ActiveChangeSet createSet(String title, IDiff[] diffs) {
        ActiveChangeSet commitSet = doCreateSet(title);
        if (diffs != null && diffs.length > 0) {
            commitSet.add(diffs);
        }
        return commitSet;
    }

    /**
     * Create a change set with the given name.
     * @param name the name of the change set
     * @return the created change set
     */
	protected ActiveChangeSet doCreateSet(String name) {
		return new ActiveChangeSet(this, name);
	}

	public abstract IDiff getDiff(IResource resource) throws CoreException;

	/**
	 * Return whether the manager allows a resource to
	 * be in multiple sets. By default, a resource
	 * may only be in one set.
	 * @return whether the manager allows a resource to
	 * be in multiple sets.
	 */
	protected boolean isSingleSetPerResource() {
	    return true;
	}

	private IPath[] getAllResources(IDiffChangeEvent event) {
		Set<IPath> allResources = new HashSet<>();
		IDiff[] addedResources = event.getAdditions();
		for (int i = 0; i < addedResources.length; i++) {
			IDiff diff = addedResources[i];
			allResources.add(diff.getPath());
		}
		IDiff[] changedResources = event.getChanges();
		for (int i = 0; i < changedResources.length; i++) {
			IDiff diff = changedResources[i];
			allResources.add(diff.getPath());
		}
		IPath[] removals = event.getRemovals();
		for (int i = 0; i < removals.length; i++) {
			IPath path = removals[i];
			allResources.add(path);
		}
	    return allResources.toArray(new IPath[allResources.size()]);
	}

	/**
	 * React to the given diffs being added to the given set.
	 * @param set the set
	 * @param diffs the diffs
	 */
	protected void handleAddedResources(ChangeSet set, IDiff[] diffs) {
	    if (isSingleSetPerResource() && ((ActiveChangeSet)set).isUserCreated()) {
	        IResource[] resources = new IResource[diffs.length];
	        for (int i = 0; i < resources.length; i++) {
				resources[i] = ((DiffChangeSet)set).getDiffTree().getResource(diffs[i]);
			}
	        // Remove the added files from any other set that contains them
	        ChangeSet[] sets = getSets();
	        for (int i = 0; i < sets.length; i++) {
	            ChangeSet otherSet = sets[i];
				if (otherSet != set && ((ActiveChangeSet)otherSet).isUserCreated()) {
	                otherSet.remove(resources);
	            }
	        }
	    }
	}

	private void handleSyncSetChange(IResourceDiffTree tree, IDiff[] addedDiffs, IPath[] allAffectedResources) {
	    ChangeSet changeSet = getChangeSet(tree);
	    if (tree.isEmpty() && changeSet != null) {
	        remove(changeSet);
	    }
	    fireResourcesChangedEvent(changeSet, allAffectedResources);
	    handleAddedResources(changeSet, addedDiffs);
	}

	/**
	 * Make the given set the default set into which all new modifications that
	 * are not already in another set go.
	 *
	 * @param set
	 *            the set which is to become the default set or
	 *            <code>null</code> to unset the default set
	 */
	public void makeDefault(ActiveChangeSet set) {
	    // The default set must be an active set
		if (set != null && !contains(set)) {
			add(set);
		}
	    ActiveChangeSet oldSet = defaultSet;
	    defaultSet = set;
	    fireDefaultChangedEvent(oldSet, defaultSet);
	}

	/**
	 * Return whether the given set is the default set into which all
	 * new modifications will be placed.
	 * @param set the set to test
	 * @return whether the set is the default set
	 */
	public boolean isDefault(ActiveChangeSet set) {
	    return set == defaultSet;
	}

    /**
     * Return the set which is currently the default or
     * <code>null</code> if there is no default set.
     * @return the default change set
     */
    public ActiveChangeSet getDefaultSet() {
        return defaultSet;
    }

	/**
	 * If the given traversals contain any resources in the active change sets, ensure
	 * that the traversals cover all the resources in the overlapping change set.
	 * @param traversals the traversals
	 * @return the traversals adjusted to contain all the resources of intersecting change sets
	 */
	public ResourceTraversal[] adjustInputTraversals(ResourceTraversal[] traversals) {
		CompoundResourceTraversal traversal = new CompoundResourceTraversal();
		traversal.addTraversals(traversals);
		ChangeSet[] sets = getSets();
		for (int i = 0; i < sets.length; i++) {
			ChangeSet set = sets[i];
			handleIntersect(traversal, set);
		}
		return traversal.asTraversals();
	}

	private void handleIntersect(CompoundResourceTraversal traversal, ChangeSet set) {
		IResource[] resources = set.getResources();
		for (int i = 0; i < resources.length; i++) {
			IResource resource = resources[i];
			if (traversal.isCovered(resource, IResource.DEPTH_ZERO)) {
				traversal.addResources(resources, IResource.DEPTH_ZERO);
				return;
			}
		}
	}

	/**
	 * Save the state of this manager including all its contained sets
	 * into the given preferences node.
	 * @param prefs a preferences node
	 */
	protected void save(Preferences prefs) {
		// No need to save the sets if the manager has never been initialized
		if (!isInitialized())
			return;
        // Clear the persisted state before saving the new state
        try {
            String[] oldSetNames = prefs.childrenNames();
            for (int i = 0; i < oldSetNames.length; i++) {
                String string = oldSetNames[i];
                prefs.node(string).removeNode();
            }
        } catch (BackingStoreException e) {
            TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_5, new String[] { getName() }), e);
        }
        ChangeSet[] sets = getSets();
        for (int i = 0; i < sets.length; i++) {
            ChangeSet set = sets[i];
			if (set instanceof ActiveChangeSet && !set.isEmpty()) {
			    // Since the change set title is stored explicitly, the name of
			    // the child preference node doesn't matter as long as it
			    // doesn't contain / and no two change sets get the same name.
			    String childPrefName = escapePrefName(((ActiveChangeSet)set).getTitle());
			    Preferences child = prefs.node(childPrefName);
			    ((ActiveChangeSet)set).save(child);
			}
		}
		if (getDefaultSet() != null) {
		    prefs.put(CTX_DEFAULT_SET, getDefaultSet().getTitle());
		} else {
			// unset default changeset
			prefs.remove(CTX_DEFAULT_SET);
		}
		try {
            prefs.flush();
        } catch (BackingStoreException e) {
            TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_3, new String[] { getName() }), e);
        }
    }

    /**
	 * Escape the given string for safe use as a preference node name by
	 * translating / to \s (so it's a single path component) and \ to \\ (to
	 * preserve uniqueness).
	 *
	 * @param string
	 *            Input string
	 * @return Escaped output string
	 */
	private static String escapePrefName(String string) {
		StringBuilder out = new StringBuilder();
		for (int i = 0; i < string.length(); i++) {
			char c = string.charAt(i);
			switch (c) {
			case '/':
				out.append("\\s"); //$NON-NLS-1$
				break;
			case '\\':
				out.append("\\\\"); //$NON-NLS-1$
				break;
			default:
				out.append(c);
			}
		}
		return out.toString();
	}

	/**
	 * Load the manager's state from the given preferences node.
	 *
	 * @param prefs
	 *            a preferences node
	 */
	protected void load(Preferences prefs) {
		String defaultSetTitle = prefs.get(CTX_DEFAULT_SET, null);
        try {
			String[] childNames = prefs.childrenNames();
			for (int i = 0; i < childNames.length; i++) {
			    String string = childNames[i];
			    Preferences childPrefs = prefs.node(string);
			    ActiveChangeSet set = createSet(childPrefs);
			    if (!set.isEmpty()) {
			    	if (getDefaultSet() == null && defaultSetTitle != null && set.getTitle().equals(defaultSetTitle)) {
			    	    makeDefault(set);
			    	}
			    	add(set);
			    }
			}
		} catch (BackingStoreException e) {
			TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_4, new String[] { getName() }), e);
		}
    }

	/**
	 * Return the name of this change set manager.
	 * @return the name of this change set manager
	 */
	protected abstract String getName();

    /**
     * Create a change set from the given preferences that were
     * previously saved.
     * @param childPrefs the previously saved preferences
     * @return the created change set
     */
    protected ActiveChangeSet createSet(Preferences childPrefs) {
        // Don't specify a title when creating the change set; instead, let the
        // change set read its title from the preferences.
        ActiveChangeSet changeSet = doCreateSet(null);
        changeSet.init(childPrefs);
        return changeSet;
    }

    @Override
	public void ensureChangesGrouped(IProject project, IFile[] files,
    		String name) throws CoreException {
		ActiveChangeSet set = getSet(name);
		if (set == null) {
			set = createSet(name, files);
			set.setUserCreated(false);
			add(set);
		} else {
			set.setUserCreated(false);
			set.add(files);
		}
    }
}
