package pp.s1184725.boppi.util;

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.logging.*;

import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.cli.*;

import pp.iloc.Assembler;
import pp.iloc.model.Program;
import pp.iloc.parse.FormatException;
import pp.s1184725.boppi.*;

/**
 * Command line interface for compiling and running Boppi programs.
 * 
 * @author Frank Wibbelink
 */
public class CommandLineInterface {

	private static CommandLine cmd;
	private static Level desiredLogLevel;

	/**
	 * Runs the command line.
	 * 
	 * @param args
	 *            CLI arguments
	 */
	public static void main(String[] args) {
		Options options = new Options();
		options.addOption("o", "output", true, "output file for ILOC code");
		options.addOption("d", "directory", true, "output directory for ILOC code");
		options.addOption("w", "warnings", true, "set the warning level ('all', 'error' or 'none')");
		options.addOption("r", "run", false, "run an ILOC program");
		options.addOption("h", "help", false, "display this message");

		if (args.length >= 1 && args[0].equalsIgnoreCase("interactive")) {
			InteractivePrompt.main(Arrays.copyOfRange(args, 1, args.length));
			return;
		}

		try {
			cmd = new DefaultParser().parse(options, args);
		} catch (ParseException e) {
			System.err.println(e.getLocalizedMessage());
			System.err.println("Use 'boppi -h' for help.");
			return;
		}

		if (cmd.hasOption("h") || args.length == 0) {
			HelpFormatter formatter = new HelpFormatter();
			formatter.printHelp("boppi [interactive] [OPTION]... FILE...",
					"To start an interactive prompt, have 'interactive' as the first parameter. "
							+ "This mode has its own help message.",
					options, "By default output files will be written to the same path as the "
							+ "input files but with the .iloc extension.");
			return;
		}

		if (cmd.hasOption("o") && cmd.getArgList().size() > 1) {
			System.err.println("Cannot compile multiple files to one file.");
			return;
		}

		switch (cmd.getOptionValue("w", "error")) {
		case "all":
			desiredLogLevel = Level.ALL;
			break;
		case "none":
			desiredLogLevel = Level.OFF;
			break;
		default:
			desiredLogLevel = Level.SEVERE;
		}

		if (cmd.hasOption("r")) {
			for (String fileName : cmd.getArgList())
				runFile(Paths.get(fileName));
		} else {
			for (String fileName : cmd.getArgList()) {
				Path infile = Paths.get(fileName);
				Path outfile;

				if (cmd.hasOption("o"))
					outfile = Paths.get(cmd.getOptionValue("o"));
				else if (cmd.hasOption("d"))
					outfile = replaceExtension(Paths.get(cmd.getOptionValue("d")).resolve(infile.getFileName()),
							"iloc");
				else
					outfile = replaceExtension(infile, "iloc");

				compileFile(infile, outfile);
			}
		}

	}

	/**
	 * Loads the given file as ILOC assembly and runs it with the input tied to
	 * {@link System#in} and the output tied to {@link System#out}. If the VM
	 * halts with an interrupt code, the whole program is terminated and will
	 * return it as the exit code.
	 * 
	 * @param path
	 *            the ILOC file to load
	 */
	private static void runFile(Path path) {
		try {
			Logger logger = Logger.getAnonymousLogger();
			logger.setLevel(desiredLogLevel);

			Program prog = Assembler.instance().assemble(path.toFile());

			ToolChain.execute(prog, logger, System.in, System.out);

			if (ToolChain.machine.getInterrupt() != 0)
				System.exit(ToolChain.machine.getInterrupt());
			// System.err.println(String.format("Program exited with status:
			// %8x", ToolChain.machine.getInterrupt()));
		} catch (FormatException | IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Tries to compile a Boppi file by parsing it, typechecking it and
	 * generating code. The process is broken off if at any stage a
	 * {@link Level#SEVERE} error is encountered. If the code successfully
	 * compiles, it is written to the given output location.
	 * 
	 * @param infile
	 *            the Boppi file to read from
	 * @param outfile
	 *            the ILOC file to write to
	 */
	private static void compileFile(Path infile, Path outfile) {
		Logger logger = Logger.getAnonymousLogger();
		List<LogRecord> logList = ToolChain.makeListLog(logger);
		logger.setLevel(Level.ALL);

		ParseTree ast = ToolChain.getParser(ToolChain.getLexer(ToolChain.getCharStream(infile), logger), logger)
				.program();

		if (logList.stream().noneMatch(severityAtLeast(Level.SEVERE))) {
			Annotations an = ToolChain.getAnnotations(ast, logger);

			if (logList.stream().noneMatch(severityAtLeast(Level.SEVERE))) {
				Program iloc = ToolChain.getILOC(ast, logger, an);

				if (logList.stream().noneMatch(severityAtLeast(Level.SEVERE))) {
					try {
						Files.write(outfile, iloc.prettyPrint().getBytes("utf-8"), StandardOpenOption.CREATE,
								StandardOpenOption.TRUNCATE_EXISTING);
					} catch (UnsupportedEncodingException e) {
						e.printStackTrace();
					} catch (IOException e) {
						System.err.println("Cannot write to " + outfile);
					}
				}
			}
		}

		logList.stream().filter(severityAtLeast(desiredLogLevel)).map(LogRecord::getMessage)
				.forEach(System.err::println);
	}

	private static Predicate<LogRecord> severityAtLeast(Level level) {
		return (logRecord) -> logRecord.getLevel().intValue() >= level.intValue();
	}

	private static Path replaceExtension(Path path, String newExt) {
		Path folder = path.getParent();
		String filename = path.getFileName().toString();

		return folder == null ? Paths.get(replaceExtension(filename, newExt))
				: folder.resolve(replaceExtension(filename, newExt));
	}

	private static String replaceExtension(String filename, String newExt) {
		int index = filename.lastIndexOf('.');
		if (index >= 0)
			filename = filename.substring(0, index);

		return filename + "." + newExt;
	}

}
