/*******************************************************************************
 * Copyright (c) 2011 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;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.mylyn.context.core.IInteractionContext;

/**
 * A strategy that computes Java items based on a Java stack trace in the task description.
 * 
 * @author David Green
 */
public class JavaStackTraceContextComputationStrategy extends AbstractJavaContextComputationStrategy {

	private static final String PACKAGE_PART = "([a-z][a-z0-9]*)"; //$NON-NLS-1$

	private static final String CLASS_PART = "[A-Za-z][a-zA-Z0-9_$]*"; //$NON-NLS-1$

	private static final String FQN_PART = "((" + PACKAGE_PART + "\\.)+" + CLASS_PART + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

	private static final Pattern STACK_TRACE_PATTERN = Pattern.compile("\\s*((" + "((Caused by:\\s+)|(at\\s+))?" //$NON-NLS-1$//$NON-NLS-2$
			+ FQN_PART + "((:\\s+\\w.*)|(\\.((\\<(?:cl)?init\\>)|([a-zA-Z0-9_$]+))\\(.*?\\)))?" //$NON-NLS-1$
			+ ")|(\\.\\.\\.\\s\\d+\\smore))"); //$NON-NLS-1$ 

	private final SortedSet<String> filteredPrefixes = new TreeSet<String>(Arrays.asList(new String[] { "java", //$NON-NLS-1$
			"javax", "junit.framework", "sun.reflect" })); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$

	/**
	 * public for testing only
	 */
	public static class Element {
		String fqn;

		String methodName;

		public Element(String fqn, String methodName) {
			Assert.isNotNull(fqn);
			this.fqn = fqn;
			this.methodName = methodName;
		}

		public Element() {
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append("Element [fqn="); //$NON-NLS-1$
			builder.append(fqn);
			builder.append(", methodName="); //$NON-NLS-1$
			builder.append(methodName);
			builder.append("]"); //$NON-NLS-1$
			return builder.toString();
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((fqn == null) ? 0 : fqn.hashCode());
			result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			Element other = (Element) obj;
			if (fqn == null) {
				if (other.fqn != null) {
					return false;
				}
			} else if (!fqn.equals(other.fqn)) {
				return false;
			}
			if (methodName == null) {
				if (other.methodName != null) {
					return false;
				}
			} else if (!methodName.equals(other.methodName)) {
				return false;
			}
			return true;
		}

	}

	private final int maxElements = 10;

	@Override
	public List<Object> computeContext(IInteractionContext context, IAdaptable input, IProgressMonitor monitor) {
		String text = getText(input);
		if (text != null) {
			return computeContext(text, monitor);
		}
		return Collections.emptyList();
	}

	private String getText(IAdaptable input) {
		String text = (String) input.getAdapter(String.class);
		if (text != null) {
			return text;
		}
		return null;
	}

	public List<Object> computeContext(String description, IProgressMonitor monitor) {
		SubMonitor progress = SubMonitor.convert(monitor);
		try {
			List<Element> elements = computeElements(description);
			if (!elements.isEmpty()) {

				progress.beginTask(
						Messages.JavaStackTraceContextComputationStrategy_Finding_Java_Context_Element_Progress_Label,
						elements.size());

				final List<Object> javaElements = new ArrayList<Object>();
				try {
					for (Element element : elements) {
						if (progress.isCanceled()) {
							break;
						}

						SortedSet<String> prefix = filteredPrefixes.headSet(element.fqn);
						if (prefix.isEmpty() || !element.fqn.startsWith(prefix.last())) {
							try {
								IType type = findTypeInWorkspace(element.fqn);
								if (type != null) {
									javaElements.add(type);
									if (element.methodName != null) {
										IMethod[] methods = type.getMethods();
										for (IMethod method : methods) {
											if (method.getElementName().equals(element.methodName)) {
												javaElements.add(method);
											}
										}
									}
								}
							} catch (CoreException e) {
								JavaUiBridgePlugin.getDefault().getLog().log(e.getStatus());
							}
						}
						progress.worked(1);
					}
				} finally {
					progress.done();
				}
				return javaElements;
			}
		} catch (IOException e) {
			// ignore
		}
		return Collections.emptyList();
	}

	/**
	 * Public for test purposes only
	 */
	public List<Element> computeElements(String description) throws IOException {
		List<Element> elements = new ArrayList<Element>();

		BufferedReader reader = new BufferedReader(new StringReader(description));
		for (String line = reader.readLine(); line != null && elements.size() < maxElements; line = reader.readLine()) {
			Matcher matcher = STACK_TRACE_PATTERN.matcher(line);
			if (matcher.matches()) {
				String fqn = matcher.group(6);
				if (fqn != null) {
					Element element = new Element(fqn, matcher.group(12));
					elements.add(element);
				}
			}
		}
		return elements;
	}
}
