/*******************************************************************************
 * 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.io.IOException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.part.IWorkbenchPartOrientation;

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

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.TextBlock;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.util.Strings;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.TokenScanner;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.text.IJavaPartitions;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.text.JavaCodeReader;
import org.eclipse.jdt.internal.ui.text.JavaPairMatcher;


/**
 * Provides as hover info the source of the selected JavaElement, or the source near the matching
 * opening curly brace.
 */
public class JavaSourceHover extends AbstractJavaEditorTextHover {

	/**
	 * The upward shift in location in lines for the bracket hover.
	 *
	 * @since 3.8
	 */
	private int fUpwardShiftInLines;

	/**
	 * The status text for the bracket hover.
	 *
	 * @since 3.8
	 */
	private String fBracketHoverStatus;

	/**
	 * The hovered Java element to get the source.
	 *
	 * @since 3.14
	 */
	private IJavaElement fJavaElement;

	static class JavaSourceInformationInput {
		private IJavaElement fElement;

		private String fHoverInfo;

		public JavaSourceInformationInput(IJavaElement javaElement, String hoverInfo) {
			fElement= javaElement;
			fHoverInfo= hoverInfo;
		}

		public IJavaElement getJavaElement() {
			return fElement;
		}

		public String getHoverInfo() {
			return fHoverInfo;
		}
	}

	@Override
	public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) {
		String hoverInfoString= getHoverInfo(textViewer, hoverRegion);
		if (hoverInfoString == null) {
			return null;
		}
		if (fJavaElement == null) {
			return hoverInfoString;
		}
		return new JavaSourceInformationInput(fJavaElement, hoverInfoString);
	}

	@Override
	@Deprecated
	public String getHoverInfo(ITextViewer textViewer, IRegion region) {
		IJavaElement[] result= getJavaElementsAt(textViewer, region);
		fJavaElement= null;

		fUpwardShiftInLines= 0;
		fBracketHoverStatus= null;

		if (result == null || result.length == 0) {
			String val = getBracketHoverInfo(textViewer, region);
			if (val == null) {
				return getTextBlockHoverInfo(textViewer, region);
			}
			return val;
		}

		if (result.length > 1)
			return null;

		fJavaElement= result[0];
		if ((fJavaElement instanceof IMember || fJavaElement instanceof ILocalVariable || fJavaElement instanceof ITypeParameter) && fJavaElement instanceof ISourceReference) {
			try {
				String source= ((ISourceReference) fJavaElement).getSource();

				String[] sourceLines= getTrimmedSource(source, fJavaElement);
				if (sourceLines == null)
					return null;

				String delim= StubUtility.getLineDelimiterUsed(fJavaElement);
				source= Strings.concatenate(sourceLines, delim);

				return source;
			} catch (JavaModelException ex) {
				//do nothing
			}
		}
		return null;
	}

	private String getBracketHoverInfo(final ITextViewer textViewer, IRegion region) {
		boolean isElsePart= false;
		IEditorPart editor= getEditor();
		ITypeRoot editorInput= getEditorInputJavaElement();
		if (!(editor instanceof JavaEditor) || editorInput == null) {
			return null;
		}

		int offset= region.getOffset();
		IDocument document= textViewer.getDocument();
		if (document == null)
			return null;
		try {
			char c= document.getChar(offset);
			if (c != '}')
				return null;
			JavaPairMatcher matcher= ((JavaEditor) editor).getBracketMatcher();
			if (matcher == null)
				return null;
			IRegion match= matcher.match(document, offset);
			if (match == null)
				return null;

			String delim= StubUtility.getLineDelimiterUsed(editorInput);

			CompilationUnit ast= SharedASTProviderCore.getAST(editorInput, SharedASTProviderCore.WAIT_NO, null);
			if (ast == null)
				return null;
			ASTNode bracketNode= NodeFinder.perform(ast, match.getOffset(),
					match.getLength());
			if (bracketNode == null)
				return null;
			ASTNode node;
			ASTNode parent= bracketNode.getParent();
			if (parent instanceof IfStatement) {
				IfStatement parentIfStmt= (IfStatement) parent;
				if ((parentIfStmt.getElseStatement() != null && ASTNodes.getInclusiveEnd(parentIfStmt.getElseStatement()) == offset) // if [else if]* else
						|| (parentIfStmt.getLocationInParent() == IfStatement.ELSE_STATEMENT_PROPERTY && ASTNodes.getInclusiveEnd(parentIfStmt.getThenStatement()) == offset)) { // if [else if]+ else?
					isElsePart= true;
					while (parent.getLocationInParent() == IfStatement.ELSE_STATEMENT_PROPERTY) {
						parent= parent.getParent();
					}
				}
			}
			if (bracketNode instanceof Block && !(parent instanceof Block) && !(parent instanceof SwitchStatement)) {
				node= parent;
			} else {
				node= bracketNode;
			}
			int nodeStart;
			int nodeLength;
			if (node instanceof BodyDeclaration) {
				BodyDeclaration declaration= (BodyDeclaration) node;
				Javadoc javadoc= declaration.getJavadoc();
				int lengthOfJavadoc= javadoc == null ? 0 : javadoc.getLength() +
						delim.length();
				nodeStart= node.getStartPosition() + lengthOfJavadoc;
				nodeLength= node.getLength() - lengthOfJavadoc;
			} else {
				nodeStart= node.getStartPosition();
				nodeLength= ASTNodes.getExclusiveEnd(bracketNode) - nodeStart;
			}

			int line1= document.getLineOfOffset(nodeStart);
			int sourceOffset= document.getLineOffset(line1);
			int line2= document.getLineOfOffset(nodeStart + nodeLength);
			int hoveredLine= document.getLineOfOffset(offset);
			if (line2 > hoveredLine)
				line2= hoveredLine;

			//check if line1 is visible
			final int[] topIndex= new int[1];
			StyledText textWidget= textViewer.getTextWidget();
			if (textWidget == null)
				return null;

			Display display;
			try {
				display= textWidget.getDisplay();
			} catch (SWTException ex) {
				if (ex.code == SWT.ERROR_WIDGET_DISPOSED)
					return null;
				else
					throw ex;
			}

			display.syncExec(() -> topIndex[0]= textViewer.getTopIndex());

			int topLine= topIndex[0];
			if (topLine == -1)
				return null;
			int noOfSourceLines;
			IRegion endLine;
			int skippedLines= 0;
			int wLine1= ((JavaSourceViewer) textViewer).modelLine2WidgetLine(line1);
			int wLine2= ((JavaSourceViewer) textViewer).modelLine2WidgetLine(line2);
			if ((line1 < topLine) || (wLine1 != -1 && (wLine2 - wLine1 != line2 - line1))) {
				// match not visible or content is folded - see bug 399997
				if (isElsePart) {
					return getBracketHoverInfo((IfStatement) node, bracketNode, document, editorInput, delim); // see bug 377141, 201850
				}
				noOfSourceLines= 3;
				if ((line2 - line1) < noOfSourceLines) {
					noOfSourceLines= line2 - line1;
				}
				skippedLines= Math.abs(line2 - line1 - noOfSourceLines);
				if (skippedLines == 1) {
					noOfSourceLines++;
					skippedLines= 0;
				}
				endLine= document.getLineInformation(line1 + noOfSourceLines);
				fUpwardShiftInLines= noOfSourceLines;
				if (skippedLines > 0) {
					fBracketHoverStatus= Messages.format(JavaHoverMessages.JavaSourceHover_skippedLines, Integer.valueOf(skippedLines));
				}
			} else {
				return null;
			}
			if (fUpwardShiftInLines == 0)
				return null;

			int sourceLength= (endLine.getOffset() + endLine.getLength()) - sourceOffset;
			String source= document.get(sourceOffset, sourceLength);
			String[] sourceLines= getTrimmedSource(source, editorInput);
			if (sourceLines == null)
				return null;
			String[] str= new String[noOfSourceLines];
			System.arraycopy(sourceLines, 0, str, 0, noOfSourceLines);
			source= Strings.concatenate(str, delim);
			if (skippedLines > 0) {
				source= source.concat(delim).concat(JavaHoverMessages.JavaSourceHover_skippedLinesSymbol);
				fUpwardShiftInLines++;
			}
			return source;
		} catch (BadLocationException e) {
			JavaPlugin.log(e);
			return null;
		}
	}

	/**
	 * Creates the hover text for 'else if' or 'else' closing bracket in 'if [else if]+ else?' or
	 * 'if [else if]* else' cases by stitching together all the headers when the beginning of the
	 * first 'if' is not visible in the text editor.
	 *
	 * @param ifNode the first 'if' node in the structure
	 * @param bracketNode the node at whose closing bracket the hover text is required
	 * @param document the input document of the text viewer on which the hover popup should be
	 *            shown
	 * @param editorInput the editor's input as {@link ITypeRoot}
	 * @param delim the line delimiter used for the editorInput
	 * @return the hover text for 'else if' or 'else' closing bracket in 'if [else if]+ else?' or
	 *         'if [else if]* else' cases respectively
	 * @throws BadLocationException if an attempt has been performed to access a non-existing
	 *             position in the document
	 * @since 3.9
	 */
	private String getBracketHoverInfo(IfStatement ifNode, ASTNode bracketNode, final IDocument document, final ITypeRoot editorInput, final String delim) throws BadLocationException {
		int totalSkippedLines= 0;
		String hoverText= null;

		Statement currentStatement= ifNode.getThenStatement();
		int nodeStart= ifNode.getStartPosition();
		while (currentStatement != null) {
			int nodeLength= ASTNodes.getExclusiveEnd(currentStatement) - nodeStart;
			int line1= document.getLineOfOffset(nodeStart);
			int sourceOffset= document.getLineOffset(line1);
			int line2= document.getLineOfOffset(nodeStart + nodeLength);
			int line3= line2;
			if (currentStatement != bracketNode && ifNode != null && ifNode.getElseStatement() != null) {
				int elseStartOffset= getNextElseOffset(ifNode.getThenStatement(), editorInput);
				if (elseStartOffset != -1) {
					line3= document.getLineOfOffset(elseStartOffset); // next 'else'
				}
			}
			int noOfTotalLines= (line2 == line3) ? (line2 - line1) : (line2 - line1 + 1);
			int noOfSourceLines= 3;

			if (noOfTotalLines < noOfSourceLines) {
				noOfSourceLines= noOfTotalLines;
			}
			int noOfSkippedLines= noOfTotalLines - noOfSourceLines;
			if (noOfSkippedLines == 1) {
				noOfSourceLines++;
				noOfSkippedLines= 0;
			}

			if (noOfSourceLines > 0) {
				IRegion endLine= document.getLineInformation(line1 + noOfSourceLines - 1);
				int sourceLength= (endLine.getOffset() + endLine.getLength()) - sourceOffset;
				String source= document.get(sourceOffset, sourceLength);
				String[] sourceLines= getTrimmedSource(source, editorInput);
				if (sourceLines == null)
					return null;
				source= Strings.concatenate(sourceLines, delim);
				if (noOfSkippedLines > 0) {
					source= source.concat(delim).concat(JavaHoverMessages.JavaSourceHover_skippedLinesSymbol);
					fUpwardShiftInLines++;
				}

				fUpwardShiftInLines+= noOfSourceLines;
				totalSkippedLines+= noOfSkippedLines;
				if (hoverText == null) {
					hoverText= source;
				} else {
					hoverText= hoverText.concat(delim).concat(source);
				}
			}
			// advance currentStatement to the next 'else if' or 'else' statement; set it to null when no further processing is required
			// advance ifNode to the 'if' in next 'else if'; set it to null if 'else' is reached
			if (currentStatement != bracketNode) {
				Statement thenStatement= ifNode.getThenStatement();
				Statement nextStatement= ifNode.getElseStatement();
				if (nextStatement instanceof IfStatement) {
					currentStatement= ((IfStatement) nextStatement).getThenStatement();
					ifNode= (IfStatement) nextStatement;
				} else {
					currentStatement= nextStatement;
					ifNode= null;
				}
				// update nodeStart to next 'else' start offset
				int nextStartOffset= getNextElseOffset(thenStatement, editorInput);
				if (nextStartOffset != -1) {
					nodeStart= nextStartOffset;
				} else {
					nodeStart= nextStatement.getStartPosition();
				}
			} else {
				currentStatement= null;
			}
		}

		if (fUpwardShiftInLines == 0)
			return null;
		if ((totalSkippedLines) > 0) {
			fBracketHoverStatus= Messages.format(JavaHoverMessages.JavaSourceHover_skippedLines, Integer.valueOf(totalSkippedLines));
		}
		return hoverText;
	}

	private String getTextBlockHoverInfo(final ITextViewer textViewer, IRegion region) {
		String hoverText= null;
		IEditorPart editor= getEditor();
		ITypeRoot input= getEditorInputJavaElement();
		if (!(editor instanceof JavaEditor) || input == null || region == null) {
			return null;
		}
		IDocument document= textViewer.getDocument();
		if (document == null)
			return null;

		IDocumentExtension3 docExtension= null;
		if (document instanceof IDocumentExtension3)
			docExtension= (IDocumentExtension3) document;
		else
			return null;
		IJavaProject javaProject= getProject(editor);
		if (javaProject == null) {
			return null;
		}

		try {
			int selectionOffset= region.getOffset();
			//boolean setCaratPosition= selection.getLength() > 0 ? false : true;
			ITypedRegion partition= docExtension.getPartition(IJavaPartitions.JAVA_PARTITIONING, selectionOffset, false);
			if (partition == null || !isTextBlock( partition, javaProject)) {
				return null;
			}
			CompilationUnit ast= SharedASTProviderCore.getAST(input, SharedASTProviderCore.WAIT_NO, null);
			if (ast == null) {
				return null;
			}
			ASTNode textBlockNode= NodeFinder.perform(ast, partition.getOffset(),
					partition.getLength());
			if (!(textBlockNode instanceof TextBlock)) {
				return null;
			}
			TextBlock textBlock= (TextBlock) textBlockNode;
			return textBlock.getLiteralValue();
		} catch (BadLocationException | BadPartitioningException e) {
			//do nothing
		}
		return hoverText;
	}

	private boolean isTextBlock(ITypedRegion partition, IJavaProject javaProject) {
		if (!JavaModelUtil.is15OrHigher(javaProject)) {
			return false;
		}
		boolean isTextBlock= true;
		String partitionType= partition.getType();
		if (IJavaPartitions.JAVA_MULTI_LINE_STRING.equals(partitionType)) {
			isTextBlock= true;
		}
		return isTextBlock;
	}

	private IJavaProject getProject(IEditorPart editor) {
		IJavaProject javaProject= null;
		if (editor != null) {
			ITypeRoot inputJavaElement= EditorUtility.getEditorInputJavaElement(editor, false);
			javaProject= inputJavaElement.getJavaProject();
		}
		return javaProject;
	}

	private int getNextElseOffset(Statement then, ITypeRoot editorInput) {
		int thenEnd= ASTNodes.getExclusiveEnd(then);
		try {
			TokenScanner scanner= new TokenScanner(editorInput);
			return scanner.getNextStartOffset(thenEnd, true);
		} catch (CoreException e) {
			// ignore
		}
		return -1;
	}

	/**
	 * Returns the trimmed source lines.
	 *
	 * @param source the source string, could be <code>null</code>
	 * @param javaElement the java element
	 * @return the trimmed source lines or <code>null</code>
	 */
	private String[] getTrimmedSource(String source, IJavaElement javaElement) {
		if (source == null)
			return null;
		source= removeLeadingComments(source);
		String[] sourceLines= Strings.convertIntoLines(source);
		Strings.trimIndentation(sourceLines, javaElement.getJavaProject());
		return sourceLines;
	}

	private String removeLeadingComments(String source) {
		final JavaCodeReader reader= new JavaCodeReader();
		IDocument document= new Document(source);
		int i;
		try {
			reader.configureForwardReader(document, 0, document.getLength(), true, false);
			int c= reader.read();
			while (c != -1 && (c == '\r' || c == '\n')) {
				c= reader.read();
			}
			i= reader.getOffset();
			reader.close();
		} catch (IOException ex) {
			i= 0;
		} finally {
			try {
				reader.close();
			} catch (IOException ex) {
				JavaPlugin.log(ex);
			}
		}

		if (i < 0)
			return source;
		return source.substring(i);
	}

	/*
	 * @see org.eclipse.jface.text.ITextHoverExtension#getHoverControlCreator()
	 * @since 3.0
	 */
	@Override
	public IInformationControlCreator getHoverControlCreator() {
		if (fUpwardShiftInLines > 0)
			return createInformationControlCreator(false, fBracketHoverStatus, true);
		else
			return createInformationControlCreator(false, EditorsUI.getTooltipAffordanceString(), true);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractJavaEditorTextHover#getInformationPresenterControlCreator()
	 * @since 3.0
	 */
	@Override
	public IInformationControlCreator getInformationPresenterControlCreator() {
		if (fUpwardShiftInLines > 0)
			return createInformationControlCreator(false, fBracketHoverStatus, true);
		else
			return createInformationControlCreator(true, EditorsUI.getTooltipAffordanceString(), true);
	}

	/**
	 * Returns the information control creator.
	 *
	 * @param isResizable <code>true</code> if resizable
	 * @param statusFieldText the text to be used in the optional status field or <code>null</code>
	 *            if the status field should be hidden
	 * @param doShiftUp <code>true</code> iff {@link #fUpwardShiftInLines} should be considered
	 * @return the information control creator
	 * @since 3.8
	 */
	private IInformationControlCreator createInformationControlCreator(final boolean isResizable, final String statusFieldText, final boolean doShiftUp) {
		return parent -> {
			final IEditorPart editor= getEditor();
			int orientation= SWT.NONE;
			if (editor instanceof IWorkbenchPartOrientation)
				orientation= ((IWorkbenchPartOrientation) editor).getOrientation();

			return new SourceViewerInformationControl(parent, isResizable, orientation, statusFieldText) {
				@Override
				public void setLocation(Point location) {
					Point loc= location;
					if (doShiftUp && fUpwardShiftInLines > 0) {
						Point size= super.computeSizeConstraints(0, fUpwardShiftInLines + 1);
						//bracket hover is rendered above '}'
						int y= location.y - size.y - 5; //AbstractInformationControlManager.fMarginY = 5
						Rectangle trim= computeTrim();
						loc= new Point(location.x + trim.x - getViewer().getTextWidget().getLeftMargin(), y - trim.height - trim.y);
					}
					super.setLocation(loc);
				}

				@Override
				public Point computeSizeConstraints(int widthInChars, int heightInChars) {
					if (doShiftUp && fUpwardShiftInLines > 0) {
						Point sizeConstraints= super.computeSizeConstraints(widthInChars, heightInChars);
						return new Point(sizeConstraints.x, 0); //set height as 0 to ensure selection of bottom anchor in AbstractInformationControlManager.computeInformationControlLocation(...)
					} else {
						return super.computeSizeConstraints(widthInChars, heightInChars);
					}
				}

				@Override
				public void setSize(int width, int height) {
					if (doShiftUp && fUpwardShiftInLines != 0) {
						//compute the correct height of hover, this was set to 0 in computeSizeConstraints(..)
						Point size= super.computeSizeConstraints(0, fUpwardShiftInLines);
						Rectangle trim= computeTrim();
						super.setSize(width, size.y + trim.height - trim.y);
					} else {
						super.setSize(width, height);
					}
				}

				@Override
				public IInformationControlCreator getInformationPresenterControlCreator() {
					if (doShiftUp && fUpwardShiftInLines > 0) {
						// Hack: We don't wan't to have auto-enrichment when the mouse moves into the hover,
						// but we do want F2 to persist the hover. The framework has no way to distinguish the
						// two requests, so we have to implement this aspect.
						for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
							if ("canMoveIntoInformationControl".equals(element.getMethodName()) //$NON-NLS-1$
									&& "org.eclipse.jface.text.AbstractHoverInformationControlManager".equals(element.getClassName())) //$NON-NLS-1$
								return null; //do not enrich bracket hover
						}
						return JavaSourceHover.this.createInformationControlCreator(isResizable && !isResizable, statusFieldText, false);
					} else {
						return super.getInformationPresenterControlCreator();
					}
				}
			};
		};
	}
}
