/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.core.tests.eval;

import java.io.File;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Map;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.tests.junit.extension.StopableTestCase;
import org.eclipse.jdt.core.tests.runtime.LocalVMLauncher;
import org.eclipse.jdt.core.tests.runtime.LocalVirtualMachine;
import org.eclipse.jdt.core.tests.runtime.TargetException;
import org.eclipse.jdt.core.tests.runtime.TargetInterface;
import org.eclipse.jdt.core.tests.util.*;
import org.eclipse.jdt.internal.compiler.ClassFile;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.batch.FileSystem;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.IBinaryField;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblem;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.eval.EvaluationContext;
import org.eclipse.jdt.internal.eval.EvaluationResult;
import org.eclipse.jdt.internal.eval.GlobalVariable;
import org.eclipse.jdt.internal.eval.IRequestor;
import org.eclipse.jdt.internal.eval.InstallException;

@SuppressWarnings({ "unchecked", "rawtypes" })
public class EvaluationTest extends AbstractCompilerTest implements StopableTestCase {

	public class Requestor implements IRequestor {
		public int resultIndex = -1;
		public EvaluationResult[] results = new EvaluationResult[5];
		public boolean acceptClassFiles(ClassFile[] classFiles, char[] codeSnippetClassName) {
			try {
				EvaluationTest.this.target.sendClasses(codeSnippetClassName != null, classFiles);
			} catch (TargetException e) {
				return false;
			}
			if (codeSnippetClassName != null) {
				TargetInterface.Result result = EvaluationTest.this.target.getResult();
				if (result.displayString == null) {
					acceptResult(new EvaluationResult(null, EvaluationResult.T_CODE_SNIPPET, null, null));
				} else {
					acceptResult(new EvaluationResult(null, EvaluationResult.T_CODE_SNIPPET, result.displayString, result.typeName));
				}
			} else {
				for (int i = 0, length = classFiles.length; i < length; i++) {
					char[][] compoundName = classFiles[i].getCompoundName();
					if (new String(compoundName[compoundName.length - 1]).startsWith("GlobalVariable")) {
						try {
							IBinaryField[] fields = new ClassFileReader(classFiles[i].getBytes(), null).getFields();
							if (fields != null) {
								for (int j = 0; j < fields.length; j++) {
									IBinaryField field = fields[j];
									if (Modifier.isPublic(field.getModifiers())) {
										TargetInterface.Result result = EvaluationTest.this.target.getResult();
										if (result.displayString == null) {
											acceptResult(new EvaluationResult(field.getName(), EvaluationResult.T_VARIABLE, null, null));
										} else {
											acceptResult(new EvaluationResult(field.getName(), EvaluationResult.T_VARIABLE, result.displayString, result.typeName));
										}
									}
								}
							}
						} catch (ClassFormatException e) {
							e.printStackTrace();
						}
					}
				}
			}
			return true;
		}
		public void acceptProblem(CategorizedProblem problem, char[] fragmentSource, int fragmentKind) {
			acceptResult(new EvaluationResult(fragmentSource, fragmentKind, new CategorizedProblem[]{problem}));
		}
		public void acceptResult(EvaluationResult result) {
			try {
				this.results[++this.resultIndex] = result;
			} catch (ArrayIndexOutOfBoundsException e) {
				int oldResultLength = this.results.length;
				System.arraycopy(this.results, 0, (this.results = new EvaluationResult[oldResultLength * 2]), 0, oldResultLength);
				this.results[this.resultIndex] = result;
			}
		}
	}

	String[] classPath;
	public EvaluationContext context;
	INameEnvironment env;
	LocalVirtualMachine launchedVM;
	TargetInterface target;

	/**
	 * Creates a new EvaluationTest.
	 */
	public EvaluationTest(String name) {
		super(name);
	}

	public static Test setupSuite(Class clazz) {
		ArrayList testClasses = new ArrayList();
		testClasses.add(clazz);
		return buildAllCompliancesTestSuite(clazz, EvaluationSetup.class, testClasses);
	}

	public static Test suite(Class evaluationTestClass) {
		TestSuite suite = new TestSuite(evaluationTestClass);
		return suite;
	}

	/**
	 * Asserts that two char arrays are equal. If they are not an AssertionFailedError is thrown.
	 *
	 * @param message
	 *                 the detail message for this assertion
	 * @param expected
	 *                 the expected value of a char array
	 * @param actual
	 *                 the actual value of a char array
	 */
	public void assertEquals(String message, char[] expected, char[] actual) {
		if (expected == null && actual == null)
			return;
		if (expected != null) {
			if (actual == null) {
				failNotEquals(message, expected, actual);
				return;
			}
			if (expected.length == actual.length) {
				for (int i = 0; i < expected.length; i++) {
					if (expected[i] != actual[i]) {
						failNotEquals(message, expected, actual);
						return;
					}
				}
				return;
			}
		}
		failNotEquals(message, expected, actual);
	}

	/**
	 * Build a char array from the given lines
	 */
	protected char[] buildCharArray(String[] lines) {
		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < lines.length; i++) {
			buffer.append(lines[i]);
			if (i < lines.length - 1) {
				buffer.append("\n");
			}
		}
		int length = buffer.length();
		char[] result = new char[length];
		buffer.getChars(0, length, result, 0);
		return result;
	}

	/**
	 * Returns whether the 2 given problems are equals.
	 */
	public boolean equals(CategorizedProblem pb1, CategorizedProblem pb2) {
		if ((pb1 == null) && (pb2 == null)) {
			return true;
		}
		if ((pb1 == null) || (pb2 == null)) {
			return false;
		}
		return (pb1.getID() == pb2.getID()) && (pb1.isError() == pb2.isError()) && (pb1.getSourceStart() == pb2.getSourceStart()) && (pb1.getSourceEnd() == pb2.getSourceEnd()) && (pb1.getSourceLineNumber() == pb2.getSourceLineNumber());
	}

	/**
	 * Evaluates the given code snippet and makes sure it returns a result with the given display string.
	 */
	public void evaluateWithExpectedDisplayString(Map compilerOptions, char[] codeSnippet, char[] displayString) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), compilerOptions, requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		if (requestor.resultIndex != 0) {
			for (int i = 0; i < requestor.resultIndex; i++) {
				System.out.println("unexpected result[" + i + "]: " + requestor.results[i]);
			}
		}
		assertTrue("Unexpected result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		assertTrue("Has problem", !result.hasProblems());
		assertTrue("Empty problem list", result.getProblems().length == 0);
		if (displayString == null) {
			assertTrue("Has value", !result.hasValue());
		} else {
			assertTrue("Has value", result.hasValue());
			assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
			//assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
			assertEquals("Value display string", displayString, result.getValueDisplayString());
		}
	}

	/**
	 * Evaluates the given code snippet and makes sure it returns a result with the given display string.
	 */
	public void evaluateWithExpectedDisplayString(char[] codeSnippet, char[] displayString) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		if (requestor.resultIndex != 0) {
			for (int i = 0; i < requestor.resultIndex; i++) {
				System.out.println("unexpected result[" + i + "]: " + requestor.results[i]);
			}
		}
		assertTrue("Unexpected result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		assertTrue("Has problem", !result.hasProblems());
		assertTrue("Empty problem list", result.getProblems().length == 0);
		if (displayString == null) {
			assertTrue("Has value", !result.hasValue());
		} else {
			assertTrue("Does not have expected value", result.hasValue());
			assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
			//assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
			assertEquals("Value display string", displayString, result.getValueDisplayString());
		}
	}

	/**
	 * Evaluates the given code snippet and makes sure the evaluation result has at least the given problem on the given import.
	 */
	protected void evaluateWithExpectedImportProblem(char[] codeSnippet, char[] importDeclaration, CategorizedProblem expected) {
		evaluateWithExpectedImportProblem(codeSnippet, importDeclaration, getCompilerOptions(), expected);
	}

	/**
	 * Evaluates the given code snippet and makes sure the evaluation result has at least the given problem on the given import.
	 */
	protected void evaluateWithExpectedImportProblem(char[] codeSnippet, char[] importDeclaration, Map options, CategorizedProblem expected) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), options, requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		for (int i = 0; i <= requestor.resultIndex; i++) {
			EvaluationResult result = requestor.results[i];
			assertTrue("Has value", !result.hasValue());
			assertTrue("Has problem", result.hasProblems());
			assertEquals("Evaluation type", EvaluationResult.T_IMPORT, result.getEvaluationType());
			assertEquals("Evaluation id", importDeclaration, result.getEvaluationID());
			CategorizedProblem[] problems = result.getProblems();
			if (equals(expected, problems[0])) {
				return;
			}
		}
		assertTrue("Expected problem not found", false);
	}

	/**
	 * Evaluates the given code snippet and makes sure the evaluation result has at least the given problem.
	 */
	protected void evaluateWithExpectedProblem(char[] codeSnippet, CategorizedProblem expected) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		for (int i = 0; i <= requestor.resultIndex; i++) {
			EvaluationResult result = requestor.results[i];
			assertTrue("Has value", !result.hasValue());
			assertTrue("Has problem", result.hasProblems());
			assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
			assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
			CategorizedProblem[] problems = result.getProblems();
			if (equals(expected, problems[0])) {
				return;
			}
		}
		assertTrue("Expected problem not found", false);
	}

	/**
	 * Evaluates the given code snippet and makes sure the evaluation result has at least the given problem.
	 */
	protected void evaluateWithExpectedProblem(char[] codeSnippet, String problemsString) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		assertTrue("Got one result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		assertTrue("Has value", !result.hasValue());
		assertTrue("Has problem", result.hasProblems());
		assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
		assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
		StringBuffer problemBuffer = new StringBuffer(20);
		CategorizedProblem[] problems = result.getProblems();
		for (int i = 0; i < problems.length; i++) {
			problemBuffer.append(problems[i].getMessage()).append('\n');
		}
		assertEquals("Unexpected problems", problemsString, problemBuffer.toString());
	}

	/**
	 * Evaluates the given variable and makes sure the evaluation result has at least the given problem.
	 */
	protected void evaluateWithExpectedProblem(GlobalVariable var, CategorizedProblem expected) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluateVariables(getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		for (int i = 0; i <= requestor.resultIndex; i++) {
			EvaluationResult result = requestor.results[i];
			assertTrue("Has value", !result.hasValue());
			assertTrue("Has problem", result.hasProblems());
			assertEquals("Evaluation type", EvaluationResult.T_VARIABLE, result.getEvaluationType());
			assertEquals("Evaluation id", var.getName(), result.getEvaluationID());
			CategorizedProblem[] problems = result.getProblems();
			if (equals(expected, problems[0])) {
				return;
			}
		}
		assertTrue("Expected problem not found", false);
	}

	/**
	 * Evaluates the given code snippet and makes sure it returns a result with the given type name.
	 */
	protected void evaluateWithExpectedType(char[] codeSnippet, char[] expectedTypeName) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		if (requestor.resultIndex != 0) {
			for (int i = 0; i < requestor.resultIndex; i++) {
				System.out.println("unexpected result[" + i + "]: " + requestor.results[i]);
			}
		}
		assertTrue("Got one result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		if (expectedTypeName == null) {
			assertTrue("Has value", !result.hasValue());
		} else {
			assertTrue("Does not have expected value", result.hasValue());
			assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
			//assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
			assertEquals("Value type name", expectedTypeName, result.getValueTypeName());
		}
	}

	/**
	 * Evaluates the given code snippet and makes sure it returns a result with the given display string and type name.
	 */
	protected void evaluateWithExpectedValue(char[] codeSnippet, char[] displayString, char[] typeName) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		assertTrue("Got one result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		if (displayString == null) {
			assertTrue("Missing value", !result.hasValue());
		} else {
			assertTrue("Has value", result.hasValue());
			assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
			//assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
			assertEquals("Value display string", displayString, result.getValueDisplayString());
			assertEquals("Value type name", typeName, result.getValueTypeName());
		}
	}

	/**
	 * Evaluates the given variable and makes sure it returns a result with the given display string and type name.
	 */
	protected void evaluateWithExpectedValue(GlobalVariable var, char[] displayString, char[] typeName) {
		Requestor requestor = new Requestor();
		try {
			this.context.evaluateVariable(var, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		if (requestor.resultIndex != 0) {
			for (int i = 0; i < requestor.resultIndex; i++) {
				System.out.println("unexpected result[" + i + "]: " + requestor.results[i]);
			}
		}
		assertTrue("Unexpected result", requestor.resultIndex == 0);
		EvaluationResult result = requestor.results[0];
		if (displayString == null) {
			assertTrue("Has value", !result.hasValue());
		} else {
			assertTrue("Has value", result.hasValue());
			assertEquals("Value display string", displayString, result.getValueDisplayString());
			assertEquals("Value type name", typeName, result.getValueTypeName());
		}
	}

	/**
	 * Evaluates the given code snippet and makes sure an evaluation result has at least the given warning, and that another evaluation result has the given display string.
	 */
	protected void evaluateWithExpectedWarningAndDisplayString(final char[] codeSnippet, final CategorizedProblem[] expected, final char[] displayString) {
		class ResultRequestor extends Requestor {
			ArrayList collectedProblems = new ArrayList();
			boolean gotDisplayString = false;
			public void acceptResult(EvaluationResult result) {
				assertEquals("Evaluation type", EvaluationResult.T_CODE_SNIPPET, result.getEvaluationType());
				//assertEquals("Evaluation id", codeSnippet, result.getEvaluationID());
				if (result.hasValue()) {
					if (CharOperation.equals(result.getValueDisplayString(), displayString)) {
						this.gotDisplayString = true;
					}
				} else {
					assertTrue("Has problem", result.hasProblems());
					CategorizedProblem[] problems = result.getProblems();
					for (int i = 0; i < problems.length; i++) {
						this.collectedProblems.add(problems[i]);
					}
				}
			}
		}
		ResultRequestor requestor = new ResultRequestor();
		try {
			this.context.evaluate(codeSnippet, getEnv(), getCompilerOptions(), requestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception " + e.getMessage(), false);
		}
		if (expected.length == requestor.collectedProblems.size()) {
			for (int i = 0; i < expected.length; i++) {
				assertTrue("Problem mismatch" + requestor.collectedProblems.get(i), this.equals(expected[i], (CategorizedProblem)requestor.collectedProblems.get(i)));
			}
		} else {
			assertTrue("Wrong problem count", false);
		}
		assertTrue("Expected display string", requestor.gotDisplayString);
	}

	private void failNotEquals(String message, char[] expected, char[] actual) {
		String formatted = "";
		if (message != null)
			formatted = message + " ";
		String expectedString = expected == null ? "null" : new String(expected);
		String actualString = actual == null ? "null" : new String(actual);
		fail(formatted + "expected:<" + expectedString + "> but was:<" + actualString + ">");
	}

	public Map getCompilerOptions() {
		Map defaultOptions = super.getCompilerOptions();
		defaultOptions.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.DO_NOT_GENERATE);
		defaultOptions.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.DO_NOT_GENERATE);
		defaultOptions.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.DO_NOT_GENERATE);
		defaultOptions.put(CompilerOptions.OPTION_ReportUnusedLocal, CompilerOptions.WARNING);
		defaultOptions.put(CompilerOptions.OPTION_ReportUnusedImport, CompilerOptions.IGNORE);
		defaultOptions.put(CompilerOptions.OPTION_ReportLocalVariableHiding, CompilerOptions.WARNING);
		defaultOptions.put(CompilerOptions.OPTION_ReportUnusedPrivateMember, CompilerOptions.IGNORE);
		defaultOptions.put(CompilerOptions.OPTION_ReportPossibleAccidentalBooleanAssignment, CompilerOptions.WARNING);
		return defaultOptions;
	}

	public INameEnvironment getEnv() {
		return this.env;
	}

	public IProblemFactory getProblemFactory() {
		return new DefaultProblemFactory(java.util.Locale.getDefault());
	}

	public void initialize(CompilerTestSetup setUp) {
		super.initialize(setUp);
		EvaluationSetup evalSetUp = (EvaluationSetup)setUp;
		this.context = evalSetUp.context;
		this.target = evalSetUp.target;
		this.launchedVM = evalSetUp.launchedVM;
		this.env = evalSetUp.env;
	}

	/**
	 * Installs all the variables and check that the number of installed variables is the given number.
	 */
	protected void installVariables(final int expectedNumber) {
		class InstallRequestor extends Requestor {
			int count = 0;
			public void acceptResult(EvaluationResult result) {
				assertTrue("Has problems", !result.hasProblems());
				assertTrue("Has value", result.hasValue());
				this.count++;
			}
		}

		InstallRequestor installRequestor = new InstallRequestor();
		try {
			this.context.evaluateVariables(getEnv(), getCompilerOptions(), installRequestor, getProblemFactory());
		} catch (InstallException e) {
			assertTrue("Target exception: " + e.getMessage(), false);
		}
		assertEquals("Number of installed variables", expectedNumber, installRequestor.count);
	}

	public boolean isJRockitVM() {
		final String vmName = System.getProperty("java.vm.name");
		return vmName != null && vmName.indexOf("JRockit") != -1;
	}
	/**
	 * Returns a new problem with the given id, severity, source positions and line number.
	 */
	protected DefaultProblem newProblem(int id, int severity, int startPos, int endPos, int line) {
		return new DefaultProblem(null, null, id, null, severity, startPos, endPos, line, 0);
	}

	public void resetEnv() {
		String encoding = (String)getCompilerOptions().get(CompilerOptions.OPTION_Encoding);
		if ("".equals(encoding))
			encoding = null;
		this.env = new FileSystem(Util.concatWithClassLibs(EvaluationSetup.EVAL_DIRECTORY + File.separator + LocalVMLauncher.REGULAR_CLASSPATH_DIRECTORY, false), new String[0], encoding);
	}

	public void stop() {
		if (this.target != null) {
			this.target.disconnect(); // Close the socket first so that the OS resource has a chance to be freed.
		}
		if (this.launchedVM != null) {
			try {
				int retry = 0;
				while (this.launchedVM.isRunning() && (++retry < 20)) {
					try {
						Thread.sleep(retry * 100);
					} catch (InterruptedException e) {
					}
				}
				if (this.launchedVM.isRunning()) {
					this.launchedVM.shutDown();
				}
			} catch (TargetException e) {
			}
		}
	}
}
