/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java.hover;

import java.util.Properties;

import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;

import org.eclipse.core.resources.IStorage;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.internal.text.html.HTMLPrinter;

import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;

import org.eclipse.ui.IEditorPart;

import org.eclipse.ui.editors.text.EditorsUI;

import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;

import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;
import org.eclipse.jdt.internal.corext.refactoring.nls.AccessorClassReference;
import org.eclipse.jdt.internal.corext.refactoring.nls.NLSHintHelper;

import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.internal.ui.javaeditor.NLSKeyHyperlink;


/**
 * Provides externalized string as hover info for NLS key.
 *
 * @since 3.1
 */
public class NLSStringHover extends AbstractJavaEditorTextHover {


	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractJavaEditorTextHover#getHoverRegion(org.eclipse.jface.text.ITextViewer, int)
	 */
	@Override
	public IRegion getHoverRegion(ITextViewer textViewer, int offset) {
		if (!(getEditor() instanceof JavaEditor))
			return null;

		ITypeRoot je= getEditorInputJavaElement();
		if (je == null)
			return null;

		// Never wait for an AST in UI thread.
		CompilationUnit ast= SharedASTProviderCore.getAST(je, SharedASTProviderCore.WAIT_NO, null);
		if (ast == null)
			return null;

		ASTNode node= NodeFinder.perform(ast, offset, 1);
		if (node instanceof StringLiteral) {
			StringLiteral stringLiteral= (StringLiteral)node;
			return new Region(stringLiteral.getStartPosition(), stringLiteral.getLength());
		} else if (node instanceof SimpleName) {
			SimpleName simpleName= (SimpleName)node;
			return new Region(simpleName.getStartPosition(), simpleName.getLength());
		}

		return null;
	}

	/**
	 * @deprecated see {@link org.eclipse.jface.text.ITextHover#getHoverInfo(ITextViewer, IRegion)}
	 */
	@Deprecated
	@Override
	public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
		NLSHoverControlInput info= internalGetHoverInfo(textViewer, hoverRegion);
		return info == null ? null : info.fInformation;
	}

	/*
	 * @see org.eclipse.jface.text.ITextHoverExtension2#getHoverInfo2(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion)
	 */
	@Override
	public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) {
		return internalGetHoverInfo(textViewer, hoverRegion);
	}

	/**
	 * Returns the hover input.
	 *
	 * @param textViewer the viewer on which the hover popup should be shown
	 * @param hoverRegion the text range in the viewer which is used to determine the hover display
	 *            information
	 * @return the hover popup display input, or <code>null</code> if none available
	 *
	 * @see #getHoverInfo2(ITextViewer, IRegion)
	 */
	private NLSHoverControlInput internalGetHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
		if (!(getEditor() instanceof JavaEditor))
			return null;

		ITypeRoot je= getEditorInputJavaElement();
		if (je == null)
			return null;

		CompilationUnit ast= SharedASTProviderCore.getAST(je, SharedASTProviderCore.WAIT_ACTIVE_ONLY, null);
		if (ast == null)
			return null;

		ASTNode node= NodeFinder.perform(ast, hoverRegion.getOffset(), hoverRegion.getLength());
		if (!(node instanceof StringLiteral) && !(node instanceof SimpleName))
			return null;

		if (node.getLocationInParent() == QualifiedName.QUALIFIER_PROPERTY)
			return null;

		boolean usedFullyQualifiedName= false;
		IBinding containingClassBinding= null;
		ASTNode containingClass= ASTResolving.findParentType(node);
		if (containingClass instanceof TypeDeclaration) {
			containingClassBinding= ((TypeDeclaration)containingClass).resolveBinding();
			ASTNode parentNode= node.getParent();
			if (parentNode instanceof QualifiedName) {
				IBinding qualifierBinding= (((QualifiedName)parentNode).getQualifier()).resolveBinding();
				if (qualifierBinding != null && containingClassBinding != null) {
					usedFullyQualifiedName= qualifierBinding.getKey().equals(containingClassBinding.getKey());
				}
			}
		}
		AccessorClassReference ref= NLSHintHelper.getAccessorClassReference(ast, hoverRegion, usedFullyQualifiedName);
		if (ref == null)
			return null;

		String identifier= null;
		if (node instanceof StringLiteral) {
			identifier= ((StringLiteral)node).getLiteralValue();
		} else if (!usedFullyQualifiedName && node.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
			identifier= ((SimpleName)node).getIdentifier();
			IBinding nodeBinding= ((SimpleName)node).resolveBinding();
			if (nodeBinding != null) {
				if (nodeBinding instanceof IVariableBinding && ((IVariableBinding)nodeBinding).isField()) {
					ITypeBinding nodeDeclaringType= ((IVariableBinding)nodeBinding).getDeclaringClass();
					if (nodeDeclaringType != null) {
						ITypeBinding superClass= nodeDeclaringType.getSuperclass();
						if (superClass != null) {
							String superClassName= superClass.getQualifiedName();
							if (!"org.eclipse.osgi.util.NLS".equals(superClassName)) { //$NON-NLS-1$
								identifier= null;
							}
						}
					}
				}
			}
		}
		if (identifier == null) {
			try {
				if (containingClassBinding == null)
					return null;
				IType parentType= (IType)containingClassBinding.getJavaElement();
				if (parentType == null)
					return null;
				String varName= ((SimpleName)node).getIdentifier();
				IField field= parentType.getField(varName);
				if (!"String".equals(Signature.getSignatureSimpleName(field.getTypeSignature()))) //$NON-NLS-1$
					return null;
				Object obj= field.getConstant();
				identifier= obj instanceof String ? ((String)obj).substring(1, ((String)obj).length() - 1) : null;
			} catch (JavaModelException e) {
				return null;
			}
		}
		if (identifier == null)
			return null;

		IStorage propertiesFile;
		try {
			propertiesFile= NLSHintHelper.getResourceBundle(je.getJavaProject(), ref);
			if (propertiesFile == null)
				return new NLSHoverControlInput(toHtml(JavaHoverMessages.NLSStringHover_NLSStringHover_PropertiesFileNotDetectedWarning, "", null, false), (IStorage)null, "", getEditor()); //$NON-NLS-1$ //$NON-NLS-2$
		} catch (JavaModelException ex) {
			return null;
		}

		final String propertiesFileName= propertiesFile.getName();
		Properties properties= null;
		try {
			properties= NLSHintHelper.getProperties(propertiesFile);
		} catch (IllegalArgumentException e) {
			return new NLSHoverControlInput(toHtml(propertiesFileName, JavaHoverMessages.NLSStringHover_NLSStringHover_PropertiesFileCouldNotBeReadWarning, e.getLocalizedMessage(), false),
					propertiesFile, identifier, getEditor());
		}
		if (properties == null)
			return null;
		if (properties.isEmpty())
			return new NLSHoverControlInput(toHtml(propertiesFileName, JavaHoverMessages.NLSStringHover_NLSStringHover_missingKeyWarning, null, false), propertiesFile, "", getEditor()); //$NON-NLS-1$

		String value= properties.getProperty(identifier, null);
		String buffer= toHtml(propertiesFileName, value, null, true);
		return new NLSHoverControlInput(buffer, propertiesFile, identifier, getEditor());
	}

	private String toHtml(String header, String string, String errorString, boolean addPreFormatted) {
		StringBuilder buffer= new StringBuilder();
		HTMLPrinter.addSmallHeader(buffer, header);

		if (string != null) {
			if (addPreFormatted) {
				HTMLPrinter.addParagraph(buffer, ""); //$NON-NLS-1$
				HTMLPrinter.addPreFormatted(buffer, HTMLPrinter.convertToHTMLContent(string));
			} else {
				HTMLPrinter.addParagraph(buffer, string);
			}
			if (errorString != null) {
				HTMLPrinter.addParagraph(buffer, errorString);
			}
		} else {
			HTMLPrinter.addParagraph(buffer, JavaHoverMessages.NLSStringHover_NLSStringHover_missingKeyWarning);
		}

		HTMLPrinter.insertPageProlog(buffer, 0);
		HTMLPrinter.addPageEpilog(buffer);
		return buffer.toString();
	}

	/**
	 * The input for NLS hover.
	 *
	 * @since 3.5
	 */
	private static class NLSHoverControlInput {

		private IStorage fpropertiesFile;
		private String fKeyName;
		private String fInformation;
		private IEditorPart fActiveEditor;

		/**
		 * Creates the NLS hover input.
		 *
		 * @param information the hover info (string with simple HTML)
		 * @param propertiesFile the properties file, or <code>null</code> if not found
		 * @param key the NLS key
		 * @param editor the active editor part
		 */
		public NLSHoverControlInput(String information, IStorage propertiesFile, String key, IEditorPart editor) {
			fInformation= information;
			fpropertiesFile= propertiesFile;
			fKeyName= key;
			fActiveEditor= editor;
		}
	}

	/**
	 * The NLS hover control.
	 *
	 * @since 3.5
	 */
	static class NLSHoverControl extends DefaultInformationControl implements IInformationControlExtension2 {

		/**
		 * The NLS control input.
		 */
		private NLSHoverControlInput fInput;

		/**
		 * Creates a resizable NLS hover control with the given shell as parent.
		 *
		 * @param parent the parent shell
		 * @param tbm the toolbar manager or <code>null</code> if toolbar is not desired
		 */
		public NLSHoverControl(Shell parent, ToolBarManager tbm) {
			super(parent, tbm);

		}

		/**
		 * Creates an NLS hover control with the given shell as parent.
		 *
		 * @param parent the parent shell
		 * @param tooltipAffordanceString the text to be used in the status field or
		 *            <code>null</code> to hide the status field
		 */
		public NLSHoverControl(Shell parent, String tooltipAffordanceString) {
			super(parent, tooltipAffordanceString);
		}

		/**
		 * {@inheritDoc} This control can handle {@link NLSStringHover.NLSHoverControlInput}.
		 */
		@Override
		public void setInput(Object input) {
			Assert.isLegal(input instanceof NLSHoverControlInput);

			NLSHoverControlInput info= (NLSHoverControlInput)input;
			setInformation(info.fInformation);
			fInput= info;
		}

		/**
		 * Returns the control input.
		 *
		 * @return the control input
		 */
		public NLSHoverControlInput getInput() {
			return fInput;
		}
	}

	/**
	 * Presenter control creator.
	 *
	 * @since 3.5
	 */
	private static final class PresenterControlCreator extends AbstractReusableInformationControlCreator {
		/*
		 * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractReusableInformationControlCreator#doCreateInformationControl(org.eclipse.swt.widgets.Shell)
		 */
		@Override
		public IInformationControl doCreateInformationControl(Shell parent) {
			ToolBarManager tbm= new ToolBarManager(SWT.FLAT);
			NLSHoverControl iControl= new NLSHoverControl(parent, tbm);
			OpenPropertiesFileAction openPropertiesFileAction= new OpenPropertiesFileAction(iControl);
			tbm.add(openPropertiesFileAction);
			tbm.update(true);
			return iControl;
		}
	}

	/**
	 * Hover control creator.
	 *
	 * @since 3.5
	 */
	private static final class HoverControlCreator extends AbstractReusableInformationControlCreator {

		/**
		 * The presenter control creator.
		 */
		private final IInformationControlCreator fPresenterControlCreator;

		/**
		 * Creates the hover control creator.
		 *
		 * @param presenterControlCreator the presenter control creator
		 */
		public HoverControlCreator(IInformationControlCreator presenterControlCreator) {
			fPresenterControlCreator= presenterControlCreator;
		}

		/*
		 * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractReusableInformationControlCreator#doCreateInformationControl(org.eclipse.swt.widgets.Shell)
		 */
		@Override
		public IInformationControl doCreateInformationControl(Shell parent) {
			return new NLSHoverControl(parent, EditorsUI.getTooltipAffordanceString()) {
				/*
				 * @see org.eclipse.jface.text.IInformationControlExtension5#getInformationPresenterControlCreator()
				 */
				@Override
				public IInformationControlCreator getInformationPresenterControlCreator() {
					return fPresenterControlCreator;
				}
			};
		}
	}

	/**
	 * The hover control creator.
	 *
	 * @since 3.5
	 */
	private IInformationControlCreator fHoverControlCreator;

	/**
	 * The presentation control creator.
	 *
	 * @since 3.5
	 */
	private IInformationControlCreator fPresenterControlCreator;

	/*
	 * @see ITextHoverExtension#getHoverControlCreator()
	 * @since 3.5
	 */
	@Override
	public IInformationControlCreator getHoverControlCreator() {
		if (fHoverControlCreator == null)
			fHoverControlCreator= new HoverControlCreator(getInformationPresenterControlCreator());
		return fHoverControlCreator;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractJavaEditorTextHover#getInformationPresenterControlCreator()
	 * @since 3.5
	 */
	@Override
	public IInformationControlCreator getInformationPresenterControlCreator() {
		if (fPresenterControlCreator == null)
			fPresenterControlCreator= new PresenterControlCreator();
		return fPresenterControlCreator;
	}


	/**
	 * Action that opens the current hover NLS string in properties file.
	 *
	 * @since 3.5
	 */
	private static final class OpenPropertiesFileAction extends Action {

		/**
		 * The NLS hover control.
		 */
		private NLSHoverControl fControl;

		/**
		 * Creates the action for opening properties file.
		 *
		 * @param control the NLS hover control
		 */
		public OpenPropertiesFileAction(NLSHoverControl control) {
			fControl= control;
			setText(JavaHoverMessages.NLSStringHover_open_in_properties_file);
			JavaPluginImages.setLocalImageDescriptors(this, "goto_input.png"); //$NON-NLS-1$
		}

		/*
		 * @see org.eclipse.jface.action.Action#run()
		 */
		@Override
		public void run() {
			NLSHoverControlInput input= fControl.getInput();
			NLSKeyHyperlink.openKeyInPropertiesFile(input.fKeyName, input.fpropertiesFile, input.fActiveEditor);
		}
	}
}
