/*-
 * APT - Analysis of Petri Nets and labeled Transition systems
 * Copyright (C) 2012-2013  Members of the project group APT
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package uniol.apt;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.collections4.Trie;
import org.apache.commons.collections4.trie.PatriciaTrie;
import org.apache.commons.io.FileUtils;

import uniol.apt.module.AptModuleRegistry;
import uniol.apt.module.Category;
import uniol.apt.module.Module;
import uniol.apt.module.ModuleExitStatusChecker;
import uniol.apt.module.ModuleOutputSpec;
import uniol.apt.module.ModulePreconditionsChecker;
import uniol.apt.module.ModuleRegistry;
import uniol.apt.module.PropertyModuleExitStatusChecker;
import uniol.apt.module.exception.ModuleException;
import uniol.apt.module.impl.ExitStatus;
import uniol.apt.module.impl.ModuleInvoker;
import uniol.apt.module.impl.ModuleUtils;
import uniol.apt.module.impl.Parameter;
import uniol.apt.module.impl.ReturnValue;
import uniol.apt.module.impl.SimpleModulePreconditionsChecker;
import uniol.apt.ui.AptParameterTransformation;
import uniol.apt.ui.ParameterTransformation;
import uniol.apt.ui.ParametersParser;
import uniol.apt.ui.ParametersTransformer;
import uniol.apt.ui.ReturnValueTransformationWithOptions;
import uniol.apt.ui.ReturnValuesTransformer;
import uniol.apt.ui.impl.AptParametersTransformer;
import uniol.apt.ui.impl.AptReturnValuesTransformer;
import uniol.apt.ui.impl.SimpleParametersParser;
import uniol.apt.ui.impl.UIUtils;

/**
 * @author Renke Grunwald
 */
public class APT {
	/**
	 * Symbol that signals that a file should be read from the standard input.
	 */
	public static final String STANDARD_INPUT_SYMBOL = "-";

	/**
	 * The exact APT version that is running as seen by "git describe".
	 */
	public static final String GIT_VERSION;

	/**
	 * A timestamp for when this version of APT was built.
	 */
	public static final String TIMESTAMP;

	/**
	 * A human-readable string generated from {@link GIT_VERSION} and {@link TIMESTAMP}.
	 */
	public static final String VERSION_STRING;

	static {
		Properties props = new Properties();
		try (InputStream in = APT.class.getResourceAsStream("APT.properties")) {
			if (in != null)
				props.load(in);
		} catch (IOException e) {
			// Ignore the exception and just make sure that the version will be UNKNOWN
			props = new Properties();
		}
		GIT_VERSION = props.getProperty("git-version", "UNKNOWN");
		TIMESTAMP = props.getProperty("timestamp", "UNKNOWN");
		VERSION_STRING = "APT version " + GIT_VERSION + " built on " + TIMESTAMP;
	}

	private static final ParametersParser PARAMETERS_PARSER = new SimpleParametersParser();
	private static final ParametersTransformer PARAMETERS_TRANSFORMER = AptParametersTransformer.INSTANCE;
	private static final ReturnValuesTransformer RETURN_VALUES_TRANSFORMER = AptReturnValuesTransformer.INSTANCE;
	private static final ModuleRegistry REGISTRY = AptModuleRegistry.INSTANCE;
	private static final Trie<String, String> REMOVED_MODULES = new PatriciaTrie<>();

	private static final PrintStream OUT_PRINTER = System.out;
	private static final PrintStream ERR_PRINTER = System.err;

	/**
	 * Hidden Constructor.
	 */
	private APT() {
	}

	private static void addRemovedModules() {
		REMOVED_MODULES.put("apt2baggins", "Use pn_convert / lts_convert apt baggins instead.");
		REMOVED_MODULES.put("apt2lola",    "Use pn_convert / lts_convert apt lola instead.");
		REMOVED_MODULES.put("apt2petrify", "Use pn_convert / lts_convert apt petrify instead.");
		REMOVED_MODULES.put("apt2pnml",    "Use pn_convert / lts_convert apt pnml instead.");
		REMOVED_MODULES.put("apt2synet",   "Use pn_convert / lts_convert apt synet instead.");
		REMOVED_MODULES.put("petrify2apt", "Use pn_convert / lts_convert petrify apt instead.");
		REMOVED_MODULES.put("pnml2apt",    "Use pn_convert / lts_convert pnml apt instead.");
		REMOVED_MODULES.put("synet2apt",   "Use pn_convert / lts_convert synet apt instead.");
		REMOVED_MODULES.put("info",        "Use examine_pn instead.");
		REMOVED_MODULES.put("backwards_persistent", "Use backward_persistent instead.");
	}

	/**
	 * Program entry point.
	 *
	 * @param args
	 *                command line arguments
	 */
	public static void main(String[] args) {
		addRemovedModules();

		PARAMETERS_PARSER.parse(args);

		String[] moduleNames = PARAMETERS_PARSER.getModuleNames();

		if (moduleNames.length == 0) {
			printUsageAndExit();
		}

		String moduleName = moduleNames[0]; // Only use a single module for now
		Collection<Module> foundModules = REGISTRY.findModulesByPrefix(moduleName);

		Module module = null;

		if (foundModules.isEmpty()) {
			printNoSuchModuleAndExit(moduleName);
		} else if (foundModules.size() == 1) {
			// These is only one right module and we just found it
			module = foundModules.iterator().next();
		} else {
			// Ambiguous, but maybe the name isn't a partial name after all
			module = REGISTRY.findModule(moduleName);

			if (module == null) {
				printAmbiguousModuleNameAndExit(moduleName, foundModules);
			}
		}

		List<Parameter> parameters = ModuleUtils.getParameters(module);
		List<Parameter> allParameters = ModuleUtils.getAllParameters(module);

		List<ReturnValue> returnValues = ModuleUtils.getReturnValues(module);
		List<ReturnValue> fileReturnValues = ModuleUtils.getFileReturnValues(module);

		String[] moduleArgs = PARAMETERS_PARSER.getModuleArguments(moduleName);

		try {
			if (moduleArgs.length < parameters.size()) {
				printTooFewArgumentsAndExit(module);
			}

			if (moduleArgs.length > allParameters.size() + fileReturnValues.size()) {
				printTooManyArgumentsAndExit(module);
			}

			// Number of parameters for which values are provided
			int numberOfUsedParameters;

			if (moduleArgs.length > allParameters.size()) {
				numberOfUsedParameters = allParameters.size();
			} else {
				numberOfUsedParameters = moduleArgs.length;
			}

			// First check if multiple parameters are signaled to be read from the standard input
			checkNoTwoStdinParameters(moduleArgs, numberOfUsedParameters, allParameters);

			Object[] transformedArgs = new Object[numberOfUsedParameters];
			for (int i = 0; i < numberOfUsedParameters; i++) {
				transformedArgs[i] = PARAMETERS_TRANSFORMER.transform(moduleArgs[i],
						allParameters.get(i).getKlass());
			}

			ModulePreconditionsChecker checker = new SimpleModulePreconditionsChecker();

			List<Parameter> unmetParameters = checker.check(REGISTRY, module, transformedArgs);

			if (!unmetParameters.isEmpty()) {
				printPreconditionsUnmetAndExit(unmetParameters);
			}

			ModuleInvoker invoker = new ModuleInvoker();
			List<Object> values = invoker.invoke(module, transformedArgs);

			String[] fileArgs = Arrays.copyOfRange(moduleArgs, numberOfUsedParameters, moduleArgs.length);
			printModuleOutput(fileArgs, returnValues, values);

			ModuleExitStatusChecker statusChecker = new PropertyModuleExitStatusChecker();
			ExitStatus status = statusChecker.check(module, values);

			OUT_PRINTER.flush();
			System.exit(status.getValue());
		} catch (ModuleException e) {
			ERR_PRINTER.println(String.format("Error while invoking module '%s':%n  %s",
						module.getName(), e.getMessage()));
			ERR_PRINTER.flush();
			System.exit(ExitStatus.ERROR.getValue());
		}
	}

	private static void checkNoTwoStdinParameters(String[] moduleArgs, int numberOfUsedParameters,
			List<Parameter> allParameters) {
		boolean hasStdInParameter = false;

		// First check if multiple parameters are signaled to be read from the standard input
		for (int i = 0; i < numberOfUsedParameters; i++) {
			if (moduleArgs[i].equals(STANDARD_INPUT_SYMBOL)
					&& isFileSourceParameter(allParameters.get(i))) {
				if (hasStdInParameter) {
					printCanOnlyReadOneParameterFromStdInAndExit();
				}
				hasStdInParameter = true;
			}
		}
	}

	private static boolean isFileSourceParameter(Parameter parameter) {
		Class<?> klass = parameter.getKlass();
		ParameterTransformation<?> transformation = PARAMETERS_TRANSFORMER.getTransformation(klass);
		if (transformation == null)
			return false;
		AptParameterTransformation annotation = transformation.getClass()
				.getAnnotation(AptParameterTransformation.class);
		if (annotation == null)
			return false;
		return annotation.fileSource();
	}

	private static void printModuleOutput(String[] fileArgs, List<ReturnValue> returnValues, List<Object> values)
			throws ModuleException {
		boolean hasStandardOutputFileReturnValue = false;

		for (int i = 0; i < fileArgs.length; i++) {
			if (fileArgs[i].equals(STANDARD_INPUT_SYMBOL)) {
				hasStandardOutputFileReturnValue = true;
			}
		}

		// Handle module output
		try (CloseableCollection<PrintStream> outputs = new CloseableCollection<>()) {
			boolean outputWithName[] = new boolean[values.size()];
			String extraOptions[] = new String[values.size()];

			// Figure out where the values which the module produced should be printed to
			int usedFileArgsCount = 0;
			for (int i = 0; i < values.size(); i++) {

				if (values.get(i) == null) {
					outputWithName[i] = false;
					outputs.add(null, false);
					continue;
				}

				String returnValueName = returnValues.get(i).getName();

				boolean isRawReturnValue =
					returnValues.get(i).hasProperty(ModuleOutputSpec.PROPERTY_RAW);

				boolean isFileReturnValue =
					returnValues.get(i).hasProperty(ModuleOutputSpec.PROPERTY_FILE);

				// Print the return value to file without its name
				if (isFileReturnValue) {
					// Check if the user supplied a file name for this return value
					if (fileArgs.length > usedFileArgsCount) {
						String filename = fileArgs[usedFileArgsCount];

						Class<?> klass = returnValues.get(i).getKlass();
						if (RETURN_VALUES_TRANSFORMER.getTransformation(klass)
								instanceof ReturnValueTransformationWithOptions) {
							String[] parts = filename.split(":", 2);
							if (parts.length == 2) {
								extraOptions[i] = parts[0];
								filename = parts[1];
							}
						}

						if (filename.equals(STANDARD_INPUT_SYMBOL)) {
							outputs.add(OUT_PRINTER, false);
						} else {
							outputs.add(openOutput(filename), true);
						}
						outputWithName[i] = false;

						usedFileArgsCount++;
						continue;
					}
				}

				// Only print the file return value when requested; skip the other return values
				if (hasStandardOutputFileReturnValue) {
					outputWithName[i] = false;
					outputs.add(null, false);
					continue;
				}

				// Print this ordinary return value, possibly with its name
				outputs.add(OUT_PRINTER, false);
				outputWithName[i] = !isRawReturnValue;
			}

			// Print all return values for which the module produced values
			for (int i = 0; i < values.size(); i++) {
				PrintStream out = outputs.get(i);
				if (out == null)
					continue;

				if (outputWithName[i])
					out.print(returnValues.get(i).getName() + ": ");

				OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
				if (extraOptions[i] == null) {
					RETURN_VALUES_TRANSFORMER.transform(writer,
							values.get(i), returnValues.get(i).getKlass());
				} else {
					RETURN_VALUES_TRANSFORMER.transform(writer,
							values.get(i), returnValues.get(i).getKlass(), extraOptions[i]);
				}
				writer.flush();
				out.println();
			}
		}
		catch (IOException e) {
			ERR_PRINTER.println("Error writing to file: " + e.getMessage());
			ERR_PRINTER.flush();
			System.exit(ExitStatus.ERROR.getValue());
		}
	}

	private static void printPreconditionsUnmetAndExit(List<Parameter> unmetParameters) {
		ERR_PRINTER.println("Some preconditions are unmet:");

		for (Parameter parameter : unmetParameters) {
			ERR_PRINTER.print("Parameter \"" + parameter.getName() + "\" is not");

			for (String property : parameter.getProperties()) {
				ERR_PRINTER.print(" " + property);
			}

			ERR_PRINTER.println();
		}

		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static PrintStream openOutput(String fileName) throws IOException {
		File file = new File(fileName);

		if (file.exists())
			throw new IOException("File '" + file + "' already exists");
		return new PrintStream(FileUtils.openOutputStream(file), false, "UTF-8");
	}

	private static void printVersion() {
		OUT_PRINTER.println(VERSION_STRING + ".");
		OUT_PRINTER.println();
	}

	private static void printTooManyArgumentsAndExit(Module module) throws ModuleException {
		ERR_PRINTER.println("Too many arguments");
		ERR_PRINTER.println();
		ERR_PRINTER.println(UIUtils.getModuleUsage(module, PARAMETERS_TRANSFORMER));
		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printTooFewArgumentsAndExit(Module module) throws ModuleException {
		ERR_PRINTER.println("Too few arguments");
		ERR_PRINTER.println();
		ERR_PRINTER.println(UIUtils.getModuleUsage(module, PARAMETERS_TRANSFORMER));
		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printAmbiguousModuleNameAndExit(String moduleName,
			Collection<Module> foundModules) {
		ERR_PRINTER.println("Ambiguous module name: " + moduleName);

		ERR_PRINTER.println();

		ERR_PRINTER.println("Available modules (starting with " + moduleName + "):");
		printModuleList(foundModules, ERR_PRINTER);

		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printNoSuchModuleAndExit(String moduleName) {
		ERR_PRINTER.println("No such module: " + moduleName);

		Map<String, String> messages = REMOVED_MODULES.prefixMap(moduleName);
		if (messages.size() == 1) {
			Map.Entry<String, String> entry = messages.entrySet().iterator().next();
			ERR_PRINTER.println(String.format("The module '%s' was removed. %s",
						entry.getKey(), entry.getValue()));
		}

		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printUsageAndExit() {
		printVersion();
		OUT_PRINTER.println("Usage: apt <module> <arguments>");
		OUT_PRINTER.println();

		OUT_PRINTER.println("Available modules:");
		printModuleList(REGISTRY.getModules(), OUT_PRINTER);

		OUT_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printCanOnlyReadOneParameterFromStdInAndExit() {
		ERR_PRINTER.println("Only one parameter can be read from standard input at the same time");
		ERR_PRINTER.flush();
		System.exit(ExitStatus.ERROR.getValue());
	}

	private static void printModuleList(Collection<Module> modules, PrintStream printer) {
		for (Category category : Category.values()) {
			List<Module> modulesByCategory = ModuleUtils.getModulesByCategory(modules, category);

			if (modulesByCategory.isEmpty()) {
				continue;
			}

			printer.println();
			printer.println(category.getName());

			for (int i = 0; i < category.getName().length(); i++) {
				printer.print("=");
			}

			printer.println();

			Set<Module> sortedModules = new TreeSet<>(new Comparator<Module>() {

				@Override
				public int compare(Module o1, Module o2) {
					return o1.getName().compareTo(o2.getName());
				}
			});

			sortedModules.addAll(modulesByCategory);

			// Figure out the length of the longest module name
			int longestModuleName = 0;
			for (Module module : modules) {
				longestModuleName = Math.max(longestModuleName, module.getName().length());
			}

			String format = "  %-" + Integer.toString(longestModuleName) + "s  %s";
			for (Module module : sortedModules) {
				printer.println(String.format(format, module.getName(), module.getShortDescription()));
			}
		}
	}
}

// vim: ft=java:noet:sw=8:sts=8:ts=8:tw=120
