package org.deft.repository.query;


import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.deft.extension.tokenlayouter.TokenLayouter;
import org.deft.extension.trim.JavaAutoTrimmer;
import org.deft.repository.CodeSnippetRef;
import org.deft.repository.ast.Token;
import org.deft.repository.ast.TokenNode;
import org.deft.repository.ast.TokenNodeComparator;
import org.deft.repository.ast.TreeNode;
import org.deft.repository.ast.annotation.Format;
import org.deft.repository.ast.annotation.Range;
import org.deft.repository.ast.annotation.ReplaceGroup;
import org.deft.repository.ast.annotation.Templates;
import org.deft.repository.ast.annotation.csformat.CSFormatInformation;
import org.deft.repository.ast.annotation.selected.SelectedInformation;



public class XfsrQueryManager {
	
	public XfsrQueryManager() {
	}
	

	
	public void queryAndFormat(TreeNode treeNode, Query query, 
			Format format, UUID refId) {
		List<TreeNode> queriedNodes = query(treeNode, query, refId);
		if (queriedNodes.size() > 0 && format != null) {
			doFormat(queriedNodes, format);
		}
	}
	
	
	public List<TreeNode> query(TreeNode treeNode, Query query, UUID refId) {
		List<TreeNode> queriedNodes = new LinkedList<TreeNode>();
        List<String> queryStrings = query.getQueryStrings();
        for (String qs : queryStrings) {
        	List<TreeNode> nodes = treeNode.executeXPathQuery(qs);
        	for (TreeNode node : nodes) {
        		SelectedInformation info = new SelectedInformation();            		
        		node.addInformation(info);
//commented out for standalone formatting example
//        		if (refId == null)
//        			info.addSelectedData(qs, null);
//        		else
//        			info.addSelectedData(qs, (CodeSnippetRef) RepositoryFactory.getRepository().getReference(refId));
        		info.addSelectedData(qs, null);
        		queriedNodes.add(node);
        	}    
        }		
        return queriedNodes;
	}
	
	

	/**
	 * Marks AST nodes with information such as "to be hidden" or "not to be hidden"
	 * @param queriedNodes
	 * @param csId
	 * @param format
	 */
	private void doFormat(List<TreeNode> queriedNodes, Format format) {
		if (queriedNodes.size() <= 0) 
			return;

		//for all targets
		for (String target : format.getTargetList()) {
			String sXpathTarget = "../" + target;
			//for all replace groups
			for (ReplaceGroup rg : format.getReplaceGroups(target)) {
				//mark nodes to be hidden:
				//first mark the nodes identified with the ReplaceGroup's "hide"  
				for (String sHide : rg.getHide()) {
					String sXpath = sXpathTarget + sHide;
					
					for (TreeNode node : queriedNodes) {
						//the list lNodesToHide might contain too many nodes
						//if, for example, node is the second ClassMemberDeclaration (C#), then
						//the evaluation of sXpath is done for all ClassMemberDeclarations
						//(due to the ../ in sXpathTarget)
						List<TreeNode> lNodesToHide = node.executeXPathQuery(sXpath);
						for (TreeNode n : lNodesToHide) {
							CSFormatInformation csfi = new CSFormatInformation(format, target, rg.getId());
							n.addInformation(csfi);
						}
					}
				}

				//then mark nodes which are NOT identified by ReplaceGroup's "hideExcept"
				//The idea is to mark the nodes to be removed. In a later processing stage
				//these nodes will pass their information to all their token nodes. 
				//Therefore we must take care to only mark nodes whose descendants are
				//NOT marked as "not to be deleted".
				//
				//collect the "root" nodes which are not to be hidden 
				//these are the nodes to which the hideExcept-xpaths point
				Set<TreeNode> stNoHide = new HashSet<TreeNode>();
				for (String sNoHide : rg.getHideExcept()) {
					String sXpath = sXpathTarget + sNoHide;
					for (TreeNode node : queriedNodes)
						for (TreeNode n : node.executeXPathQuery(sXpath)) {
							stNoHide.add(n);
					}
				}
				//set will contain the parents from stNoHide's elements
				Set<TreeNode> stNonHiddenNodesParents = new HashSet<TreeNode>();
				//set will contain all ancestors from stNoHide's elements
				//Nodes from this set must NOT be marked for removal, or otherwise
				//the nodes from stNoHide would be (implicitly) marked for removal
				Set<TreeNode> stNonHiddenNodesAncestors = new HashSet<TreeNode>(); 
				//for all elements not to be hidden
				for (TreeNode node : stNoHide) {
					//add parent and ancestors to the appropriate sets
					stNonHiddenNodesAncestors.add(node);
					TreeNode parent = node.getParent();
					//we are checking for instanceof Element here and below
					//because the parent of the root element is of type Document,
					//which we don't want
					if (parent != null) {
						stNonHiddenNodesParents.add(parent);
					}
					while (parent != null) {
						stNonHiddenNodesAncestors.add(parent);
						parent = parent.getParent();
					}
				}
				//from the data prepared above we can now mark the nodes to be hidden:
				//for all siblings of the nodes not to be removed
				//(this is written as: for all children of the parents of the 
				//nodes not to be removed)
				//do the following
				for (TreeNode node : stNonHiddenNodesParents) {
					List<TreeNode> children = node.getChildren();
					for (TreeNode child : children) {
						//if the current sibling is not an ancestor
						//of a "node not to be removed", mark it for removal
						if (!stNonHiddenNodesAncestors.contains(child)) {
							CSFormatInformation csfi = new CSFormatInformation(format, target, rg.getId());
							child.addInformation(csfi);
						}
					}
				}		            	
			}

		}
	}
	
	public TreeNode cutAndFormat(TreeNode root) {
		TreeNode newRoot = new TreeNode("root");
		List<TreeNode> selectedNodes = root.getDescendants(Templates.SELECTED);
		
		List<Range> lRange = new LinkedList<Range>();
		List<TokenNode> lReplacedTokens = new LinkedList<TokenNode>();
		for (TreeNode tn : selectedNodes) {
			
			newRoot.addChild(tn);
			List<TreeNode> lReplaced = replaceHiddenNodes(tn);
			for (TreeNode tnReplaced : lReplaced) {
				tnReplaced.serialize(lReplacedTokens);
				Range range = getRange(tnReplaced);
				if (range != null) {
					lRange.add(range);
				}
			}
		}
		
		adjustTokenPosition(newRoot, lRange);

		return newRoot;
	}
	

	private List<TreeNode> replaceHiddenNodes(TreeNode root) {
		List<TreeNode> lReplace = new LinkedList<TreeNode>();
		List<TreeNode> hiddenNodes = root.getDescendants(Templates.CSFORMAT);
		Set<ReplaceGroup> lAppliedGroups = new HashSet<ReplaceGroup>();
		AutoTrimmer at=new JavaAutoTrimmer(root);  //INFO added
		TokenLayouter tl= new TokenLayouter(root.serialize());
		
		//for all nodes that should be hidden
		for (TreeNode tn : hiddenNodes) { 
			CSFormatInformation csfi = 
				(CSFormatInformation)tn.getInformation(Templates.CSFORMAT);
			Format format = csfi.getFormat();
			String target = csfi.getTarget();
			ReplaceGroup replaceGroup = format.getReplaceGroup(target, csfi.getReplaceGroupId());

			//if no node from this group had the replacement applied
			if (!lAppliedGroups.contains(replaceGroup)) {
				//compute the new token node
				TokenNode tnRep = getTokenNode(tn, replaceGroup.getReplaceString());	
				if (tnRep != null) {
					//if a replace was defined, replace old token node with new token node
					tn.getParent().replaceChild(tn, tnRep);	
				} else {
					//if no replace was defined, just remove old token node
					tn.getParent().removeChild(tn);
				}
				//save that we used the replaceGroup
				lAppliedGroups.add(replaceGroup);
				//save replaced node in return list
				lReplace.add(tn);
			} else {
				//if replacement has already been applied: just remove old node

				//INFO added
				tl.trimLeft(tn.getFirstToken());
				tl.trimRight(tn.getLastToken());
				
				
				tn.getParent().removeChild(tn);
				//save removed node in return list
				lReplace.add(tn);
			}

		}
		return lReplace;
	}
	
	
	
	private TokenNode getTokenNode(TreeNode del, String newToken) {
		if (newToken == null || newToken.length() == 0) {
			return null;
		}	
		List<TokenNode> lTokens = new ArrayList<TokenNode>();
		del.serialize(lTokens);

		//if the node to be replaced did not exist, return null.
		//We want no replacement for nodes that don't exist.
		//This can happen if for example the ClassBody node of
		//an empty class should be hidden - the empty class has no
		//ClassBody node to hide		
		if (lTokens.size() == 0) {
			return null;
		}
		Collections.sort(lTokens, new TokenNodeComparator());
		Token firstToken = lTokens.get(0).getToken();
		return new TokenNode("UNKNOWN", new Token(firstToken.getLine(),
				firstToken.getCol(), firstToken.getOffset(), newToken));

	}	
	
	/**
	 * This Method is responsible for adjusting the token positions after
	 * removing parts of the tree. The changes, that occured are defined by the
	 * given Range.
	 * 
	 * @param node
	 * @param delRange
	 */
	private void adjustTokenPosition(TreeNode node, List<Range> delRange) {

		// sort the range list and the token list
		sortRange(delRange);
		List<TokenNode> serializedTokenNodes = serializeAndSort(node);
		
		if (delRange == null || delRange.size() == 0) {
			shiftTokenToLeftBorder(serializedTokenNodes);
			return;
		}

		// hold changes to adjust next range
		int el = 0; // holds the line adjustment of the last range
		int ec = 0;	// holds the column adjustment of the last range
		int preRangeLine = 0; // holds the line number of the last range
		
		
		for (Range range : delRange) {

			boolean colshift = false;

			// reset column adjustment when range is defined for a new line
			if (range.getStartLine() != preRangeLine) {
				ec = 0;
			}
			
			preRangeLine = range.getEndLine();
			
			//adjust range with changes from earlier range adjustments
			range.setStartLine(range.getStartLine() - el);
			range.setEndLine(range.getEndLine() - el);
			range.setStartCol(range.getStartCol() - ec);
			range.setEndCol(range.getEndCol() - ec);
			

			int lineMove = (range.getEndLine() - range.getStartLine());
			int colMove = (range.getEndCol() - range.getStartCol());

			for (TokenNode tokenNode : serializedTokenNodes) {
				Token token = tokenNode.getToken();
				
				// if there is a token on a position, that matches a delete range
				// then this token was replaced by user defined text.
				// in this case we have to adjust colMove to the new text.
				if (token.getLine() == range.getStartLine()
						&& token.getCol() == range.getStartCol()) {
					String tokenText = token.getText();
					if (tokenText.length() != 0) {
						colMove -= token.getText().length();
						colshift = true;
					}
				} else
				// if the token is placed in a line where a delete range ends
				if (token.getLine() == range.getEndLine()) {
					colshift = true;
					// if the token is placed after a delete range, adjust the token column position
					if (token.getCol() >= range.getEndCol()) {
						token.setCol(token.getCol() - colMove);
						token.setLine(token.getLine() - lineMove);
					}

				} else
				// if the token is placed in a line without a delete range, but there was
			    // a delete range in a line before, adjust the line position
				if (token.getLine() > range.getEndLine()) {
					if (colshift) {
						token.setLine(token.getLine() - lineMove);
					} else {
						token.setLine(token.getLine() - (lineMove + 1));
					}
				}
			}
			
			
			// el and ec hold line and column changes that appeared earlier. this is
			// necessary to adjust the following delete ranges from the list.
			if (colshift) {
				el += lineMove;	
			} else {
				el += lineMove + 1;
			}
			ec += colMove;

		}

		shiftTokenToLeftBorder(serializedTokenNodes);

	}

	/**
	 * Shifts all token to the left, so that the first token in the first line
	 * will start at column 1
	 * 
	 * @param tokenNodes
	 */
	private static void shiftTokenToLeftBorder(List<TokenNode> tokenNodes) {

		if (tokenNodes.size() > 0) {

			TokenNode tn = tokenNodes.get(0);
			int colShift = tn.getToken().getCol() - 1;

			for (TokenNode node : tokenNodes) {
				Token token = node.getToken();
				token.setCol(token.getCol() - colShift);
			}
		}

	}

	private static void sortRange(List<Range> delRange) {
		Collections.sort(delRange, new Comparator<Range>() {

			public int compare(Range r1, Range r2) {
				if ((r1.getStartLine() < r2.getStartLine())
						|| (r1.getStartLine() == r2.getStartLine() && r1
								.getStartCol() < r2.getStartCol())) {
					return -1;
				}
				if ((r1.getStartLine() > r2.getStartLine())
						|| (r1.getStartLine() == r2.getStartLine() && r1
								.getStartCol() > r2.getStartCol())) {
					return 1;
				}
				return 0;
			}

		});

	}
	
	
	/**
	 * Returns a Range object which holds information about the deleted part of
	 * the tree.
	 * 
	 * @param del
	 * @return
	 */
	private static Range getRange(TreeNode del) {
		List<TokenNode> token = new ArrayList<TokenNode>();
		token = serialize(del);
		Collections.sort(token, new TokenNodeComparator());
		if (token.size() == 0) {
			return null;
		}
		Token firstToken = token.get(0).getToken();
		Token lastToken = token.get(token.size() - 1).getToken();
		return new Range(firstToken.getLine(), firstToken.getCol(), lastToken
				.getEndLine(), lastToken.getEndCol());
	}



	private static String getName(TreeNode node) {
		/*
		 * This is bad for performance because i iterate over all children over
		 * and over again. This must be done better in the future.
		 */

		TreeNode parent = node.getParent();
		int count = 0;

		for (TreeNode child : parent.getChildren()) {
			if (child.getName().equals(node.getName())) {
				if (child.getSiblingIndex() != node.getSiblingIndex()) {
					count++;
				} else {
					break;
				}
			}
		}
		return node.getName() + "[" + count + "]";
	}

	/**
	 * Returns a sorted list of TokenNodes from a given TreeNode
	 * 
	 * @param node
	 * @return
	 */
	private static List<TokenNode> serializeAndSort(TreeNode node) {
		List<TokenNode> result = serialize(node);
		Collections.sort(result, new TokenNodeComparator());
		return result;
	}

	/**
	 * Returns an unsorted list of TokenNodes from a given TreeNode.
	 * 
	 * @param node
	 * @return
	 */
	private static List<TokenNode> serialize(TreeNode node) {
		List<TokenNode> result = new ArrayList<TokenNode>();

		if (node instanceof TokenNode) {
			result.add((TokenNode) node);
		}

		for (TreeNode n : node.getChildren()) {
			result.addAll(serialize(n));
		}
		return result;
	}	

}
