/*******************************************************************************
 * Copyright (c) 2004, 2010 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
 *     Raphael Ackermann - spell checking support on bug 195514
 *     Jingwen Ou - extensibility improvements
 *     David Green - fix for bug 256702 
 *******************************************************************************/

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

import java.util.Iterator;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.mylyn.internal.provisional.commons.ui.CommonFormUtil;
import org.eclipse.mylyn.internal.provisional.commons.ui.CommonTextSupport;
import org.eclipse.mylyn.internal.provisional.commons.ui.CommonThemes;
import org.eclipse.mylyn.internal.tasks.ui.commands.ViewSourceHandler;
import org.eclipse.mylyn.internal.tasks.ui.editors.RepositoryTextViewerConfiguration.Mode;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.ui.editors.AbstractRenderingEngine;
import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorExtension;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextActivation;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.ui.themes.IThemeManager;

/**
 * A text attribute editor that can switch between a editor, preview and source view.
 * 
 * @author Raphael Ackermann
 * @author Steffen Pingel
 * @author Jingwen Ou
 */
public class RichTextEditor {

	public enum State {
		DEFAULT, BROWSER, EDITOR, PREVIEW;
	};

	public static class StateChangedEvent {

		public State state;

	}

	public interface StateChangedListener {

		public void stateChanged(StateChangedEvent event);

	}

	public class ViewSourceAction extends Action {

		public ViewSourceAction() {
			super(Messages.RichTextAttributeEditor_Viewer_Source, SWT.TOGGLE);
			setChecked(false);
			setEnabled(false);
		}

		@Override
		public void run() {
			if (isChecked()) {
				showDefault();
			} else {
				showEditor();
			}
			if (editorLayout != null) {
				EditorUtil.reflow(editorLayout.topControl);
			}
			ViewSourceHandler.setChecked(isChecked());
		}

	}

	private static final String KEY_TEXT_VERSION = "org.eclipse.mylyn.tasks.ui.textVersion"; //$NON-NLS-1$

	private BrowserPreviewViewer browserViewer;

	private IContextActivation contextActivation;

	private final IContextService contextService;

	private Control control;

	private SourceViewer defaultViewer;

	private Composite editorComposite;

	private StackLayout editorLayout;

	private SourceViewer editorViewer;

	private final AbstractTaskEditorExtension extension;

	private Mode mode;

	private SourceViewer previewViewer;

	boolean readOnly;

	private AbstractRenderingEngine renderingEngine;

	private final TaskRepository repository;

	private boolean spellCheckingEnabled;

	private final int style;

	private FormToolkit toolkit;

	private final IAction viewSourceAction;

	private String text;

	/**
	 * Changed each time text is updated.
	 */
	private int textVersion;

	private final ListenerList stateChangedListeners = new ListenerList(ListenerList.IDENTITY);

	private final ITask task;

	@Deprecated
	public RichTextEditor(TaskRepository repository, int style) {
		this(repository, style, null, null, null);
	}

	@Deprecated
	public RichTextEditor(TaskRepository repository, int style, IContextService contextService,
			AbstractTaskEditorExtension extension) {
		this(repository, style, contextService, extension, null);
	}

	public RichTextEditor(TaskRepository repository, int style, IContextService contextService,
			AbstractTaskEditorExtension extension, ITask task) {
		this.repository = repository;
		this.style = style;
		this.contextService = contextService;
		this.extension = extension;
		this.text = ""; //$NON-NLS-1$
		this.viewSourceAction = new ViewSourceAction();
		setMode(Mode.DEFAULT);
		this.task = task;
	}

	private SourceViewer configure(final SourceViewer viewer, Document document, boolean readOnly) {
		// do this before setting the document to not require invalidating the presentation
		installHyperlinkPresenter(viewer, repository, task, getMode());

		updateDocument(viewer, document, readOnly);
		if (readOnly) {
			if (extension != null) {
				// setting view source action
				viewer.getControl().setData(ViewSourceHandler.VIEW_SOURCE_ACTION, viewSourceAction);
				viewer.getControl().addFocusListener(new FocusAdapter() {
					@Override
					public void focusGained(FocusEvent e) {
						ViewSourceHandler.setChecked(getViewer() == defaultViewer);
					}
				});
			}
		} else {
			installListeners(viewer);
			viewer.getControl().setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
		}

		// enable cut/copy/paste
		CommonTextSupport.setTextViewer(viewer.getTextWidget(), viewer);
		viewer.setEditable(!readOnly);
		viewer.getTextWidget().setFont(getFont());
		if (toolkit != null) {
			toolkit.adapt(viewer.getControl(), false, false);
		}
		EditorUtil.addScrollListener(viewer.getTextWidget());
		return viewer;
	}

	/** Configures annotation model for spell checking. */
	private void updateDocument(SourceViewer viewer, Document document, boolean readOnly) {
		if (new Integer(this.textVersion).equals(viewer.getData(KEY_TEXT_VERSION))) {
			// already up-to-date, skip re-loading of the document
			return;
		}

		if (readOnly) {
			viewer.setDocument(document);
		} else {
			AnnotationModel annotationModel = new AnnotationModel();
			viewer.showAnnotations(false);
			viewer.showAnnotationsOverview(false);
			IAnnotationAccess annotationAccess = new DefaultMarkerAnnotationAccess();
			final SourceViewerDecorationSupport support = new SourceViewerDecorationSupport(viewer, null,
					annotationAccess, EditorsUI.getSharedTextColors());
			Iterator<?> e = new MarkerAnnotationPreferences().getAnnotationPreferences().iterator();
			while (e.hasNext()) {
				support.setAnnotationPreference((AnnotationPreference) e.next());
			}
			support.install(EditorsUI.getPreferenceStore());
			viewer.getTextWidget().addDisposeListener(new DisposeListener() {
				public void widgetDisposed(DisposeEvent e) {
					support.uninstall();
				}
			});
			//viewer.getTextWidget().setIndent(2);
			viewer.setDocument(document, annotationModel);
		}
		viewer.setData(KEY_TEXT_VERSION, this.textVersion);
	}

	public void createControl(Composite parent, FormToolkit toolkit) {
		this.toolkit = toolkit;

		int style = this.style;
		if (!isReadOnly() && (style & SWT.NO_SCROLL) == 0) {
			style |= SWT.V_SCROLL;
		}

		if (extension != null || renderingEngine != null) {
			editorComposite = new Composite(parent, SWT.NULL);
			editorLayout = new StackLayout() {
				@Override
				protected Point computeSize(Composite composite, int hint, int hint2, boolean flushCache) {
					return topControl.computeSize(hint, hint2, flushCache);
				}
			};
			editorComposite.setLayout(editorLayout);
			setControl(editorComposite);

			if (extension != null) {
				if (isReadOnly()) {
					editorViewer = extension.createViewer(repository, editorComposite, style,
							createHyperlinkDetectorContext());
				} else {
					editorViewer = extension.createEditor(repository, editorComposite, style,
							createHyperlinkDetectorContext());
					editorViewer.getTextWidget().addFocusListener(new FocusListener() {
						public void focusGained(FocusEvent e) {
							setContext();
						}

						public void focusLost(FocusEvent e) {
							unsetContext();
						}
					});
					editorViewer.getTextWidget().addDisposeListener(new DisposeListener() {
						public void widgetDisposed(DisposeEvent e) {
							unsetContext();
						}
					});
				}
				configure(editorViewer, new Document(getText()), isReadOnly());
				show(editorViewer.getControl());
			} else {
				defaultViewer = createDefaultEditor(editorComposite, style);
				configure(defaultViewer, new Document(getText()), isReadOnly());
				show(defaultViewer.getControl());
			}

			if (!isReadOnly() && (style & SWT.NO_SCROLL) == 0) {
				editorComposite.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
			}

			viewSourceAction.setEnabled(true);
		} else {
			defaultViewer = createDefaultEditor(parent, style);
			configure(defaultViewer, new Document(getText()), isReadOnly());
			setControl(defaultViewer.getControl());

			viewSourceAction.setEnabled(false);
		}
	}

	@SuppressWarnings({ "rawtypes" })
	private IAdaptable createHyperlinkDetectorContext() {
		return new IAdaptable() {
			public Object getAdapter(Class adapter) {
				if (adapter == TaskRepository.class) {
					return repository;
				}
				if (adapter == ITask.class) {
					return task;
				}
				return null;
			}
		};
	}

	private SourceViewer createDefaultEditor(Composite parent, int styles) {
		SourceViewer defaultEditor = new SourceViewer(parent, null, styles | SWT.WRAP);

		RepositoryTextViewerConfiguration viewerConfig = new RepositoryTextViewerConfiguration(repository, task,
				isSpellCheckingEnabled() && !isReadOnly());
		viewerConfig.setMode(getMode());
		defaultEditor.configure(viewerConfig);

		return defaultEditor;
	}

	private BrowserPreviewViewer getBrowserViewer() {
		if (editorComposite == null || renderingEngine == null) {
			return null;
		}

		if (browserViewer == null) {
			browserViewer = new BrowserPreviewViewer(getRepository(), renderingEngine);
			browserViewer.createControl(editorComposite, toolkit);
		}
		return browserViewer;
	}

	public Control getControl() {
		return control;
	}

	public SourceViewer getDefaultViewer() {
		if (defaultViewer == null) {
			defaultViewer = createDefaultEditor(editorComposite, style);
			configure(defaultViewer, new Document(getText()), isReadOnly());

			// fixed font size
			defaultViewer.getTextWidget().setFont(JFaceResources.getFontRegistry().get(JFaceResources.TEXT_FONT));
			// adapt maximize action
			defaultViewer.getControl().setData(EditorUtil.KEY_TOGGLE_TO_MAXIMIZE_ACTION,
					editorViewer.getControl().getData(EditorUtil.KEY_TOGGLE_TO_MAXIMIZE_ACTION));
			// adapt menu to the new viewer
			installMenu(defaultViewer.getControl(), editorViewer.getControl().getMenu());
		}
		return defaultViewer;
	}

	public SourceViewer getEditorViewer() {
		return editorViewer;
	}

	private Font getFont() {
		if (mode == Mode.DEFAULT) {
			IThemeManager themeManager = PlatformUI.getWorkbench().getThemeManager();
			Font font = themeManager.getCurrentTheme().getFontRegistry().get(CommonThemes.FONT_EDITOR_COMMENT);
			return font;
		} else {
			return EditorUtil.TEXT_FONT;
		}
	}

	public Mode getMode() {
		return mode;
	}

	/**
	 * @return The preview source viewer or null if there is no extension available or the attribute is read only
	 */
	private SourceViewer getPreviewViewer() {
		if (extension == null) {
			return null;
		}

		// construct as needed
		if (previewViewer == null) {
			// previewer should always have a vertical scroll bar if it's editable
			int previewViewerStyle = style;
			if (getEditorViewer() != null) {
				previewViewerStyle |= SWT.V_SCROLL;
			}
			previewViewer = extension.createViewer(repository, editorComposite, previewViewerStyle,
					createHyperlinkDetectorContext());
			configure(previewViewer, new Document(getText()), true);
			// adapt maximize action
			previewViewer.getControl().setData(EditorUtil.KEY_TOGGLE_TO_MAXIMIZE_ACTION,
					editorViewer.getControl().getData(EditorUtil.KEY_TOGGLE_TO_MAXIMIZE_ACTION));
			installMenu(previewViewer.getControl(), editorViewer.getControl().getMenu());
			//set the background color in case there is an incoming to show
			previewViewer.getTextWidget().setBackground(editorComposite.getBackground());
		}
		return previewViewer;
	}

	public AbstractRenderingEngine getRenderingEngine() {
		return renderingEngine;
	}

	public TaskRepository getRepository() {
		return repository;
	}

	public String getText() {
		return this.text;
	}

	public SourceViewer getViewer() {
		if (editorLayout == null) {
			return defaultViewer;
		}
		if (defaultViewer != null && editorLayout.topControl == defaultViewer.getControl()) {
			return defaultViewer;
		} else if (previewViewer != null && editorLayout.topControl == previewViewer.getControl()) {
			return previewViewer;
		} else {
			return editorViewer;
		}
	}

	public IAction getViewSourceAction() {
		return viewSourceAction;
	}

	public boolean hasBrowser() {
		return renderingEngine != null;
	}

	public boolean hasPreview() {
		return extension != null && !isReadOnly();
	}

	public static RepositoryTextViewerConfiguration installHyperlinkPresenter(ISourceViewer viewer,
			TaskRepository repository, ITask task, Mode mode) {
		RepositoryTextViewerConfiguration configuration = new RepositoryTextViewerConfiguration(repository, task, false);
		configuration.setMode(mode);

		// do not configure viewer, this has already been done in extension

		AbstractHyperlinkTextPresentationManager manager;
		if (mode == Mode.DEFAULT) {
			manager = new HighlightingHyperlinkTextPresentationManager();
			manager.setHyperlinkDetectors(configuration.getDefaultHyperlinkDetectors(viewer, null));
			manager.install(viewer);

			manager = new TaskHyperlinkTextPresentationManager();
			manager.setHyperlinkDetectors(configuration.getDefaultHyperlinkDetectors(viewer, Mode.TASK));
			manager.install(viewer);
		} else if (mode == Mode.TASK_RELATION) {
			manager = new TaskHyperlinkTextPresentationManager();
			manager.setHyperlinkDetectors(configuration.getDefaultHyperlinkDetectors(viewer, Mode.TASK_RELATION));
			manager.install(viewer);
		}

		return configuration;
	}

	private void installListeners(final SourceViewer viewer) {
		viewer.addTextListener(new ITextListener() {
			public void textChanged(TextEvent event) {
				// filter out events caused by text presentation changes, e.g. annotation drawing
				String value = viewer.getTextWidget().getText();
				if (!RichTextEditor.this.text.equals(value)) {
					RichTextEditor.this.text = value;
					RichTextEditor.this.textVersion++;
					viewer.setData(KEY_TEXT_VERSION, RichTextEditor.this.textVersion);
					valueChanged(value);
					CommonFormUtil.ensureVisible(viewer.getTextWidget());
				}
			}
		});
		// ensure that tab traverses to next control instead of inserting a tab character unless editing multi-line text
		if ((style & SWT.MULTI) != 0 && mode != Mode.DEFAULT) {
			viewer.getTextWidget().addListener(SWT.Traverse, new Listener() {
				public void handleEvent(Event event) {
					switch (event.detail) {
					case SWT.TRAVERSE_TAB_NEXT:
					case SWT.TRAVERSE_TAB_PREVIOUS:
						event.doit = true;
						break;
					}
				}
			});
		}
	}

	private void installMenu(final Control control, Menu menu) {
		if (menu != null) {
			control.setMenu(menu);
			control.addDisposeListener(new DisposeListener() {
				public void widgetDisposed(DisposeEvent e) {
					control.setMenu(null);
				}
			});
		}
	}

	public boolean isReadOnly() {
		return readOnly;
	}

	public boolean isSpellCheckingEnabled() {
		return spellCheckingEnabled;
	}

	private void setContext() {
		if (contextService == null) {
			return;
		}
		if (contextActivation != null) {
			contextService.deactivateContext(contextActivation);
			contextActivation = null;
		}
		if (contextService != null && extension.getEditorContextId() != null) {
			contextActivation = contextService.activateContext(extension.getEditorContextId());
		}
	}

	private void setControl(Control control) {
		this.control = control;
	}

	public void setMode(Mode mode) {
		Assert.isNotNull(mode);
		this.mode = mode;
	}

	public void setReadOnly(boolean readOnly) {
		this.readOnly = readOnly;
	}

	public void setRenderingEngine(AbstractRenderingEngine renderingEngine) {
		this.renderingEngine = renderingEngine;
	}

	public void setSpellCheckingEnabled(boolean spellCheckingEnabled) {
		this.spellCheckingEnabled = spellCheckingEnabled;
	}

	public void setText(String value) {
		this.text = value;
		this.textVersion++;
		SourceViewer viewer = getViewer();
		if (viewer != null) {
			viewer.getDocument().set(value);
		}
	}

	/**
	 * Brings <code>control</code> to top.
	 */
	private void show(Control control) {
		// no extension is available
		if (editorComposite == null) {
			return;
		}
		editorLayout.topControl = control;
		if (editorComposite.getParent().getLayout() instanceof FillWidthLayout) {
			((FillWidthLayout) editorComposite.getParent().getLayout()).flush();
		}
		editorComposite.layout();
		control.setFocus();
		fireStateChangedEvent();
	}

	protected void fireStateChangedEvent() {
		if (stateChangedListeners.isEmpty()) {
			return;
		}
		StateChangedEvent event = new StateChangedEvent();
		if (defaultViewer != null && defaultViewer.getControl() == editorLayout.topControl) {
			event.state = State.DEFAULT;
		} else if (editorViewer != null && editorViewer.getControl() == editorLayout.topControl) {
			event.state = State.EDITOR;
		} else if (previewViewer != null && previewViewer.getControl() == editorLayout.topControl) {
			event.state = State.PREVIEW;
		} else if (browserViewer != null && browserViewer.getControl() == editorLayout.topControl) {
			event.state = State.BROWSER;
		}
		Object[] listeners = stateChangedListeners.getListeners();
		for (Object listener : listeners) {
			((StateChangedListener) listener).stateChanged(event);
		}
	}

	/**
	 * Brings <code>viewer</code> to top.
	 */
	private void show(SourceViewer viewer) {
		// WikiText modifies the document therefore, set a new document every time a viewer is changed to synchronize content between viewers 
		// ensure that editor has an annotation model
		updateDocument(viewer, new Document(getText()), !viewer.isEditable());
		show(viewer.getControl());
	}

	public void showBrowser() {
		BrowserPreviewViewer viewer = getBrowserViewer();
		viewer.update(getText());
		if (viewer != null) {
			show(viewer.getControl());
		}
	}

	public void showDefault() {
		show(getDefaultViewer());
	}

	public void showEditor() {
		if (getEditorViewer() != null) {
			show(getEditorViewer());
		} else {
			show(getDefaultViewer());
		}
	}

	private void showPreview(boolean sticky) {
		if (!isReadOnly() && getPreviewViewer() != null) {
			show(getPreviewViewer());
		}
	}

	public void showPreview() {
		showPreview(true);
	}

	private void unsetContext() {
		if (contextService == null) {
			return;
		}
		if (contextActivation != null) {
			contextService.deactivateContext(contextActivation);
			contextActivation = null;
		}
	}

	protected void valueChanged(String value) {
	}

	public void enableAutoTogglePreview() {
		if (!isReadOnly() && getPreviewViewer() != null) {
			final MouseAdapter listener = new MouseAdapter() {
				private boolean toggled;

				@Override
				public void mouseUp(MouseEvent e) {
					if (!toggled && e.count == 1) {
						// delay switching in case user intended to select text
						Display.getDefault().timerExec(Display.getDefault().getDoubleClickTime(), new Runnable() {

							public void run() {
								if (previewViewer.getTextWidget() == null || previewViewer.getTextWidget().isDisposed()) {
									return;
								}

								if (previewViewer.getTextWidget().getSelectionCount() == 0) {
									int offset = previewViewer.getTextWidget().getCaretOffset();
									showEditor();
									editorViewer.getTextWidget().setCaretOffset(offset);

									// only do this once, let the user manage toggling from then on
									toggled = true;
								}
							}
						});
					}
				}
			};
			previewViewer.getTextWidget().addMouseListener(listener);
//			editorViewer.getTextWidget().addFocusListener(new FocusAdapter() {
//				@Override
//				public void focusLost(FocusEvent e) {
//					if (!previewSticky) {
//						showPreview(false);
//					}
//				}
//			});
		}
	}

	/**
	 * Sets the background color for all instantiated viewers
	 * 
	 * @param color
	 */
	public void setBackground(Color color) {
		if (editorComposite != null && !editorComposite.isDisposed()) {
			editorComposite.setBackground(color);
			for (Control child : editorComposite.getChildren()) {
				child.setBackground(color);
			}
		}
	}

	public void addStateChangedListener(StateChangedListener listener) {
		stateChangedListeners.add(listener);
	}

	public void removeStateChangedListener(StateChangedListener listener) {
		stateChangedListeners.remove(listener);
	}

}
