/************************************************************************
 *
 * Copyright 2018 Jochen Staerk
 *
 * Use is subject to license terms.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *********************************************************************** */
package org.mustangproject.commandline;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;

import javax.xml.transform.TransformerException;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.io.FilenameUtils;
import org.mustangproject.EStandard;
import org.mustangproject.FileAttachment;
import org.mustangproject.CII.CIIToUBL;
import org.mustangproject.ZUGFeRD.DXExporterFromA1;
import org.mustangproject.ZUGFeRD.IZUGFeRDExporter;
import org.mustangproject.ZUGFeRD.OXExporterFromA1;
import org.mustangproject.ZUGFeRD.Profile;
import org.mustangproject.ZUGFeRD.Profiles;
import org.mustangproject.ZUGFeRD.ValidationLogVisualizer;
import org.mustangproject.ZUGFeRD.XMLUpgrader;
import org.mustangproject.ZUGFeRD.ZUGFeRDExporterFromA1;
import org.mustangproject.ZUGFeRD.ZUGFeRDExporterFromPDFA;
import org.mustangproject.ZUGFeRD.ZUGFeRDImporter;
import org.mustangproject.ZUGFeRD.ZUGFeRDVisualizer;
import org.mustangproject.validator.ZUGFeRDValidator;
import org.slf4j.LoggerFactory;

/***
 * the Main class of the command line version
 */
public class Main {
	private static org.slf4j.Logger LOGGER; // log output

	private static void printUsage() {
		System.err.println(getUsage());
	}

	private static String getUsage() {
		return "Usage: --action metrics|combine|extract|a3only|ubl|validate|validateExpectInvalid|validateExpectValid|visualize [-d,--directory] [-l,--listfromstdin] [-i,--ignore fileextension, PDF/A errors] [--disable-file-logging] | [-h,--help] \r\n"
				+ "        --action license   display open source license and notice\n"
				+ "        --action metrics\n"
				+ "          -d, --directory count ZUGFeRD files in directory to be scanned\n"
				+ "                If it is a directory, it will recurse.\n"
				+ "          -l, --listfromstdin     count ZUGFeRD files from a list of linefeed separated files on runtime.\n"
				+ "                It will start once a blank line has been entered.\n" + "\n"
				+ "        Additional parameter for both count operations\n"
				+ "        [-i, --ignorefileextension]     Check for all files (*.*) instead of PDF files only (*.pdf) in metrics, ignore PDF/A input file errors in combine\n"
				+ "        [--disable-file-logging]     disable logging to file.\n"
				+ "        --action extract   extract Factur-X PDF to XML file\n"
				+ "                Additional parameters (optional - user will be prompted if not defined)\n"
				+ "                [--source <filename>]: set input PDF file\n"
				+ "                [--out <filename>]: set output XML file\n"
				+ "        --action a3only    upgrade from PDF/A1 to A3 only (no ZUGFeRD data attached)\n"
				+ "                Additional parameters (optional - user will be prompted if not defined)\n"
				+ "                [--source <filename>]: set input PDF file\n"
				+ "                [--out <filename>]: set output PDF file\n"
				+ "        --action combine   combine XML and PDF file to Factur-X PDF file\n"
				+ "                Additional parameters (optional - user will be prompted if not defined)\n"
				+ "                [--source <filename>]: set input PDF file\n"
				+ "                [--source-xml <filename>]: set input XML file\n"
				+ "                [--out <filename>]: set output PDF file\n"
				+ "                [--format <fx|zf|ox|da>]: set Factur-X, ZUGFeRD, Order-X or Cross Industry Despatch Advice\n"
				+ "                [--version <1|2>]: set ZUGFeRD version\n"
				+ "                [--profile <...>]: set ZUGFeRD profile\n"
				+ "                        For ZUGFeRD v1 or Order-X: <B>ASIC, <C>OMFORT or EX<T>ENDED\n"
				+ "                        For ZUGFeRD v2: <M>INIMUM, BASIC <W>L, <B>ASIC, <C>IUS, <E>N16931, <X>Rechnung, EXTENDED-CTC-[F]R, EX<T>ENDED\n"
				+ "                [--attachments <filenames>]: list of file attachments (passing a single empty file name prevents prompting)\n"
				+ "                [--no-additional-attachments]: prevent prompting for attachments\n"
				+ "        --action ubl  convert UN/CEFACT 2016b CII XML to UBL XML\n"
				+ "                [--source <filename>]: set input XML file\n"
				+ "                [--out <filename>]: set output XML file\n"
				+ "                [--profileID <text>]: set profile ID, e.g. 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'\n"
				+ "                [--customizationID <text>]: set customization ID, e.g. 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0'\n"
				+ "        --action upgrade  upgrade ZUGFeRD XML to ZUGFeRD 2 XML\n"
				+ "                Additional parameters (optional - user will be prompted if not defined)\n"
				+ "                [--source <filename>]: set input XML ZUGFeRD 1 file\n"
				+ "                [--out <filename>]: set output XML ZUGFeRD 2 file\n"
				+ "        --action validate  validate XML or PDF file \n"
				+ "                [--no-notices]: refrain from reporting notices\n"
				+ "                [--no-arithmetic-check]: skip the arithmetic recalculation check\n"
				+ "                [--logAppend <text>]: text to be added to log line\n"
				+ "                Additional parameters (optional - user will be prompted if not defined)\n"
				+ "                [--source <filename>]: input PDF or XML file\n"
				+ "                [--log-as-pdf]: save log output as pdf\n"
				+ "        --action validateExpectInvalid  validate directory recursively expecting negative results \n"
				+ "                [--no-notices]: refrain from reporting notices\n"
				+ "                Additional parameters (user will be prompted if not defined)\n"
				+ "                -d, --directory to check recursively\n"
				+ "                --exclude: comma-separated list of filenames to ignore\n"
				+ "        --action validateExpectValid  validate directory recursively expecting positive results \n"
				+ "                [--no-notices]: refrain from reporting notices\n"
				+ "                Additional parameters (user will be prompted if not defined)\n"
				+ "                -d, --directory to check recursively \n"
				+ "                --exclude: comma-separated list of filenames to ignore\n"
				+ "        --action visualize  convert XML to HTML \n"
				+ "                [--language <lang>]: set output lang (en, fr or de)\n"
				+ "                [--source <filename>]: set input XML file\n"
				+ "                [--out <filename>]: set output HTML file\n"
				+ "        --action pdf  convert XML to PDF \n"
				+ "                [--source <filename>]: set input XML file\n"
				+ "                [--out <filename>]: set output PDF file\n";
	}

	private static void printHelp() {
		System.out.println("Mustangproject.org " + org.mustangproject.ZUGFeRD.Version.VERSION + " \r\n"
				+ "A Apache Public License tool for e-invoices with\r\n"
				+ "ZUGFeRD Metadata (http://www.zugferd.org)\r\n" + "\r\n" + getUsage() + "\r\n");
	}

	/**
	 * Asks the user (repeatedly, if necessary) on the command line for a String
	 * (offering a defaultValue) conforming to a Regex pattern
	 *
	 * @param prompt       the question to be asked to the user
	 * @param defaultValue the default return value if user hits enter
	 * @param pattern      a regex of acceptable values
	 * @return the user answer conforming to pattern
	 * @throws Exception if pattern not compilable or IOexception on input
	 */
	protected static String getStringFromUser(String prompt, String defaultValue, String pattern) throws Exception {
		String input = "";
		if (!defaultValue.matches(pattern)) {
			throw new Exception("Default value must match pattern");
		}
		boolean firstInput = true;
		do {
			// for a more sophisticated dialogue maybe https://github.com/mabe02/lanterna/
			// could be taken into account
			System.out.print(prompt + " (default: " + defaultValue + ")");
			if (!firstInput && !pattern.isEmpty()) {
				System.out.print("\n(allowed pattern: " + pattern + ")");

			}
			System.out.print(":");
			BufferedReader buffer = new BufferedReader(new InputStreamReader(System.in));
			try {
				input = buffer.readLine();
			} catch (IOException e) {
				LOGGER.error(e.getMessage(), e);

			}

			if (input.isEmpty()) {
				// pressed return without entering anything
				input = defaultValue;
			}

			firstInput = false;
		} while (!input.matches(pattern));

		return input;
	}

	/**
	 * Prompts the user for a input or output filename
	 *
	 * @param prompt              the text the user is asked
	 * @param defaultFilename     a default Filename. Passing an empty string
	 *                            indicates that specifying a file is optional
	 * @param expectedExtension   will warn if filename does not match expected file
	 *                            extension, "or" possible with e.g. pdf|xml
	 * @param ensureFileExists    will warn if file does NOT exist (for input files)
	 * @param ensureFileNotExists will warn if file DOES exist (for output files)
	 * @return String
	 */
	protected static String getFilenameFromUser(String prompt, String defaultFilename, String expectedExtension,
			boolean ensureFileExists, boolean ensureFileNotExists) {
		boolean fileExistenceOK = false;
		String selectedName = "";
		do {
			// for a more sophisticated dialogue maybe https://github.com/mabe02/lanterna/
			// could be taken into account
			System.out.print(prompt + (defaultFilename.isEmpty() ? ":" : " (default: " + defaultFilename + "):"));
			BufferedReader buffer = new BufferedReader(new InputStreamReader(System.in));
			try {
				selectedName = buffer.readLine();
			} catch (IOException e) {
				LOGGER.error(e.getMessage(), e);

			}

			if (selectedName.isEmpty()) {
				// pressed return without entering anything
				if (defaultFilename.isEmpty()) {
					return ""; // no default -> this is an optional filename
				}
				selectedName = defaultFilename;
			}

			boolean hasCorrectExtension = false;
			String[] expectedExtensions = expectedExtension.split("\\|");
			for (String currentExtension : expectedExtensions) {
				if (selectedName.toLowerCase().endsWith(currentExtension.toLowerCase())) {
					hasCorrectExtension = true;
					break;
				}
			}
			// error cases
			if (ensureFileExists) {
				if (fileExists(selectedName)) {
					fileExistenceOK = true;
				} else {
					System.out.println("File does not exist, try again or CTRL+C to cancel");
					// discard the input, a scanner.reset is not sufficient
					fileExistenceOK = false;
				}
			} else if (!hasCorrectExtension) {
				System.err.println("Expected " + expectedExtension
						+ " extension, this may corrupt your file. Do you still want to continue?(Y|N)");
				String selectedAnswer = "";
				try {
					selectedAnswer = buffer.readLine();
				} catch (IOException e) {
					LOGGER.error(e.getMessage(), e);
				}
				if (!"Y".equalsIgnoreCase(selectedAnswer)) {
					System.err.println("Aborted by user");
					System.exit(-1);
				}

			} else {
				fileExistenceOK = true;

				if (ensureFileNotExists) {
					if (fileExists(selectedName)) {
						fileExistenceOK = false;
						System.out.println("Output file already exists, try again or CTRL+C to cancel");
						// discard the input, a scanner.reset is not sufficient
					}
				} else {
					fileExistenceOK = true;
				}
			}

		} while (!fileExistenceOK);

		return selectedName;
	}

	private static void performUpgrade(String xmlName, String outName) throws IOException, TransformerException {

		// Get params from user if not already defined
		if (xmlName == null) {
			xmlName = getFilenameFromUser("ZUGFeRD 1.0 XML source", "ZUGFeRD-invoice.xml", "xml", true, false);
		} else {
			System.out.println("ZUGFeRD 1.0 XML source set to " + xmlName);
		}
		if (outName == null) {
			outName = getFilenameFromUser("ZUGFeRD 2.0 XML target", "factur-x.xml", "xml", false, true);
		} else {
			System.out.println("ZUGFeRD 2 XML target to " + outName);
		}

		// Verify params
		ensureFileExists(xmlName);
		ensureFileNotExists(outName);

		// All params are good! continue...
		XMLUpgrader zmi = new XMLUpgrader();
		String xml = zmi.migrateFromV1ToV2(xmlName);
		Files.write(Paths.get(outName), xml.getBytes());
		System.out.println("Written to " + outName);

	}

	/***
	 * converts from CII to UBL
	 *
	 * @param xmlName the name of the xml file
	 * @param outName the name of the output file
	 * @throws IOException
	 * @throws TransformerException
	 */
	private static void performUBL(String xmlName, String outName, String profileID, String customizationID) throws IOException {

		// Get params from user if not already defined
		if (xmlName == null) {
			xmlName = getFilenameFromUser("Factur-X XML source", "factur-x.xml", "xml", true, false);
		} else {
			System.out.println("Factur-X XML source set to " + xmlName);
		}
		if (outName == null) {
			outName = getFilenameFromUser("UBL target", "ubl-invoice.xml", "xml", false, true);
		} else {
			System.out.println("UBL target to " + outName);
		}

		// Verify params
		ensureFileExists(xmlName);
		ensureFileNotExists(outName);

		// All params are good! continue...
		CIIToUBL c2u = new CIIToUBL();
		c2u.convert(new File(xmlName), new File(outName), profileID, customizationID);
		System.out.println("Written to " + outName);

	}

	/***
	 * System out the APL
	 */
	public static void printLicense() {
		InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE");
		System.out.println(convertInputStreamToString(is));
		is = Thread.currentThread().getContextClassLoader().getResourceAsStream("NOTICE");
		System.out.println(convertInputStreamToString(is));

	}

	// Plain Java
	// based on
	// https://mkyong.com/java/how-to-convert-inputstream-to-string-in-java/
	private static String convertInputStreamToString(InputStream is) {
		try (InputStream inputStream = is) {
			int DEFAULT_BUFFER_SIZE = 8192;
			ByteArrayOutputStream result = new ByteArrayOutputStream();
			byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
			int length;

			while ((length = inputStream.read(buffer)) != -1) {
				result.write(buffer, 0, length);
			}

			// Java 1.1
			return result.toString(StandardCharsets.UTF_8.name());
		} catch (IOException e) {
			e.printStackTrace();
			return null;
			// Java 10
			// return result.toString(StandardCharsets.UTF_8);
		}
	}

	/***
	 * the main function of the commandline tool...
	 *
	 * @param args the commandline args, see also
	 *             https://www.mustangproject.org/commandline/#verbose
	 */
	public static void main(String[] args) {
		try {
			CommandLine cmd;
			CommandLineParser parser = new DefaultParser();

			// create Options object
			Options options = new Options();
			options.addOption(new Option("h", "help", false, "display usage"));
			options.addOption(new Option("a", "action", true, "which action to perform"));
			options.addOption(new Option("f", "format", true, "which format to output"));
			options.addOption(new Option("version", "version", true, "which version of the standard to use"));
			options.addOption(new Option("profile", "profile", true, "which profile of the standard to use"));
			options.addOption(new Option("no-additional-attachments", "no-additional-attachments", false,
					"prevent prompting for attachments"));
			Option attachmentOpt = new Option("attachments", "attachments", true, "File attachments");
			attachmentOpt.setValueSeparator(',');
			attachmentOpt.setArgs(Option.UNLIMITED_VALUES);
			options.addOption(attachmentOpt);
			Option excludeOpt = new Option("exclude", "exclude", true,
					"Files to exclude from recursive directory traversal");
			excludeOpt.setValueSeparator(',');
			excludeOpt.setArgs(Option.UNLIMITED_VALUES);
			options.addOption(excludeOpt);
			options.addOption(new Option("source", "source", true, "which source file to use"));
			options.addOption(new Option("source-xml", "source-xml", true, "which source file to use"));
			options.addOption(new Option("language", "language", true, "output language (en, de or fr)"));
			options.addOption(new Option("out", "out", true, "which output file to write to"));
			options.addOption(new Option("no-notices", "no-notices", false, "suppress non-fatal errors"));
			options.addOption(new Option("no-arithmetic-check", "no-arithmetic-check", false, "skip the arithmetic recalculation check during validation"));
			options.addOption(
					new Option("logAppend", "logAppend", true, "freeform text to be appended to log messages"));
			options.addOption(
					new Option("disable-file-logging", "disable-file-logging", false, "suppress logging to file"));
			options.addOption(new Option("d", "directory", true, "which directory to operate on"));
			options.addOption(new Option("i", "ignorefileextension", false, "ignore non-matching file extensions"));
			options.addOption(new Option("l", "listfromstdin", false, "take list of files from commandline"));
			options.addOption(new Option("log-as-pdf", "log-as-pdf", false, "saving log output to pdf file"));
			options.addOption(new Option("profileID", "profileID", true, "set profile ID"));
			options.addOption(new Option("customizationID", "customizationID", true, "set customization ID"));

			boolean optionsRecognized = false;
			String action = "";

			boolean disableFileLogging = false;
			try {
				cmd = parser.parse(options, args);

				// Retrieve all options
				action = cmd.getOptionValue("action");
				String directoryName = cmd.getOptionValue("directory");
				boolean filesFromStdIn = cmd.hasOption("listfromstdin"); // ((Number)cmdLine.getParsedOptionValue("integer-option")).intValue();
				boolean ignoreFileExt = cmd.hasOption("ignorefileextension");
				boolean noAttachments = cmd.hasOption("no-additional-attachments");
				boolean helpRequested = cmd.hasOption("help") || action != null && action.equals("help");
				disableFileLogging = cmd.hasOption("disable-file-logging");

				String sourceName = cmd.getOptionValue("source");
				String sourceXMLName = cmd.getOptionValue("source-xml");
				String outName = cmd.getOptionValue("out");
				String format = cmd.getOptionValue("format");
				String lang = cmd.getOptionValue("language");
				boolean noNotices = cmd.hasOption("no-notices");
				boolean noArithmeticCheck = cmd.hasOption("no-arithmetic-check");
				boolean LogAsPDF = cmd.hasOption("log-as-pdf");
				String profileID = cmd.getOptionValue("profileID");
				String customizationID = cmd.getOptionValue("customizationID");

				String zugferdVersion = cmd.getOptionValue("version");
				String zugferdProfile = cmd.getOptionValue("profile");

				String[] attachmentFilenames = cmd.hasOption("attachments") ? cmd.getOptionValues("attachments") : null;
				String[] excludedFilenames = cmd.hasOption("exclude") ? cmd.getOptionValues("exclude") : null;

				ArrayList<FileAttachment> attachments = new ArrayList<>();
				/*
				 * setting system property to disable FILE appender and
				 * suppress creation of files and folders.
				 */
				System.setProperty("FILE_APPENDER_ENABLED", disableFileLogging ? Boolean.TRUE.toString() : Boolean.FALSE.toString() );

				LOGGER = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

				if (helpRequested) {
					printHelp();
					optionsRecognized = true;
				} else if (action != null && action.equals("metrics")) {
					performMetrics(directoryName, filesFromStdIn, ignoreFileExt);
					optionsRecognized = true;
				} else if (action != null && action.equals("combine")) {
					performCombine(sourceName, sourceXMLName, outName, format, zugferdVersion, zugferdProfile,
							ignoreFileExt, attachmentFilenames, attachments, noAttachments);
					optionsRecognized = true;
				} else if (action != null && action.equals("extract")) {
					performExtract(sourceName, outName);
					optionsRecognized = true;
				} else if (action != null && action.equals("a3only")) {
					performConvert(sourceName, outName);
					optionsRecognized = true;
				} else if (action != null && action.equals("pdf")) {
					performVisualization(sourceName, lang, outName, true);
					optionsRecognized = true;
				} else if (action != null && action.equals("visualize")) {
					performVisualization(sourceName, lang, outName, false);
					optionsRecognized = true;
				} else if (action != null && action.equals("upgrade")) {
					performUpgrade(sourceName, outName);
					optionsRecognized = true;
				} else if (action != null && action.equals("ubl")) {
					performUBL(sourceName, outName, profileID, customizationID);
					optionsRecognized = true;
				} else if (action != null && action.equals("validate")) {
					optionsRecognized = performValidate(sourceName, noNotices, noArithmeticCheck, cmd.getOptionValue("logAppend"), LogAsPDF);
				} else if (action != null && action.equals("validateExpectValid")) {
					optionsRecognized = performValidateExpect(true, directoryName, excludedFilenames);
				} else if (action != null && action.equals("validateExpectInvalid")) {
					optionsRecognized = performValidateExpect(false, directoryName, excludedFilenames);
				}

			} catch (UnrecognizedOptionException ex) {
				optionsRecognized = false;
			}

			if (!optionsRecognized) {
				// no argument or argument unknown
				printUsage();
				System.exit(2);
			}
		} catch (Exception e) {
			if (LOGGER != null) {
				LOGGER.error(e.getMessage(), e);
			} else {
				System.err.println(e.getMessage());
			}
			System.exit(-1);
		}

	}

	private static boolean performValidate(String sourceName, boolean noNotices, boolean noArithmeticCheck, String logAppend, boolean createLogAsPDF) {
		boolean optionsRecognized;
		if (sourceName == null) {
			sourceName = getFilenameFromUser("Source PDF or XML", "invoice.pdf", "pdf|xml", true, false);
		}
		ZUGFeRDValidator zfv = new ZUGFeRDValidator();
		if (logAppend != null && !logAppend.isEmpty()) {
			zfv.setLogAppend(logAppend);
		}
		if (noNotices) {
			zfv.disableNotices();
		}
		if (noArithmeticCheck) {
			zfv.disableArithmeticCheck();
		}

		String validationResultXML = zfv.validate(sourceName);
		System.out.println(validationResultXML);

		if (createLogAsPDF) {
			ValidationLogVisualizer vlvi = new ValidationLogVisualizer();
			String fileBasename = FilenameUtils.getBaseName(sourceName);

			vlvi.toPDF(validationResultXML, fileBasename + "_result.pdf");
		}
		optionsRecognized = !zfv.hasOptionsError();
		if (!zfv.wasCompletelyValid()) {
			System.exit(-1);
		}
		return optionsRecognized;
	}

	private static boolean performValidateExpect(boolean valid, String dirName, String[] excludedFiles) {
		ValidatorFileWalker zfWalk = new ValidatorFileWalker(valid, excludedFiles);
		Path startingDir = Paths.get(dirName);
		try {
			Files.walkFileTree(startingDir, zfWalk);

		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		String totalResult = "valid";
		if (!zfWalk.getResult()) {
			totalResult = "invalid";
			System.exit(-1);
		}

		System.out.println("Overall test result: " + totalResult);

		return true;
	}

	private static void performConvert(String pdfName, String outName) throws IOException {
		/*
		 * ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory()
		 * .setProducer("toecount") .setCreator(System.getProperty("user.name"))
		 * .loadFromPDFA1("invoice.pdf");
		 */
		// Get params from user if not already defined
		if (pdfName == null) {
			pdfName = getFilenameFromUser("Source PDF", "invoice.pdf", "pdf", true, false);
		} else {
			System.out.println("Source PDF set to " + pdfName);
		}
		if (outName == null) {
			outName = getFilenameFromUser("Target PDF", "invoice.a3.pdf", "pdf", false, true);
		} else {
			System.out.println("Target PDF set to " + outName);
		}

		// Verify params
		ensureFileExists(pdfName);
		ensureFileNotExists(outName);

		// All params are good! continue...
		try (ZUGFeRDExporterFromA1 ze = new ZUGFeRDExporterFromA1()) {
			ze.convertOnly().load(pdfName);
			ze.export(outName);
			System.out.println("Written to " + outName);
		}
	}

	private static void performExtract(String pdfName, String xmlName) throws IOException {
		// Get params from user if not already defined
		if (pdfName == null) {
			pdfName = getFilenameFromUser("Source PDF", "invoice.pdf", "pdf", true, false);
		} else {
			System.out.println("Source PDF set to " + pdfName);
		}
		if (xmlName == null) {
			xmlName = getFilenameFromUser("ZUGFeRD XML", "factur-x.xml", "xml", false, true);
		} else {
			System.out.println("ZUGFeRD XML set to " + pdfName);
		}

		// Verify params
		ensureFileExists(pdfName);
		ensureFileNotExists(xmlName);

		// All params are good! continue...
		ZUGFeRDImporter zi = new ZUGFeRDImporter();
		zi.doIgnoreCalculationErrors();
		zi.setPDFFilename(pdfName);
		byte[] XMLContent = zi.getRawXML();
		if (XMLContent == null) {
			System.err.println("No ZUGFeRD XML found in PDF file");

		} else {
			Files.write(Paths.get(xmlName), XMLContent);
			System.out.println("Written to " + xmlName);
		}
	}

	private static void performCombine(String pdfName, String xmlName, String outName, String format, String zfVersion,
			String zfProfile, boolean ignoreInputErrors, String[] attachmentFilenames,
			ArrayList<FileAttachment> attachments, boolean noAttachments) throws Exception {
		/*
		 * ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory()
		 * .setProducer("toecount") .setCreator(System.getProperty("user.name"))
		 * .loadFromPDFA1("invoice.pdf");
		 */
		IZUGFeRDExporter ze = null;
		try {
			int zfIntVersion;
			Profile zfConformanceLevelProfile;

			if (pdfName == null) {
				pdfName = getFilenameFromUser("Source PDF", "invoice.pdf", "pdf", true, false);
			} else {
				System.out.println("Source PDF set to " + pdfName);
			}

			if (xmlName == null) {
				xmlName = getFilenameFromUser("ZUGFeRD XML", "factur-x.xml", "xml", true, false);
			} else {
				System.out.println("ZUGFeRD XML set to " + xmlName);
			}

			if (outName == null) {
				outName = getFilenameFromUser("Output PDF", "invoice.ZUGFeRD.pdf", "pdf", false, true);
			} else {
				System.out.println("Output PDF set to " + outName);
			}

			if (attachmentFilenames == null) {
				byte[] attachmentContents = null;
				String attachmentFilename, attachmentMime;
				if (!noAttachments) {
					attachmentFilename = getFilenameFromUser("Additional file attachments filename (empty for none)", "", "pdf", true, false);
					if (!attachmentFilename.isEmpty()) {
						attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename));
						attachmentMime = Files.probeContentType(Paths.get(attachmentFilename));
						attachments.add(
								new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents));
					}
				}
			} else {
				for (int i = 0; i < attachmentFilenames.length; i++) {
					String attachmentFilename = attachmentFilenames[i];
					if (!attachmentFilename.isEmpty()) {
						byte[] attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename));
						String attachmentMime = Files.probeContentType(Paths.get(attachmentFilename));
						attachments.add(
								new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents));
					}
				}
			}

			if (format == null) {
				try {
					format = getStringFromUser(
							"Format (fx=Factur-X, zf=ZUGFeRD, ox=Order-X, da=Cross Industry Despatch Advice)", "fx",
							"fx|zf|ox|da");
				} catch (Exception e) {
					LOGGER.error(e.getMessage(), e);
				}
			} else {
				System.out.println("Format set to " + format);
			}

			if (zfVersion == null) {
				try {
					zfVersion = getStringFromUser("Version (1 or 2)", "1", "1|2"); // fx default version is 1
				} catch (Exception e) {
					LOGGER.error(e.getMessage(), e);
				}
			} else {
				System.out.println("Version set to " + zfVersion);
			}
			zfIntVersion = Integer.valueOf(zfVersion);

			if (zfProfile == null) {
				try {
					if ("zf".equals(format) && zfIntVersion == 1 || "ox".equals(format)) {
						zfProfile = getStringFromUser("Profile (b)asic, (c)omfort or ex(t)ended", "t", "B|b|C|c|T|t");
					} else if ("da".equals(format)) {
						zfProfile = getStringFromUser("Profile (p)ilot", "p", "P|p");
					} else {
						zfProfile = getStringFromUser(
								"Profile  [M]INIMUM, BASIC [W]L, [B]ASIC,\n"
										+ "[C]IUS, [E]N16931, EX[T]ENDED or [X]RECHNUNG",
								"E",
								"M|m|W|w|B|b|C|c|E|e|T|t|X|x|");
					}
				} catch (Exception e) {
					LOGGER.error(e.getMessage(), e);
				}
			} else {
				System.out.println("Profile set to " + zfProfile);
			}
			zfProfile = zfProfile.toLowerCase();

			// Verify params
			ensureFileExists(pdfName);
			ensureFileExists(xmlName);
			ensureFileNotExists(outName);

			EStandard standard = EStandard.FACTUR_X;
			if ("zf".equals(format)) {
				standard = EStandard.ZUGFERD;
			}
			if ("da".equals(format)) {
				standard = EStandard.DELIVER_X;

				zfConformanceLevelProfile = Profiles.getByName(standard, "PILOT", 1);
			} else if ("zf".equals(format) && zfIntVersion == 1 || "ox".equals(format)) {
				if ("ox".equals(format)) {
					standard = EStandard.ORDER_X;
				}
				if (zfProfile.equals("b")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "BASIC", zfIntVersion);
				} else if (zfProfile.equals("c")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "COMFORT", zfIntVersion);
				} else if (zfProfile.equals("t")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED", zfIntVersion);
				} else {
					throw new Exception(String.format("Unknown ZUGFeRD profile '%s'", zfProfile));
				}
			} else if (format.equals("zf") && zfIntVersion == 2 || format.equals("fx")) {
				if (zfProfile.equals("m")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "MINIMUM", zfIntVersion);
				} else if (zfProfile.equals("w")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "BASICWL", zfIntVersion);
				} else if (zfProfile.equals("b")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "BASIC", zfIntVersion);
				} else if (zfProfile.equals("c")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "CIUS", zfIntVersion);
				} else if (zfProfile.equals("e")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "EN16931", zfIntVersion);
				} else if (zfProfile.equals("f")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED-CTC-FR", zfIntVersion);
				} else if (zfProfile.equals("t")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED", zfIntVersion);
				} else if (zfProfile.equals("x")) {
					zfConformanceLevelProfile = Profiles.getByName(standard, "XRECHNUNG", zfIntVersion);
				} else {
					throw new Exception(String.format("Unknown ZUGFeRD profile '%s'", zfProfile));
				}
			} else {
				throw new Exception(String.format("Unknown version '%i'", zfIntVersion));
			}

			// All params are good! continue...
			if (format.equals("ox")) {
				ze = new OXExporterFromA1();
				if (ignoreInputErrors) {
					((OXExporterFromA1) ze).ignorePDFAErrors();
				}
			} else if (format.equals("da")) {
				ze = new DXExporterFromA1();
				if (ignoreInputErrors) {
					((DXExporterFromA1) ze).ignorePDFAErrors();
				}
			} else {
				if (format.equals("fx")) {
					zfIntVersion = 2; // actually we are talking of generation, not version
					// so even if someone correctly requested factur-x 1 internally we call it
					// zugferd 2 :-(
				}
				ze = new ZUGFeRDExporterFromPDFA();
				if (ignoreInputErrors) {
					((ZUGFeRDExporterFromPDFA) ze).ignorePDFAErrors();
				}
			}

			ze.load(pdfName);
			ze.setProducer("Mustang-cli")
					.setZUGFeRDVersion(zfIntVersion)
					.setCreator(System.getProperty("user.name")).setProfile(zfConformanceLevelProfile);

			if (format.equals("zf")) {
				ze.disableFacturX();
			}

			for (FileAttachment attachment : attachments) {
				ze.attachFile(attachment.getFilename(), attachment.getData(), attachment.getMimetype(),
						attachment.getRelation());
			}

			ze.setXML(Files.readAllBytes(Paths.get(xmlName)));

			ze.export(outName);
			System.out.println("Written to " + outName);

		} catch (Exception e) {
			if (LOGGER != null) {
				LOGGER.error(e.getMessage(), e);
			}
			System.err.println(e.getMessage());
		} finally {
			if ( ze != null ) {
				ze.close();
			}
		}
	}

	private static void performMetrics(String directoryName, boolean filesFromStdIn, boolean ignoreFileExt)
			throws IOException {

		StatRun sr = new StatRun();
		if (ignoreFileExt) {
			sr.ignoreFileExtension();
		}
		if (directoryName != null) {
			Path startingDir = Paths.get(directoryName);

			if (Files.isRegularFile(startingDir)) {
				String filename = startingDir.toString();
				FileChecker fc = new FileChecker(filename, sr);

				fc.checkForZUGFeRD();
				System.out.print(fc.getOutputLine());

			} else if (Files.isDirectory(startingDir)) {
				FileTraverser pf = new FileTraverser(sr);
				Files.walkFileTree(startingDir, pf);
			}
		}

		if (filesFromStdIn) {
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			String s;
			while ((s = in.readLine()) != null && !s.isEmpty()) {
				FileChecker fc = new FileChecker(s, sr);

				fc.checkForZUGFeRD();
				System.out.print(fc.getOutputLine());

			}

		}
		System.out.println(sr.getSummaryLine());
	}

	private static void performVisualization(String sourceName, String lang, String outName, boolean intoPDF) {
		// Get params from user if not already defined
		if (sourceName == null) {
			sourceName = getFilenameFromUser("ZUGFeRD XML source", "factur-x.xml", "xml", true, false);
		} else {
			System.out.println("ZUGFeRD XML source set to " + sourceName);
		}
		if (lang == null) {
			try {
				lang = getStringFromUser("Output language", "en", "en|de|fr");
			} catch (Exception e) {
				LOGGER.error(e.getMessage(), e);
			}
		} else {
			System.out.println("Output language set to " + lang);
		}

		if (outName == null) {
			String defaultFilename = "factur-x.html";
			String defaultExtension = "html";
			if (intoPDF) {
				defaultFilename = "factur-x.pdf";
				defaultExtension = "pdf";
			}
			outName = getFilenameFromUser("Target file", defaultFilename, defaultExtension, false, true);
		} else {
			System.out.println("Target set to " + outName);
		}

		// Verify params
		try {
			ensureFileExists(sourceName);
			ensureFileNotExists(outName);

			// stylesheets/ZUGFeRD_1p0_c1p0_s1p0.xslt
		} catch (IOException e) {
			LOGGER.error(e.getMessage(), e);
		}

		ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer();
		String xml = null;
		try {
			ZUGFeRDVisualizer.Language langCode = ZUGFeRDVisualizer.Language.EN;
			if (lang.equalsIgnoreCase("de")) {
				langCode = ZUGFeRDVisualizer.Language.DE;
			}
			if (lang.equalsIgnoreCase("fr")) {
				langCode = ZUGFeRDVisualizer.Language.FR;
			}
			if (!intoPDF) {
				xml = zvi.visualize(sourceName, langCode);
				Files.write(Paths.get(outName), xml.getBytes());
			} else {
				zvi.toPDF(sourceName, outName, langCode);
			}
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
			System.err.println(e.getMessage());
		}
		System.out.println("Written to " + outName);

		if (!intoPDF) {

			try {
				exportResource("/xrechnung-viewer.css");
				exportResource("/xrechnung-viewer.js");

				System.out.println("xrechnung-viewer.css and xrechnung-viewer.js written as well (to local working dir)");
			} catch (Exception e) {
				LOGGER.error(e.getMessage(), e);
			}
		}

	}

	/**
	 * Export a resource embedded into a Jar file to the local file path.
	 *
	 * @param resourceName ie.: "/SmartLibrary.dll"
	 * @return The path to the exported resource
	 * @throws Exception e.g. if the specified resource does not exist at the
	 *                   specified location
	 */
	private static String exportResource(String resourceName) throws Exception {
		String jarFolder;
		try (InputStream stream = Main.class.getResourceAsStream(resourceName)) { // note that each / is a directory down
																					// in the "jar tree" been the jar
																					// the root of the tree
			if (stream == null) {
				throw new Exception("Cannot get resource \"" + resourceName + "\" from Jar file.");
			}

			int readBytes;
			byte[] buffer = new byte[4096];
			jarFolder = System.getProperty("user.dir");
			try (FileOutputStream resStreamOut = new FileOutputStream(jarFolder + resourceName)) {
				while ((readBytes = stream.read(buffer)) > 0) {
					resStreamOut.write(buffer, 0, readBytes);
				}
			}
		}

		return jarFolder + resourceName;
	}

	private static void ensureFileExists(String fileName) throws IOException {
		if (!fileExists(fileName)) {
			throw new IOException(String.format("File %s does not exists", fileName));
		}
	}

	private static void ensureFileNotExists(String fileName) throws IOException {
		if (fileExists(fileName)) {
			throw new IOException(String.format("File %s already exists", fileName));
		}
	}

	private static boolean fileExists(String fileName) {
		if (fileName == null) {
			return false;
		}
		File f = new File(fileName);
		// "exists" also returns true for directories
		return f.isFile();
	}

}
