package pp.s1184725.boppi;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.*;

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import org.apache.commons.lang3.tuple.Pair;

import pp.iloc.Simulator;
import pp.iloc.model.Program;
import pp.s1184725.boppi.antlr.BoppiLexer;
import pp.s1184725.boppi.antlr.BoppiParser;

/**
 * This class provides methods for all steps in the Boppi tool chain.
 *
 */
public class ToolChain {

	/**
	 * Opens a file for reading and returns its charstream. Throws an unhandled
	 * exception if the file could not be read.
	 * 
	 * @param file
	 *            the file to read
	 * @return a {@link CharStream} to be used in the lexer phase
	 */
	public static CharStream getCharStream(Path file) {
		try {
			return CharStreams.fromFileName(file.toString());
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Reads a string as a charstream.
	 * 
	 * @param code
	 *            the string to read
	 * @return a {@link CharStream} to be used in the lexer phase
	 */
	public static CharStream getCharStream(String code) {
		return CharStreams.fromString(code);
	}

	/**
	 * Creates and initialises a lexer for a given character {@code stream} with
	 * the given {@code logger} attached to it.
	 * 
	 * @param stream
	 *            the character stream to read from
	 * @param logger
	 *            the logger to attach
	 * @return the initialised lexer
	 */
	public static BoppiLexer getLexer(CharStream stream, Logger logger) {
		BoppiLexer lexer = new BoppiLexer(stream);
		lexer.removeErrorListeners();
		lexer.addErrorListener(new BaseErrorListener() {
			@Override
			public void syntaxError(Recognizer<?, ?> r, Object oSym, int l, int c, String msg, RecognitionException e) {
				logger.severe("" + l + ":" + c + " " + msg);
			}
		});

		return lexer;
	}

	/**
	 * Creates and initialises a parser for a given {@code lexer} with the given
	 * {@code logger} attached to it.
	 * 
	 * @param lexer
	 *            the lexer to read from
	 * @param logger
	 *            the logger to attach
	 * @return the initialised parser
	 */
	public static BoppiParser getParser(BoppiLexer lexer, Logger logger) {
		BoppiParser parser = new BoppiParser(new CommonTokenStream(lexer));
		parser.removeErrorListeners();
		parser.addErrorListener(new BaseErrorListener() {
			@Override
			public void syntaxError(Recognizer<?, ?> r, Object oSym, int l, int c, String msg, RecognitionException e) {
				logger.severe("" + l + ":" + c + " " + msg);
			}
		});

		return parser;
	}

	/**
	 * Checks a (sub)program with the {@link BoppiChecker} and returns the
	 * annotations.
	 * 
	 * @param program
	 *            the parse tree to check
	 * @param logger
	 *            the logger to write checker messages to
	 * @return the annotations made by the checker
	 */
	public static Annotations getAnnotations(ParseTree program, Logger logger) {
		return BoppiChecker.checkProgram(program, logger);
	}

	/**
	 * Generates ILOC code for an annotated program.
	 * 
	 * @param program
	 *            the parse tree to convert to ILOC
	 * @param annotations
	 *            the annotations object provided by the checking phase
	 * @param logger
	 *            the logger to write checker messages to
	 * @return an ILOC program
	 */
	public static Program getILOC(ParseTree program, Logger logger, Annotations annotations) {
		return BoppiGenerator.generateProgram(program, annotations, logger);
	}

	/**
	 * Replaces all handlers for the given {@code logger} with a handler that
	 * appends to the list that is returned.
	 * 
	 * @param logger
	 *            the logger to change
	 * @return the list to which messages are logged
	 */
	public static List<LogRecord> makeListLog(Logger logger) {
		List<LogRecord> log = new ArrayList<LogRecord>();
		Handler listLogger = new Handler() {
			@Override
			public void close() throws SecurityException {
			}

			@Override
			public void flush() {
			}

			@Override
			public void publish(LogRecord record) {
				log.add(record);
			}
		};

		for (Handler handler : logger.getHandlers())
			logger.removeHandler(handler);

		logger.setUseParentHandlers(false);
		logger.addHandler(listLogger);

		return log;
	}

	/**
	 * Shorthand for the compiler chain from {@link CharStream} to
	 * {@link Program}.
	 * 
	 * @param stream
	 *            the charstream to compile
	 * @return a pair of the ILOC program and the logger
	 */
	public static Pair<Program, List<LogRecord>> compile(CharStream stream) {
		Logger logger = Logger.getAnonymousLogger();
		List<LogRecord> logs = makeListLog(logger);

		ParseTree ast = getParser(getLexer(stream, logger), logger).program();
		return Pair.of(getILOC(ast, logger, getAnnotations(ast, logger)), logs);
	}

	/**
	 * Shorthand for the compiler chain from {@link CharStream} to
	 * {@link Program}.
	 * 
	 * @param stream
	 *            the charstream to compile
	 * @param logger
	 *            the logger to report compiler messages to
	 * @return a pair of the ILOC program and the logger
	 */
	public static Program compile(CharStream stream, Logger logger) {
		ParseTree ast = getParser(getLexer(stream, logger), logger).program();
		return getILOC(ast, logger, getAnnotations(ast, logger));
	}

	/**
	 * Runs a program using the given input and output streams. Run errors will
	 * be logged as {@link Level#SEVERE}.
	 * 
	 * @param program
	 *            the program to run
	 * @param logger
	 *            the logger to write runtime errors to
	 * @param in
	 *            the input stream the program can read from
	 * @param out
	 *            the output stream the program can write to
	 */
	public static void execute(Program program, Logger logger, InputStream in, OutputStream out) {
		Simulator s = new Simulator(program);
		s.setIn(in);
		s.setOut(out);
		try {
			s.run();
		} catch (Exception e) {
			logger.severe(e.toString());
		}
	}

	/**
	 * Runs a program with a given input string and returns all output as a
	 * string. Run errors will be logged as {@link Level#SEVERE} in the logger.
	 * 
	 * @param program
	 *            the program to run
	 * @param logger
	 *            the logger to write runtime messages to
	 * @param input
	 *            the string to use as input for the program
	 * @return the output of the program
	 */
	public static String execute(Program program, Logger logger, String input) {
		InputStream in = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		execute(program, logger, in, out);
		return out.toString().replaceAll("\r\n|\r", "\n");
	}

	/**
	 * Converts an annotated {@link ParseTree} to a colourful DOT graph.
	 * 
	 * @param tree
	 *            the tree to build
	 * @param annotater
	 *            the objct containing annotations, if any
	 * @return a string containing the DOT graph
	 */
	public static String getAnnotatedDOT(ParseTree tree, Annotations annotater) {
		StringBuilder sb = new StringBuilder();
		sb.append("digraph {\n");

		new ParseTreeWalker().walk(new ParseTreeListener() {
			private String escape(String str) {
				return str.replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
			}

			@Override
			public void visitTerminal(TerminalNode node) {
				sb.append("\tn" + node.hashCode() + "[label=<" + escape(node.getText()) + ">;shape=rect]\n");
			}

			@Override
			public void visitErrorNode(ErrorNode node) {
				sb.append("\tn" + node.hashCode() + "[label=<" + escape(node.getText())
						+ ">;style=filled;fillcolor=red]\n");
			}

			@Override
			public void exitEveryRule(ParserRuleContext ctx) {
				float hue = (ctx.getClass().hashCode() % 65521) / 65521.0f;

				sb.append("\tn" + ctx.hashCode() + "[label=<");
				sb.append("rule: " + escape(ctx.getClass().getSimpleName()) + "<br/>");
				if (annotater.registers.get(ctx) != null)
					sb.append("reg: <b>" + escape(annotater.registers.get(ctx).getName()) + "</b><br/>");
				if (annotater.variables.get(ctx) != null)
					sb.append("var: " + escape(annotater.variables.get(ctx).toString()) + "<br/>");
				if (annotater.types.get(ctx) != null)
					sb.append("type: " + escape(annotater.types.get(ctx).toString()) + "<br/>");
				sb.append(">;style=filled;fillcolor=\"" + hue + "+0.1+1\"]\n");

				if (ctx.children != null)
					for (ParseTree child : ctx.children)
						sb.append("\tn" + ctx.hashCode() + " -> n" + child.hashCode() + "\n");
			}

			@Override
			public void enterEveryRule(ParserRuleContext ctx) {
			}
		}, tree);
		sb.append("}\n");

		return sb.toString();
	}
}
