/*******************************************************************************
 * Copyright (c) 2018, 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.debug.core.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.debug.core.IJavaFieldVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaStackFrame;
import org.eclipse.jdt.internal.debug.core.logicalstructures.JDILambdaVariable;
import org.eclipse.jdt.internal.debug.eval.ast.engine.IRuntimeContext;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Location;
import com.sun.jdi.Method;

/**
 * Utility class for Lambda Expressions and Stack frames Place holder for all Lambda operation encapsulation.
 */
public class LambdaUtils {

	private static final String LAMBDA_METHOD_PREFIX = "lambda$"; //$NON-NLS-1$

	/**
	 * Inspects the top stack frame of the context; if that frame is a lambda frame, looks for a variable with the specified name in that frame and
	 * outer frames visible from that frame.
	 *
	 * @param context
	 *            The context in which to check.
	 * @param variableName
	 *            The name of the variable.
	 * @return The variable with the specified name if found, {@code null} otherwise. Also returns {@code null} if no thread or top stack frame is
	 *         available. If there are multiple variables with the same name in the lambda context, the most local one is returned.
	 * @throws DebugException
	 *             If accessing the top stack frame or the local variables on stack frames fails, due to failure to communicate with the debug target.
	 */
	public static IVariable findLambdaFrameVariable(IRuntimeContext context, String variableName) throws DebugException {
		IStackFrame stackFrame = context.getFrame();
		if (stackFrame != null) {
			List<IVariable> variables = getLambdaFrameVariables(stackFrame);
			for (IVariable variable : variables) {
				if (variable.getName().equals(variableName)) {
					return variable;
				}
			}
		}
		return null;
	}

	/**
	 * Collects variables visible from a lambda stack frame.
	 *
	 * If the debugging class generates all debugging info in its classfile (e.g. line number and source file name), we can use these info to find all
	 * enclosing frames of the paused line number and collect their variables. Otherwise, collect variables from that lambda frame and two more frames
	 * below it.
	 *
	 * @param frame
	 *            The lambda frame at which to check.
	 * @return The variables visible from the stack frame. An empty list if the specified stack frame is not a lambda frame. The variables are ordered
	 *         top-down, i.e. if shadowing occurs, the more local variable will be first in the resulting list.
	 * @throws DebugException
	 *             If accessing the top stack frame or the local variables on stack frames fails, due to failure to communicate with the debug target.
	 */
	public static List<IVariable> getLambdaFrameVariables(IStackFrame frame) throws DebugException {
		if (LambdaUtils.isLambdaFrame(frame)) {
			int lineNumber = frame.getLineNumber();
			String sourceName = ((IJavaStackFrame) frame).getSourceName();
			if (lineNumber == -1 || sourceName == null) {
				return collectVariablesFromLambdaFrame(frame);
			}
			return collectVariablesFromEnclosingFrames(frame);
		}
		return Collections.emptyList();
	}

	/**
	 * Collects variables visible from a lambda stack frame and two frames below that frame.
	 *
	 * Inside a lambda expression, variable names are mangled by the compiler. Its therefore necessary to check the outer frame when at a lambda
	 * frame, in order to find a variable with its name. The lambda expression itself is called by a synthetic static method, which is the first frame
	 * below the lambda frame. So in total we collect variables from 3 stack frames.
	 *
	 * @param frame
	 *            The lambda frame at which to check.
	 * @return The variables visible from the stack frame. The variables are ordered top-down, i.e. if shadowing occurs, the more local variable will
	 *         be first in the resulting list.
	 * @throws DebugException
	 *             If accessing the top stack frame or the local variables on stack frames fails, due to failure to communicate with the debug target.
	 */
	private static List<IVariable> collectVariablesFromLambdaFrame(IStackFrame frame) throws DebugException {
		List<IVariable> variables = new ArrayList<>();
		IThread thread = frame.getThread();
		// look for two frames below the frame which is provided instead starting from first frame.
		List<IStackFrame> stackFrames = Stream.of(thread.getStackFrames()).dropWhile(f -> f != frame)
				.limit(3).collect(Collectors.toUnmodifiableList());
		for (IStackFrame stackFrame : stackFrames) {
			IVariable[] stackFrameVariables = stackFrame.getVariables();
			variables.addAll(Arrays.asList(stackFrameVariables));
			for (IVariable frameVariable : stackFrameVariables) {
				if (isLambdaObjectVariable(frameVariable)) {
					variables.addAll(extractVariablesFromLambda(frameVariable));
				}
			}
		}
		return Collections.unmodifiableList(variables);
	}

	/**
	 * Collect variables from all enclosing frames starting from the provided frame.
	 */
	private static List<IVariable> collectVariablesFromEnclosingFrames(IStackFrame frame) throws DebugException {
		List<IVariable> variables = new ArrayList<>();
		IThread thread = frame.getThread();
		List<IStackFrame> stackFrames = Stream.of(thread.getStackFrames()).dropWhile(f -> f != frame)
				.collect(Collectors.toUnmodifiableList());
		int pausedLineNumber = frame.getLineNumber();
		String pausedSourceName = ((IJavaStackFrame) frame).getSourceName();
		String pausedSourcePath = ((IJavaStackFrame) frame).getSourcePath();
		boolean isFocusFrame = true;
		for (IStackFrame stackFrame : stackFrames) {
			JDIStackFrame jdiFrame = (JDIStackFrame) stackFrame;
			if (isFocusFrame) {
				isFocusFrame = false;
			} else {
				if (!Objects.equals(pausedSourceName, jdiFrame.getSourceName())
						|| !Objects.equals(pausedSourcePath, jdiFrame.getSourcePath())) {
					continue;
				}
				List<Location> locations;
				try {
					locations = jdiFrame.getUnderlyingMethod().allLineLocations();
				} catch (AbsentInformationException e) {
					continue;
				}
				if (locations.isEmpty()) {
					continue;
				}
				int methodStartLine = locations.get(0).lineNumber();
				int methodEndLine = locations.get(locations.size() - 1).lineNumber();
				if (methodStartLine > pausedLineNumber || methodEndLine < pausedLineNumber) {
					continue;
				}
			}
			IVariable[] stackFrameVariables = jdiFrame.getVariables();
			variables.addAll(Arrays.asList(stackFrameVariables));
			for (IVariable frameVariable : stackFrameVariables) {
				if (isLambdaObjectVariable(frameVariable)) {
					variables.addAll(extractVariablesFromLambda(frameVariable));
				}
			}
		}
		return Collections.unmodifiableList(variables);
	}

	/**
	 * Evaluates if the input frame is a lambda frame.
	 *
	 * @param frame
	 *            the frame which needs to be evaluated
	 * @return <code>True</code> if the frame is a lambda frame else return <code>False</Code>
	 */
	public static boolean isLambdaFrame(IStackFrame frame) throws DebugException {
		return frame instanceof IJavaStackFrame && isLambdaFrame((IJavaStackFrame) frame);
	}

	/**
	 * Evaluates if the input frame is a lambda frame.
	 *
	 * @param frame
	 *            the frame which needs to be evaluated
	 * @return <code>True</code> if the frame is a lambda frame else return <code>False</Code>
	 * @since 3.8
	 */
	public static boolean isLambdaFrame(IJavaStackFrame frame) throws DebugException {
		return frame.isSynthetic() && frame.getName().startsWith(LAMBDA_METHOD_PREFIX);
	}

	/**
	 * Returns if the variable represent a variable embedded into Lambda object.
	 *
	 * @param variable
	 *            the variable which needs to be evaluated
	 * @return <code>True</code> if the variable is inside the Lambda object else return <code>False</code>
	 * @since 3.15
	 */
	public static boolean isLambdaField(IVariable variable) throws DebugException {
		return (variable instanceof IJavaFieldVariable) && ((IJavaFieldVariable) variable).getDeclaringType().getName().contains("$Lambda$"); //$NON-NLS-1$
	}

	/**
	 * Returns if the method is a lambda method.
	 *
	 * @param method
	 *            the method for which to check
	 * @return <code>True</code> if the method is a lambda method else return <code>False</code>
	 * @since 3.20
	 */
	public static boolean isLambdaMethod(Method method) {
		return method.name().startsWith(LAMBDA_METHOD_PREFIX);
	}

	private static boolean isLambdaObjectVariable(IVariable variable) {
		return variable instanceof JDILambdaVariable;
	}

	private static List<IVariable> extractVariablesFromLambda(IVariable variable) throws DebugException {
		if (variable.getValue() instanceof IJavaObject) {
			return Arrays.asList(variable.getValue().getVariables());
		}
		return Collections.emptyList();
	}
}
