/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * 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
 *     Sebastian Davids <sdavids@gmx.de> bug 38745
 *******************************************************************************/
package org.eclipse.ui.texteditor;

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.osgi.framework.Bundle;

import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IUndoableOperation;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;

import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.window.Window;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.IVerticalRulerInfo;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.undo.CreateMarkersOperation;
import org.eclipse.ui.ide.undo.DeleteMarkersOperation;

/**
 * A ruler action which can add and remove markers which have a visual
 * representation in the ruler.
 * <p>
 * This class may be instantiated but is not intended for sub-classing.
 * </p>
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class MarkerRulerAction extends ResourceAction implements IUpdate {

	/** The maximum length of an proposed label. */
	private static final int MAX_LABEL_LENGTH= 80;

	/** The vertical ruler info of the editor. */
	private IVerticalRulerInfo fRuler;
	/** The associated editor */
	private ITextEditor fTextEditor;
	/** The of the marker to be created/removed. */
	private String fMarkerType;
	/** The cached list of markers covering a particular vertical ruler position. */
	private List<IMarker> fMarkers;
	/** The flag indicating whether user interaction is required. */
	private boolean fAskForLabel;
	/** The action's resource bundle. */
	private ResourceBundle fBundle;
	/** The prefix used for resource bundle look ups. */
	private String fPrefix;
	/** The cached action label when adding a marker. */
	private String fAddLabel;
	/** The cached action label when removing a marker. */
	private String fRemoveLabel;


	/**
	 * Creates a new action for the given ruler and editor. The action configures
	 * its visual representation from the given resource bundle.
	 *
	 * @param bundle the resource bundle
	 * @param prefix a prefix to be prepended to the various resource keys
	 *   (described in {@link org.eclipse.ui.texteditor.ResourceAction} constructor), or  <code>null</code> if none
	 * @param editor the editor
	 * @param ruler the ruler
	 * @param markerType the type of marker
	 * @param askForLabel <code>true</code> if the user should be asked for a label when a new marker is created
	 * @see ResourceAction#ResourceAction(ResourceBundle, String)
	 * @since 2.0
	 */
	public MarkerRulerAction(ResourceBundle bundle, String prefix,  ITextEditor editor, IVerticalRulerInfo ruler, String markerType, boolean askForLabel) {
		super(bundle, prefix);
		Assert.isLegal(editor != null);

		fRuler= ruler;
		fTextEditor= editor;
		fMarkerType= markerType;
		fAskForLabel= askForLabel;

		fBundle= bundle;
		fPrefix= prefix;

		fAddLabel= getString(bundle, prefix + "add.label", prefix + "add.label"); //$NON-NLS-2$ //$NON-NLS-1$
		fRemoveLabel= getString(bundle, prefix + "remove.label", prefix + "remove.label"); //$NON-NLS-2$ //$NON-NLS-1$
	}

	/**
	 * Creates a new action for the given ruler and editor. The action configures
	 * its visual representation from the given resource bundle.
	 *
	 * @param bundle the resource bundle
	 * @param prefix a prefix to be prepended to the various resource keys
	 * @param ruler the ruler
	 * @param editor the editor
	 * @param markerType the type of the marker
	 * @param askForLabel <code>true</code> if the user should be asked for a label
	 * @deprecated use <code>MarkerRulerAction(ResourceBundle, String,  ITextEditor, IVerticalRulerInfo, String, boolean)</code> instead
	 */
	@Deprecated
	public MarkerRulerAction(ResourceBundle bundle, String prefix, IVerticalRuler ruler, ITextEditor editor, String markerType, boolean askForLabel) {
		this(bundle, prefix, editor, ruler, markerType, askForLabel);
	}


	/**
	 * Returns this action's text editor.
	 *
	 * @return this action's text editor
	 */
	protected ITextEditor getTextEditor() {
		return fTextEditor;
	}

	/**
	 * Returns this action's vertical ruler.
	 *
	 * @return this action's vertical ruler
	 * @deprecated use <code>getVerticalRulerInfo</code> instead
	 */
	@Deprecated
	protected IVerticalRuler getVerticalRuler() {
		if (fRuler instanceof IVerticalRuler verticalRuler)
			return verticalRuler;
		return null;
	}

	/**
	 * Returns this action's vertical ruler info.
	 *
	 * @return this action's vertical ruler info
	 * @since 2.0
	 */
	protected IVerticalRulerInfo getVerticalRulerInfo() {
		return fRuler;
	}

	/**
	 * Returns this action's resource bundle.
	 *
	 * @return this action's resource bundle
	 */
	protected ResourceBundle getResourceBundle() {
		return fBundle;
	}

	/**
	 * Returns this action's resource key prefix.
	 *
	 * @return this action's resource key prefix
	 */
	protected String getResourceKeyPrefix() {
		return fPrefix;
	}

	@Override
	public void update() {
		//bug 38745
		IDocument document= getDocument();
		if (document != null) {
			int line= getVerticalRuler().getLineOfLastMouseButtonActivity() + 1;
			if (line > document.getNumberOfLines()) {
				setEnabled(false);
				setText(fAddLabel);
			} else {
				fMarkers= getMarkers();
				setEnabled(getResource() != null && (fMarkers.isEmpty() || markersUserEditable(fMarkers)));
				setText(fMarkers.isEmpty() ? fAddLabel : fRemoveLabel);
			}
		}
	}

	/**
	 * Returns whether the given markers are all editable by the user.
	 *
	 * @param markers the list of markers to test
	 * @return boolean <code>true</code> if they are all editable
	 * @since 3.2
	 */
	private boolean markersUserEditable(List<IMarker> markers) {
		Iterator<IMarker> iter= markers.iterator();
		while (iter.hasNext()) {
			if (!isUserEditable(iter.next()))
				return false;
		}
		return true;
	}

	/**
	 * Returns whether the given marker is editable by the user.
	 *
	 * @param marker the marker to test
	 * @return boolean <code>true</code> if it is editable
	 * @since 3.2
	 */
	private boolean isUserEditable(IMarker marker) {
		return marker != null && marker.exists() && marker.getAttribute(IMarker.USER_EDITABLE, true);
	}

	@Override
	public void run() {
		if (fMarkers.isEmpty())
			addMarker();
		else
			removeMarkers(fMarkers);
	}

	/**
	 * Returns the resource for which to create the marker,
	 * or <code>null</code> if there is no applicable resource.
	 *
	 * @return the resource for which to create the marker or <code>null</code>
	 */
	protected IResource getResource() {
		IEditorInput input= fTextEditor.getEditorInput();

		IResource resource= input.getAdapter(IFile.class);

		if (resource == null)
			resource= input.getAdapter(IResource.class);

		return resource;
	}

	/**
	 * Returns the <code>AbstractMarkerAnnotationModel</code> of the editor's input.
	 *
	 * @return the marker annotation model
	 */
	protected AbstractMarkerAnnotationModel getAnnotationModel() {
		IDocumentProvider provider= fTextEditor.getDocumentProvider();
		IAnnotationModel model= provider.getAnnotationModel(fTextEditor.getEditorInput());
		if (model instanceof AbstractMarkerAnnotationModel markerModel)
			return markerModel;
		return null;
	}

	/**
	 * Returns the <code>IDocument</code> of the editor's input.
	 *
	 * @return the document of the editor's input
	 */
	protected IDocument getDocument() {
		IDocumentProvider provider= fTextEditor.getDocumentProvider();
		return provider.getDocument(fTextEditor.getEditorInput());
	}

	/**
	 * Checks whether a position includes the ruler's line of activity.
	 *
	 * @param position the position to be checked
	 * @param document the document the position refers to
	 * @return <code>true</code> if the line is included by the given position
	 */
	protected boolean includesRulerLine(Position position, IDocument document) {

		if (position != null) {
			try {
				int markerLine= document.getLineOfOffset(position.getOffset());
				int line= fRuler.getLineOfLastMouseButtonActivity();
				if (line == markerLine)
					return true;
				// commented because of "1GEUOZ9: ITPJUI:ALL - Confusing UI for multi-line Bookmarks and Tasks"
				// return (markerLine <= line && line <= document.getLineOfOffset(position.getOffset() + position.getLength()));
			} catch (BadLocationException x) {
			}
		}

		return false;
	}

	/**
	 * Handles core exceptions. This implementation logs the exceptions
	 * with the workbench plug-in and shows an error dialog.
	 *
	 * @param exception the exception to be handled
	 * @param message the message to be logged with the given exception
	 */
	protected void handleCoreException(CoreException exception, String message) {
		Bundle bundle= Platform.getBundle(PlatformUI.PLUGIN_ID);
		ILog log= ILog.of(bundle);

		if (message != null)
			log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, message, exception));
		else
			log.log(exception.getStatus());


		Shell shell= getTextEditor().getSite().getShell();
		String title= getString(fBundle, fPrefix + "error.dialog.title", fPrefix + "error.dialog.title"); //$NON-NLS-2$ //$NON-NLS-1$
		String msg= getString(fBundle, fPrefix + "error.dialog.message", fPrefix + "error.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$

		ErrorDialog.openError(shell, title, msg, exception.getStatus());
	}

	/**
	 * Returns all markers which include the ruler's line of activity.
	 *
	 * @return all a list of markers which include the ruler's line of activity
	 */
	protected List<IMarker> getMarkers() {

		List<IMarker> markers= new ArrayList<>();

		IResource resource= getResource();
		IDocument document= getDocument();
		AbstractMarkerAnnotationModel model= getAnnotationModel();

		if (resource != null && model != null && resource.exists()) {
			try {
				IMarker[] allMarkers= resource.findMarkers(fMarkerType, true, IResource.DEPTH_ZERO);
				if (allMarkers != null) {
					for (IMarker allMarker : allMarkers) {
						if (includesRulerLine(model.getMarkerPosition(allMarker), document)) {
							markers.add(allMarker);
						}
					}
				}
			} catch (CoreException x) {
				handleCoreException(x, TextEditorMessages.MarkerRulerAction_getMarker);
			}
		}

		return markers;
	}

	/**
	 * Creates a new marker according to the specification of this action and
	 * adds it to the marker resource.
	 */
	protected void addMarker() {
		IResource resource= getResource();
		if (resource == null)
			return;
		Map<String, Object> attributes= getInitialAttributes();
		if (fAskForLabel) {
			if (!askForLabel(attributes))
				return;
		}
		execute(new CreateMarkersOperation(fMarkerType, attributes, resource, getOperationName()));
	}

	/**
	 * Removes the given markers.
	 *
	 * @param markers the markers to be deleted
	 */
	protected void removeMarkers(final List<? extends IMarker> markers) {
		IMarker[] markersArray= markers.toArray(new IMarker[markers.size()]);
		execute(new DeleteMarkersOperation(markersArray, getOperationName()));
	}

	/**
	 * Asks the user for a marker label. Returns <code>true</code> if a label
	 * is entered, <code>false</code> if the user cancels the input dialog.
	 * Sets the value of the attribute <code>message</code> in the given
	 * map of attributes.
	 *
	 * @param attributes the map of attributes
	 * @return <code>true</code> if the map of attributes has successfully been initialized
	 */
	protected boolean askForLabel(Map<String, Object> attributes) {

		Object o= attributes.get("message"); //$NON-NLS-1$
		String proposal= (o instanceof String s) ? s : ""; //$NON-NLS-1$
		String title= getString(fBundle, fPrefix + "add.dialog.title", fPrefix + "add.dialog.title"); //$NON-NLS-2$ //$NON-NLS-1$
		String message= getString(fBundle, fPrefix + "add.dialog.message", fPrefix + "add.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
		String addButtonText= getString(fBundle, fPrefix + "add.dialog.addbutton", fPrefix + "add.dialog.addbutton"); //$NON-NLS-1$ //$NON-NLS-2$
		IInputValidator inputValidator= newText -> (newText == null || newText.trim().isEmpty()) ? " " : null;
		AddBookmarkDialog dialog= new AddBookmarkDialog(fTextEditor.getSite().getShell(), title, message, proposal, inputValidator, addButtonText);

		String label= null;
		if (dialog.open() != Window.CANCEL)
			label= dialog.getValue();

		if (label == null)
			return false;

		label= label.trim();
		if (label.isEmpty())
			return false;

		MarkerUtilities.setMessage(attributes, label);
		return true;
	}

	class AddBookmarkDialog extends InputDialog {

		private final String addButtonText;

		public AddBookmarkDialog(Shell parentShell, String dialogTitle, String dialogMessage, String initialValue, IInputValidator validator, String addButtonText) {
			super(parentShell, dialogTitle, dialogMessage, initialValue, validator);
			this.addButtonText= addButtonText;
		}

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			super.createButtonsForButtonBar(parent);
			Button okButton= getOkButton();
			okButton.setText(addButtonText);
		}

	}

	/**
	 * Returns the attributes with which a newly created marker will be
	 * initialized.
	 *
	 * @return the initial marker attributes
	 */
	protected Map<String, Object> getInitialAttributes() {

		Map<String, Object> attributes= new HashMap<>(11);

		IDocumentProvider provider= fTextEditor.getDocumentProvider();
		IDocument document= provider.getDocument(fTextEditor.getEditorInput());
		int line= fRuler.getLineOfLastMouseButtonActivity();
		int start= -1;
		int end= -1;
		int length= 0;

		try {

			IRegion lineInformation= document.getLineInformation(line);
			start= lineInformation.getOffset();
			length= lineInformation.getLength();

			end= start + length;


		} catch (BadLocationException x) {
		}

		// marker line numbers are 1-based
		MarkerUtilities.setMessage(attributes, getLabelProposal(document, start, length));
		MarkerUtilities.setLineNumber(attributes, line + 1);
		MarkerUtilities.setCharStart(attributes, start);
		MarkerUtilities.setCharEnd(attributes, end);

		return attributes;
	}

	/**
	 * Returns the initial label for the marker.
	 *
	 * @param document the document from which to extract a label proposal
	 * @param offset the document offset of the range from which to extract the label proposal
	 * @param length the length of the range from which to extract the label proposal
	 * @return the label proposal
	 * @since 3.0
	 */
	protected String getLabelProposal(IDocument document, int offset, int length) {
		try {
			String label= document.get(offset, length).trim();
			if (label.length() <= MAX_LABEL_LENGTH)
				return label;
			return label.substring(0, MAX_LABEL_LENGTH);
		} catch (BadLocationException x) {
			// don't propose label then
			return null;
		}
	}

	/**
	 * Returns the name to be used for the operation.
	 *
	 * @return the operation name
	 * @since 3.3
	 */
	private String getOperationName() {
		String name= getText();
		return name == null ? TextEditorMessages.AddMarkerAction_addMarker : name;
	}

	/**
	 * Execute the specified undoable operation.
	 *
	 * @param operation the operation to execute
	 * @since 3.3
	 */
	private void execute(IUndoableOperation operation) {
		final Shell shell= getTextEditor().getSite().getShell();
		IAdaptable context= new IAdaptable() {
			@SuppressWarnings("unchecked")
			@Override
			public <T> T getAdapter(Class<T> adapter) {
				if (adapter == Shell.class)
					return (T) shell;
				return null;
			}
		};

		IOperationHistory operationHistory= PlatformUI.getWorkbench().getOperationSupport().getOperationHistory();
		try {
			operationHistory.execute(operation, null, context);
		} catch (ExecutionException e) {
			Bundle bundle= Platform.getBundle(PlatformUI.PLUGIN_ID);
			ILog log= ILog.of(bundle);
			String msg= getString(fBundle, fPrefix + "error.dialog.message", fPrefix + "error.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
			log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, msg, e));
		}
	}
}
