/*******************************************************************************
 * Copyright (c) 2008, 2020 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.wst.xml.ui.internal.actions;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Event;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.TextEditorAction;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.ui.internal.XMLUIMessages;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;

/**
 * Moves the cursor to the end tag if it is in a start tag, and vice versa.
 * Also updates the matching tag annotation in the active editor.
 * 
 * @author nitin
 * 
 */
class GoToMatchingTagAction extends TextEditorAction {

	private class UpdateListener implements ISelectionChangedListener {
		public void selectionChanged(SelectionChangedEvent event) {
			updateFor(event.getSelection());
		}
	}

	private static final String ANNOTATION_TYPE = "org.eclipse.wst.xml.ui.matching.tag"; //$NON-NLS-1$
	private ISelectionChangedListener fUpdateListener = null;
	static final boolean DEBUG = false;

	/**
	 * @param bundle
	 * @param prefix
	 * @param editor
	 * @param style
	 */
	GoToMatchingTagAction(ResourceBundle bundle, String prefix, ITextEditor editor) {
		super(bundle, prefix, editor);
		fUpdateListener = new UpdateListener();
	}

	void removeAnnotation(boolean allMatching) {
		ITextEditor textEditor = getTextEditor();
		if (textEditor == null) {
			if (DEBUG) {
				System.out.println("no editor"); //$NON-NLS-1$
			}
			return;
		}
		IDocumentProvider documentProvider = textEditor.getDocumentProvider();
		if (documentProvider == null) {
			if (DEBUG) {
				System.out.println("no document provider"); //$NON-NLS-1$
			}
			return;
		}
		IAnnotationModel annotationModel = documentProvider.getAnnotationModel(textEditor.getEditorInput());
		if (annotationModel == null) {
			if (DEBUG) {
				System.out.println("no annotation model"); //$NON-NLS-1$
			}
			return;
		}

		Iterator<Annotation> annotationIterator = annotationModel.getAnnotationIterator();
		List<Annotation> oldAnnotations = new ArrayList<>();
		while (annotationIterator.hasNext()) {
			Annotation annotation = annotationIterator.next();
			if (ANNOTATION_TYPE.equals(annotation.getType())) {
				annotation.markDeleted(true);
				/**
				 * Sometimes it is supported, sometime's it is not. Confusing.
				 */
				try {
					annotationIterator.remove();
				}
				catch (UnsupportedOperationException e) {
					oldAnnotations.add(annotation);
				}
				if (DEBUG) {
					System.out.println("removed " + annotation); //$NON-NLS-1$
				}
				if (!allMatching)
					break;
			}
		}
		if (!oldAnnotations.isEmpty()) {
			int size = oldAnnotations.size();
			for (int i = 0; i < size; i++) {
				annotationModel.removeAnnotation(oldAnnotations.get(i));
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.action.Action#runWithEvent(org.eclipse.swt.widgets.Event)
	 */
	@Override
	public void runWithEvent(Event event) {
		super.runWithEvent(event);
		if (getTextEditor() == null)
			return;

		ISelection selection = getTextEditor().getSelectionProvider().getSelection();
		if (!selection.isEmpty() && selection instanceof IStructuredSelection && selection instanceof ITextSelection) {
			Object o = ((IStructuredSelection) selection).getFirstElement();
			if (o instanceof IDOMNode) {
				int offset = ((ITextSelection) selection).getOffset();
				IStructuredDocumentRegion matchRegion = null;
				if (((Node) o).getNodeType() == Node.ATTRIBUTE_NODE) {
					o = ((Attr) o).getOwnerElement();
				}

				int targetOffset = -1;
				if (o instanceof IDOMNode) {
					IDOMNode node = (IDOMNode) o;
					IStructuredDocumentRegion startStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
					if (startStructuredDocumentRegion != null && startStructuredDocumentRegion.containsOffset(offset)) {
						matchRegion = ((IDOMNode) o).getEndStructuredDocumentRegion();
						if (matchRegion != null)
							targetOffset = matchRegion.getStartOffset() + 2;
					}
					else {
						IStructuredDocumentRegion endStructuredDocumentRegion = node.getEndStructuredDocumentRegion();
						if (endStructuredDocumentRegion != null && endStructuredDocumentRegion.containsOffset(offset)) {
							matchRegion = ((IDOMNode) o).getStartStructuredDocumentRegion();
							if (matchRegion != null)
								targetOffset = matchRegion.getStartOffset() + 1;
						}
					}
				}

				if (targetOffset >= 0) {
					getTextEditor().getSelectionProvider().setSelection(new TextSelection(targetOffset, 0));
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
	 */
	@Override
	public void setEditor(ITextEditor editor) {
		ITextEditor textEditor = getTextEditor();
		if (textEditor != null) {
			removeAnnotation(true);

			ISelectionProvider selectionProvider = textEditor.getSelectionProvider();
			if (selectionProvider instanceof IPostSelectionProvider) {
				((IPostSelectionProvider) selectionProvider).removePostSelectionChangedListener(fUpdateListener);
			}
		}
		super.setEditor(editor);
		if (editor != null) {
			ISelectionProvider selectionProvider = editor.getSelectionProvider();
			if (selectionProvider instanceof IPostSelectionProvider) {
				((IPostSelectionProvider) selectionProvider).addPostSelectionChangedListener(fUpdateListener);
			}

			updateFor(selectionProvider.getSelection());
		}
	}

	@Override
	public void update() {
		setEnabled(true);

	}

	void updateFor(ISelection selection) {
		ITextEditor textEditor = getTextEditor();
		if (textEditor == null) {
			if (DEBUG) {
				System.out.println("no editor"); //$NON-NLS-1$
			}
			return;
		}
		IDocumentProvider documentProvider = textEditor.getDocumentProvider();
		if (documentProvider == null) {
			if (DEBUG) {
				System.out.println("no document provider"); //$NON-NLS-1$
			}
			return;
		}
		IAnnotationModel annotationModel = documentProvider.getAnnotationModel(textEditor.getEditorInput());
		if (annotationModel == null || !(annotationModel instanceof IAnnotationModelExtension)) {
			if (DEBUG) {
				System.out.println("no annotation model"); //$NON-NLS-1$
			}
			return;
		}

		List<Annotation> oldAnnotations = new ArrayList<>(2);
		Iterator<Annotation> annotationIterator = annotationModel.getAnnotationIterator();
		while (annotationIterator.hasNext()) {
			Annotation annotation = annotationIterator.next();
			if (ANNOTATION_TYPE.equals(annotation.getType())) {
				annotation.markDeleted(true);
				if (DEBUG) {
					System.out.println("removing " + annotation); //$NON-NLS-1$
				}
				oldAnnotations.add(annotation);
			}
		}

		Map<Annotation, Position> newAnnotations = new HashMap<>();
		if (!selection.isEmpty() && selection instanceof IStructuredSelection && selection instanceof ITextSelection) {
			Object o = ((IStructuredSelection) selection).getFirstElement();
			if (o instanceof IDOMNode) {
				int offset = ((ITextSelection) selection).getOffset();
				IStructuredDocumentRegion matchRegion = null;
				if (((Node) o).getNodeType() == Node.ATTRIBUTE_NODE) {
					o = ((Attr) o).getOwnerElement();
				}

				Position pStart = null;
				Position pEnd = null;
				String tag = ""; //$NON-NLS-1$
				if (o instanceof IDOMNode) {
					IDOMNode node = (IDOMNode) o;
					IStructuredDocumentRegion startStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
					if (startStructuredDocumentRegion != null && startStructuredDocumentRegion.containsOffset(offset)) {
						if (startStructuredDocumentRegion.getNumberOfRegions() > 1) {
							ITextRegion nameRegion = startStructuredDocumentRegion.getRegions().get(1);
							pStart = new Position(startStructuredDocumentRegion.getStartOffset(nameRegion), nameRegion.getTextLength());
							tag = startStructuredDocumentRegion.getText(nameRegion);
						}
						matchRegion = ((IDOMNode) o).getEndStructuredDocumentRegion();
						if (matchRegion != null && matchRegion.getNumberOfRegions() > 1) {
							ITextRegion nameRegion = matchRegion.getRegions().get(1);
							pEnd = new Position(matchRegion.getStartOffset(nameRegion), nameRegion.getTextLength());
						}
					}
					else {
						IStructuredDocumentRegion endStructuredDocumentRegion = node.getEndStructuredDocumentRegion();
						if (endStructuredDocumentRegion != null && endStructuredDocumentRegion.containsOffset(offset)) {
							if (endStructuredDocumentRegion.getNumberOfRegions() > 1) {
								ITextRegion nameRegion = endStructuredDocumentRegion.getRegions().get(1);
								pEnd = new Position(endStructuredDocumentRegion.getStartOffset(nameRegion), nameRegion.getTextLength());
								tag = endStructuredDocumentRegion.getText(nameRegion);
							}
							matchRegion = ((IDOMNode) o).getStartStructuredDocumentRegion();
							if (matchRegion != null && matchRegion.getNumberOfRegions() > 1) {
								ITextRegion nameRegion = matchRegion.getRegions().get(1);
								pStart = new Position(matchRegion.getStartOffset(nameRegion), nameRegion.getTextLength());
							}
						}
					}
				}
				if (pStart != null && pEnd != null) {
					Annotation annotation = new Annotation(ANNOTATION_TYPE, false, NLS.bind(XMLUIMessages.gotoMatchingTag_start, tag));
					newAnnotations.put(annotation, pStart);
					if (DEBUG) {
						System.out.println("adding " + annotation); //$NON-NLS-1$
					}
					annotation = new Annotation(ANNOTATION_TYPE, false, NLS.bind(XMLUIMessages.gotoMatchingTag_end, tag));
					newAnnotations.put(annotation, pEnd);
					if (DEBUG) {
						System.out.println("adding " + annotation); //$NON-NLS-1$
					}
				}
			}
		}
		((IAnnotationModelExtension) annotationModel).replaceAnnotations(oldAnnotations.toArray(new Annotation[oldAnnotations.size()]), newAnnotations);
	}
}
