/*******************************************************************************
 * Copyright (c) 2007 Google, Inc 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.internal.core.pdom.indexer;

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

import org.eclipse.cdt.core.dom.ast.IASTComment;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.internal.core.CharOperation;

public class TodoTaskParser {
	private static final Task[] EMPTY_TASK_ARRAY = new Task[0];

	private final char[][] tags;
	private final int[] priorities;
	private final boolean isTaskCaseSensitive;
	private final int[] order;

	public TodoTaskParser(char[][] taskTags, int[] taskPriorities, boolean isTaskCaseSensitive) {
		assert taskPriorities.length == taskTags.length;
		this.tags = taskTags;
		this.priorities = taskPriorities;
		this.isTaskCaseSensitive = isTaskCaseSensitive;

		// Calculate task checking order that gives preference to the longest matching tag.
		this.order = new int[taskTags.length];
		for (int i = 0; i < order.length; i++) {
			order[i] = i;
		}
		// Sort order array in reverse order of tag lengths.
		// Shell sort algorithm from http://en.wikipedia.org/wiki/Shell_sort
		for (int inc = order.length / 2; inc > 0; inc /= 2) {
			for (int i = inc; i < order.length; i++) {
				for (int j = i;
						j >= inc && taskTags[order[j - inc]].length < taskTags[order[j]].length;
						j -= inc) {
					int temp = order[j];
					order[j] = order[j - inc];
					order[j - inc] = temp;
				}
			}
		}
	}

	public Task[] parse(IASTComment[] comments) {
		List/*<Task>*/ tasks = new ArrayList();
		for (int i = 0; i < comments.length; i++) {
			IASTComment comment = comments[i];
			IASTFileLocation location = comment.getFileLocation();
			if (location != null) { // be defensive, bug 213307
				parse(comment.getComment(), location.getFileName(), location.getNodeOffset(),
				location.getStartingLineNumber(), tasks);
			}
		}
		if (tasks.isEmpty()) {
			return EMPTY_TASK_ARRAY;
		}
		return (Task[]) tasks.toArray(new Task[tasks.size()]);
	}
	
    private void parse(char[] comment, String filename, int offset, int lineNumber,
    		List/*<Task>*/ tasks) {
        int commentLength = comment.length;

    	int foundTaskIndex = tasks.size();
    	char previous = comment[1]; // Should be '*' or '/'
    	for (int i = 2; i < commentLength; i++) {
    		char[] tag = null;
			nextTag : for (int j = 0; j < order.length; j++) {
				int itag = order[j];
				tag = tags[itag];
				int tagLength = tag.length;
				if (tagLength == 0 || i + tagLength > commentLength)
					continue nextTag;
	
				// Ensure tag is not leaded with letter if tag starts with a letter
				if (isIdentifierStart(tag[0]) && isIdentifierPart(previous)) {
					continue nextTag;
				}
	
				for (int t = 0; t < tagLength; t++) {
					int x = i + t;
					if (x >= commentLength)
						continue nextTag;
					char sc = comment[x];
					char tc = tag[t];
					if (sc != tc) { 											 // case sensitive check
						if (isTaskCaseSensitive || Character.toLowerCase(sc) != Character.toLowerCase(tc)) { // case insensitive check
							continue nextTag;
						}
					}
				}
				// Ensure tag is not followed with letter if tag finishes with a letter
				if (i + tagLength < commentLength && isIdentifierPart(comment[i + tagLength - 1])) {
					if (isIdentifierPart(comment[i + tagLength]))
						continue nextTag;
				}
				
				Task task = new Task(filename, i, i + tagLength, lineNumber,
						String.valueOf(tag), "", priorities[itag]); //$NON-NLS-1$
				tasks.add(task);
				i += tagLength - 1; // Will be incremented when looping
				break nextTag;
			}
    		previous = comment[i];
    	}

    	boolean containsEmptyTask = false;
    	for (int i = foundTaskIndex; i < tasks.size(); i++) {
    		Task task = (Task) tasks.get(i);
    		// Retrieve message start and end positions
    		int msgStart = task.start + task.tag.length();
    		int maxValue = i + 1 < tasks.size() ?
    				((Task) tasks.get(i + 1)).start : commentLength;
    		// At most beginning of next task
    		if (maxValue < msgStart) {
    			maxValue = msgStart; // Would only occur if tag is before EOF.
    		}
    		int end = -1;
    		char c;
    		for (int j = msgStart; j < maxValue; j++) {
    			if ((c = comment[j]) == '\n' || c == '\r') {
    				end = j;
    				break;
    			}
    		}
    		if (end == -1) {
    			for (int j = maxValue; --j >= msgStart;) {
    				if ((c = comment[j]) == '*') {
    					end = j;
    					break;
    				}
    			}
    			if (end == -1) {
    				end = maxValue;
    			}
    		}
    		// Trim the message
    		while (msgStart < end && CharOperation.isWhitespace(comment[end - 1]))
    			end--;
    		while (msgStart < end && CharOperation.isWhitespace(comment[msgStart]))
    			msgStart++;
    		if (msgStart == end) {
    			// If the description is empty, we might want to see if two tags
    			// are not sharing the same message.
    			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=110797
    			containsEmptyTask = true;
    			continue;
    		}
    		// Update the end position of the task
    		task.end = end;
    		// Get the message source
    		int messageLength = end - msgStart;
    		task.message = String.valueOf(comment, msgStart, messageLength);
    	}

    	if (containsEmptyTask) {
    		for (int i = foundTaskIndex; i < tasks.size(); i++) {
    			Task task1 = (Task) tasks.get(i);
    			if (task1.message.length() == 0) {
    				for (int j = i + 1; j < tasks.size(); j++) {
    	    			Task task2 = (Task) tasks.get(j);
    					if (task2.message.length() != 0) {
    						task1.message = task2.message;
    						task1.end = task2.end;
    						break;
    					}
    				}
    			}
    		}
    	}
    	
    	// Add comment offset.
		for (int i = foundTaskIndex; i < tasks.size(); i++) {
			Task task = (Task) tasks.get(i);
			task.lineNumber += getLineOffset(comment, task.start);
			task.start += offset;
			task.end += offset;
		}
    }
	
    /**
     * Returns zero-based line number for a given character position.
     */
	private static int getLineOffset(char[] buffer, int pos) {
		int count = 0;
		for (int i = 0; i < pos && i < buffer.length; i++) {
			if (buffer[i] == '\n') {
				count++;
			}
		}
		return count;
	}

	private static boolean isIdentifierStart(char c) {
		return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_'
				|| Character.isUnicodeIdentifierPart(c);
	}
	
	private static boolean isIdentifierPart(char c) {
		return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_'
		        || c >= '0' && c <= '9'
		        || Character.isUnicodeIdentifierPart(c);
	}


	public static class Task {
		private String fileLocation;
		private int start;
		private int end;
		private int lineNumber;
		private String tag;
		private String message;
		private int priority;
		
		Task(String fileLocation, int start, int end, int lineNumber,
				String tag, String message, int priority) {
			this.fileLocation = fileLocation;
			this.start = start;
			this.end = end;
			this.lineNumber = lineNumber;
			this.tag = tag;
			this.message = message;
			this.priority = priority;
		}

		public String getFileLocation() {
			return fileLocation;
		}

		public int getStart() {
			return start;
		}

		public int getEnd() {
			return end;
		}

		public int getLineNumber() {
			return lineNumber;
		}

		public String getTag() {
			return tag;
		}

		public String getMessage() {
			return message;
		}

		public int getPriority() {
			return priority;
		}
	}
}
