/*******************************************************************************
 * Copyright (c) 2004, 2008 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.pde.ui;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.internal.resources.File;
import org.eclipse.core.internal.resources.Workspace;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
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.ContextCorePlugin;
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.ide.ui.XmlNodeHelper;
import org.eclipse.mylyn.internal.java.ui.search.XmlActiveSearchUpdater;
import org.eclipse.mylyn.internal.resources.ui.ResourcesUiBridgePlugin;
import org.eclipse.search.core.text.TextSearchScope;
import org.eclipse.search.internal.ui.text.FileSearchQuery;
import org.eclipse.search.internal.ui.text.FileSearchResult;
import org.eclipse.search.ui.ISearchResult;
import org.eclipse.search.ui.text.FileTextSearchScope;
import org.eclipse.search.ui.text.Match;

/**
 * @author Shawn Minto
 * @author Mik Kersten
 */
@SuppressWarnings("restriction")
public class XmlJavaRelationProvider extends AbstractRelationProvider {

	public static final String SOURCE_ID = "org.eclipse.mylyn.xml.search.references"; //$NON-NLS-1$

	public static final String NAME = "referenced by"; //$NON-NLS-1$

	public static final int DEFAULT_DEGREE = 3;

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

	public static final Map<Match, XmlNodeHelper> nodeMap = new HashMap<Match, XmlNodeHelper>();

	public XmlJavaRelationProvider() {
		// TODO: should this be a generic XML extension?
		super(PdeStructureBridge.CONTENT_TYPE, SOURCE_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.getContentType().equals("java")) { //$NON-NLS-1$
			return;
		}
		IJavaElement javaElement = JavaCore.create(node.getHandleIdentifier());
		if (javaElement == null || javaElement instanceof ICompilationUnit || !javaElement.exists()) {
			return;
		}
		if (!acceptElement(javaElement)) {
			return;
		}

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

	protected TextSearchScope createTextSearchScope(int degreeOfSeparation) {
		Set<IInteractionElement> landmarks = ContextCore.getContextManager().getActiveLandmarks();

		switch (degreeOfSeparation) {
		case 1:
			// create a search scope for the projects of landmarks
			Set<IResource> l = new HashSet<IResource>();
			for (IInteractionElement landmark : landmarks) {
				if (landmark.getContentType().equals(PdeStructureBridge.CONTENT_TYPE)) {
					// ||
					// landmark.getContentType().equals(AntStructureBridge.CONTENT_TYPE))
					// {
					String handle = landmark.getHandleIdentifier();
					IResource element = null;
					int first = handle.indexOf(";"); //$NON-NLS-1$
					String filename = handle;
					if (first != -1) {
						filename = handle.substring(0, first);
					}
					try {
						// change the file into a document
						IPath path = new Path(filename);
						element = ((Workspace) ResourcesPlugin.getWorkspace()).newResource(path, IResource.FILE);
					} catch (Exception e) {
						StatusHandler.log(new Status(IStatus.WARNING, PdeUiBridgePlugin.ID_PLUGIN,
								"Scope creation failed", e)); //$NON-NLS-1$
					}
					l.add(element);
				}
			}

			IResource[] res = new IResource[l.size()];
			res = l.toArray(res);
			TextSearchScope doiScope = FileTextSearchScope.newSearchScope(res,
					new String[] { PdeStructureBridge.CONTENT_TYPE }, false);
			return l.isEmpty() ? null : doiScope;
		case 2:
			// create a search scope for the projects of landmarks
			Set<IProject> projectsToSearch = new HashSet<IProject>();
			for (IInteractionElement landmark : landmarks) {
				AbstractContextStructureBridge bridge = ContextCore.getStructureBridge(landmark.getContentType());
				IResource resource = ResourcesUiBridgePlugin.getDefault().getResourceForElement(landmark, true);
				IProject project = null;
				if (resource != null) {
					project = resource.getProject();
				} else {
					Object object = bridge.getObjectForHandle(landmark.getHandleIdentifier());
					if (object instanceof IJavaElement) {
						project = ((IJavaElement) object).getJavaProject().getProject();
					}
				}
				if (project != null) {
					projectsToSearch.add(project);
				}
			}

			res = new IProject[projectsToSearch.size()];
			res = projectsToSearch.toArray(res);
			TextSearchScope projScope = FileTextSearchScope.newSearchScope(res,
					new String[] { PdeStructureBridge.CONTENT_TYPE }, false);

			return projectsToSearch.isEmpty() ? null : projScope;
		case 3:
			// create a search scope for the workspace
			return FileTextSearchScope.newSearchScope(new IResource[] { ResourcesPlugin.getWorkspace().getRoot() },
					new String[] { PdeStructureBridge.CONTENT_TYPE }, false);
		case 4:
			// create a search scope for the workspace
			return FileTextSearchScope.newSearchScope(new IResource[] { ResourcesPlugin.getWorkspace().getRoot() },
					new String[] { PdeStructureBridge.CONTENT_TYPE }, false);
		default:
			return null;
		}

	}

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

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

		// get the fully qualified name and if it is null, don't search
		String fullyQualifiedName = getFullyQualifiedName(javaElement);

		if (fullyQualifiedName == null) {
			return;
		}

		// Create the search query
		final XMLSearchOperation query = (XMLSearchOperation) getSearchOperation(node, 0, degreeOfSeparation);
		if (query != null) {
			XMLSearchJob job = new XMLSearchJob(query.getLabel(), query);
			query.addListener(new IActiveSearchListener() {

				private boolean gathered = false;

				public void searchCompleted(List<?> l) {
					// deal with File
					if (l.isEmpty()) {
						return;
					}

					Map<String, String> nodes = new HashMap<String, String>();

					if (l.get(0) instanceof FileSearchResult) {
						FileSearchResult fsr = (FileSearchResult) l.get(0);

						Object[] far = fsr.getElements();
						for (Object element : far) {
							Match[] mar = fsr.getMatches(element);

							if (element instanceof File) {
								File f = (File) element;

								// change the file into a document
								// FileEditorInput fei = new FileEditorInput(f);

								for (Match m : mar) {
									try {
										AbstractContextStructureBridge bridge = ContextCorePlugin.getDefault()
												.getStructureBridge(f.getName());
										String handle = bridge.getHandleForOffsetInObject(f, m.getOffset());
										if (handle != null) {
											String second = handle.substring(handle.indexOf(";")); //$NON-NLS-1$

											XmlNodeHelper xnode = new XmlNodeHelper(f.getFullPath().toString(), second);
											nodeMap.put(m, xnode);
											Object o = bridge.getObjectForHandle(handle);
											String name = bridge.getLabel(o);
											if (o != null) {
												nodes.put(handle, name);
											}
										}
									} catch (Exception e) {
										StatusHandler.log(new Status(IStatus.ERROR, PdeUiBridgePlugin.ID_PLUGIN,
												"Unable to create match", e)); //$NON-NLS-1$
									}
								}
							}
						}
					}

					for (String handle : nodes.keySet()) {

						incrementInterest(node, PdeStructureBridge.CONTENT_TYPE, handle, degreeOfSeparation);
					}
					gathered = true;
					XmlJavaRelationProvider.this.searchCompleted(node);
				}

				public boolean resultsGathered() {
					return gathered;
				}
			});
			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());
		TextSearchScope scope = createTextSearchScope(degreeOfSeparation);
		if (scope == null) {
			return null;
		}

		String fullyQualifiedName = getFullyQualifiedName(javaElement);

		return new XMLSearchOperation(scope, fullyQualifiedName);
	}

	private String getFullyQualifiedName(IJavaElement je) {
		if (!(je instanceof IMember)) {
			return null;
		}

		IMember m = (IMember) je;
		if (m.getDeclaringType() == null) {
			return ((IType) m).getFullyQualifiedName();
		} else {
			return m.getDeclaringType().getFullyQualifiedName() + "." + m.getElementName(); //$NON-NLS-1$
		}
	}

	public static class XMLSearchJob extends Job {

		private final XMLSearchOperation op;

		/**
		 * Constructor
		 * 
		 * @param name
		 */
		public XMLSearchJob(String name, XMLSearchOperation op) {
			super(name);
			this.op = op;
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			return op.run(monitor);
		}

	}

	public static class XMLSearchOperation extends FileSearchQuery implements IActiveSearchOperation {

		@Override
		public ISearchResult getSearchResult() {
			try {
				// get the current page of the outline
				Class<?> clazz = FileSearchQuery.class;
				Field field = clazz.getDeclaredField("fResult"); //$NON-NLS-1$
				field.setAccessible(true);
				FileSearchResult fResult = (FileSearchResult) field.get(this);
				if (fResult == null) {
					fResult = new FileSearchResult(this);
					field.set(this, fResult);
					new XmlActiveSearchUpdater(fResult);
				}
				return fResult;
			} catch (Exception e) {
				StatusHandler.log(new Status(IStatus.WARNING, PdeUiBridgePlugin.ID_PLUGIN,
						"Failed to get search result: " + e.getMessage())); //$NON-NLS-1$
			}
			return super.getSearchResult();
		}

		@Override
		public IStatus run(IProgressMonitor monitor) {
			try {
				super.run(monitor);
				ISearchResult result = getSearchResult();
				if (result instanceof FileSearchResult) {
					List<Object> l = new ArrayList<Object>();
					if (((FileSearchResult) result).getElements().length != 0) {
						l.add(result);
					}

					notifySearchCompleted(l);
				}
				return Status.OK_STATUS;
			} catch (Throwable t) {
				return new Status(IStatus.WARNING, ContextCorePlugin.ID_PLUGIN, 0,
						Messages.XmlJavaRelationProvider_Skipped_XML_search, null);
			}
		}

		/**
		 * Constructor
		 * 
		 * @param data
		 */
		public XMLSearchOperation(TextSearchScope scope, String searchString) {
			super(searchString, false, true, (FileTextSearchScope) scope);
		}

		/** 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);
		}

		/**
		 * Notify 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(List<Object> l) {
			// go through all of the listeners and call
			// searchCompleted(colelctor,
			// member)
			for (IActiveSearchListener listener : listeners) {
				listener.searchCompleted(l);
			}
		}

	}

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

	@Override
	protected String getSourceId() {
		return SOURCE_ID;
	}

	@Override
	public String getName() {
		return NAME;
	}

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

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