/*
 * Copyright (C) 2010 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok.ast.grammar;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.TreeMap;

import lombok.Getter;
import lombok.ast.Comment;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.JavadocContainer;
import lombok.ast.Node;
import lombok.ast.Position;

import org.parboiled.Context;
import org.parboiled.RecoveringParseRunner;
import org.parboiled.errors.ParseError;
import org.parboiled.support.ParsingResult;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;

public class Source {
	@Getter private final String name;
	@Getter private final String rawInput;
	private List<Node> nodes;
	private List<ParseProblem> problems;
	private List<Comment> comments;
	private boolean parsed;
	private ParsingResult<Node> parsingResult;
	
	private TreeMap<Integer, Integer> positionDeltas;
	private Map<org.parboiled.Node<Node>, Node> registeredStructures;
	private Map<org.parboiled.Node<Node>, List<Comment>> registeredComments;
	private String preprocessed;
	private Map<Node, Collection<SourceStructure>> cachedSourceStructures;
	private List<Integer> lineEndings;
	
	public Source(String rawInput, String name) {
		this.rawInput = rawInput;
		this.name = name;
		clear();
	}
	
	public List<Node> getNodes() {
		parseCompilationUnit();
		if (!parsed) throw new IllegalStateException("Code hasn't been parsed yet.");
		return nodes;
	}
	
	public List<ParseProblem> getProblems() {
		parseCompilationUnit();
		return problems;
	}
	
	public void clear() {
		nodes = Lists.newArrayList();
		problems = Lists.newArrayList();
		comments = Lists.newArrayList();
		lineEndings = ImmutableList.of();
		parsed = false;
		parsingResult = null;
		positionDeltas = Maps.newTreeMap();
		registeredComments = new MapMaker().weakKeys().makeMap();
		registeredStructures = new MapMaker().weakKeys().makeMap();
		cachedSourceStructures = null;
	}
	
	public String getOverviewProfileInformation() {
		clear();
		preProcess();
		ParserGroup group = new ParserGroup(this);
		ProfilerParseRunner<Node> runner = new ProfilerParseRunner<Node>(group.structures.compilationUnitEoi(), preprocessed);
		this.parsingResult = runner.run();
		StringBuilder out = new StringBuilder();
		out.append(runner.getOverviewReport());
		postProcess();
		return out.toString();
	}
	
	public List<String> getDetailedProfileInformation(int top) {
		clear();
		preProcess();
		ParserGroup group = new ParserGroup(this);
		ProfilerParseRunner<Node> runner = new ProfilerParseRunner<Node>(group.structures.compilationUnitEoi(), preprocessed);
		this.parsingResult = runner.run();
		List<String> result = Lists.newArrayList();
		result.add(runner.getOverviewReport());
		result.addAll(runner.getExtendedReport(top));
		postProcess();
		return result;
	}
	
	private List<Integer> calculateLineEndings() {
		ImmutableList.Builder<Integer> builder = ImmutableList.builder();
		
		boolean atCR = false;
		for (int i = 0; i < rawInput.length(); i++) {
			char c = rawInput.charAt(i);
			if (c == '\n' && !atCR) builder.add(i);
			atCR = c == '\r';
			if (atCR) builder.add(i);
		}
		return builder.build();
	}
	
	public void parseCompilationUnit() {
		if (parsed) return;
		preProcess();
		ParserGroup group = new ParserGroup(this);
		parsingResult = RecoveringParseRunner.run(group.structures.compilationUnitEoi(), preprocessed);
		postProcess();
	}
	
	public void parseMember() {
		if (parsed) return;
		preProcess();
		ParserGroup group = new ParserGroup(this);
		parsingResult = RecoveringParseRunner.run(group.structures.typeBodyMember(), preprocessed);
		postProcess();
	}
	
	public void parseStatement() {
		if (parsed) return;
		preProcess();
		ParserGroup group = new ParserGroup(this);
		parsingResult = RecoveringParseRunner.run(group.statements.anyStatement(), preprocessed);
		postProcess();
	}
	
	public void parseExpression() {
		if (parsed) return;
		preProcess();
		ParserGroup group = new ParserGroup(this);
		parsingResult = RecoveringParseRunner.run(group.expressions.anyExpression(), preprocessed);
		postProcess();
	}
	public void parseVariableDefinition() {
		if (parsed) return;
		preProcess();
		ParserGroup group = new ParserGroup(this);
		parsingResult = RecoveringParseRunner.run(group.structures.variableDefinition(), preprocessed);
		postProcess();
	}
	
	private void postProcess() {
		for (ParseError error : parsingResult.parseErrors) {
			int errStart = error.getStartIndex();
			int errEnd = error.getEndIndex();
			problems.add(new ParseProblem(new Position(mapPosition(errStart), mapPosition(errEnd)), error.toString()));
		}
		
		if (parsingResult.parseTreeRoot != null) {
			nodes.add(parsingResult.parseTreeRoot.getValue());
			gatherComments(parsingResult.parseTreeRoot);
		}
		
		comments = Collections.unmodifiableList(comments);
		nodes = Collections.unmodifiableList(nodes);
		problems = Collections.unmodifiableList(problems);
		
		rtrimPositions(nodes, comments);
		
		//TODO Write test case with javadoc intermixed with empty declares.
		//TODO test javadoc on a package declaration.
		//TODO javadoc in between keywords.
		
		associateJavadoc(comments, nodes);
		
		fixPositions(nodes);
		fixPositions(comments);
		
		parsed = true;
	}
	
	void registerStructure(Node node, org.parboiled.Node<Node> pNode) {
		registeredStructures.put(pNode, node);
	}
	
	public Map<Node, Collection<SourceStructure>> getSourceStructures() {
		if (cachedSourceStructures != null) return cachedSourceStructures;
		parseCompilationUnit();
		ListMultimap<Node, SourceStructure> map = LinkedListMultimap.create();
		
		org.parboiled.Node<Node> pNode = parsingResult.parseTreeRoot;
		
		buildSourceStructures(pNode, null, map);
		
		Map<Node, Collection<SourceStructure>> result = map.asMap();
		
		for (Collection<SourceStructure> structures : result.values()) {
			for (SourceStructure structure : structures) {
				structure.setPosition(new Position(
						mapPosition(structure.getPosition().getStart()),
						mapPosition(structure.getPosition().getEnd())));
			}
		}
		
		return cachedSourceStructures = result;
	}
	
	private void addSourceStructure(ListMultimap<Node, SourceStructure> map, Node node, SourceStructure structure) {
		if (structure.getPosition().size() > 0 && structure.getContent().trim().length() > 0 &&
				!structure.getPosition().equals(node.getPosition())) {
			
			map.put(node, structure);
		}
	}
	
	private void buildSourceStructures(org.parboiled.Node<Node> pNode, Node owner, ListMultimap<Node, SourceStructure> map) {
		Node target = registeredStructures.remove(pNode);
		if (target != null || pNode.getChildren().isEmpty()) {
			int start = pNode.getStartIndex();
			int end = pNode.getEndIndex();
			String text = preprocessed.substring(start, end);
			SourceStructure structure = new SourceStructure(new Position(start, end), text);
			if (target != null) addSourceStructure(map, target, structure);
			else if (pNode.getValue() != null && !(pNode.getValue() instanceof TemporaryNode)) addSourceStructure(map, pNode.getValue(), structure);
			else if (owner != null) addSourceStructure(map, owner, structure);
		} else {
			Node possibleOwner = pNode.getValue();
			if (possibleOwner instanceof TemporaryNode) possibleOwner = null;
			for (org.parboiled.Node<Node> child : pNode.getChildren()) {
				if (child.getValue() == null || child.getValue() instanceof TemporaryNode) continue;
				/* If the next if holds true, then we aren't the true generator; the child generated the node and this pNode adopted it */
				if (child.getValue() == possibleOwner) possibleOwner = null;
			}
			
			if (possibleOwner != null) owner = possibleOwner;
			
			for (org.parboiled.Node<Node> child : pNode.getChildren()) {
				buildSourceStructures(child, owner, map);
			}
		}
	}
	
	/**
	 * The end positions of all nodes include their trailing whitespace which isn't very convenient.
	 * We'll 'fix' the end marker of each node by trimming it back. This is somewhat complicated as comments also need to be trimmed across.
	 * We also adjust all positions to conform with the raw input (undoing any positional shifts caused by preprocessing).
	 */
	private void rtrimPositions(List<Node> nodes, List<Comment> comments) {
		final boolean[] whitespace = new boolean[preprocessed.length()];
		for (Comment comment : comments) {
			Position p = comment.getPosition();
			if (!p.isUnplaced()) {
				for (int i = p.getStart(); i < p.getEnd(); i++) whitespace[i] = true;
			}
		}
		
		/* Process actual whitespace in preprocessed source data */ {
			char[] chars = preprocessed.toCharArray();
			for (int i = 0; i < chars.length; i++) if (Character.isWhitespace(chars[i])) whitespace[i] = true;
		}
		
		for (Node node : nodes) node.accept(new ForwardingAstVisitor() {
			@Override public boolean visitNode(Node node) {
				Position p = node.getPosition();
				if (p.isUnplaced()) return false;
				
				int trimmed = Math.min(whitespace.length, p.getEnd());
				while (trimmed > 0 && whitespace[trimmed-1]) trimmed--;
				
				int start, end;
				
				if (p.getEnd() - p.getStart() == 0) {
					if (node.getParent() != null) {
						start = Math.min(node.getParent().getPosition().getEnd(), Math.max(node.getParent().getPosition().getStart(), p.getStart()));
						end = start;
					} else {
						start = p.getStart();
						end = start;
					}
				} else {
					start = p.getStart();
					end = Math.max(trimmed, start);
				}
				
				node.setPosition(new Position(start, end));
				
				return false;
			}
		});
	}
	
	private void fixPositions(List<? extends Node> nodes) {
		for (Node node : nodes) node.accept(new ForwardingAstVisitor() {
			@Override public boolean visitNode(Node node) {
				Position p = node.getPosition();
				if (!p.isUnplaced()) {
					node.setPosition(new Position(mapPosition(p.getStart()), mapPosition(p.getEnd())));
				}
				if (node instanceof Expression) {
					List<Position> list = ((Expression)node).astParensPositions();
					if (list != null) {
						ListIterator<Position> li = list.listIterator();
						while (li.hasNext()) {
							Position parenPos = li.next();
							if (!parenPos.isUnplaced()) {
								parenPos = new Position(mapPosition(parenPos.getStart()), mapPosition(parenPos.getEnd()));
								li.set(parenPos);
							}
						}
					}
				}
				return false;
			}
		});
	}
	
	/**
	 * Associates comments that are javadocs to the node they belong to, by checking if the node that immediately follows a javadoc node is a JavadocContainer.
	 */
	private void associateJavadoc(List<Comment> comments, List<Node> nodes) {
		final TreeMap<Integer, Node> startPosMap = Maps.newTreeMap();
		for (Node node : nodes) node.accept(new ForwardingAstVisitor() {
			@Override public boolean visitNode(Node node) {
				if (node.isGenerated()) return false;
				int startPos = node.getPosition().getStart();
				Node current = startPosMap.get(startPos);
				if (current == null || !(current instanceof JavadocContainer)) {
					startPosMap.put(startPos, node);
				}
				
				return false;
			}
		});
		
		for (Comment comment : comments) {
			if (!comment.isJavadoc()) continue;
			Map<Integer, Node> tailMap = startPosMap.tailMap(comment.getPosition().getEnd());
			if (tailMap.isEmpty()) continue;
			Node assoc = tailMap.values().iterator().next();
			if (!(assoc instanceof JavadocContainer)) continue;
			JavadocContainer jc = (JavadocContainer) assoc;
			if (jc.rawJavadoc() != null) {
				if (jc.rawJavadoc().getPosition().getEnd() >= comment.getPosition().getEnd()) continue;
			}
			jc.rawJavadoc(comment);
		}
	}
	
	void registerComment(Context<Node> context, Comment c) {
		List<Comment> list = registeredComments.get(context);
		if (list == null) {
			list = Lists.newArrayList();
			registeredComments.put(context.getSubNodes().get(0), list);
		}
		list.add(c);
	}
	
	/**
	 * Delves through the parboiled node tree to find comments.
	 */
	private boolean gatherComments(org.parboiled.Node<Node> parsed) {
		boolean foundComments = false;
		for (org.parboiled.Node<Node> child : parsed.getChildren()) {
			foundComments |= gatherComments(child);
		}
		
		List<Comment> cmts = registeredComments.get(parsed);
		if (cmts != null) for (Comment c : cmts) {
			comments.add(c);
			return true;
		}
		
		return foundComments;
	}
	
	private void setPositionDelta(int position, int delta) {
		Integer i = positionDeltas.get(position);
		if (i == null) i = 0;
		positionDeltas.put(position, i + delta);
	}
	
	public List<Integer> getLineEndingsTable() {
		return lineEndings;
	}
	
	public long lineColumn(int index) {
		//Possible efficiency improvement: Store in a list the index into the line table with the first line that's over 500, 1000, 1500, ... chars.
		//Or just binary search.
		int oldIdx = 0;
		int line = 0;
		
		for (; line < lineEndings.size(); line++) {
			int pos = lineEndings.get(line);
			if (pos > index) break;
			oldIdx = pos;
		}
		return ((long) line << 32 | index - oldIdx);
	}
	
	/**
	 * Maps a position in the {@code preprocessed} string to the equivalent character in the {@code rawInput}.
	 * 
	 * The difference is caused by decoding backslash-U unicode escapes, for example.
	 */
	int mapPosition(int position) {
		int out = position;
		for (int delta : positionDeltas.headMap(position, true).values()) {
			out += delta;
		}
		return out;
	}
	
	private String preProcess() {
		preprocessed = rawInput;
		this.lineEndings = calculateLineEndings();
		applyBackslashU();
//		applyBraceMatching();
		return preprocessed;
	}
	
	/**
	 * @see <a href="http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.3">JLS section 3.3</a>
	 */
	private void applyBackslashU() {
		StringBuilder buffer = new StringBuilder();
		StringBuilder out = new StringBuilder();
		
		int state = 0;
		int idx = 0;
		for (char c : preprocessed.toCharArray()) {
			idx++;
			switch (state) {
			case 0:	//normal mode. Anything that isn't a backslash is not interesting.
				if (c != '\\') {
					out.append(c);
					break;
				}
				
				state = 1;
				break;
			case 1:	//Last character read is an (uneven amount of) backslash.
				if (c != 'u') {
					out.append('\\');
					out.append(c);
					state = 0;
				} else {
					buffer.setLength(0);
					buffer.append("\\u");
					state = 2;
				}
				break;
				//TODO add a test for more backslash-U stuff.
			default:
				//Gobbling hex digits. state-2 is our current position. We want 4.
				buffer.append(c);
				if (c == 'u') {
					//JLS Puzzler: backslash-u-u-u-u-u-u-u-u-u-4hexdigits means the same thing as just 1 u.
					//So, we just keep going as if nothing changed.
				} else if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
					state++;
					if (state == 6) {
						//We've got our 4 hex digits.
						out.append((char)Integer.parseInt(buffer.substring(buffer.length()-4), 0x10));
						int delta = buffer.length() -1;	//Buffer goes away but 1 character appears in its place.
						setPositionDelta(idx - delta, delta);
						buffer.setLength(0);
						//We don't have to check if this char is a backslash and set state to 1; JLS says backslash-u is not recursively applied.
						state = 0;
					}
				} else {
					//Invalid unicode escape.
					problems.add(new ParseProblem(new Position(idx-buffer.length(), idx), "Invalid backslash-u escape: \\u is supposed to be followed by 4 hex digits."));
					out.append(buffer.toString());
					state = 0;
				}
				break;
			}
		}
		
		preprocessed = out.toString();
	}
}
