/*****************************************************************************
 * Copyright (c) 2011, 2018 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.tabletree;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.INavigationLocation;
import org.eclipse.ui.NavigationLocation;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.TextSelectionNavigationLocation;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;

/**
 * {@link NavigationLocation} that is loosely based on {@link TextSelectionNavigation}
 * but operates on the {@link XMLMultiPageEditorPart}'s design page. History is
 * preserved as positions, but the selection set on the viewer is the indexed region
 *
 */
class DesignPageNavigationLocation extends NavigationLocation {

	// Memento tags and values
	private static final String TAG_X = "x"; //$NON-NLS-1$
	private static final String TAG_Y = "y"; //$NON-NLS-1$
	private static final String TAG_INFO = "info"; //$NON-NLS-1$
	private static final String INFO_DELETED = "deleted"; //$NON-NLS-1$
	private static final String INFO_NOT_DELETED = "not_deleted"; //$NON-NLS-1$

	private static final String CATEGORY = "__navigation_" + TextSelectionNavigationLocation.class.hashCode(); //$NON-NLS-1$
	private static final IPositionUpdater fgPositionUpdater = new DefaultPositionUpdater(CATEGORY);

	private Position fPosition;
	private IDocument fDocument;
	private Position fSavedPosition;
	private IDesignViewer fViewer;

	DesignPageNavigationLocation(IEditorPart part, IDesignViewer viewer, boolean initialize) {
		super(part);

		fViewer = viewer;
		if (initialize) {
			final ISelection selection = fViewer.getSelectionProvider().getSelection();

			IEditorPart textPart = getTextEditorPart();
			if (textPart != null) {
				IDocument document = getDocument((ITextEditor) textPart);
				if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
					Object first = ((IStructuredSelection) selection).getFirstElement();
					if (first instanceof IDOMNode) {
						IDOMNode node = (IDOMNode) first;
						Position position= new Position(node.getStartOffset(), node.getLength());
						if (installOnDocument(document, position)) {
							fDocument = document;
							fPosition = position;
							if (!part.isDirty())
								fSavedPosition = new Position(fPosition.offset, fPosition.length);
						}
					}
				}
				else { // The editor may not necessarily have a selection when opened
					Position position= new Position(0, 0);
					if (installOnDocument(document, position)) {
						fDocument = document;
						fPosition = position;
						if (!part.isDirty())
							fSavedPosition = new Position(fPosition.offset, fPosition.length);
					}
				}
			}
		}

	}

	/**
	 * Returns the {@link ITextEditor} associated with this editor part
	 * @return {@link IEditorPart} that is o
	 */
	protected ITextEditor getTextEditorPart() {
		IEditorPart part = super.getEditorPart();
		if (part != null) {
			return part.getAdapter(ITextEditor.class);
		}
		return null;
	}

	/**
	 * Returns the text editor's document.
	 * 
	 * @param part
	 *            the text editor
	 * @return the document of the given text editor
	 */
	private IDocument getDocument(ITextEditor part) {
		IDocumentProvider provider = part.getDocumentProvider();
		return provider.getDocument(part.getEditorInput());
	}

	/**
	 * Installs the given position on the given document.
	 * 
	 * @param document
	 *            the document
	 * @param position
	 *            the position
	 * @return <code>true</code> if the position could be installed
	 */
	private boolean installOnDocument(IDocument document, Position position) {

		if (document != null && position != null) {

			if (!document.containsPositionCategory(CATEGORY)) {
				document.addPositionCategory(CATEGORY);
				document.addPositionUpdater(fgPositionUpdater);
			}

			try {
				document.addPosition(CATEGORY, position);
				return true;
			} catch (BadLocationException e) {
			} catch (BadPositionCategoryException e) {
			}
		}

		return false;
	}

	/**
	 * Uninstalls the given position from the given document.
	 * 
	 * @param document
	 *            the document
	 * @param position
	 *            the position
	 * @return <code>true</code> if the position could be uninstalled
	 */
	private boolean uninstallFromDocument(IDocument document, Position position) {

		if (document != null && position != null) {
			try {

				document.removePosition(CATEGORY, position);

				Position[] category = document.getPositions(CATEGORY);
				if (category == null || category.length == 0) {
					document.removePositionCategory(CATEGORY);
					document.removePositionUpdater(fgPositionUpdater);
				}
				return true;

			} catch (BadPositionCategoryException e) {
			}
		}

		return false;
	}

	/*
	 * @see Object#toString()
	 */
	public String toString() {
		return "Selection<" + fPosition + ">"; //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Tells whether this location is equal to the current location in the given
	 * text editor.
	 * 
	 * @param part
	 *            the text editor
	 * @return <code>true</code> if the locations are equal
	 */
	private boolean equalsLocationOf() {

		if (fPosition == null)
			return true;

		if (fPosition.isDeleted)
			return false;

		final ISelection selection = fViewer.getSelectionProvider().getSelection();
		if (selection instanceof IStructuredSelection) {
			final Object first = ((IStructuredSelection) selection).getFirstElement();
			if (first instanceof IDOMNode) {
				final IDOMNode node = (IDOMNode) first;
				return fPosition.offset == node.getStartOffset() && fPosition.length == node.getLength();
			}
		}

		return false;
	}

	public void dispose() {
		uninstallFromDocument(fDocument, fPosition);
		fDocument = null;
		fPosition = null;
		fSavedPosition = null;
		super.dispose();
	}

	/**
	 * Releases the state of this location.
	 */
	public void releaseState() {
		// deactivate
		uninstallFromDocument(fDocument, fPosition);
		fDocument = null;
		fPosition = null;
		fSavedPosition = null;
		super.releaseState();
	}

	/**
	 * Merges the given location into this one.
	 * 
	 * @param location
	 *            the location to merge into this one
	 * @return <code>true<code> if merging was successful
	 */
	public boolean mergeInto(INavigationLocation location) {

		if (location == null)
			return false;

		if (getClass() != location.getClass())
			return false;

		if (fPosition == null || fPosition.isDeleted)
			return true;

		DesignPageNavigationLocation s = (DesignPageNavigationLocation) location;
		if (s.fPosition == null || s.fPosition.isDeleted) {
			uninstallFromDocument(fDocument, fPosition);
			s.fDocument = fDocument;
			s.fPosition = fPosition;
			s.fSavedPosition = fSavedPosition;
			return true;
		}

		if (s.fDocument == fDocument) {
			if (s.fPosition.overlapsWith(fPosition.offset, fPosition.length) || fPosition.offset + fPosition.length == s.fPosition.offset || s.fPosition.offset + s.fPosition.length == fPosition.offset) {
				s.fPosition.offset = fPosition.offset;
				s.fPosition.length = fPosition.length;
				return true;
			}
		}

		return false;
	}

	/**
	 * Restores this location.
	 */
	public void restoreLocation() {
		if (fPosition == null || fPosition.isDeleted)
			return;

		if (fViewer instanceof Viewer) {
			((Viewer) fViewer).setSelection(getSelection(), true);
		}
	}

	private ISelection getSelection() {
		ISelection selection = null;
		IStructuredModel model = null;
		final ITextEditor editor = getTextEditorPart();
		if (editor != null) {
			try {
				final IDocument document = getDocument(editor);
				if (document instanceof IStructuredDocument) {
					model = StructuredModelManager.getModelManager().getModelForRead((IStructuredDocument) document);
					if (model != null) {
						final IndexedRegion region = model.getIndexedRegion(fPosition.offset);
						if (region != null) {
							selection = new StructuredSelection(region);
						}
					}
				}
			}
			finally {
				if (model != null) {
					model.releaseFromRead();
				}
			}
		}
		return selection;
	}

	/**
	 * Restores the object state from the given memento.
	 * 
	 * @param memento
	 *            the memento
	 */
	public void restoreState(IMemento memento) {

		final ITextEditor part = getTextEditorPart();
		if (part != null) {

			// restore
			fDocument = getDocument(part);

			Integer offset = memento.getInteger(TAG_X);
			Integer length = memento.getInteger(TAG_Y);
			String deleted = memento.getString(TAG_INFO);

			if (offset != null && length != null) {
				Position p = new Position(offset.intValue(), length.intValue());
				if (deleted != null)
					p.isDeleted = INFO_DELETED.equals(deleted) ? true : false;

				// activate
				if (installOnDocument(fDocument, p)) {
					fPosition = p;
					if (!part.isDirty())
						fSavedPosition = new Position(fPosition.offset, fPosition.length);
				}
			}
		}
	}

	/**
	 * Stores the object state into the given memento.
	 * 
	 * @param memento
	 *            the memento
	 */
	public void saveState(IMemento memento) {
		if (fSavedPosition != null) {
			memento.putInteger(TAG_X, fSavedPosition.offset);
			memento.putInteger(TAG_Y, fSavedPosition.length);
			memento.putString(TAG_INFO, (fSavedPosition.isDeleted ? INFO_DELETED : INFO_NOT_DELETED));
		}
	}

	/**
	 * Hook method which is called when the given editor has been saved.
	 * 
	 * @param part
	 *            the editor part
	 */
	public void partSaved(IEditorPart part) {
		// http://dev.eclipse.org/bugs/show_bug.cgi?id=25440
		if (fPosition == null || fPosition.isDeleted())
			fSavedPosition = null;
		else
			fSavedPosition = new Position(fPosition.offset, fPosition.length);
	}

	/**
	 * Updates the this location.
	 */
	public void update() {
		final IEditorPart part = getEditorPart();

		if (part == null || equalsLocationOf())
			return;

		final ISelection selection = fViewer.getSelectionProvider().getSelection();
		if (selection == null || selection.isEmpty())
			return;

		if (selection instanceof IStructuredSelection) {
			Object first = ((IStructuredSelection) selection).getFirstElement();
			if (first instanceof IDOMNode) {
				IDOMNode node = (IDOMNode) first;
				fPosition.offset = node.getStartOffset();
				fPosition.length = node.getLength();
				fPosition.isDeleted = false;
				if (!part.isDirty())
					fSavedPosition = new Position(fPosition.offset, fPosition.length);
			}
		}
	}
}