package de.ugoe.cs.swe.bnftools.validation;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.parsetree.AbstractNode;
import org.eclipse.xtext.parsetree.CompositeNode;
import org.eclipse.xtext.parsetree.LeafNode;
import org.eclipse.xtext.parsetree.NodeUtil;
import org.eclipse.xtext.resource.IResourceDescriptions;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.CheckType;

import com.google.inject.Inject;

import de.ugoe.cs.swe.bnftools.analysis.EbnfAnalysisUtils;
import de.ugoe.cs.swe.bnftools.ebnf.DefinitionList;
import de.ugoe.cs.swe.bnftools.ebnf.EtsiBnf;
import de.ugoe.cs.swe.bnftools.ebnf.ExtRule;
import de.ugoe.cs.swe.bnftools.ebnf.Import;
import de.ugoe.cs.swe.bnftools.ebnf.Rule;
import de.ugoe.cs.swe.bnftools.ebnf.SingleDefinition;

public class EbnfJavaValidator extends AbstractEbnfJavaValidator {

	@Inject
	IResourceDescriptions resourceDescriptions;

	public static final String ruleReferencedOneDescription = "The rule is only referenced by one other rule";
	public static final String passthroughRuleDescription = "The rule is a passthrough rule";
	public static final String unreferencedPassthroughRuleDescription = "The rule is an unreferenced passthrough rule";
	public static final String unusedRuleDescription = "The rule is not referenced anywhere";
	public static final String equalAlternativeDescription = "The rule contains equal alternatives";
	public static final String duplicateRulesDescription = "The rule is a duplicate";
	public static final String duplicateSubRulesDescription = "A part of rule is a duplicate";
	public static final String packageConsistencyCheckRuleMissingDescription = "Rule has been removed or renamed in the updated grammar. The package rule is therefore now an additional rule instead of a replacement rule. The package rule might therefore be inconsistent with the updated core grammar.";
	public static final String packageConsistencyCheckRuleDifferentDescription = "Corresponding core grammar rule has been changed in updated grammar. The package rule might have become inconsistent with the updated core grammar.";
	public static final String packageConsistencyCheckCorrespondingRuleMissingDescription = "Corresponding rule is missing in core grammar. Rule has been manually altered in the delta grammar (delta grammar is outdated) or the /core and /update flags are somehow switched.";

	public static boolean checkReferencedOnlyOnce = false;
	public static boolean checkPassthroughRule = false;
	public static boolean checkUnusedRule = false;
	public static boolean checkEqualAlternative = false;
	public static boolean checkDuplicateRules = false;
	public static boolean checkSubruleDuplicates = false;
	public static boolean checkUpdatedGrammarConsistency = false;

	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkUpdateGrammarConsistency(EtsiBnf bnf) {
		if (!checkUpdatedGrammarConsistency)
			return;

		CompositeNode compNode = NodeUtil.getRootNode(bnf);
		XtextResourceSet set = new XtextResourceSet();
		URI uri = compNode.getElement().eResource().getURI();
		Iterable<AbstractNode> allNodes = NodeUtil.getAllContents(compNode);
		if(!bnf.getType().equals("/delta"))
			return;
		
		Resource coreRes = null;
		Resource updatedRes = null;
		EList<Import> imports = bnf.getImports();

		for(int j=0; j<imports.size(); j++) {
			if(imports.get(j).getGrammarType().equals("core")) {
				String packageUri = uri.trimSegments(1).toPlatformString(true);
				String fullUri = packageUri + "/" +imports.get(j).getImportURI();
				coreRes= set.getResource( URI.createPlatformResourceURI(fullUri, true), true);
			}
			if(imports.get(j).getGrammarType().equals("update")) {
				String packageUri = uri.trimSegments(1).toPlatformString(true);
				String fullUri = packageUri + "/" +imports.get(j).getImportURI();
				updatedRes= set.getResource( URI.createPlatformResourceURI(fullUri, true), true);
			}
		}
		
		if( (coreRes==null) || (updatedRes==null))
			return;

		XtextResource coreResource = null;
		XtextResource updatedResource = null;

		if(coreRes instanceof XtextResource)
			coreResource = (XtextResource) coreRes;
		if(updatedRes instanceof XtextResource)
			updatedResource = (XtextResource) updatedRes;

		/*
		 * the idea: get the core grammar and the updated grammar
		 * for each extension rule in the delta grammar,
		 * 		find the corresponding rule in the updated grammar and the core grammar
		 * 		if it doesn't exist in the updated grammar write the inconsistency
		 * 		if it exists, compare to the rule in the core grammar
		 */
		for(AbstractNode node: allNodes) {
			if(node.getElement() instanceof ExtRule) {
				checkForConsistency(node, coreResource, updatedResource, compNode);
			}
		}
	}
	
	private boolean checkForConsistency(AbstractNode node, XtextResource coreResource, XtextResource updatedResource, CompositeNode compNode) {
		//updated grammar:
		AbstractNode updatedRule = null;
		AbstractNode coreRule = null;
		Rule currentRule = null;
		for(AbstractNode uNode : NodeUtil.getAllContents(updatedResource.getParseResult().getRootNode())) {
			if (uNode.getElement() instanceof Rule) {
				currentRule  = (Rule) uNode.getElement();
				if (currentRule.getName().equals(((ExtRule) node.getElement()).getName())) {
					updatedRule = uNode;
					break;
				}
			}
		}
		
		if(updatedRule == null) {
			warning(EbnfJavaValidator.packageConsistencyCheckRuleMissingDescription, node.getElement(), 1);
			return false;
		} else { //core grammar
			for(AbstractNode cNode : NodeUtil.getAllContents(coreResource.getParseResult().getRootNode())) {
				if(cNode.getElement() instanceof Rule)
					if(((Rule) cNode.getElement()).getName().equals(((ExtRule) node.getElement()).getName())) {
						coreRule = cNode;
						break;
					}
			}
			
			if(coreRule == null){
				warning(EbnfJavaValidator.packageConsistencyCheckCorrespondingRuleMissingDescription, node.getElement(), 1);
				return false;
			}
			
			if (compareRules(coreRule, updatedRule)) {
				return true;
			} else {
				warning(EbnfJavaValidator.packageConsistencyCheckRuleDifferentDescription, node.getElement(), 1);
				return false;
			}
			
		}
	}

	// returns true, if the rules are equal
	private boolean compareRules(AbstractNode cNode, AbstractNode uNode) {
		EList<LeafNode> cLeaves = removeWS(cNode.getLeafNodes());
		EList<LeafNode> uLeaves = removeWS(uNode.getLeafNodes());
		if (cLeaves.size() != uLeaves.size())
			return false;
		
		for(int i=0; i < cLeaves.size(); i++) {
			if(!(cLeaves.get(i).serialize().equals(uLeaves.get(i).serialize())))
				return false;
			
		}
		return true;
	}

	private EList<LeafNode> removeWS(EList<LeafNode> leaves) {
		for(int i=0; i < leaves.size(); i++) {
			if(!(leaves.get(i).getGrammarElement() instanceof org.eclipse.xtext.impl.TerminalRuleImpl))
				leaves.remove(i);
		}
		return leaves;
	}
	
	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkReferencedOnlyOnce(Rule rule) {
		if (!checkReferencedOnlyOnce)
			return;

		if (EbnfAnalysisUtils.isTokenRule(rule))
			return;

		List<Rule> references = EbnfAnalysisUtils.findReferences(rule,
				resourceDescriptions);

		if (references.size() == 1) {
			warning(EbnfJavaValidator.ruleReferencedOneDescription, 1,
					EbnfJavaValidator.ruleReferencedOneDescription);
		}
	}

	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkPassthroughRule(Rule rule) {
		if (!checkPassthroughRule)
			return;

		List<Rule> references = EbnfAnalysisUtils.findReferences(rule,
				resourceDescriptions);

		if (EbnfAnalysisUtils.isPassthroughRule(rule)) {
			if (references.size() == 0) {
				warning(
						EbnfJavaValidator.unreferencedPassthroughRuleDescription,
						1,
						EbnfJavaValidator.unreferencedPassthroughRuleDescription);
			} else {
				warning(EbnfJavaValidator.passthroughRuleDescription, 1,
						EbnfJavaValidator.passthroughRuleDescription);
			}
		}
	}

	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkUnusedRule(Rule rule) {
		if (!checkUnusedRule)
			return;

		List<Rule> references = EbnfAnalysisUtils.findReferences(rule,
				resourceDescriptions);

		if ((references.size() == 0) && (rule.getRulenumber() != 1))
			warning(EbnfJavaValidator.unusedRuleDescription, 1,
					EbnfJavaValidator.unusedRuleDescription);
	}

	// ----------------------------------------------------------------------------------------------------

	private List<DefinitionList> collectDefinitionLists(CompositeNode o) {
		List<DefinitionList> list = new ArrayList<DefinitionList>();

		for (int i = 0; i < o.getChildren().size(); i++) {
			AbstractNode child = o.getChildren().get(i);
			if (child.getElement() instanceof DefinitionList) {
				list.add((DefinitionList) child.getElement());
			}

			if (child instanceof CompositeNode) {
				list.addAll(collectDefinitionLists((CompositeNode) child));
			}
		}

		return list;
	}

	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkEqualAlternative(Rule rule) {
		if (!checkEqualAlternative)
			return;

		// TODO: is not per rule, but global!

		// quadratic!
		CompositeNode startNode = NodeUtil.getNodeAdapter(rule).getParserNode();

		List<DefinitionList> definitionLists = collectDefinitionLists(startNode);

		for (int k = 0; k < definitionLists.size(); k++) {

			EList<SingleDefinition> singleDefinitions = definitionLists.get(k)
					.getSingleDefinition();
			for (int i = 0; i < singleDefinitions.size(); i++) {
				for (int j = 0; j < singleDefinitions.size(); j++) {
					if (i == j)
						continue;

					SingleDefinition upper = singleDefinitions.get(i);
					SingleDefinition lower = singleDefinitions.get(j);
					CompositeNode upperNode = NodeUtil.getNodeAdapter(upper)
							.getParserNode();
					CompositeNode lowerNode = NodeUtil.getNodeAdapter(lower)
							.getParserNode();

					String upperString = upperNode.serialize().trim()
							.replaceAll("[ \t\n\r]", "");
					String lowerString = lowerNode.serialize().trim()
							.replaceAll("[ \t\n\r]", "");

					if (lowerString.equals(upperString))
						warning(EbnfJavaValidator.equalAlternativeDescription,
								1,
								EbnfJavaValidator.equalAlternativeDescription);
				}
			}
		}

	}

	// ----------------------------------------------------------------------------------------------------

	@Check(CheckType.EXPENSIVE)
	public void checkDuplicateRules(Rule rule) {
		if (!checkDuplicateRules)
			return;

		CompositeNode node = NodeUtil.getNodeAdapter(rule).getParserNode();
		CompositeNode root = node.getParent();
		
		AbstractNode last = node.getChildren().get(
				node.getChildren().size() - 1);

		if (!(last instanceof CompositeNode))
			return;

		CompositeNode rightHandSideNode = (CompositeNode) last;
		String rightHandSideText = rightHandSideNode.serialize().trim()
				.replaceAll("[ \t\n\r]", "");

		for (int i = 0; i < root.getChildren().size(); i++) {
			if (root.getChildren().get(i) == node)
				continue;
			if (root.getChildren().get(i) == null)
				continue;
			if (!(root.getChildren().get(i) instanceof CompositeNode))
				continue;
			
			CompositeNode child = (CompositeNode) root.getChildren().get(i);
			
			AbstractNode childLastChild = child.getChildren().get(
					child.getChildren().size() - 1);
			if (childLastChild instanceof CompositeNode) {
				CompositeNode childLastChildCompositeNode = (CompositeNode) childLastChild;
				String text = childLastChildCompositeNode.serialize().trim()
						.replaceAll("[ \t\n\r]", "");
				if (text.equals(rightHandSideText)) {
					Rule matchingRule = (Rule) child.getElement();
					String description = EbnfJavaValidator.duplicateRulesDescription
							+ " with rule "
							+ matchingRule.getRulenumber()
							+ " (Line "
							+ child.getLine() + ")";
					warning(description, 1, description);
				}
			}
		}
	}
	
	// ----------------------------------------------------------------------------------------------------
	
	@Check(CheckType.EXPENSIVE)
	public void checkSubruleDuplicates(Rule rule) {
		if (!checkSubruleDuplicates)
			return;

		//TODO: currently compares complete rules and not subrules
		
		CompositeNode node = NodeUtil.getNodeAdapter(rule).getParserNode();
		CompositeNode root = node.getParent();
		
		AbstractNode last = node.getChildren().get(
				node.getChildren().size() - 1);

		if (!(last instanceof CompositeNode))
			return;

		CompositeNode rightHandSideNode = (CompositeNode) last;
		String rightHandSideText = rightHandSideNode.serialize().trim()
				.replaceAll("[ \t\n\r]", "");

		for (int i = 0; i < root.getChildren().size(); i++) {
			if (root.getChildren().get(i) == node)
				continue;
			if (root.getChildren().get(i) == null)
				continue;
			if (!(root.getChildren().get(i) instanceof CompositeNode))
				continue;
			
			CompositeNode child = (CompositeNode) root.getChildren().get(i);
			
			AbstractNode childLastChild = child.getChildren().get(
					child.getChildren().size() - 1);
			if (childLastChild instanceof CompositeNode) {
				CompositeNode childLastChildCompositeNode = (CompositeNode) childLastChild;
				String text = childLastChildCompositeNode.serialize().trim()
						.replaceAll("[ \t\n\r]", "");
				if (text.equals(rightHandSideText)) {
					Rule matchingRule = (Rule) child.getElement();
					String description = EbnfJavaValidator.duplicateSubRulesDescription
							+ " with rule "
							+ matchingRule.getRulenumber()
							+ " (Line "
							+ child.getLine() + ")";
					warning(description, 1, description);
				}
			}
		}
	}
	
}
