/*******************************************************************************
 * Copyright (c) 2004, 2010 Tasktop Technologies 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.resources.ui;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.context.core.ContextCore;
import org.eclipse.mylyn.monitor.core.InteractionEvent;
import org.eclipse.mylyn.resources.ui.ResourcesUi;

/**
 * @author Mik Kersten
 */
public class ResourceChangeMonitor implements IResourceChangeListener {

	private static final int MAX_FILE_DELTA_SIZE = 10;

	private static final int MAX_NEW_FOLDER_DELTA_SIZE = 3;

	private class ResourceDeltaVisitor implements IResourceDeltaVisitor {

		private final Set<IResource> addedResources;

		private final Set<IResource> changedResources;

		private boolean haveTeamPrivateMember;

		private final List<IResourceExclusionStrategy> resourceExclusions;

		private int numNonExcludedFiles = 0;

		private int numAddedFolders = 0;

		public ResourceDeltaVisitor(List<IResourceExclusionStrategy> resourceExclusions) {
			this.resourceExclusions = resourceExclusions;
			this.addedResources = new HashSet<IResource>();
			this.changedResources = new HashSet<IResource>();

			// make sure that the exclusions are updated 
			for (IResourceExclusionStrategy exclusion : exclusions) {
				exclusion.update();
			}
		}

		public boolean hasValidResult() {
			return !haveTeamPrivateMember;
		}

		public boolean visit(IResourceDelta delta) {

			IResource deltaResource = delta.getResource();
			if (deltaResource instanceof IProject
					&& (delta.getKind() == IResourceDelta.REMOVED || (delta.getFlags() & IResourceDelta.OPEN) != 0)) {
				// the project was either opened, closed or deleted, so lets ignore this so that we don't add every file to the context
				return false;
			}

			if (hasTeamPrivate(deltaResource)) {
				return false;
			}

			if (isExcluded(deltaResource)) {
				return false;
			}

			IResourceDelta[] added = delta.getAffectedChildren(IResourceDelta.ADDED);
			for (IResourceDelta element : added) {
				IResource resource = element.getResource();
				if ((resource instanceof IFile || resource instanceof IFolder) && !isExcluded(resource)) {

					if (hasTeamPrivate(resource)) {
						return false;
					}

					if (resource instanceof IFile) {
						numNonExcludedFiles++;
					} else if (resource instanceof IFolder) {
						numAddedFolders++;
					}

					addedResources.add(resource);
				}
			}

			IResourceDelta[] changed = delta.getAffectedChildren(IResourceDelta.CHANGED | IResourceDelta.REMOVED);
			for (IResourceDelta element : changed) {
				IResource resource = element.getResource();
				// special rule for feature.xml files: bug 249856 
				if (resource instanceof IFile && !isExcluded(resource) && !"feature.xml".equals(resource.getName())) { //$NON-NLS-1$
					if (element.getKind() == IResourceDelta.CHANGED
							&& (element.getFlags() & IResourceDelta.CONTENT) == 0) {
						// make sure that there was a content change and not just a markers change
						continue;
					}

					if (hasTeamPrivate(resource)) {
						return false;
					}
					if (resource instanceof IFile) {
						numNonExcludedFiles++;
					}
					changedResources.add(resource);
				}
			}
			return true;
		}

		private boolean hasTeamPrivate(IResource resource) {
			if (haveTeamPrivateMember) {
				return true;
			}
			if (resource.isTeamPrivateMember()) {
				haveTeamPrivateMember = true;
				return true;
			}
			return false;
		}

		private boolean isExcluded(IResource resource) {
			for (IResourceExclusionStrategy exclusion : resourceExclusions) {
				if (exclusion.isExcluded(resource)) {
					return true;
				}
			}
			return false;
		}

		public Set<IResource> getChangedResources() {
			return changedResources;
		}

		public Set<IResource> getAddedResources() {
			return addedResources;
		}

		public int getNumChangedAndAddedFiles() {
			return numNonExcludedFiles;
		}

		public int getNumAddedFolders() {
			return numAddedFolders;
		}

	};

	private boolean enabled;

	private final List<IResourceExclusionStrategy> exclusions = new ArrayList<IResourceExclusionStrategy>();

	public ResourceChangeMonitor() {
		this.enabled = true;
		// ant based pattern exclusion
		exclusions.add(new ResourcePatternExclusionStrategy());
		// exclude resources not modified while task active
		exclusions.add(new ResourceModifiedDateExclusionStrategy());

		for (IResourceExclusionStrategy exclusion : exclusions) {
			exclusion.init();
		}
	}

	public void dispose() {
		for (IResourceExclusionStrategy exclusion : exclusions) {
			exclusion.dispose();
		}
		exclusions.clear();
	}

	// TODO investigate moving computation to the background?
	public void resourceChanged(IResourceChangeEvent event) {
		if (!enabled || !ContextCore.getContextManager().isContextActive()) {
			return;
		}
		if (event.getType() != IResourceChangeEvent.POST_CHANGE) {
			return;
		}
		IResourceDelta rootDelta = event.getDelta();
		if (rootDelta != null) {
			ResourceDeltaVisitor visitor = new ResourceDeltaVisitor(exclusions);
			try {
				rootDelta.accept(visitor, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
				if (visitor.hasValidResult()) {
					// discard large changes as to not pollute the task context
					if (visitor.getNumChangedAndAddedFiles() <= MAX_FILE_DELTA_SIZE
							&& visitor.getNumAddedFolders() <= MAX_NEW_FOLDER_DELTA_SIZE) {
						ResourcesUi.addResourceToContext(visitor.getChangedResources(),
								InteractionEvent.Kind.PREDICTION);
						ResourcesUi.addResourceToContext(visitor.getAddedResources(), InteractionEvent.Kind.PROPAGATION);
					}
				}
			} catch (CoreException e) {
				StatusHandler.log(new Status(IStatus.ERROR, ResourcesUiBridgePlugin.ID_PLUGIN,
						"Could not accept marker visitor", e)); //$NON-NLS-1$
			}
		}
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}
}
