/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     Terry Parker <tparker@google.com> (Google Inc.) - Bug 458852 - Speed up JDT text searches by supporting parallelism in its TextSearchRequestors
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.propertiesfileeditor;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import com.ibm.icu.text.Collator;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ILabelProvider;
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.hyperlink.IHyperlink;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.dialogs.TwoPaneElementSelector;
import org.eclipse.ui.model.IWorkbenchAdapter;
import org.eclipse.ui.model.WorkbenchLabelProvider;

import org.eclipse.ui.texteditor.IEditorStatusLine;
import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.search.core.text.TextSearchEngine;
import org.eclipse.search.core.text.TextSearchMatchAccess;
import org.eclipse.search.core.text.TextSearchRequestor;
import org.eclipse.search.core.text.TextSearchScope;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.corext.util.SearchUtils;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;
import org.eclipse.jdt.internal.ui.util.PatternConstructor;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;


/**
 * Properties key hyperlink.
 *
 * @since 3.1
 */
public class PropertyKeyHyperlink implements IHyperlink {

/**
 * Note: this class KeyReference has a natural ordering that is inconsistent with equals
 *
 */
	private static class KeyReference extends PlatformObject implements IWorkbenchAdapter, Comparable<KeyReference> {

		private static final Collator fgCollator= Collator.getInstance();

		private IResource resource;
		private IJavaElement element;
		private int offset;
		private int length;
		private final boolean showLineNumber;


		private KeyReference(IResource resource, IJavaElement element, int offset, int length, boolean showLineNumber) {
			Assert.isNotNull(resource);
			this.resource= resource;
			this.offset= offset;
			this.length= length;
			this.element= element;
			this.showLineNumber= showLineNumber;
		}

		@SuppressWarnings("unchecked")
		@Override
		public <T> T getAdapter(Class<T> adapter) {
			if (adapter == IWorkbenchAdapter.class)
				return (T) this;
			else
				return super.getAdapter(adapter);
		}
		/*
		 * @see org.eclipse.ui.model.IWorkbenchAdapter#getChildren(java.lang.Object)
		 */
		@Override
		public Object[] getChildren(Object o) {
			return null;
		}
		/*
		 * @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
		 */
		@Override
		public ImageDescriptor getImageDescriptor(Object object) {
			IWorkbenchAdapter wbAdapter= resource.getAdapter(IWorkbenchAdapter.class);
			if (wbAdapter != null)
				return wbAdapter.getImageDescriptor(resource);
			return null;
		}
		/*
		 * @see org.eclipse.ui.model.IWorkbenchAdapter#getLabel(java.lang.Object)
		 */
		@Override
		public String getLabel(Object o) {

			ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
			try {
				manager.connect(resource.getFullPath(), LocationKind.NORMALIZE, null);
				try {
					ITextFileBuffer buffer= manager.getTextFileBuffer(resource.getFullPath(), LocationKind.NORMALIZE);
					IDocument document= buffer.getDocument();
					if (document != null) {
						int line= document.getLineOfOffset(offset) + 1;
						String pathLabel= BasicElementLabels.getPathLabel(resource.getFullPath(), false);
						Object[] args= new Object[] { Integer.valueOf(line), pathLabel };
						return showLineNumber ? Messages.format(PropertiesFileEditorMessages.OpenAction_SelectionDialog_elementLabel, args) : pathLabel;
					}
				} finally {
					manager.disconnect(resource.getFullPath(), LocationKind.NORMALIZE, null);
				}
			} catch (CoreException e) {
				JavaPlugin.log(e.getStatus());
			} catch (BadLocationException e) {
				JavaPlugin.log(e);
			}

			return BasicElementLabels.getPathLabel(resource.getFullPath(), false);
		}
		/*
		 * @see org.eclipse.ui.model.IWorkbenchAdapter#getParent(java.lang.Object)
		 */
		@Override
		public Object getParent(Object o) {
			return null;
		}

		@Override
		public int compareTo(KeyReference otherRef) {
			String thisPath= resource.getFullPath().toString();
			String otherPath= otherRef.resource.getFullPath().toString();
			int result= fgCollator.compare(thisPath, otherPath);
			if (result != 0)
				return result;
			else
				return offset - otherRef.offset;
		}
	}


	private static class ResultCollector extends TextSearchRequestor {

		private final List<KeyReference> fResult;
		private final boolean fIsKeyDoubleQuoted;

		public ResultCollector(List<KeyReference> result, boolean isKeyDoubleQuoted) {
			fResult= result;
			fIsKeyDoubleQuoted= isKeyDoubleQuoted;
		}

		@Override
		public boolean canRunInParallel() {
			return true;
		}

		@Override
		public boolean acceptPatternMatch(TextSearchMatchAccess matchAccess) throws CoreException {
			int start= matchAccess.getMatchOffset();
			int length= matchAccess.getMatchLength();

			if (fIsKeyDoubleQuoted) {
				start= start + 1;
				length= length - 2;
			}
			synchronized(fResult) {
				fResult.add(new KeyReference(matchAccess.getFile(), null, start, length, true));
			}
			return true;
		}
	}


	private IRegion fRegion;
	private String fPropertiesKey;
	private Shell fShell;
	private IStorage fStorage;
	private ITextEditor fEditor;
	private boolean fIsFileEditorInput;

	/**
	 * Creates a new properties key hyperlink.
	 *
	 * @param region the region
	 * @param key the properties key
	 * @param editor the text editor
	 */
	public PropertyKeyHyperlink(IRegion region, String key, ITextEditor editor) {
		Assert.isNotNull(region);
		Assert.isNotNull(key);
		Assert.isNotNull(editor);

		fRegion= region;
		fPropertiesKey= key;
		fEditor= editor;
		fIsFileEditorInput= fEditor.getEditorInput() instanceof IFileEditorInput;
		IStorageEditorInput storageEditorInput= (IStorageEditorInput)fEditor.getEditorInput();
		fShell= fEditor.getEditorSite().getShell();
		try {
			fStorage= storageEditorInput.getStorage();
		} catch (CoreException e) {
			fStorage= null;
		}
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.javaeditor.IHyperlink#getHyperlinkRegion()
	 */
	@Override
	public IRegion getHyperlinkRegion() {
		return fRegion;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.javaeditor.IHyperlink#open()
	 */
	@Override
	public void open() {
		// Search the key
		KeyReference[] references= search(fPropertiesKey);
		if (references == null)
			return; // canceled by the user

		if (references.length == 0) {
			String message= PropertiesFileEditorMessages.OpenAction_error_messageNoResult;
			showErrorInStatusLine(message);
			return;
		}

		open(references);

	}

	private void open(KeyReference[] keyReferences) {
		Assert.isLegal(keyReferences != null && keyReferences.length > 0);

		if (keyReferences.length == 1)
			open(keyReferences[0]);
		else
			open(select(keyReferences));
	}

	/**
	 * Opens a dialog which allows to select a key reference.
	 *
	 * @param keyReferences the array of key references
	 * @return the selected key reference or <code>null</code> if canceled by the user
	 */
	private KeyReference select(final KeyReference[] keyReferences) {
		Arrays.sort(keyReferences);
		final int length= keyReferences.length;
		ILabelProvider labelProvider= new WorkbenchLabelProvider() {
			@Override
			public String decorateText(String input, Object element) {
				KeyReference keyRef= (KeyReference)element;
				IJavaElement javaElement= keyRef.element;
				IResource resource= keyRef.resource;

				String name= javaElement != null ? JavaElementLabels.getElementLabel(javaElement, JavaElementLabels.ALL_DEFAULT | JavaElementLabels.ALL_FULLY_QUALIFIED
						| JavaElementLabels.USE_RESOLVED | JavaElementLabels.P_COMPRESSED) : resource.getName();

				if (name == null)
					return input;

				int count= 0;
				for (int i= 0; i < length; i++) {
					if (javaElement != null) {
						if (keyReferences[i].element.equals(javaElement))
							count++;
					} else {
						if (keyReferences[i].resource.equals(resource))
							count++;
					}
				}
				if (count > 1) {
					Object[] args= new Object[] { BasicElementLabels.getResourceName(name), Integer.valueOf(count) };
					name= Messages.format(PropertiesFileEditorMessages.OpenAction_SelectionDialog_elementLabelWithMatchCount, args);
				}

				return name;
			}

			@Override
			protected ImageDescriptor decorateImage(ImageDescriptor input, Object element) {
				KeyReference keyRef= (KeyReference)element;
				IJavaElement javaElement= keyRef.element;
				return javaElement != null ? new JavaElementImageProvider().getJavaImageDescriptor(javaElement, JavaElementImageProvider.SMALL_ICONS) : super.decorateImage(input, element);
			}
		};

		TwoPaneElementSelector dialog= new TwoPaneElementSelector(fShell, labelProvider, new WorkbenchLabelProvider());
		dialog.setLowerListLabel(PropertiesFileEditorMessages.OpenAction_SelectionDialog_details);
		dialog.setLowerListComparator((o1, o2) -> 0);
		dialog.setTitle(PropertiesFileEditorMessages.OpenAction_SelectionDialog_title);
		dialog.setMessage(PropertiesFileEditorMessages.OpenAction_SelectionDialog_message);
		dialog.setElements(keyReferences);

		if (dialog.open() == Window.OK) {
			Object[] result= dialog.getResult();
			if (result != null && result.length == 1)
			 return (KeyReference)result[0];
		}

		return null;
	}

	private void open(KeyReference keyReference) {
		if (keyReference == null)
			return;

		try {
			IEditorPart part= keyReference.element != null ? EditorUtility.openInEditor(keyReference.element, true) : EditorUtility.openInEditor(keyReference.resource, true);
			if (part != null)
				EditorUtility.revealInEditor(part, keyReference.offset, keyReference.length);
		} catch (PartInitException x) {

			String message= null;

			IWorkbenchAdapter wbAdapter= ((IAdaptable)keyReference).getAdapter(IWorkbenchAdapter.class);
			if (wbAdapter != null)
				message= Messages.format(PropertiesFileEditorMessages.OpenAction_error_messageArgs,
						new String[] { wbAdapter.getLabel(keyReference), x.getLocalizedMessage() } );

			if (message == null)
				message= Messages.format(PropertiesFileEditorMessages.OpenAction_error_message, x.getLocalizedMessage());

			MessageDialog.openError(fShell,
				PropertiesFileEditorMessages.OpenAction_error_messageProblems,
				message);
		}
	}

	private String getErrorDialogTitle() {
		return PropertiesFileEditorMessages.OpenAction_error_title;
	}

	private void showError(CoreException e) {
		ExceptionHandler.handle(e, fShell, getErrorDialogTitle(), PropertiesFileEditorMessages.OpenAction_error_message);
	}

	private void showErrorInStatusLine(final String message) {
		fShell.getDisplay().beep();
		final IEditorStatusLine statusLine= fEditor.getAdapter(IEditorStatusLine.class);
		if (statusLine != null) {
			fShell.getDisplay().asyncExec(() -> statusLine.setMessage(true, message, null));
		}
	}

	/**
	 * Returns whether we search the key in double-quotes or not.
	 * <p>
	 * XXX: This is a hack to improve the accuracy of matches, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=81140
	 * </p>
	 *
	 * @return <code>true</code> if we search for double-quoted key
	 */
	private boolean useDoubleQuotedKey() {
		if (fStorage == null)
			return false;

		String name= fStorage.getName();

		return name != null && !"about.properties".equals(name) && !"feature.properties".equals(name) && !"plugin.properties".equals(name);  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Searches references to the given key in the given scope.
	 *
	 * @param key the properties key
	 * @return the references or <code>null</code> if the search has been canceled by the user
	 */
	private KeyReference[] search(final String key) {
		if (key == null)
			return new KeyReference[0];

		final List<KeyReference> result= new ArrayList<>(5);
		try {
			fEditor.getEditorSite().getWorkbenchWindow().getWorkbench().getProgressService().busyCursorWhile(
				monitor -> {
					if (monitor == null)
						monitor= new NullProgressMonitor();

					monitor.beginTask("", 5); //$NON-NLS-1$
					try {
						// XXX: This is a hack to improve the accuracy of matches, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=81140
						boolean useDoubleQuotedKey= useDoubleQuotedKey();
						if (useDoubleQuotedKey) {
							SearchPattern pattern= SearchPattern.createPattern(key, IJavaSearchConstants.FIELD, IJavaSearchConstants.REFERENCES, SearchPattern.R_PATTERN_MATCH
									| SearchPattern.R_CASE_SENSITIVE);
							if (pattern == null)
								return;
							try {
								new SearchEngine().search(pattern, SearchUtils.getDefaultSearchParticipants(), SearchEngine.createWorkspaceScope(), new SearchRequestor() {
									@Override
									public void acceptSearchMatch(SearchMatch match) throws CoreException {
										IResource resource= match.getResource();
										if (resource != null)
											result.add(new KeyReference(resource, (IJavaElement) match.getElement(), match.getOffset(), match.getLength(), fIsFileEditorInput));
									}
								}, new SubProgressMonitor(monitor, 1));
							} catch (CoreException e) {
								throw new InvocationTargetException(e);
							}
						}
						if (result.isEmpty()) {
							//maybe not an eclipse style NLS string
							String searchString;
							if (useDoubleQuotedKey) {
								StringBuilder buf= new StringBuilder("\""); //$NON-NLS-1$
								buf.append(key);
								buf.append('"');
								searchString= buf.toString();
							} else
								searchString= key;
							ResultCollector collector= new ResultCollector(result, useDoubleQuotedKey);
							TextSearchEngine engine= TextSearchEngine.create();
							Pattern searchPattern= PatternConstructor.createPattern(searchString, true, false);

							/* <p>
							 * XXX: This does not work for properties files coming from a JAR.
							 * For details see https://bugs.eclipse.org/bugs/show_bug.cgi?id=23341
							 * </p>
							*/
							if (fStorage instanceof IResource) {
								engine.search(createScope(((IResource)fStorage).getProject()), collector, searchPattern, new SubProgressMonitor(monitor, 4));
							}
						} else {
							monitor.worked(1);
						}
					} finally {
						monitor.done();
					}
				}
			);
		} catch (InvocationTargetException ex) {
			String message= PropertiesFileEditorMessages.OpenAction_error_messageErrorSearchingKey;
			showError(new CoreException(new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, IStatus.OK, message, ex.getTargetException())));
		} catch (InterruptedException ex) {
			return null; // canceled
		}

		return result.toArray(new KeyReference[result.size()]);
	}

	private static TextSearchScope createScope(IResource scope) {
		ArrayList<String> fileNamePatternStrings= new ArrayList<>();

		// XXX: Should be configurable via preference, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=81117
		for (String javaExtension : JavaCore.getJavaLikeExtensions()) {
			fileNamePatternStrings.add("*." + javaExtension); //$NON-NLS-1$
		}
		fileNamePatternStrings.add("*.xml"); //$NON-NLS-1$
		fileNamePatternStrings.add("*.ini"); //$NON-NLS-1$

		String[] allPatternStrings= fileNamePatternStrings.toArray(new String[fileNamePatternStrings.size()]);
		Pattern fileNamePattern= PatternConstructor.createPattern(allPatternStrings, false, false);

		return TextSearchScope.newSearchScope(new IResource[] { scope }, fileNamePattern, false);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.javaeditor.IHyperlink#getTypeLabel()
	 */
	@Override
	public String getTypeLabel() {
		return null;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.javaeditor.IHyperlink#getHyperlinkText()
	 */
	@Override
	public String getHyperlinkText() {
		return Messages.format(PropertiesFileEditorMessages.OpenAction_hyperlinkText, fPropertiesKey);
	}
}
