/*******************************************************************************
 * Copyright (c) 2004, 2009 Frank Becker 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:
 *     Frank Becker - initial API and implementation
 *     Tasktop Technologies - improvements
 *******************************************************************************/

package org.eclipse.mylyn.internal.tasks.ui.editors;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.mylyn.commons.ui.CommonImages;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.LocalTask;
import org.eclipse.mylyn.internal.tasks.core.TaskActivationHistory;
import org.eclipse.mylyn.internal.tasks.core.TaskList;
import org.eclipse.mylyn.internal.tasks.ui.TasksUiPlugin;
import org.eclipse.mylyn.internal.tasks.ui.actions.CopyTaskDetailsAction;
import org.eclipse.mylyn.internal.tasks.ui.util.TasksUiInternal;
import org.eclipse.mylyn.internal.tasks.ui.workingsets.TaskWorkingSetUpdater;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.ui.TaskElementLabelProvider;
import org.eclipse.mylyn.tasks.ui.editors.TaskEditorInput;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;

/**
 * @author Frank Becker
 * @author Steffen Pingel
 * @since 3.0
 */
public class RepositoryCompletionProcessor implements IContentAssistProcessor {

	public static class TaskCompletionProposal implements ICompletionProposal {

		private final LabelProvider labelProvider;

		private final TaskRepository repository;

		private final ITask task;

		private final int replacementOffset;

		private final int replacementLength;

		private String replacement;

		private final String defaultReplacement;

		private final boolean includePrefix;

		public TaskCompletionProposal(TaskRepository repository, ITask task, LabelProvider labelProvider,
				String defaultReplacement, boolean includePrefix, int replacementOffset, int replacementLength) {
			this.labelProvider = labelProvider;
			this.repository = repository;
			this.task = task;
			this.defaultReplacement = defaultReplacement;
			this.includePrefix = includePrefix;
			this.replacementOffset = replacementOffset;
			this.replacementLength = replacementLength;
		}

		public void apply(IDocument document) {
			try {
				document.replace(replacementOffset, replacementLength, getReplacement());
			} catch (BadLocationException x) {
				// ignore
			}
		}

		public String getReplacement() {
			if (replacement == null) {
				// add an absolute reference to the task if the viewer does not have a repository
				if (defaultReplacement == null || repository == null
						|| !repository.getRepositoryUrl().equals(task.getRepositoryUrl())) {
					replacement = CopyTaskDetailsAction.getTextForTask(task);
				} else if (includePrefix) {
					replacement = TasksUiInternal.getTaskPrefix(task.getConnectorKind()) + defaultReplacement;
				} else {
					replacement = defaultReplacement;
				}
			}
			return replacement;
		}

		public String getAdditionalProposalInfo() {
			return null;
		}

		public IContextInformation getContextInformation() {
			return null;
		}

		public String getDisplayString() {
			return labelProvider.getText(task);
		}

		public Image getImage() {
			return labelProvider.getImage(task);
		}

		public Point getSelection(IDocument document) {
			return new Point(replacementOffset + getReplacement().length(), 0);
		}

		public TaskRepository getRepository() {
			return repository;
		}

		public ITask getTask() {
			return task;
		}

	}

	private class ProposalComputer {

		public static final String LABEL_SEPARATOR = " -------------------------------------------- "; //$NON-NLS-1$

		private final Set<ITask> addedTasks = new HashSet<ITask>();

		private boolean addSeparator;

		private final int offset;

		private final String prefix;

		private final List<ICompletionProposal> resultList = new ArrayList<ICompletionProposal>();

		public ProposalComputer(ITextViewer viewer, int offset) {
			this.offset = offset;
			this.prefix = extractPrefix(viewer, offset).toLowerCase();
		}

		private void addProposal(ITask task, String replacement, boolean includeTaskPrefix) {
			if (addSeparator) {
				if (!addedTasks.isEmpty()) {
					resultList.add(createSeparator());
				}
				addSeparator = false;
			}

			resultList.add(new TaskCompletionProposal(repository, task, labelProvider, replacement, includeTaskPrefix,
					offset - prefix.length(), prefix.length()));

			addedTasks.add(task);
		}

		public void addSeparator() {
			addSeparator = true;
		}

		public void addTasks(List<AbstractTask> tasks) {
			for (AbstractTask task : tasks) {
				addTask(task);
			}
		}

		public void addTask(ITask task) {
			if (addedTasks.contains(task)) {
				return;
			}

			if (getNeverIncludePrefix() && !task.getRepositoryUrl().equals(repository.getRepositoryUrl())) {
				return;
			}

			String taskKey = task.getTaskKey();
			if (prefix.length() == 0) {
				addProposal(task, taskKey, !getNeverIncludePrefix());
			} else if (taskKey != null && taskKey.startsWith(prefix)) {
				// don't include prefix if completing id since it was most likely already added
				addProposal(task, taskKey, false);
			} else if (containsPrefix(task)) {
				addProposal(task, taskKey, !getNeverIncludePrefix());
			}
		}

		private boolean containsPrefix(ITask task) {
			String haystack = TasksUiInternal.getTaskPrefix(task.getConnectorKind())
					+ " " + labelProvider.getText(task); //$NON-NLS-1$
			String[] haystackTokens = haystack.split("\\s"); //$NON-NLS-1$
			String[] needles = prefix.trim().split("\\*"); //$NON-NLS-1$
			if (haystackTokens.length == 0 || needles.length == 0) {
				return false;
			}
			// check if all needles are contained in haystack  
			for (String needle : needles) {
				boolean matched = false;
				haystack: for (String haystackToken : haystackTokens) {
					if (haystackToken.toLowerCase().startsWith(needle)) {
						matched = true;
						break haystack;
					}
				}
				if (!matched) {
					return false;
				}
			}
			return true;
		}

		private CompletionProposal createSeparator() {
			return new CompletionProposal("", offset, 0, 0, CommonImages.getImage(CommonImages.SEPARATOR_LIST), //$NON-NLS-1$
					LABEL_SEPARATOR, null, null);
		}

		/**
		 * Returns the prefix of the currently completed text. Assumes that any character that is not a line break or
		 * white space can be part of a task id.
		 */
		private String extractPrefix(ITextViewer viewer, int offset) {
			int i = offset;
			IDocument document = viewer.getDocument();
			if (i > document.getLength()) {
				return ""; //$NON-NLS-1$
			}

			try {
				while (i > 0) {
					char c = document.getChar(i - 1);
					if (Character.isWhitespace(c) || c == '(' || c == ':') {
						break;
					}
					i--;
				}
				if (i == offset && repository != null) {
					// check if document contains "{prefix} "
					String taskPrefix = TasksUiInternal.getTaskPrefix(repository.getConnectorKind());
					if (taskPrefix.length() > 1) {
						try {
							if (taskPrefix.equals(document.get(offset - taskPrefix.length(), taskPrefix.length()))) {
								return taskPrefix;
							}
						} catch (BadLocationException e) {
							// ignore
						}
					}
				}
				return document.get(i, offset - i);
			} catch (BadLocationException e) {
				return ""; //$NON-NLS-1$
			}
		}

		public void filterTasks(List<AbstractTask> tasks) {
			for (Iterator<AbstractTask> it = tasks.iterator(); it.hasNext();) {
				ITask task = it.next();
				if (!select(task)) {
					it.remove();
				}
			}
		}

		private boolean select(ITask task) {
			return !(task instanceof LocalTask) //
					&& (repository == null || task.getRepositoryUrl().equals(repository.getRepositoryUrl()));
		}

		public ICompletionProposal[] getResult() {
			return resultList.toArray(new ICompletionProposal[resultList.size()]);
		}

	}

	private static final int MAX_OPEN_EDITORS = 10;

	private static final int MAX_ACTIVATED_TASKS = 10;

	private final TaskElementLabelProvider labelProvider = new TaskElementLabelProvider(false);

	private final TaskRepository repository;

	private boolean neverIncludePrefix;

	public RepositoryCompletionProcessor(TaskRepository taskRepository) {
		this.repository = taskRepository;
		this.neverIncludePrefix = false;
	}

	public boolean getNeverIncludePrefix() {
		return neverIncludePrefix;
	}

	public void setNeverIncludePrefix(boolean includePrefix) {
		this.neverIncludePrefix = includePrefix;
	}

	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
		ITextSelection selection = (ITextSelection) viewer.getSelectionProvider().getSelection();
		// adjust offset to end of normalized selection
		if (selection.getOffset() == offset) {
			offset = selection.getOffset() + selection.getLength();
		}

		ProposalComputer proposalComputer = new ProposalComputer(viewer, offset);

		// add tasks from navigation history
//		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
//		if (window != null) {
//			IWorkbenchPage page = window.getActivePage();
//			if (page != null) {
//				INavigationHistory history = page.getNavigationHistory();
//				INavigationLocation[] locations = history.getLocations();
//				if (locations != null) {
//					for (INavigationLocation location : locations) {
//						// location is always null
//					}
//				}
//			}
//		}

		// add open editor
		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		if (window != null) {
			IWorkbenchPage page = window.getActivePage();
			if (page != null) {
				IEditorReference[] editorReferences = page.getEditorReferences();
				int count = 0;
				for (int i = editorReferences.length - 1; i >= 0 && count < MAX_OPEN_EDITORS; i--) {
					try {
						if (editorReferences[i].getEditorInput() instanceof TaskEditorInput) {
							TaskEditorInput input = (TaskEditorInput) editorReferences[i].getEditorInput();
							ITask task = input.getTask();
							if (task != null && !(task instanceof LocalTask)) {
								proposalComputer.addTask(task);
								count++;
							}
						}
					} catch (PartInitException e) {
						// ignore
					}
				}
			}
		}

		// add tasks from activation history
		TaskActivationHistory taskHistory = TasksUiPlugin.getTaskActivityManager().getTaskActivationHistory();
		List<AbstractTask> tasks = taskHistory.getPreviousTasks(TasksUiInternal.getContainersFromWorkingSet(TaskWorkingSetUpdater.getActiveWorkingSets(window)));
		int count = 0;
		for (int i = tasks.size() - 1; i >= 0 && count < MAX_ACTIVATED_TASKS; i--) {
			AbstractTask task = tasks.get(i);
			if (!(task instanceof LocalTask)) {
				proposalComputer.addTask(task);
			}
		}

		// add all remaining tasks for repository
		if (repository != null) {
			proposalComputer.addSeparator();

			TaskList taskList = TasksUiPlugin.getTaskList();
			tasks = new ArrayList<AbstractTask>(taskList.getAllTasks());
			proposalComputer.filterTasks(tasks);
			Collections.sort(tasks, new Comparator<AbstractTask>() {
				public int compare(AbstractTask o1, AbstractTask o2) {
					return labelProvider.getText(o1).compareTo(labelProvider.getText(o2));
				}
			});
			proposalComputer.addTasks(tasks);
		}

		return proposalComputer.getResult();
	}

	public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
		return null;
	}

	public char[] getCompletionProposalAutoActivationCharacters() {
		return null;
	}

	public char[] getContextInformationAutoActivationCharacters() {
		return null;
	}

	public IContextInformationValidator getContextInformationValidator() {
		return null;
	}

	public String getErrorMessage() {
		return null;
	}

}