/*******************************************************************************
 * Copyright (c) 2017 Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *   Red Hat, Inc. - initial API and implementation
 ******************************************************************************/
package org.jboss.tools.windup.ui.internal.rules.delegate;

import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import javax.annotation.PostConstruct;

import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.URLHyperlink;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.IOverviewRuler;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.OverviewRuler;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.mylyn.internal.wikitext.ui.WikiTextUiPlugin;
import org.eclipse.mylyn.internal.wikitext.ui.editor.MarkupProjectionViewer;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.FastMarkupPartitioner;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.MarkupDocumentProvider;
import org.eclipse.mylyn.wikitext.markdown.MarkdownLanguage;
import org.eclipse.mylyn.wikitext.parser.Attributes;
import org.eclipse.mylyn.wikitext.parser.MarkupParser;
import org.eclipse.mylyn.wikitext.parser.builder.HtmlDocumentBuilder;
import org.eclipse.mylyn.wikitext.parser.markup.AbstractMarkupLanguage;
import org.eclipse.mylyn.wikitext.ui.editor.MarkupSourceViewerConfiguration;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.ProgressAdapter;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IWorkbenchPreferenceConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.editors.text.EditorsPlugin;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.jboss.tools.windup.ui.WindupUIPlugin;
import org.jboss.tools.windup.ui.internal.Messages;
import org.jboss.tools.windup.ui.internal.editor.AddNodeAction;
import org.jboss.tools.windup.ui.internal.editor.ElementAttributesContainer;
import org.jboss.tools.windup.ui.internal.editor.RulesetElementUiDelegateFactory.RulesetConstants;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.common.base.Objects;

@SuppressWarnings("restriction")
public class HintMessageTab extends ElementAttributesContainer {
	
	protected static final int VERTICAL_RULER_WIDTH = 12;
	
	private static final String CSS_CLASS_EDITOR_PREVIEW = "editorPreview"; //$NON-NLS-1$
	
	private Browser browser;
	private IDocument document;
	
	private SourceViewer sourceViewer;
	private MarkdownLanguage language = new MarkdownLanguage();
	
	private IHandlerService handlerService;
	private IHandlerActivation contentAssistHandlerActivation;
	
	private Section sourceSection;
	private Section previewSection;
	
	@PostConstruct
	public void createControls(Composite parent) {
		handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class);
		
		CTabFolder folder = createMessagesSection(parent);
		createSourceTab(folder);
		CTabItem previewItem = createPreviewTab(folder);
		folder.setSelection(previewItem);
		
		updatePreview();
	}
	
	private CTabFolder createMessagesSection(Composite parent) {
		parent = toolkit.createComposite(parent);
		GridLayoutFactory.fillDefaults().extendedMargins(0, 0, 0, 5).applyTo(parent);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(parent);

		ColorRegistry reg = JFaceResources.getColorRegistry();
		Color c1 = reg.get("org.eclipse.ui.workbench.ACTIVE_TAB_BG_START"), //$NON-NLS-1$
			  c2 = reg.get("org.eclipse.ui.workbench.ACTIVE_TAB_BG_END"); //$NON-NLS-1$
		CTabFolder folder = new CTabFolder(parent, SWT.NO_REDRAW_RESIZE | SWT.FLAT);
		folder.setSelectionBackground(new Color[] {c1, c2},	new int[] {100}, true);
		folder.setSelectionForeground(reg.get("org.eclipse.ui.workbench.ACTIVE_TAB_TEXT_COLOR")); //$NON-NLS-1$
		folder.setSimple(PlatformUI.getPreferenceStore().getBoolean(IWorkbenchPreferenceConstants.SHOW_TRADITIONAL_STYLE_TABS));
		folder.setBorderVisible(true);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(folder);
		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.horizontalSpan = 2;
		folder.setLayoutData(gd);
		folder.setFont(parent.getFont());
		return folder;
	}
	
	private CTabItem createSourceTab(CTabFolder folder) {
		CTabItem item = new CTabItem(folder, SWT.BORDER);
		item.setText(Messages.RulesetEditor_messageSection);
		item.setImage(WindupUIPlugin.getDefault().getImageRegistry().get(WindupUIPlugin.IMG_MARKDOWN));
		Composite client = toolkit.createComposite(folder);
		GridLayoutFactory.fillDefaults().applyTo(client);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(client);
		item.setControl(client);
		createSourceViewer(client);
		return item;
	}
	
	private CTabItem createPreviewTab(CTabFolder folder) {
		CTabItem item = new CTabItem(folder, SWT.BORDER);
		item.setText(Messages.RulesetEditor_previewSection);
		item.setImage(WindupUIPlugin.getDefault().getImageRegistry().get(WindupUIPlugin.IMG_REPORT));
		Composite client = toolkit.createComposite(folder);
		GridLayoutFactory.fillDefaults().applyTo(client);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(client);
		item.setControl(client);
		createBrowser(client);
		return item;	
	}
	
	@Override
	protected void bind() {
		String message = getElementMessage();
		if (!Objects.equal(document.get(), message)) {
			document.set(getElementMessage());
		}
		sourceViewer.invalidateTextPresentation();
		updatePreview();
	}
	
	public Section getSourceEditorContainer() {
		return sourceSection;
	}
	
	public Section getBrowserContainer() {
		return previewSection;
	}
	
	private void createBrowser(Composite parent) {
		browser = new Browser(parent, SWT.NONE);
		GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 150).applyTo(browser);
		browser.addLocationListener(new LocationListener() {
			public void changed(LocationEvent event) {
				event.doit = false;
			}
			public void changing(LocationEvent event) {
				// if it looks like an absolute URL
				if (event.location.matches("([a-zA-Z]{3,8})://?.*")) { //$NON-NLS-1$
					// workaround for browser problem (bug 262043)
					int idxOfSlashHash = event.location.indexOf("/#"); //$NON-NLS-1$
					if (idxOfSlashHash != -1) {
						// allow javascript-based scrolling to work
						if (!event.location.startsWith("file:///#")) { //$NON-NLS-1$
							event.doit = false;
						}
						return;
					}
					// workaround end
					event.doit = false;
					try {
						IWebBrowser webBrowser = PlatformUI.getWorkbench()
								.getBrowserSupport()
								.createBrowser("org.eclipse.ui.browser"); //$NON-NLS-1$
						webBrowser.openURL(new URL(event.location));
					} catch (Exception e) {
						new URLHyperlink(new Region(0, 1), event.location).open();
					}
				}
			}
		});
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private CMElementDeclaration getMessageCmNode() {
		List candidates = modelQuery.getAvailableContent(element, elementDeclaration, 
				ModelQuery.VALIDITY_STRICT);
		Optional<CMElementDeclaration> found = candidates.stream().filter(candidate -> {
			if (candidate instanceof CMElementDeclaration) {
				return RulesetConstants.MESSAGE.equals(((CMElementDeclaration)candidate).getElementName());
			}
			return false;
		}).findFirst();
		if (found.isPresent()) {
			return found.get();
		}
		return null;
	} 
	
	private Node getMessageNode() {
		NodeList list = element.getElementsByTagName(RulesetConstants.MESSAGE);
		if (list.getLength() > 0) {
			return list.item(0);
		}
		return null;
	}
	
	private String getElementMessage() {
		String message = "";
		Element node = (Element)getMessageNode();
		if (node != null) {
			message = getCdataMessage(node);
			if (message == null) {
				message = contentHelper.getNodeValue(node);
			}
		}
		return message == null ? "" : message;
	}
	
	private String getCdataMessage(Node messageNode) {
		String message = null;
		Node cdataNode = getCdataNode(messageNode);
		if (cdataNode != null) {
			message = contentHelper.getNodeValue(cdataNode);
		}
		return message;
	}

	private Node getCdataNode(Node messageNode) {
		NodeList childList = messageNode.getChildNodes();
		for (int i = 0; i < childList.getLength(); i++) {
			Node child = childList.item(i);
			if (Objects.equal(child.getNodeName(), RulesetConstants.CDATA)) {
				return child;
			}
		}
		return null;
	}
	
	protected void setMessage(String value) {
		Node node = getMessageNode();
		CMNode cmNode = getMessageCmNode();
		try {
			model.aboutToChangeModel();
			if (node != null) {
				Node cdataNode = getCdataNode(node);
				String currentMessage = contentHelper.getNodeValue(node);
				if (currentMessage != null && !currentMessage.isEmpty()) {
					contentHelper.setNodeValue(node, value);
				}
				else if (cdataNode != null) {
					contentHelper.setNodeValue(cdataNode, value);
				}
				else {
					createCdataNode(node, value);
				}
			}
			else {
				AddNodeAction newNodeAction = new AddNodeAction(model, cmNode, element, element.getChildNodes().getLength());
				newNodeAction.runWithoutTransaction();
				List<Node> newNodes = newNodeAction.getResult();
				if (!newNodes.isEmpty()) {
					node = newNodes.get(0);
					contentHelper.setNodeValue(node, "");
					createCdataNode(node, value);
				}
			}
		}
		finally {
			model.changedModel();
		}
	}
	
	private void createCdataNode(Node messageNode, String value) {
		Node cdataNode = messageNode.getOwnerDocument().createCDATASection(value);
		messageNode.appendChild(cdataNode);
	}
	
	protected void createSourceViewer(Composite parent) {
		String string = getElementMessage();
		IStorage storage = new StringInput.StringStorage(string);
		IEditorInput editorInput = new StringInput(storage);
		CompositeRuler ruler = new CompositeRuler();
		ISharedTextColors colors = EditorsPlugin.getDefault().getSharedTextColors();
		int styles = SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION | SWT.WRAP;
		IOverviewRuler overviewRuler = new OverviewRuler(new DefaultMarkerAnnotationAccess(), VERTICAL_RULER_WIDTH, colors);
		
		this.sourceViewer = new MarkupProjectionViewer(parent, ruler, overviewRuler, true, styles);
		ElementDetailsSection.addScrollListener(sourceViewer.getTextWidget());
		
		GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 100).applyTo(sourceViewer.getControl());
		
		try {
			MarkupDocumentProvider documentProvider = new MarkupDocumentProvider();
			documentProvider.connect(editorInput);
			this.document = documentProvider.getDocument(editorInput);
			//sourceViewer.setDocument(document);
			
			((AbstractMarkupLanguage) language).setEnableMacros(false);
			documentProvider.setMarkupLanguage(language);
			
			MarkupSourceViewerConfiguration sourceViewerConfiguration = new MarkupSourceViewerConfiguration(WikiTextUiPlugin.getDefault().getPreferenceStore());
			sourceViewerConfiguration.initializeDefaultFonts();
			sourceViewerConfiguration.setMarkupLanguage(language);
			sourceViewer.configure(sourceViewerConfiguration);
			
			IDocumentPartitioner partitioner = document.getDocumentPartitioner();
			FastMarkupPartitioner fastMarkupPartitioner = (FastMarkupPartitioner) partitioner;
			fastMarkupPartitioner.setMarkupLanguage(language);
		} catch (CoreException e) {
			WindupUIPlugin.log(e);
		}
		
		sourceViewer.getTextWidget().addFocusListener(new FocusListener() {
			@Override
			public void focusLost(FocusEvent e) {
				deactivateEditorHandlers();
			}
			@Override
			public void focusGained(FocusEvent e) {
				activatEditorHandlers();
			}
		});
		
		IDocumentListener documentListener = new IDocumentListener() {
			public void documentAboutToBeChanged(DocumentEvent event) {}
			public void documentChanged(DocumentEvent event) {
				if (!blockNotification) {
					setMessage(document.get());
					updatePreview();
				}
			}
		};
		
		sourceViewer.getTextWidget().addDisposeListener(new DisposeListener() {
			@Override
			public void widgetDisposed(DisposeEvent e) {
				document.removeDocumentListener(documentListener);
				deactivateEditorHandlers();
			}
		});
		
		document.addDocumentListener(documentListener);
		
		configureAsEditor(sourceViewer, (Document)document);
	}
	
	private void configureAsEditor(ISourceViewer viewer, Document document) {
		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();
			}
		});
		AnnotationModel annotationModel = new AnnotationModel();
		viewer.setDocument(document, annotationModel);
	}
	
	private void activatEditorHandlers() {
		contentAssistHandlerActivation = handlerService.activateHandler(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, 
				new ActionHandler(new AssistAction(sourceViewer)));
	}
	
	private void deactivateEditorHandlers() {
		if (contentAssistHandlerActivation != null) {
			handlerService.deactivateHandler(contentAssistHandlerActivation);
			contentAssistHandlerActivation = null;
		}
	}
	
	private static class AssistAction extends Action {
		private ITextOperationTarget fOperationTarget;

		public AssistAction(SourceViewer sourceViewer) {
			this.fOperationTarget = sourceViewer.getTextOperationTarget();
		}
		
		@Override
		public void run() {
			BusyIndicator.showWhile(Display.getDefault(), new Runnable() {
				@Override
				public void run() {
					fOperationTarget.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
				}
			});
		}
	}
	
	private void updatePreview() {
		final int verticalScrollbarPos = 0;
		String title = file == null ? "" : file.getName(); //$NON-NLS-1$
		if (title.lastIndexOf('.') != -1) {
			title = title.substring(0, title.lastIndexOf('.'));
		}
		StringWriter writer = new StringWriter();
		HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
			@Override
			protected void emitAnchorHref(String href) {
				if (href != null && href.startsWith("#")) { //$NON-NLS-1$
					writer.writeAttribute("onclick", //$NON-NLS-1$
							String.format("javascript: window.location.hash = '%s'; return false;", href)); //$NON-NLS-1$
					writer.writeAttribute("href", "#"); //$NON-NLS-1$//$NON-NLS-2$
				} else {
					super.emitAnchorHref(href);
				}
			}

			@Override
			public void beginHeading(int level, Attributes attributes) {
				attributes.appendCssClass(CSS_CLASS_EDITOR_PREVIEW);
				super.beginHeading(level, attributes);
			}

			@Override
			public void beginBlock(BlockType type, Attributes attributes) {
				attributes.appendCssClass(CSS_CLASS_EDITOR_PREVIEW);
				super.beginBlock(type, attributes);
			}
		};
		builder.setTitle(title);

		String css = WikiTextUiPlugin.getDefault().getPreferences().getMarkupViewerCss();
		builder.addCssStylesheet(new HtmlDocumentBuilder.Stylesheet(new StringReader(css)));

		AbstractMarkupLanguage markupLanguage = (AbstractMarkupLanguage)language.clone();
		markupLanguage.setEnableMacros(true);

		markupLanguage.setFilterGenerativeContents(false);
		markupLanguage.setBlocksOnly(false);

		MarkupParser markupParser = new MarkupParser();
		markupParser.setBuilder(builder);
		markupParser.setMarkupLanguage(markupLanguage);

		markupParser.parse(document.get());
		browser.addProgressListener(new ProgressAdapter() {
			@Override
			public void completed(ProgressEvent event) {
				browser.removeProgressListener(this);
				browser.execute(String.format("window.scrollTo(0,%d);", verticalScrollbarPos)); //$NON-NLS-1$
			}
		});
		String xhtml = writer.toString();
		browser.setText(xhtml);
	}
}
