package org.eclipse.emf.test.ecore.xcore.legacy_xpect_runner;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.emf.common.util.URI;
import org.eclipse.xtext.nodemodel.ILeafNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.parsetree.reconstr.impl.NodeIterator;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.Exceptions;
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.util.Wrapper;
import org.eclipse.xtext.util.formallang.FollowerFunctionImpl;
import org.eclipse.xtext.util.formallang.Nfa;
import org.eclipse.xtext.util.formallang.NfaUtil;
import org.eclipse.xtext.util.formallang.NfaUtil.BacktrackHandler;
import org.eclipse.xtext.util.formallang.StringProduction;
import org.eclipse.xtext.util.formallang.StringProduction.ElementType;
import org.eclipse.xtext.util.formallang.StringProduction.ProdElement;
import org.junit.Test;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;

/**
 * This class will be removed in the next release after 2.4.2
 * 
 * @author Moritz Eysholdt - Initial contribution and API
 */
@SuppressWarnings("restriction")
public class XpectParameterProvider implements IParameterProvider {

	protected static class AssignedProduction extends StringProduction {

		public AssignedProduction(String production) {
			super(production);
		}

		@Override
		protected ProdElement parsePrim(Stack<Pair<Token, String>> tokens) {
			Pair<Token, String> current = tokens.pop();
			switch (current.getFirst()) {
			case PL:
				ProdElement result1 = parseAlt(tokens);
				if (tokens.peek().getFirst().equals(Token.PR))
					tokens.pop();
				else
					throw new RuntimeException("')' expected, but "
							+ tokens.peek().getFirst() + " found");
				parseCardinality(tokens, result1);
				return result1;
			case STRING:
				ProdElement result2 = createElement(ElementType.TOKEN);
				result2.setValue(current.getSecond());
				parseCardinality(tokens, result2);
				return result2;
			case ID:
				ProdElement result3 = createElement(ElementType.TOKEN);
				result3.setName(current.getSecond());
				Pair<Token, String> eq = tokens.pop();
				if (eq.getFirst() == Token.EQ) {
					Pair<Token, String> val = tokens.pop();
					switch (val.getFirst()) {
					case ID:
					case STRING:
						result3.setValue(val.getSecond());
						break;
					default:
						throw new RuntimeException("Unexpected token "
								+ current.getFirst());
					}
				} else
					throw new RuntimeException("Unexpected token "
							+ eq.getFirst() + ", expected '='");
				parseCardinality(tokens, result3);
				return result3;
			default:
				throw new RuntimeException("Unexpected token "
						+ current.getFirst());
			}
		}
	}

	protected static class BacktrackItem {
		protected int offset;
		protected ProdElement token;
		protected String value;

		public BacktrackItem(int offset) {
			super();
			this.offset = offset;
		}

		public BacktrackItem(int offset, ProdElement token, String value) {
			super();
			this.offset = offset;
			this.token = token;
			this.value = value;
		}

		@Override
		public String toString() {
			return token + ":" + value;
		}
	}

	protected static class Expectation implements IExpectation {
		protected String expectation;
		protected String indentation = null;
		protected int lenght;
		protected int offset;

		public Expectation(int offset, int lenght, String expectation) {
			super();
			this.offset = offset;
			this.lenght = lenght;
			this.expectation = expectation;
		}

		public Expectation(int offset, int lenght, String expectation,
				String indentation) {
			super();
			this.offset = offset;
			this.lenght = lenght;
			this.expectation = expectation;
			this.indentation = indentation;
		}

		public String getExpectation() {
			return expectation;
		}

		public String getIndentation() {
			return indentation;
		}

		public int getLength() {
			return lenght;
		}

		public int getOffset() {
			return offset;
		}

	}

	protected enum Token {
		ID("[a-zA-Z][a-zA-Z0-9_]*"), //
		INT("[0-9]+"), //
		OFFSET("'([^']*)'|[^\\s]+"), //
		STRING("'([^']*)'"), //
		TEXT("[^\\s]+");

		public final Pattern pattern;

		private Token(String regex) {
			this.pattern = Pattern.compile("^" + regex);
		}
	}

	public final static String PARAM_OFFSET = "offset";
	public final static String PARAM_RESOURCE = "resource";

	protected static final Pattern WS = Pattern.compile("^[\\s]+");

	protected static Pattern XPECT_PATTERN = Pattern
			.compile("(\\S)?XPECT(_CLASS|_IMPORT)?\\s+([a-zA-Z0-9]*)");

	public void collectParameters(Class<?> testClass, XtextResource resource,
			IParameterAcceptor acceptor) {
		collectTestMethods(testClass, resource, acceptor);
		for (ILeafNode leaf : resource.getParseResult().getRootNode()
				.getLeafNodes())
			if (leaf.isHidden() && leaf.getText().contains("XPECT"))
				parseLeaf(testClass, resource, leaf, acceptor);

	}

	protected void collectTestMethods(Class<?> testClass, XtextResource res,
			IParameterAcceptor acceptor) {
		for (Method meth : testClass.getMethods()) {
			if (Modifier.isPublic(meth.getModifiers())
					&& !Modifier.isStatic(meth.getModifiers())) {
				Test annotation = meth.getAnnotation(Test.class);
				if (annotation != null)
					acceptor.acceptTest(null, meth.getName(),
							getDefaultParams(res, 0), null, false);
			}
		}
	}

	protected Iterable<Object> convertValue(XtextResource res, INode ctx,
			int offset, Token token, String value) {
		switch (token) {
		case OFFSET:
			int add = value.indexOf('|');
			if (add >= 0)
				value = value.substring(0, add) + value.substring(add + 1);
			else
				add = 0;
			String text = ctx.getRootNode().getText();
			int result = text.indexOf(value, offset);
			if (result >= 0) {
				int off = result + add;
				return Lists.newArrayList(off, new Offset(res, off));
			} else
				throw new RuntimeException("OFFSET '" + value + "' not found");
		case INT:
			List<Object> r = Lists.newArrayList();
			try {
				r.add(Integer.valueOf(value));
			} catch (NumberFormatException e) {
			}
			try {
				r.add(new BigInteger(value));
			} catch (NumberFormatException e) {
			}
			r.add(value);
			return r;
		case ID:
		case STRING:
		case TEXT:
			return Collections.<Object> singleton(value);
		}
		return Collections.<Object> singleton(value);
	}

	protected Multimap<String, Object> getDefaultParams(XtextResource res,
			int offset) {
		Multimap<String, Object> result = HashMultimap.create();
		result.put(PARAM_RESOURCE, res);
		result.put(PARAM_OFFSET, offset);
		result.put(PARAM_OFFSET, new Offset(res, offset));
		return result;
	}

	protected String getIndentation(INode ctx, int offset) {
		String text = ctx.getRootNode().getText();
		int nl = text.lastIndexOf("\n", offset);
		if (nl < 0)
			nl = 0;
		StringBuilder result = new StringBuilder();
		for (int i = nl + 1; i < text.length()
				&& Character.isWhitespace(text.charAt(i)); i++)
			result.append(text.charAt(i));
		return result.toString();
	}

	protected int getOffsetOfNextSemanticNode(INode node) {
		Iterator<INode> it = new NodeIterator(node);
		while (it.hasNext()) {
			INode n = it.next();
			if (n instanceof ILeafNode && !((ILeafNode) n).isHidden())
				return n.getOffset();
		}
		return node.getEndOffset();
	}

	protected Nfa<ProdElement> getParameterNfa(String syntax) {
		AssignedProduction prod = new AssignedProduction(syntax);
		FollowerFunctionImpl<ProdElement, String> ff = new FollowerFunctionImpl<ProdElement, String>(
				prod);
		ProdElement start = prod.new ProdElement(ElementType.TOKEN);
		ProdElement stop = prod.new ProdElement(ElementType.TOKEN);
		Nfa<ProdElement> result = new NfaUtil().create(prod, ff, start, stop);
		return result;
	}

	protected String getParameterSyntax(Class<?> testClass, String methodName) {
		try {
			Method method = testClass.getMethod(methodName);
			ParameterSyntax annotation = method
					.getAnnotation(ParameterSyntax.class);
			if (annotation != null)
				return annotation.value();
		} catch (SecurityException e) {
			Exceptions.throwUncheckedException(e);
		} catch (NoSuchMethodException e) {
			Exceptions.throwUncheckedException(e);
		}
		return null;
	}

	protected void parseLeaf(Class<?> testClass, XtextResource resource,
			ILeafNode leaf, IParameterAcceptor acceptor) {
		String text = leaf.getText();
		Matcher matcher = XPECT_PATTERN.matcher(text);
		int offset = 0;
		while (offset < text.length() && matcher.find(offset)) {
			if (matcher.group(2) == null) {
				int newOffset;
				if ((newOffset = parseXpect(testClass, resource, leaf, text,
						matcher.group(3), matcher.end(), acceptor,
						matcher.group(1) != null)) >= 0)
					offset = newOffset;
				else
					offset = matcher.end();
			} else if ("_IMPORT".equals(matcher.group(2))) {
				offset = parseXpectImport(resource, text, matcher.end(2),
						acceptor);
			}
			// } else {
			// int newOffset;
			// if ((newOffset = parseXpectTest(testClass, text, matcher.end(),
			// acceptor)) >= 0)
			// offset = newOffset;
			// else
			// offset = matcher.end();
			// }
		}
	}

	protected int parseString(String text, int offset, Wrapper<String> value) {
		if (offset < text.length() && text.charAt(offset) == '"') {
			int i = offset + 1;
			while (offset < text.length() && text.charAt(i - 1) == '\\'
					|| text.charAt(i) != '"')
				i++;
			if (text.charAt(i) == '"') {
				value.set(text.substring(offset + 1, i - 1));
				return i;
			}
		}
		return -1;
	}

	protected int parseStringOrText(String text, int offset,
			Wrapper<String> value) {
		int newOffset = parseString(text, offset, value);
		if (newOffset < 0)
			newOffset = parseText(text, offset, value);
		return newOffset;
	}

	protected int parseText(String text, int offset, Wrapper<String> value) {
		int i = offset;
		while (i < text.length())
			switch (text.charAt(i)) {
			case ' ':
			case '\t':
			case '\r':
			case '\n':
				value.set(text.substring(offset, i));
				return i;
			default:
				i++;
			}
		value.set(text.substring(offset, i));
		return i;
	}

	protected int parseXpect(Class<?> testClass, XtextResource res, INode ctx,
			String text, String method, int offset,
			IParameterAcceptor acceptor, boolean ignore) {
		int newOffset;
		Multimap<String, Object> params = HashMultimap.create();
		Wrapper<Expectation> expectation = new Wrapper<Expectation>(null);
		offset = skipWhitespace(text, offset);
		if ((newOffset = parseXpectParams(testClass, res, ctx, method, text,
				offset, params)) >= 0)
			offset = newOffset;
		offset = skipWhitespace(text, offset);
		if ((newOffset = parseXpectSLExpectation(ctx, text, offset, expectation)) >= 0)
			offset = newOffset;
		else if ((newOffset = parseXpectMLExpectation(ctx, text, offset,
				expectation)) >= 0)
			offset = newOffset;
		acceptor.acceptTest(null, method, params, expectation.get(), ignore);
		return offset;
	}

	protected int parseXpectImport(XtextResource res, String text, int offset,
			IParameterAcceptor acceptor) {
		offset = skipWhitespace(text, offset);
		int end = text.indexOf("\n", offset);
		String fileName = text.substring(offset, end).trim();
		URI uri = URI.createURI(fileName);
		if (uri.isRelative() && !res.getURI().isRelative())
			uri = uri.resolve(res.getURI());
		acceptor.acceptImportURI(uri);
		return end;
	}

	protected int parseXpectMLExpectation(INode node, String text, int offset,
			Wrapper<Expectation> expectation) {
		if (offset + 3 < text.length()
				&& text.substring(offset, offset + 3).equals("---")) {
			String indentation = getIndentation(node, node.getOffset() + offset);
			int start = text.indexOf('\n', offset + 3);
			int end = text.indexOf("---", offset + 3);
			if (start >= 0 && end >= 0) {
				String substring = text.substring(start + 1, end);
				end = substring.lastIndexOf('\n');
				if (end >= 0) {
					String exp = substring.substring(0, end);
					int len = exp.length();
					if (exp.startsWith(indentation))
						exp = exp.substring(indentation.length());
					exp = exp.replace("\n" + indentation, "\n");
					expectation.set(new Expectation(node.getOffset() + start
							+ 1, len, exp, indentation));
					return end + start + 1;
				}
			}
		}
		return -1;
	}

	protected int parseXpectParams(Class<?> testClass, XtextResource res,
			INode node, String methodName, final String text, int offset,
			Multimap<String, Object> params) {
		int semanticOffset = getOffsetOfNextSemanticNode(node);
		params.putAll(getDefaultParams(res, semanticOffset));
		String paramSyntax = getParameterSyntax(testClass, methodName);
		if (Strings.isEmpty(paramSyntax))
			return -1;
		Nfa<ProdElement> nfa = getParameterNfa(paramSyntax);
		List<BacktrackItem> trace = new NfaUtil().backtrack(nfa,
				new BacktrackItem(offset),
				new BacktrackHandler<ProdElement, BacktrackItem>() {
					public BacktrackItem handle(ProdElement state,
							BacktrackItem previous) {
						if (Strings.isEmpty(state.getValue()))
							return previous;
						if (Strings.isEmpty(state.getName())) {
							if (text.regionMatches(previous.offset, state
									.getValue(), 0, state.getValue().length())) {
								int newOffset = previous.offset
										+ state.getValue().length();
								Matcher ws = WS.matcher(text).region(newOffset,
										text.length());
								int childOffset = ws.find() ? ws.end()
										: newOffset;
								return new BacktrackItem(childOffset, state,
										state.getValue());
							}
						} else {
							Token t = Token.valueOf(state.getValue());
							Matcher matcher = t.pattern.matcher(text).region(
									previous.offset, text.length());
							if (matcher.find()) {
								Matcher ws = WS.matcher(text).region(
										matcher.end(), text.length());
								int childOffset = ws.find() ? ws.end()
										: matcher.end();
								String value = matcher.groupCount() > 0
										&& matcher.group(1) != null ? matcher
										.group(1) : matcher.group(0);
								return new BacktrackItem(childOffset, state,
										value);
							}
						}
						return null;
					}

					public boolean isSolution(BacktrackItem result) {
						return true;
					}

					public Iterable<ProdElement> sortFollowers(
							BacktrackItem result,
							Iterable<ProdElement> followers) {
						return followers;
					}
				});
		if (trace != null && !trace.isEmpty()) {
			for (BacktrackItem item : trace)
				if (item.token != null && item.token.getName() != null) {
					String key = item.token.getName();
					params.removeAll(key);
					params.putAll(
							key,
							convertValue(res, node, semanticOffset,
									Token.valueOf(item.token.getValue()),
									item.value));
				}
			return trace.get(trace.size() - 1).offset;
		}
		return -1;
	}

	protected int parseXpectSLExpectation(INode node, String text, int offset,
			Wrapper<Expectation> expectation) {
		if (offset + 3 < text.length()
				&& text.substring(offset, offset + 3).equals("-->")) {
			int begin = offset + 3;
			if (text.charAt(begin) == '\r' || text.charAt(begin) == '\n') {
				expectation
						.set(new Expectation(node.getOffset() + begin, 0, ""));
				return begin;
			} else if (Character.isWhitespace(text.charAt(begin)))
				begin++;
			int end = text.indexOf('\n', begin);
			if (end < 0)
				end = text.length();
			String exp = text.substring(begin, end);
			expectation.set(new Expectation(node.getOffset() + begin, exp
					.length(), exp));
			return end;
		}
		return -1;
	}

	// protected int parseXpectTest(Wrapper<Class<?>> test, String text, int
	// offset, IParameterAcceptor acceptor) {
	// int index = text.indexOf("\n", offset);
	// if (index > offset) {
	// String name = text.substring(offset, index).trim();
	// try {
	// Class<?> clazz = Class.forName(name);
	// acceptor.acceptTestClass(clazz);
	// } catch (ClassNotFoundException e) {
	// Exceptions.throwUncheckedException(e);
	// }
	// return index;
	// }
	// return -1;
	// }

	protected int skipWhitespace(String text, int offset) {
		int i = offset;
		while (i < text.length())
			if (Character.isWhitespace(text.charAt(i)))
				i++;
			else
				return i;
		return i;
	}

}
