package org.etsi.t3q;

import java.io.File;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.ParseException;
import org.etsi.t3q.config.QualityCheckProfile;
import org.etsi.t3q.config.T3QConfig;
import org.etsi.t3q.config.T3QOptionsHandler;
import org.etsi.t3q.exceptions.TTCN3BehaviorException;
import org.etsi.t3q.exceptions.TTCN3ParserException;
import org.etsi.t3q.visitor.T3QVisitor;
import org.etsi.common.InputInterface;
import org.etsi.common.MiscTools;
import org.etsi.common.configuration.ConfigTools;
import org.etsi.common.exceptions.TerminationException;
import org.etsi.common.logging.LoggingInterface.LogLevel;
import org.etsi.common.logging.LoggingInterface.MessageClass;

import antlr.MismatchedTokenException;
import antlr.RecognitionException;
import antlr.TokenStreamException;
import de.ugoe.cs.swe.trex.core.analyzer.rfparser.LocationAST;
import de.ugoe.cs.swe.trex.core.analyzer.rfparser.TTCN3Analyzer;
import de.ugoe.cs.swe.trex.core.analyzer.rfparser.TTCN3AnalyzerFlyweightFactory;
import de.ugoe.cs.swe.trex.core.analyzer.rfparser.TTCN3Parser;
import de.ugoe.cs.swe.trex.core.formatter.TTCN3Formatter;

public class T3Q {

	private static String versionNumber = "v1.0.2";
	//set during automated server builds
	private static String buildStamp = "---BUILD_STAMP---";
	public static QualityCheckProfile activeProfile = null;
	private String configurationClassName = T3QConfig.class.getName();
	private String configurationProfileClassName = QualityCheckProfile.class.getName();
	private String configurationFilename;
	private String selectedProfileName = null;
	private static LogLevel logLevel = LogLevel.INFORMATION;
	
	private HashMap<String, String> argsMap = new HashMap<String, String>();
	private ArrayList<String> inputPaths = new ArrayList<String>();
	private String destinationPath = null;
	
	private HashMap<String, Integer> linesOfCodeMap = new HashMap<String, Integer>();
	private int totalLoc = 0;

	private boolean generateNewConfiguration = false;
	
	private boolean formattingEnabled = false;
	

	//TODO: statistics shall be ideally based on XSLT, meaning the output shall be transformed into XML 
	//TODO: also add exception checking for the profiles 
	public T3Q() {
	}

	// --------------------------------------------------------------------------

	public void showHelp() {
		System.out.println("\nHelp:");
		// TODO: update help
		// TODO: transfer to t3d as well
		// System.out.println("  t3q[.bat] [options] (path | filename)+");
		// System.out.println("");
		// System.out.println("  Options (specify in any order): ");
		// System.out.println("    --help - prints this screen");
		// System.out
		// .println("    --profile [profilename] - allows manual profile selection, overriding the selected default profile");
		// // System.out.println("    --output [path] ");
		// System.out.println("");

		HelpFormatter formatter = new HelpFormatter();
		formatter.setOptionComparator(new Comparator<Option>() {
			
			@Override
			public int compare(Option o1, Option o2) {
				if (o1.getId() > o2.getId()) {
					return 1;
				} else {
					return -1;
				}
			}
		});
//		formatter.setWidth(120);
		formatter.setSyntaxPrefix("  Usage: ");
		formatter.printHelp("t3q [options] (filename | path)+", "  Options: (in any order, config is required)",
				new T3QOptionsHandler().getOptions(), "");
		System.out.println("");
	}

	// --------------------------------------------------------------------------
	private void showDebugInfo(String[] args) {
		if (getLogLevel().equals(LogLevel.DEBUG)) {
			System.out.println("==================ARGS:===================");
			for (int a = 0; a < args.length; a++) {
				System.out.println("   " + args[a]);
			}
			System.out.println("==================PROPERTIES:=============");
			for(Object key : System.getProperties().keySet()) {
				System.out.println("   "+key.toString() + " : "+System.getProperty(key.toString()));
			}
			
			System.out.println("==========================================");
		}
	}

	// --------------------------------------------------------------------------
//TODO: Revise and reorganize for reusability
	public void run(String[] args) {
		System.out.println("T3Q " + getVersionNumber());
		System.out.println("Build " + getBuildStamp());
		System.out.println("  TTCN-3 version supported: "
				+ TTCN3Parser.getSupportedVersion());
		System.out.println("==========================================");
		try {
			if (handleCommandLineArguments(args) == false) {
				return;
			}
			showDebugInfo(args);
			if (selectedProfileName != null) {
				handleConfig(selectedProfileName);
			} else {
				handleConfig(null);
			}
		} catch (TerminationException e) {
			// TODO: handle exception
			System.out.println("ERRORING OUT!");
		}
		
		TTCN3Parser.disableStatementBlockCompatibilityMode();
		TTCN3Parser.enableStatementBlockCompatibilityMode();

		List<String> ttcn3Resources = new InputInterface(T3Q.activeProfile.getResourceExtensionsRegExp(), T3Q.activeProfile.getProjectExtension(),
				T3Q.activeProfile.isSettingRecursiveProcessing()).getInputFromParameterList(inputPaths);
		ttcn3Resources = InputInterface.filterAbsoluteDuplicates(ttcn3Resources); 
		
		
		if (ttcn3Resources.isEmpty()) {
			// Terminate
			System.out.println("No ttcn3 files found!");
			showHelp();
			return;
		}

		long startTime = System.currentTimeMillis();
		TTCN3Analyzer analyzer = null;

		try {
			System.out.println("Parsing files...");
			for (int i = 0; i < ttcn3Resources.size(); i++) {
				String resourcePath = ttcn3Resources.get(i);
				analyzeFile(resourcePath);
				TTCN3AnalyzerFlyweightFactory analyzerFactory = TTCN3AnalyzerFlyweightFactory
						.getInstance();
				analyzer = analyzerFactory.getTTCN3Analyzer(ttcn3Resources
						.get(i));
				if (analyzer.getExceptions().size() > 0) {
					String exceptionMessage = "Error while parsing file "
						+ analyzer.getFilename(); 
					//TODO: adapt to T3D as well
					String tree = "";
					tree = LocationAST.dumpTree((LocationAST)analyzer.getParser().getAST(),1,tree);
					if (getLogLevel().equals(LogLevel.DEBUG)) {
						exceptionMessage+="\n" +
								"Parse-tree trace:\n" +
								tree;
					}
					if (T3Q.activeProfile.isSettingAbortOnError()) {
						throw new TTCN3ParserException(exceptionMessage);
					} else {
						try {
							throw new TTCN3ParserException(exceptionMessage);
						} catch (TTCN3ParserException e) {
							System.err.println(e.getLocalizedMessage());
							for (int i1 = 0; i1 < analyzer.getExceptions()
									.size(); i1++) {
								System.err.println("Line "
										+ analyzer.getExceptions().get(i1)
												.getLine()
										+ ": "
										+ analyzer.getExceptions().get(i1)
												.getMessage());
							}
							// e.printStackTrace();
						}
					}
				}
			}
			long endTime = System.currentTimeMillis();
			long elapsed = endTime - startTime;
			double elapsedMinutes = ((double) elapsed) / 1000.0 / 60.0;
			System.out.println("Done parsing in " + elapsed + "ms ("
					+ MiscTools.doubleToString(elapsedMinutes) + " minutes).");

			if (T3Q.activeProfile.isStatShowLOC()) {
				System.out.println("Total lines of code parsed: " + totalLoc);
			}

			TTCN3AnalyzerFlyweightFactory analyzerFactory = TTCN3AnalyzerFlyweightFactory
					.getInstance();

			System.out.println("Postprocessing...");
			startTime = System.currentTimeMillis();
			analyzerFactory.postProcess();
			endTime = System.currentTimeMillis();
			elapsed = endTime - startTime;
			elapsedMinutes = ((double) elapsed) / 1000.0 / 60.0;
			System.out.println("Done processing in " + elapsed + "ms ("
					+ MiscTools.doubleToString(elapsedMinutes) + " minutes).");
			startTime = System.currentTimeMillis();

			System.out.println("==========================================");
			//TODO: up to here mostly identical, make reusable
			
			//TODO: core functionality, encapsulate and extract
			for (int i = 0; i < ttcn3Resources.size(); i++) {
				analyzer = analyzerFactory.getTTCN3Analyzer(ttcn3Resources
						.get(i));
				System.out.println("Analyzing: " + analyzer.getFilename());
				T3QVisitor visitor = new T3QVisitor();
				visitor.setFilename(analyzer.getFilename());
				visitor.acceptDFS((LocationAST) analyzer.getParser().getAST());
			}
			endTime = System.currentTimeMillis();
			elapsed = endTime - startTime;
			elapsedMinutes = ((double) elapsed) / 1000.0 / 60.0;
			System.out.println("Quality checks finished in " + elapsed + "ms ("
					+ MiscTools.doubleToString(elapsedMinutes) + " minutes).");

			//TODO: custom functionality
			analyzer = handleFormatter(ttcn3Resources, analyzer,
					analyzerFactory);
			if (T3Q.activeProfile.isStatShowLOC()) {
				System.out
						.println("Total lines of code processed: " + totalLoc);
			}
			
			if (T3Q.activeProfile.isStatShowSummary()) {
				System.out.println("Brief statistics summary of occurences in message classes:");
				for (MessageClass m : MessageClass.values()) {
					System.out.println("\t" + m.getDescription() + " : "
							+ m.getOccurenceCount());
				}
			}
			
		} catch (TTCN3BehaviorException e) {
			System.err.println(e.getLocalizedMessage());
		} catch (TTCN3ParserException e) {
			// Default setting where processing is terminated in the event of a
			// parsing error
			System.err.println(e.getLocalizedMessage());
			for (int i = 0; i < analyzer.getExceptions().size(); i++) {
				System.err.println("Line "
						+ analyzer.getExceptions().get(i).getLine() + ": "
						+ analyzer.getExceptions().get(i).getMessage());
			}
			// TODO: Isolate different steps and implement a recovery mechanism:

		}

	}

	
	//TODO: copy back output handling tricks from T3D
	private TTCN3Analyzer handleFormatter(List<String> ttcn3Resources,
			TTCN3Analyzer analyzer,
			TTCN3AnalyzerFlyweightFactory analyzerFactory) {
		long startTime;
		long endTime;
		long elapsed;
		double elapsedMinutes;
		if (this.isFormattingEnabled()) {
			// TODO: consider ways to avoid reparsing (performing the comment
			// collection during the first parse)
			System.out.println("==========================================");
			System.out.println("Formatting Code...");
			startTime = System.currentTimeMillis();

			TTCN3Formatter formatter = new TTCN3Formatter();

			for (int i = 0; i < ttcn3Resources.size(); i++) {
				analyzer = analyzerFactory.getTTCN3Analyzer(ttcn3Resources
						.get(i));
				System.out.println("  Formatting file: "
						+ analyzer.getFilename());
				try {

					String resourcePath = ttcn3Resources.get(i);

					String source = MiscTools.readFile(resourcePath);

					String formatted = formatter.formatTTCN3Source(source,
							T3Q.activeProfile.getFormattingParameters());

					// calculate the target path for the current resource

					// TODO: look into it and fix it -> is it not fixed now?
					// reason is: absolute paths are difficult to handle
					// String subPath = resourcePath;
//					String subPath = "/" + new File(resourcePath).getName(); 
					
					String canonicalResourcePath = new File(resourcePath).getCanonicalPath();
					String canonicalWorkingPath = new File(".").getCanonicalPath();
										
					String destinationSubPath = canonicalResourcePath;
					
					//TODO: make this subtraction optional and document the feature
					//CF: Manuel's remark and my answer - it introduces a dependency on the working path which may not be easily understood and has at least be documented properly
					if (canonicalResourcePath.startsWith(canonicalWorkingPath)){
						destinationSubPath = canonicalResourcePath.substring(canonicalWorkingPath.length());
					}

					if (destinationSubPath.substring(0,3).endsWith(":\\")){
						destinationSubPath=destinationSubPath.substring(2);
					}
					
					String outputPathArg = new File(getDestinationPath()).getPath(); // also
																		// strips
																		// the
																		// slash
																		// if
																		// present
					String outputPath = outputPathArg + destinationSubPath;

					
					
//					System.out.println("****************************");
//					System.out.println("ABSOLUTE: \t\t"+canonicalResourcePath);
//					System.out.println("WORKING: \t\t"+canonicalWorkingPath);
//					System.out.println("ResourcePath: \t\t"+resourcePath);
//					System.out.println("SubPath: \t\t"+destinationSubPath);
//					System.out.println("OutputPath: \t\t"+outputPath);
//					System.out.println("****************************");
//					System.out.println(formatted);
//					System.out.println(outputPath);
					MiscTools.writeFile(outputPath, formatted);
					System.out.println("File \""+outputPath+"\" written successfully!");

				} catch (RecognitionException e1) {
					System.err.println("Recognition exception:");
					System.err.println(e1.getLocalizedMessage());
				} catch (TokenStreamException e) {
					System.err.println("Token stream exception:");
					e.printStackTrace();
				} catch (Exception e) {
					System.err.println("Exception:");
					e.printStackTrace();
				}

			}
			endTime = System.currentTimeMillis();
			elapsed = endTime - startTime;
			elapsedMinutes = ((double) elapsed) / 1000.0 / 60.0;
			System.out.println("Code formatting finished in " + elapsed
					+ "ms (" + MiscTools.doubleToString(elapsedMinutes) + " minutes).");

		}
		return analyzer;
	}
	//TODO: DUPLICATED IN T3D
	private boolean handleCommandLineArguments(String[] args) throws TerminationException {
		//parseCommandLineArguments(args);
		CommandLine commandLine = parseCommandLineArguments(args);
		if (commandLine == null) {
			return false;
		}
		String arguments[] = evaluateCommandLineOptions(commandLine);

		if (commandLine.hasOption("help")) {
			showHelp();
			return false;
		}

		if (commandLine.hasOption("config") && arguments.length < 1) {
			System.out.println("ERROR: Missing input location(s)");
			showHelp();
			return false;
		}

		for (String arg : arguments) {
			//TODO: add validity checks
			inputPaths.add(arg);
		}
		
		return true;
	}

	// --------------------------------------------------------------------------
	//TODO: DUPLICATED IN T3D
	private CommandLine parseCommandLineArguments(String[] args) {
		CommandLineParser parser = new GnuParser();
		T3QOptionsHandler optionsHandler = new T3QOptionsHandler();
		CommandLine commandLine = null;
		try {
			commandLine = parser.parse(optionsHandler.getOptions(), args);
		} catch (ParseException e) {
			System.out.println("ERROR: " + e.getMessage() );
			showHelp();
		}
		return commandLine;
	}
	
	// --------------------------------------------------------------------------
	//TODO: DUPLICATED MOSTLY IN T3D
	private String[] evaluateCommandLineOptions(CommandLine commandLine) throws TerminationException {
		if (commandLine.hasOption("generate-config")) {
			this.setConfigurationFilename(commandLine.getOptionValue("generate-config"));
			this.setGenerateNewConfiguration(true);
		} else if (commandLine.hasOption("config")) {
			this.setConfigurationFilename(commandLine.getOptionValue("config"));
		} else {
			System.out.println("ERROR: No configuration file selected!");
			showHelp();
			throw new TerminationException("");
		}
		
		if (commandLine.hasOption("profile")) {
			this.setSelectedProfileName(commandLine.getOptionValue("profile"));
		}
		if (commandLine.hasOption("format")){
			this.setFormattingEnabled(true);
		}
		if (commandLine.hasOption("verbosity")){
			this.selectLogLevel(commandLine.getOptionValue("verbosity"));
		}
		if (commandLine.hasOption("output-path")){
			this.setDestinationPath(commandLine.getOptionValue("output-path"));
		}
		return commandLine.getArgs();
	}
	
	// --------------------------------------------------------------------------

	//TODO: THIS SHALL BE DEPRECATED NOW 
//	private void parseCommandLineArguments(String[] args) {
//		String key = "";
//		String value = "";
//		//targetPath = "";
//
//		boolean lastKey = false;
//		for (int i = 0; i < args.length; i++) {
//			if (args[i].startsWith("--")) {
//				key = args[i].replaceAll("--", "").toLowerCase();
//
//				if (lastKey) {
//					argsMap.put(key, "true");
//					key = null;
//					value = null;
//					lastKey = false;
//				}
//
//				lastKey = true;
//			} else {
//				value = args[i];
//				if ((key != null) && (argsMap.get(key) == null)
//						&& (key.length() > 0)) {
//					argsMap.put(key, value);
//					key = null;
//					value = null;
//				} else {
//					inputPaths.add(value);
//				}
//				lastKey = false;
//			}
//		}
//
//		if (key != null) {
//			if ((argsMap.get(key) == null) && (key.length() > 0)) {
//				argsMap.put(key, "true");
//			}
//		}
//	}
 
	// --------------------------------------------------------------------------
	//TODO: DUPLICATED MOSTLY IN T3D
	private void handleConfig(String specifiedProfile) throws TerminationException {
		ConfigTools configTools = new ConfigTools(configurationClassName, configurationProfileClassName);
		configTools.setToolVersion(getVersionNumber());
		
		try {
			if (isGenerateNewConfiguration()) {
				configTools.initializeNewDefaultConfig(getConfigurationFilename());
				System.exit(0);
			} else {
				configTools.loadConfig(getConfigurationFilename());
				activeProfile = (QualityCheckProfile) configTools.selectProfile(specifiedProfile);
				if (this.getDestinationPath()==null) {
					setDestinationPath(T3Q.activeProfile.getPathFormattedOutputPath());
				}
			}
		} catch (InstantiationException e) {
			throw new TerminationException("ERROR: Instantiation problems encountered while loading configuration profile. "+e.getMessage());
		} catch (IllegalAccessException e) {
			throw new TerminationException("ERROR: Instantiation problems encountered while loading configuration profile. "+e.getMessage());
		} catch (ClassNotFoundException e) {
			throw new TerminationException("ERROR: Instantiation problems encountered while loading configuration profile. "+e.getMessage());
		}
		
		if (!isProfileVersionValid()) {
			System.out.println("\nERROR: Selected profile \"" + activeProfile.getProfileName()
					+ "\" has a mismatching or no version (required: \""+getVersionNumber()+"\").\n" +
							"  Consider upgrading the profile by transfering the relevant parts to an auto-generated profile or selecting a different profile.\n");
			throw new TerminationException("");
		}
	}
	//TODO: DUPLICATED IN T3D
	private boolean isProfileVersionValid() {
		if (activeProfile.getProfileVersion() != null && activeProfile.getProfileVersion().equals(getVersionNumber())){
			return true;
		} else {
			return false;
		}
	}
	//TODO: DUPLICATED IN T3D
	private void selectLogLevel(String logLevel) throws TerminationException{
		boolean selected = false;
		String possibleValues ="";
		for (LogLevel l : LogLevel.values()){
			if (l.toString().equals(logLevel)){
				setLogLevel(l);
				selected = true;
				System.out.println("Selected log level \""+logLevel+"\"");
			}
			if (!l.toString().equals("FIXME") && !l.toString().equals("DEBUG"))
			possibleValues+=l.toString()+", ";
		}
		if (!selected){
			System.out.println("\nERROR: No valid log level provided! Possible values are (in ascending inclusive order): "+ possibleValues.substring(0,possibleValues.length()-2)+".");
			throw new TerminationException("");
		}
		
	}
	

//	// --------------------------------------------------------------------------
//	//TODO: should be obsolete now?
//	List<String> findTTCN3Resources(String directory) {
//		List<String> files = new LinkedList<String>();
//
//		File f = new File(directory);
//
//		File[] fileNames = f.listFiles(new FileFilter() {
//			public boolean accept(File pathname) {
//				if (pathname.getPath().endsWith(".ttcn3")
//						|| pathname.getPath().endsWith(".ttcn")
//						|| pathname.getPath().endsWith(".3mp"))
//					return true;
//				return false;
//			}
//		});
//
//		for (int i = 0; i < fileNames.length; i++) {
//			files.add(fileNames[i].getPath());
//		}
//
//		File[] directories = f.listFiles(new FileFilter() {
//			public boolean accept(File pathname) {
//				if (pathname.isDirectory())
//					return true;
//				return false;
//			}
//		});
//
//		if (T3Q.activeProfile.isSettingRecursiveProcessing()) {
//			for (int i = 0; i < directories.length; i++) {
//				files.addAll(findTTCN3Resources(directories[i].getPath()));
//			}
//		}
//
//		return files;
//	}

	// --------------------------------------------------------------------------
	//TODO: DUPLICATE IN T3D
	private TTCN3Analyzer analyzeFile(String filename) {
		TTCN3AnalyzerFlyweightFactory analyzerFactory = TTCN3AnalyzerFlyweightFactory
				.getInstance();
		analyzerFactory.setStandaloneUsage(true);
		String code = MiscTools.readFile(filename);
		
		int loc = MiscTools.getLOC(filename);
		linesOfCodeMap.put(filename, loc);
		totalLoc += loc;

		System.out.println("  Parsing file: " + filename + " (LOC: "
				+ linesOfCodeMap.get(filename) + ") ...");
		long startTime = System.currentTimeMillis();

		TTCN3Analyzer analyzer = analyzerFactory.getTTCN3Analyzer(filename,
				code);
		try {
			analyzer.analyze();
		} catch (MismatchedTokenException e) {
			e.printStackTrace();
		} catch (RecognitionException e) {
			e.printStackTrace();
		} catch (TokenStreamException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		long endTime = System.currentTimeMillis();
		long elapsed = endTime - startTime;
		double elapsedMinutes = ((double) elapsed) / 1000.0 / 60.0;

		System.out.println("    ...done in " + elapsed + "ms ("
				+ MiscTools.doubleToString(elapsedMinutes) + " minutes).");

		return analyzer;
	}

	// --------------------------------------------------------------------------
	//TODO: DUPLICATED IN T3D
	public static void setVersionNumber(String versionNumber) {
		T3Q.versionNumber = versionNumber;
	}
	//TODO: DUPLICATED IN T3D
	public static String getVersionNumber() {
		return versionNumber;
	}

	public static void main(String[] args) {
		T3Q tool = new T3Q();
		tool.run(args);
	}
	//TODO: DUPLICATED IN T3D
	public void setConfigurationFilename(String configurationFilename) {
		this.configurationFilename = configurationFilename;
	}
	//TODO: DUPLICATED IN T3D
	public String getConfigurationFilename() {
		return configurationFilename;
	}
	//TODO: DUPLICATED IN T3D
	public static void setLogLevel(LogLevel logLevel) {
		T3Q.logLevel = logLevel;
	}
	//TODO: DUPLICATED IN T3D
	public static LogLevel getLogLevel() {
		return logLevel;
	}
	//TODO: DUPLICATED IN T3D
	public void setSelectedProfileName(String selectedProfileName) {
		this.selectedProfileName = selectedProfileName;
	}
	//TODO: DUPLICATED IN T3D
	public String getSelectedProfileName() {
		return selectedProfileName;
	}

	public void setFormattingEnabled(boolean formattingEnabled) {
		this.formattingEnabled = formattingEnabled;
	}
	
	public boolean isFormattingEnabled() {
		return formattingEnabled;
	}

	public void setGenerateNewConfiguration(boolean generateNewConfiguration) {
		this.generateNewConfiguration = generateNewConfiguration;
	}

	public boolean isGenerateNewConfiguration() {
		return generateNewConfiguration;
	}

	public void setDestinationPath(String destinationPath) {
		this.destinationPath = destinationPath;
	}

	public String getDestinationPath() {
		return destinationPath;
	}

	public static void setBuildStamp(String buildStamp) {
		T3Q.buildStamp = buildStamp;
	}

	public static String getBuildStamp() {
		return buildStamp;
	}

}
