/*******************************************************************************
 * Copyright (c) 2013 Tasktop Technologies 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

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

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

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ControlContribution;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.mylyn.commons.ui.CommonImages;
import org.eclipse.mylyn.commons.workbench.forms.CommonFormUtil;
import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorCommentPart.CommentGroupViewer;
import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorCommentPart.CommentViewer;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorPage;
import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorPart;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.forms.IFormPart;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.FormToolkit;

/**
 * Adds support for finding text to the task editor.
 * 
 * @author Jingwen Ou
 * @author Lily Guo
 * @author Sam Davis
 */
public class TaskEditorFindSupport {

	private Action toggleFindAction;

	private static final Color HIGHLIGHTER_YELLOW = new Color(Display.getDefault(), 255, 238, 99);

	private static final Color ERROR_NO_RESULT = new Color(Display.getDefault(), 255, 150, 150);

	private final List<StyledText> styledTexts = new ArrayList<StyledText>();

	private final List<CommentGroupViewer> commentGroupViewers = new ArrayList<CommentGroupViewer>();

	private final AbstractTaskEditorPage taskEditorPage;;

	public TaskEditorFindSupport(AbstractTaskEditorPage page) {
		Assert.isNotNull(page);
		this.taskEditorPage = page;
	}

	public void toggleFind() {
		if (toggleFindAction != null) {
			toggleFindAction.setChecked(!toggleFindAction.isChecked());
			toggleFindAction.run();
		}
	}

	public void addFindAction(IToolBarManager toolBarManager) {
		if (toggleFindAction != null && toggleFindAction.isChecked()) {
			ControlContribution findTextboxControl = new ControlContribution(Messages.TaskEditorFindSupport_Find) {
				@Override
				protected Control createControl(Composite parent) {
					FormToolkit toolkit = taskEditorPage.getEditor().getHeaderForm().getToolkit();
					final Composite findComposite = toolkit.createComposite(parent);

					GridLayout findLayout = new GridLayout();
					findLayout.marginHeight = 4;
					findComposite.setLayout(findLayout);
					findComposite.setBackground(null);

					final Text findText = toolkit.createText(findComposite, "", SWT.FLAT); //$NON-NLS-1$
					findText.setLayoutData(new GridData(100, SWT.DEFAULT));
					findText.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
					findText.setFocus();
					toolkit.adapt(findText, false, false);

					findText.addModifyListener(new ModifyListener() {
						@Override
						public void modifyText(ModifyEvent e) {
							if (findText.getText().equals("")) { //$NON-NLS-1$
								clearSearchResults();
								findText.setBackground(null);
							}
						}
					});

					findText.addSelectionListener(new SelectionAdapter() {
						@Override
						public void widgetDefaultSelected(SelectionEvent event) {
							searchTaskEditor(findText);
						}
					});
					toolkit.paintBordersFor(findComposite);
					return findComposite;
				}

			};
			toolBarManager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, findTextboxControl);
		}

		if (toggleFindAction == null) {
			toggleFindAction = new Action("", SWT.TOGGLE) { //$NON-NLS-1$
				@Override
				public void run() {
					if (!this.isChecked()) {
						clearSearchResults();
					}
					taskEditorPage.getEditor().updateHeaderToolBar();
				}

			};
			toggleFindAction.setImageDescriptor(CommonImages.FIND);
			toggleFindAction.setToolTipText(Messages.TaskEditorFindSupport_Find);
		}
		toolBarManager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, toggleFindAction);
	}

	protected void searchTaskEditor(final Text findBox) {
		try {
			taskEditorPage.setReflow(false);
			findBox.setBackground(null);
			if (findBox.getText().equals("")) { //$NON-NLS-1$
				return;
			}
			clearSearchResults();
			String searchString = findBox.getText().toLowerCase();
			for (IFormPart part : taskEditorPage.getManagedForm().getParts()) {
				if (!(part instanceof AbstractTaskEditorPart)) {
					continue;
				}
				Control control = ((AbstractTaskEditorPart) part).getControl();
				if (part instanceof TaskEditorSummaryPart) {
					if (contains(taskEditorPage.getModel().getTaskData(), TaskAttribute.SUMMARY, searchString)) {
						gatherStyledTexts(control, styledTexts);
					}
				} else if (part instanceof TaskEditorPlanningPart) {
					RichTextEditor noteEditor = ((TaskEditorPlanningPart) part).getPlanningPart().getNoteEditor();
					if (noteEditor != null && noteEditor.getText() != null
							&& noteEditor.getText().toLowerCase().contains(searchString)) {
						gatherStyledTexts(control, styledTexts);
					}
				} else if (part instanceof TaskEditorDescriptionPart) {
					if (contains(taskEditorPage.getModel().getTaskData(), TaskAttribute.DESCRIPTION, searchString)) {
						gatherStyledTexts(control, styledTexts);
					}
				} else if (part instanceof TaskEditorCommentPart) {
					commentGroupViewers.clear();
					commentGroupViewers.addAll(((TaskEditorCommentPart) part).getCommentGroupViewers());
					searchCommentPart(searchString, (TaskEditorCommentPart) part, commentGroupViewers, styledTexts);
				}
			}

			for (StyledText styledText : styledTexts) {
				highlightMatches(searchString, styledText);
			}
			if (styledTexts.isEmpty()) {
				findBox.setBackground(ERROR_NO_RESULT);
			}
		} finally {
			taskEditorPage.setReflow(true);
		}
		taskEditorPage.reflow();
		findBox.setFocus();
	}

	protected static boolean contains(TaskData taskData, String attributeId, String searchString) {
		TaskAttribute attribute = taskData.getRoot().getMappedAttribute(attributeId);
		if (attribute != null) {
			return attribute.getValue().toLowerCase().contains(searchString);
		}
		return false;
	}

	private void searchCommentPart(final String searchString, final TaskEditorCommentPart part,
			List<CommentGroupViewer> commentGroupViewers, final List<StyledText> styledTexts) {
		TaskData taskData = taskEditorPage.getModel().getTaskData();
		List<TaskAttribute> commentAttributes = taskData.getAttributeMapper().getAttributesByType(taskData,
				TaskAttribute.TYPE_COMMENT);

		if (!anyCommentContains(commentAttributes, searchString)) {
			return;
		}

		if (!part.isCommentSectionExpanded()) {
			try {
				part.setReflow(false);
				part.expandAllComments(false);
			} finally {
				part.setReflow(true);
			}
		}

		int end = commentAttributes.size();
		boolean expandMatchingGroup = true;
		for (int i = commentGroupViewers.size() - 1; i >= 0; i--) {
			final CommentGroupViewer group = commentGroupViewers.get(i);
			List<CommentViewer> commentViewers = group.getCommentViewers();
			int start = end - commentViewers.size();
			List<TaskAttribute> groupAttributes = commentAttributes.subList(start, end);
			if (expandMatchingGroup && anyCommentContains(groupAttributes, searchString)) {
				if (!group.isExpanded()) {
					try {
						part.setReflow(false);
						group.setExpanded(true);
					} finally {
						part.setReflow(true);
					}
				}
				// once we've seen a matching group, don't expand any more groups
				expandMatchingGroup = false;
			}
			final List<CommentViewer> matchingViewers = searchComments(groupAttributes, commentViewers, searchString);
			if (!group.isRenderedInSubSection() || group.isExpanded()) {
				try {
					part.setReflow(false);
					gatherStyledTexts(matchingViewers, styledTexts);
				} finally {
					part.setReflow(true);
				}
				group.clearSectionHyperlink();
			} else if (!matchingViewers.isEmpty()) {
				addShowMoreLink(group, matchingViewers, part, searchString, styledTexts);
			} else {
				group.clearSectionHyperlink();
			}
			end = start;
		}
	}

	protected void addShowMoreLink(final CommentGroupViewer group, final List<CommentViewer> matchingViewers,
			final TaskEditorCommentPart part, final String searchString, final List<StyledText> styledTexts) {
		HyperlinkAdapter listener = new HyperlinkAdapter() {
			@Override
			public void linkActivated(HyperlinkEvent e) {
				List<StyledText> commentStyledTexts = new ArrayList<StyledText>();
				try {
					taskEditorPage.setReflow(false);
					part.setReflow(false);
					group.setExpanded(true);
					gatherStyledTexts(matchingViewers, commentStyledTexts);
				} finally {
					taskEditorPage.setReflow(true);
					part.setReflow(true);
				}
				for (StyledText styledText : commentStyledTexts) {
					highlightMatches(searchString, styledText);
					styledTexts.add(styledText);
				}
				group.clearSectionHyperlink();
				taskEditorPage.reflow();
			}
		};
		group.createSectionHyperlink(
				NLS.bind(Messages.TaskEditorFindSupport_Show_X_more_results, matchingViewers.size()), listener);
	}

	private static boolean anyCommentContains(List<TaskAttribute> commentAttributes, String text) {
		for (TaskAttribute commentAttribute : commentAttributes) {
			if (commentContains(commentAttribute, text)) {
				return true;
			}
		}
		return false;
	}

	private static boolean commentContains(TaskAttribute commentAttribute, String searchString) {
		TaskAttribute attribute = commentAttribute.getMappedAttribute(TaskAttribute.COMMENT_TEXT);
		return attribute.getValue().toLowerCase().contains(searchString);
	}

	private static List<CommentViewer> searchComments(List<TaskAttribute> commentAttributes,
			List<CommentViewer> commentViewers, String searchString) {
		List<CommentViewer> matchingViewers = new ArrayList<TaskEditorCommentPart.CommentViewer>();
		for (int i = 0; i < commentViewers.size(); i++) {
			CommentViewer viewer = commentViewers.get(i);
			if (commentContains(commentAttributes.get(i), searchString)) {
				matchingViewers.add(viewer);
			}
		}
		return matchingViewers;
	}

	protected static void gatherStyledTexts(List<CommentViewer> commentViewers, List<StyledText> styledTexts) {
		for (CommentViewer viewer : commentViewers) {
			try {
				ExpandableComposite composite = (ExpandableComposite) viewer.getControl();
				viewer.suppressSelectionChanged(true);
				if (composite != null && !composite.isExpanded()) {
					CommonFormUtil.setExpanded(composite, true);
				}
				gatherStyledTextsInComposite(composite, styledTexts);
			} finally {
				viewer.suppressSelectionChanged(false);
			}
		}
	}

	private static void gatherStyledTexts(Control control, List<StyledText> result) {
		if (control instanceof ExpandableComposite) {
			ExpandableComposite composite = (ExpandableComposite) control;
			if (!composite.isExpanded()) {
				CommonFormUtil.setExpanded(composite, true);
			}
			gatherStyledTextsInComposite(composite, result);
		} else if (control instanceof Composite) {
			gatherStyledTextsInComposite((Composite) control, result);
		}
	}

	private static void gatherStyledTextsInComposite(Composite composite, List<StyledText> result) {
		if (composite != null && !composite.isDisposed()) {
			for (Control child : composite.getChildren()) {
				if (child instanceof StyledText) {
					result.add((StyledText) child);
				} else if (child instanceof Composite) {
					gatherStyledTextsInComposite((Composite) child, result);
				}
			}
		}
	}

	private static void highlightMatches(String searchString, StyledText styledText) {
		String text = styledText.getText().toLowerCase();
		for (int index = 0; index < text.length(); index += searchString.length()) {
			index = text.indexOf(searchString, index);
			if (index == -1) {
				break;
			}
			styledText.setStyleRange(new StyleRange(index, searchString.length(), null, HIGHLIGHTER_YELLOW));
		}
	}

	private void clearSearchResults() {
		for (StyledText oldText : styledTexts) {
			List<StyleRange> otherRanges = new ArrayList<StyleRange>();
			if (!oldText.isDisposed()) {
				for (StyleRange styleRange : oldText.getStyleRanges()) {
					if (styleRange.background == null || !styleRange.background.equals(HIGHLIGHTER_YELLOW)) {
						otherRanges.add(styleRange); // preserve ranges that aren't from highlighting search results
					}
				}
				oldText.setStyleRanges(otherRanges.toArray(new StyleRange[otherRanges.size()]));
			}
		}
		styledTexts.clear();
		for (CommentGroupViewer group : commentGroupViewers) {
			group.clearSectionHyperlink();
		}
	}

}
