/*******************************************************************************
 * Copyright (c) 2004, 2009 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.java.ui.search;

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

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IInitializer;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.ui.search.JavaSearchQuery;
import org.eclipse.jdt.internal.ui.search.JavaSearchResult;
import org.eclipse.jdt.ui.search.ElementQuerySpecification;
import org.eclipse.jdt.ui.search.QuerySpecification;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.context.core.AbstractContextStructureBridge;
import org.eclipse.mylyn.context.core.ContextCore;
import org.eclipse.mylyn.context.core.IInteractionElement;
import org.eclipse.mylyn.internal.context.core.AbstractRelationProvider;
import org.eclipse.mylyn.internal.context.core.DegreeOfSeparation;
import org.eclipse.mylyn.internal.context.core.IActiveSearchListener;
import org.eclipse.mylyn.internal.context.core.IActiveSearchOperation;
import org.eclipse.mylyn.internal.context.core.IDegreeOfSeparation;
import org.eclipse.mylyn.internal.java.ui.JavaStructureBridge;
import org.eclipse.mylyn.internal.java.ui.JavaUiBridgePlugin;
import org.eclipse.mylyn.internal.resources.ui.ResourcesUiBridgePlugin;
import org.eclipse.osgi.util.NLS;
import org.eclipse.search.ui.ISearchResult;
import org.eclipse.search2.internal.ui.InternalSearchUI;

/**
 * @author Mik Kersten
 */
@SuppressWarnings("restriction")
public abstract class AbstractJavaRelationProvider extends AbstractRelationProvider {

	public static final String ID_GENERIC = "org.eclipse.mylyn.java.relation"; //$NON-NLS-1$

	public static final String NAME = "Java relationships"; //$NON-NLS-1$

	private static final int DEFAULT_DEGREE = 2;

	private static final List<Job> runningJobs = new ArrayList<Job>();

	@Override
	public String getGenericId() {
		return ID_GENERIC;
	}

	protected AbstractJavaRelationProvider(String structureKind, String id) {
		super(structureKind, id);
	}

	@Override
	public List<IDegreeOfSeparation> getDegreesOfSeparation() {
		List<IDegreeOfSeparation> separations = new ArrayList<IDegreeOfSeparation>();
		separations.add(new DegreeOfSeparation(DOS_0_LABEL, 0));
		separations.add(new DegreeOfSeparation(DOS_1_LABEL, 1));
		separations.add(new DegreeOfSeparation(DOS_2_LABEL, 2));
		separations.add(new DegreeOfSeparation(DOS_3_LABEL, 3));
		separations.add(new DegreeOfSeparation(DOS_4_LABEL, 4));
		separations.add(new DegreeOfSeparation(DOS_5_LABEL, 5));
		return separations;
	}

	@Override
	protected void findRelated(final IInteractionElement node, int degreeOfSeparation) {
		if (node == null) {
			return;
		}
		if (node.getContentType() == null) {
			StatusHandler.log(new Status(IStatus.WARNING, JavaUiBridgePlugin.ID_PLUGIN, "Null content type for: " //$NON-NLS-1$
					+ node));
			return;
		}
		if (!node.getContentType().equals(JavaStructureBridge.CONTENT_TYPE)) {
			return;
		}
		IJavaElement javaElement = JavaCore.create(node.getHandleIdentifier());
		if (!acceptElement(javaElement) || !javaElement.exists() || javaElement instanceof IInitializer) {
			return;
		}

		IJavaSearchScope scope = createJavaSearchScope(javaElement, degreeOfSeparation);
		if (scope != null) {
			runJob(node, degreeOfSeparation, getId());
		}
	}

	private IJavaSearchScope createJavaSearchScope(IJavaElement element, int degreeOfSeparation) {
		Set<IInteractionElement> landmarks = ContextCore.getContextManager().getActiveLandmarks();
		List<IInteractionElement> interestingElements = ContextCore.getContextManager()
				.getActiveContext()
				.getInteresting();

		Set<IJavaElement> searchElements = new HashSet<IJavaElement>();
		int includeMask = IJavaSearchScope.SOURCES;
		if (degreeOfSeparation == 1) {
			for (IInteractionElement landmark : landmarks) {
				AbstractContextStructureBridge bridge = ContextCore.getStructureBridge(landmark.getContentType());
				if (includeNodeInScope(landmark, bridge)) {
					Object o = bridge.getObjectForHandle(landmark.getHandleIdentifier());
					if (o instanceof IJavaElement) {
						IJavaElement landmarkElement = (IJavaElement) o;
						if (landmarkElement.exists()) {
							if (landmarkElement instanceof IMember && !landmark.getInterest().isPropagated()) {
								searchElements.add(((IMember) landmarkElement).getCompilationUnit());
							} else if (landmarkElement instanceof ICompilationUnit) {
								searchElements.add(landmarkElement);
							}
						}
					}
				}
			}
		} else if (degreeOfSeparation == 2) {
			for (IInteractionElement interesting : interestingElements) {
				AbstractContextStructureBridge bridge = ContextCore.getStructureBridge(interesting.getContentType());
				if (includeNodeInScope(interesting, bridge)) {
					Object object = bridge.getObjectForHandle(interesting.getHandleIdentifier());
					if (object instanceof IJavaElement) {
						IJavaElement interestingElement = (IJavaElement) object;
						if (interestingElement.exists()) {
							if (interestingElement instanceof IMember && !interesting.getInterest().isPropagated()) {
								searchElements.add(((IMember) interestingElement).getCompilationUnit());
							} else if (interestingElement instanceof ICompilationUnit) {
								searchElements.add(interestingElement);
							}
						}
					}
				}
			}
		} else if (degreeOfSeparation == 3 || degreeOfSeparation == 4) {
			for (IInteractionElement interesting : interestingElements) {
				AbstractContextStructureBridge bridge = ContextCore.getStructureBridge(interesting.getContentType());
				if (includeNodeInScope(interesting, bridge)) {
					// TODO what to do when the element is not a java element,
					// how determine if a javaProject?
					IResource resource = ResourcesUiBridgePlugin.getDefault().getResourceForElement(interesting, true);
					if (resource != null) {
						IProject project = resource.getProject();
						if (project != null && JavaProject.hasJavaNature(project) && project.exists()) {
							IJavaProject javaProject = JavaCore.create(project);// ((IJavaElement)o).getJavaProject();
							if (javaProject != null && javaProject.exists()) {
								searchElements.add(javaProject);
							}
						}
					}
				}
			}
			if (degreeOfSeparation == 4) {

				includeMask = IJavaSearchScope.SOURCES | IJavaSearchScope.APPLICATION_LIBRARIES
						| IJavaSearchScope.SYSTEM_LIBRARIES;
			}
		} else if (degreeOfSeparation == 5) {
			return SearchEngine.createWorkspaceScope();
		}

		if (searchElements.size() == 0) {
			return null;
		} else {
			IJavaElement[] elements = new IJavaElement[searchElements.size()];
			int j = 0;
			for (IJavaElement searchElement : searchElements) {
				elements[j] = searchElement;
				j++;
			}
			return SearchEngine.createJavaSearchScope(elements, includeMask);
		}
	}

	/**
	 * Only include Java elements and files.
	 */
	private boolean includeNodeInScope(IInteractionElement interesting, AbstractContextStructureBridge bridge) {
		if (interesting == null || bridge == null) {
			return false;
		} else {
			if (interesting.getContentType() == null) {
				// TODO: remove
				StatusHandler.log(new Status(IStatus.WARNING, JavaUiBridgePlugin.ID_PLUGIN, "Null content type for: " //$NON-NLS-1$
						+ interesting.getHandleIdentifier()));
				return false;
			} else {
				return interesting.getContentType().equals(JavaStructureBridge.CONTENT_TYPE)
						|| bridge.isDocument(interesting.getHandleIdentifier());
			}
		}
	}

	protected boolean acceptResultElement(IJavaElement element) {
		return !(element instanceof IImportDeclaration);
	}

	protected boolean acceptElement(IJavaElement javaElement) {
		return javaElement != null && (javaElement instanceof IMember || javaElement instanceof IType);
	}

	private void runJob(final IInteractionElement node, final int degreeOfSeparation, final String kind) {

		int limitTo = 0;
		if (kind.equals(JavaReferencesProvider.ID)) {
			limitTo = IJavaSearchConstants.REFERENCES;
		} else if (kind.equals(JavaImplementorsProvider.ID)) {
			limitTo = IJavaSearchConstants.IMPLEMENTORS;
		} else if (kind.equals(JUnitReferencesProvider.ID)) {
			limitTo = IJavaSearchConstants.REFERENCES;
		} else if (kind.equals(JavaReadAccessProvider.ID)) {
			limitTo = IJavaSearchConstants.REFERENCES;
		} else if (kind.equals(JavaWriteAccessProvider.ID)) {
			limitTo = IJavaSearchConstants.REFERENCES;
		}

		final JavaSearchOperation query = (JavaSearchOperation) getSearchOperation(node, limitTo, degreeOfSeparation);
		if (query == null) {
			return;
		}

		JavaSearchJob job = new JavaSearchJob(query.getLabel(), query);
		query.addListener(new IActiveSearchListener() {

			private boolean gathered = false;

			public boolean resultsGathered() {
				return gathered;
			}

			@SuppressWarnings("rawtypes")
			public void searchCompleted(List l) {
				if (l == null) {
					return;
				}
				List<IJavaElement> relatedHandles = new ArrayList<IJavaElement>();
				Object[] elements = l.toArray();
				for (Object element : elements) {
					if (element instanceof IJavaElement) {
						relatedHandles.add((IJavaElement) element);
					}
				}

				for (IJavaElement element : relatedHandles) {
					if (!acceptResultElement(element)) {
						continue;
					}
					incrementInterest(node, JavaStructureBridge.CONTENT_TYPE, element.getHandleIdentifier(),
							degreeOfSeparation);
				}
				gathered = true;
				AbstractJavaRelationProvider.this.searchCompleted(node);
			}

		});
		InternalSearchUI.getInstance();

		runningJobs.add(job);
		job.setPriority(Job.DECORATE - 10);
		job.schedule();
	}

	@Override
	public IActiveSearchOperation getSearchOperation(IInteractionElement node, int limitTo, int degreeOfSeparation) {
		IJavaElement javaElement = JavaCore.create(node.getHandleIdentifier());
		if (javaElement == null || !javaElement.exists()) {
			return null;
		}

		IJavaSearchScope scope = createJavaSearchScope(javaElement, degreeOfSeparation);

		if (scope == null) {
			return null;
		}

		QuerySpecification specs = new ElementQuerySpecification(javaElement, limitTo, scope,
				Messages.AbstractJavaRelationProvider_Mylyn_degree_of_separation + degreeOfSeparation);

		return new JavaSearchOperation(specs);
	}

	protected static class JavaSearchJob extends Job {

		private final JavaSearchOperation op;

		public JavaSearchJob(String name, JavaSearchOperation op) {
			super(name);
			this.op = op;
		}

		/**
		 * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@Override
		protected IStatus run(IProgressMonitor monitor) {
			return op.run(monitor);
		}

	}

	protected static class JavaSearchOperation extends JavaSearchQuery implements IActiveSearchOperation {
		private ISearchResult result = null;

		@Override
		public ISearchResult getSearchResult() {
			if (result == null) {
				result = new JavaSearchResult(this);
			}
			new JavaActiveSearchResultUpdater((JavaSearchResult) result);
			return result;
		}

		@Override
		public IStatus run(IProgressMonitor monitor) {
			IStatus runStatus = super.run(monitor);
			ISearchResult result = getSearchResult();
			if (result instanceof JavaSearchResult) {
				Object[] objs = ((JavaSearchResult) result).getElements();
				if (objs == null) {
					notifySearchCompleted(null);
				} else {
					List<Object> l = new ArrayList<Object>();
					for (Object obj : objs) {
						l.add(obj);
					}
					notifySearchCompleted(l);
				}
			}
			return runStatus;
		}

		/**
		 * Constructor
		 * 
		 * @param data
		 */
		public JavaSearchOperation(QuerySpecification data) {
			super(data);

		}

		/** List of listeners wanting to know about the searches */
		private final List<IActiveSearchListener> listeners = new ArrayList<IActiveSearchListener>();

		/**
		 * Add a listener for when the bugzilla search is completed
		 * 
		 * @param l
		 *            The listener to add
		 */
		public void addListener(IActiveSearchListener l) {
			// add the listener to the list
			listeners.add(l);
		}

		/**
		 * Remove a listener for when the bugzilla search is completed
		 * 
		 * @param l
		 *            The listener to remove
		 */
		public void removeListener(IActiveSearchListener l) {
			// remove the listener from the list
			listeners.remove(l);
		}

		/**
		 * Notifies all of the listeners that the bugzilla search is completed
		 * 
		 * @param doiList
		 *            A list of BugzillaSearchHitDoiInfo
		 * @param member
		 *            The IMember that the search was performed on
		 */
		public void notifySearchCompleted(final List<Object> l) {
			for (final IActiveSearchListener listener : listeners) {
				SafeRunner.run(new ISafeRunnable() {
					public void run() throws Exception {
						listener.searchCompleted(l);
					}

					public void handleException(Throwable e) {
						StatusHandler.log(new Status(IStatus.ERROR, JavaUiBridgePlugin.ID_PLUGIN, NLS.bind(
								"Unexpected error during listener invocation: {0}", listener.getClass()), e)); //$NON-NLS-1$
					}
				});
			}
		}

	}

	@Override
	public void stopAllRunningJobs() {
		for (Job j : runningJobs) {
			j.cancel();
		}
		runningJobs.clear();
	}

	@Override
	protected int getDefaultDegreeOfSeparation() {
		return DEFAULT_DEGREE;
	}
}
