package pp.s1184725.boppi.test;

import java.util.List;
import java.util.logging.*;

import java.io.*;

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.apache.commons.lang3.StringUtils;

import pp.iloc.Simulator;
import pp.iloc.eval.Machine;
import pp.iloc.model.Program;
import pp.s1184725.boppi.*;

/**
 * Test suite for all Boppi language features. This suite contains helper
 * functions and attributes to assist test cases. Each test case should consider
 * a single language feature across all compiler stages.
 *
 * @author Frank Wibbelink
 */
@RunWith(Suite.class)
@SuiteClasses({ ExpressionTest.class, SimpleVariableTest.class, ConditionalTest.class, SimpleFunctionTest.class,
		ClosureTest.class, ArrayTest.class })
public class BoppiTests {
	/**
	 * The default logging level
	 */
	public static final Level DEFAULT_LOG_LEVEL = Level.SEVERE;

	/**
	 * Log records of last test
	 */
	public static List<LogRecord> log;

	/**
	 * Output of last {@link #compileAndRun} call split by line breaks
	 */
	public static String[] out;

	/**
	 * Machine used for the last {@link #compileAndRun} call.
	 */
	public static Machine vm;

	/**
	 * The level of error reporting to use for the tests. Can be changed to
	 * include finer tests.
	 */
	private static Level logLevel = DEFAULT_LOG_LEVEL;

	/**
	 * Runs a test with warnings enabled.
	 *
	 * @param test
	 *            the test to run
	 */
	public static void withWarnings(Runnable test) {
		logLevel = Level.WARNING;
		test.run();
		logLevel = DEFAULT_LOG_LEVEL;
	}

	/**
	 * Writes all program output and log messages to the standard output.
	 */
	public static void printOutput() {
		if (out != null)
			for (String line : out)
				System.out.println(">>> " + line);

		System.out.println();
		log.forEach((message) -> System.out.println(message.getMessage()));
		System.out.println();
	}

	/**
	 * Parses a string of code, logging to {@link #log}.
	 *
	 * @param code
	 *            the code to parse
	 */
	public static void parseString(String code) {
		parse(codeFromString(code));
	}

	/**
	 * Parses code from a file, logging to {@link #log}.
	 *
	 * @param file
	 *            the file name
	 */
	public static void parseFile(String file) {
		parse(codeFromFile(file));
	}

	/**
	 * Parses and type checks a string of code, logging to {@link #log}.
	 *
	 * @param code
	 *            the code to type check
	 */
	public static void checkString(String code) {
		check(parse(codeFromString(code)));
	}

	/**
	 * Parses and type checks code from a file, logging to {@link #log}.
	 *
	 * @param file
	 *            the file name
	 */
	public static void checkFile(String file) {
		check(parse(codeFromFile(file)));
	}

	/**
	 * Compiles and runs a program from a string, logging to {@link #log} and
	 * writing lines of output to {@link #out}.
	 *
	 * @param code
	 *            the code to type check
	 * @param input
	 *            lines of input for the program
	 */
	public static void compileAndRunString(String code, String... input) {
		ParseTree ast = parse(codeFromString(code));
		run(generate(ast, check(ast)), input);
	}

	/**
	 * Compiles and runs a program from a file, logging to {@link #log} and
	 * writing lines of output to {@link #out}.
	 *
	 * @param file
	 *            the file name
	 * @param input
	 *            lines of input for the program
	 */
	public static void compileAndRunFile(String file, String... input) {
		ParseTree ast = parse(codeFromFile(file));
		run(generate(ast, check(ast)), input);
	}



	private static CharStream codeFromString(String code) {
		printCode(new StringReader(code));
		return ToolChain.getCharStream(code);
	}

	private static CharStream codeFromFile(String file) {
		printCode(new InputStreamReader(BoppiTests.class.getResourceAsStream(file)));
		return ToolChain.getCharStream(BoppiTests.class, file);
	}

	private static ParseTree parse(CharStream stream) {
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		printHeader("uncaught parse errors");
			ParseTree tree = ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program();
		printHeader("parse log");
			printLog(log);

		return tree;
	}

	private static Annotations check(ParseTree tree) {
		Logger logger = Logger.getAnonymousLogger();
		List<LogRecord> tempLog = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		printHeader("uncaught checker errors");
			Annotations an = ToolChain.getAnnotations(tree, logger);
		printHeader("checker log");
			printLog(tempLog);

		log.addAll(tempLog);
		return an;
	}

	private static Program generate(ParseTree ast, Annotations an) {
		Logger logger = Logger.getAnonymousLogger();
		List<LogRecord> tempLog = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		printHeader("uncaught generation errors");
			Program program = ToolChain.getILOC(ast, logger, an);
		printHeader("generation log");
			printLog(tempLog);

		log.addAll(tempLog);
		return program;
	}

	private static void run(Program program, String[] in) {
		Logger logger = Logger.getAnonymousLogger();
		List<LogRecord> tempLog = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		printHeader("iloc code");
			String iloc = program.prettyPrint();
			System.out.println("...");
			System.out.println(iloc.substring(iloc.indexOf("end of stdlib")));
		printHeader("execution errors");
			String input  = String.join("\n", in) + "\n";
			String output = ToolChain.execute(program, logger, input);
		printHeader("execution log");
			printLog(tempLog);
		printHeader("input");
			System.out.print(input);
		printHeader("output");
			System.out.println(output);

		log.addAll(tempLog);
		out = output.split("\n");
		vm = ToolChain.machine;
	}

	/**
	 * Compiles and runs a program in debug mode. This writes the ILOC code, the
	 * simulation, program output and all logged messages to the standard
	 * output.
	 *
	 * @param code
	 *            the code to run
	 * @param input
	 *            lines of input for the program
	 */
	public static void debugRunString(String code, String... input) {
		debugRun(ToolChain.getCharStream(code), String.join("\n", input) + "\n");
	}

	/**
	 * Compiles and runs a program in debug mode. This writes the ILOC code, the
	 * simulation, program output and all logged messages to the standard
	 * output.
	 *
	 * @param file
	 *            the file name
	 * @param input
	 *            lines of input for the program
	 */
	public static void debugRunFile(String file, String... input) {
		debugRun(ToolChain.getCharStream(BoppiTests.class, file), String.join("\n", input) + "\n");
	}

	/**
	 * Compiles and runs a program in debug mode, printing the generated
	 * program, a simulation of the program and all log messages.
	 *
	 * @param stream
	 *            a character stream with the program code
	 * @param input
	 *            lines of input for the program
	 */
	public static void debugRun(CharStream stream, String... input) {
		Simulator.DEBUG = true;
		String in = String.join("\n", input) + "\n";
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(Level.FINEST);

		ParseTree ast = ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program();
		logger.info("done parsing");
		Annotations annotations = ToolChain.getAnnotations(ast, logger);
		logger.info("done checking");
		Program program = ToolChain.getILOC(ast, logger, annotations);
		logger.info("done generating");

		out = ToolChain.execute(program, logger, in).split("\n");
		vm = ToolChain.machine;

		System.out.println(program.prettyPrint());
		System.out.println();

		for (String line : out)
			System.out.println(">>> " + line);

		System.out.println();

		log.forEach((entry) -> System.out.println(entry.getMessage()));
		Simulator.DEBUG = false;
	}



	private static void printHeader(String header) {
		printHeader(header, '-');
	}

	private static void printHeader(String header, char separator) {
		System.out.println(StringUtils.center(" "+header.toUpperCase()+" ", 150, separator));
	}

	private static void printCode(Reader r) {
		System.out.println("\n\n\n"+StringUtils.repeat("=", 150));

		BufferedReader bufReader = new BufferedReader(r);
		printHeader("boppi code");
		try {
			for (String line = bufReader.readLine(); line != null; line = bufReader.readLine())
				System.out.println("> " + line);
		}
		catch (IOException e) {}
	}

	private static void printLog(List<LogRecord> logs) {
		logs.forEach((entry) -> System.out.println(entry.getMessage()));
	}


}
