// SPDX-FileCopyrightText: 2020 Roberto Posenato <roberto.posenato@univr.it>
//
// SPDX-License-Identifier: LGPL-3.0-or-later
package it.univr.di.cstnu.util;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import it.unimi.dsi.fastutil.objects.*;
import it.univr.di.Debug;
import it.univr.di.cstnu.algorithms.PSTN;
import it.univr.di.cstnu.algorithms.STNURTE;
import it.univr.di.cstnu.graph.*;
import it.univr.di.labeledvalue.Constants;
import org.apache.commons.math3.stat.Frequency;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.StringArrayOptionHandler;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Simple class to determine some statistics about the execution of PSTNs
 * <p>
 * The main idea is the following:
 * <ul>
 *  <li>give some DC PSTNs, this runner executes each of them for #times and registers how much time the instance was executed without constraint violation.
 * </ul>
 *
 * @author posenato
 * @version $Rev: 732 $
 */
public class PSTNRTEBenchmarkRunner {

	/**
	 * The possible exit of an RTE
	 */
	public enum ExitExecution {
		/**
		 * successful with all contingent durations inside the contingent bounds
		 */
		okInBound,
		/**
		 * successful with some contingent durations outside the contingent bounds
		 */
		okOutBounds,
		/**
		 * not successful with some contingent durations outside the contingent bounds
		 */
		notOkOutBounds
	}

	/**
	 * Maintains the map (contingent node, chosen duration of its contingent link), the number of chosen duration outside the bounds of the relative contingent
	 * link, and the conjunct probability mass of the contingent ranges w.r.t. the probability distribution functions of the contingent durations.
	 */
	@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Ok..., but it is not so important!")
	public record PSTNSituation(Object2IntMap<LabeledNode> situation, double maxOutlierSpan, int durationsOutOfBounds, double conjunctProbabilityMass) {

		/**
		 * @return true if there is no durations out of the admissible range.
		 */
		public boolean isWithinBounds() {
			return this.durationsOutOfBounds == 0;
		}
	}

	/**
	 * Represents a key composed by {@code (nodes, contingents)}.
	 * <p>
	 * Implements a natural order based on increasing pair {@code (nodes, contingents)}.
	 *
	 * @author posenato
	 */
	static private class GlobalStatisticsKey implements Comparable<GlobalStatisticsKey> {
		/**
		 * # contingents
		 */
		final int contingents;
		/**
		 * # of nodes
		 */
		final int nodes;

		/**
		 * default constructor
		 *
		 * @param inputNodes       #nodes
		 * @param inputContingents # cont
		 */
		GlobalStatisticsKey(final int inputNodes, final int inputContingents) {
			nodes = inputNodes;
			contingents = inputContingents;
		}

		/**
		 * The order is respect to #nodes,#contingents, and #propositions.
		 *
		 * @param o the object to be compared.
		 *
		 * @return negative if this has, in order, fewer nodes or fewer contingents or fewer proposition than the parameter 'o'; a positive value in the
		 * 	opposite case, 0 when all three values are equals to the corresponding values in 'o'.
		 */
		@Override
		public int compareTo(@Nonnull GlobalStatisticsKey o) {
			final long d = (long) nodes - o.nodes;
			if (d == 0) {
				return contingents - o.contingents;
			}
			return (int) d;
		}

		@Override
		public boolean equals(Object o) {
			if (o == this) {
				return true;
			}
			if (!(o instanceof GlobalStatisticsKey o1)) {
				return false;
			}
			return o1.compareTo(this) == 0;
		}

		/**
		 * @return #cont
		 */
		public int getContingent() {
			return contingents;
		}

		/**
		 * @return #nodes
		 */
		public int getNodes() {
			return nodes;
		}

		@Override
		public int hashCode() {
			return (contingents) * 100000 + nodes;
		}
	}

	/**
	 * Each instance of this class represents a set of global statistics. Each global statistics element represents a map
	 * {@code (GlobalStatisticsKey, SummaryStatistics)}. Each SummaryStatistics element allows the determination of different statistics of all added item to
	 * the element.
	 */
	static private class GlobalStatistics {
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> networkEdges = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> execTimeInSec = new Object2ObjectAVLTreeMap<>();
		//		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> minimizationExecTimeInSec = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> probConjunctedProbMass = new Object2ObjectAVLTreeMap<>();
//		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> numberMinimizationProblem = new Object2ObjectAVLTreeMap<>();
		/**
		 * Three possible values: okInBound, okOutBounds, notOkOutBounds for early_execution strategy (_E suffix) and middle_point_execution_strategy (_M
		 * suffix)
		 */
		Object2ObjectMap<GlobalStatisticsKey, Frequency> executionExit_E = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> durationOutsideBoundsInOK_E = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> durationOutsideBoundsInNOTOK_E = new Object2ObjectAVLTreeMap<>();

		Object2ObjectMap<GlobalStatisticsKey, Frequency> executionExit_M = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> durationOutsideBoundsInOK_M = new Object2ObjectAVLTreeMap<>();
		Object2ObjectMap<GlobalStatisticsKey, SummaryStatistics> durationOutsideBoundsInNOTOK_M = new Object2ObjectAVLTreeMap<>();
	}

	/**
	 * CSV separator
	 */
	static final String CSVSep = ";\t";
	/**
	 * Global header
	 */
	static final String GLOBAL_HEADER =
		"%n%nGlobal statistics%n"
		+ "#networks" + CSVSep
		+ "#nodes" + CSVSep
		+ "#contingents" + CSVSep
		+ "#avgEdges" + CSVSep
		+ "stdDevEdges" + CSVSep
		+ "avgExeTime[s]" + CSVSep
		+ "stdDevExeTime[s]" + CSVSep
		+ "avgConjunctedProbMass" + CSVSep
		+ "stdDevConjunctedProbMass" + CSVSep
		+ "%%InBoundOK_E" + CSVSep
		+ "%%OutBoundOK_E" + CSVSep
		+ "%%OutBoundNOTOK_E" + CSVSep
		+ "#OutBoundDDuraOK_E" + CSVSep
		+ "#OutBoundDDuraOK_E" + CSVSep
		+ "%%InBoundOK_M" + CSVSep
		+ "%%OutBoundOK_M" + CSVSep
		+ "%%OutBoundNOTOK_M" + CSVSep
		+ "#OutBoundDDuraOK_M" + CSVSep
		+ "#OutBoundDDuraOK_M" + CSVSep
		+ "%n";
	/**
	 *
	 */
	static final String GLOBAL_HEADER_ROW =
		"%d" + CSVSep
		+ "%d" + CSVSep
		+ "%d" + CSVSep
		+ "%E" + CSVSep // avgEdges
		+ "%E" + CSVSep // stdDevEdges
		+ "%E" + CSVSep // avgExeTime
		+ "%E" + CSVSep // stdDevExeTime
		+ "%E" + CSVSep // avgConjunctedProbMass
		+ "%E" + CSVSep // scale ConjunctedProbMass
		+ "%6.4f" + CSVSep // "%InBoundOK_E"
		+ "%6.4f" + CSVSep // "%OutBoundOK_E"
		+ "%6.4f" + CSVSep // "%OutBoundNOTOK_E"
		+ "%6.4f" + CSVSep // #OutBoundDDuraInOK_E"
		+ "%6.4f" + CSVSep  // #OutBoundDDuraInOK_E"
		+ "%6.4f" + CSVSep // "%InBoundOK_M"
		+ "%6.4f" + CSVSep // "%OutBoundOK_M"
		+ "%6.4f" + CSVSep // "%OutBoundNOTOK_M"
		+ "%6.4f" + CSVSep // #OutBoundDDuraInOK_M"
		+ "%6.4f" + CSVSep  // #OutBoundDDuraInOK_M"
		+ "%n";
	/**
	 * class logger
	 */
	static final Logger LOG = Logger.getLogger(PSTNRTEBenchmarkRunner.class.getName());
	/**
	 * Output file header
	 */
	static final String OUTPUT_HEADER =
		String.format("%31s%s", "fileName", CSVSep)
		+ String.format("%6s%s", "#nodes", CSVSep)
		+ String.format("%4s%s", "#ctg", CSVSep)
		+ String.format("%5s%s", "#edges", CSVSep)
		+ String.format("%14s%s", "avgExeTime[s]", CSVSep)
		+ String.format("%14s%s", "std.dev.[s]", CSVSep)
		+ String.format("%14s%s", "ConjProbMass", CSVSep)
		+ String.format("%8s%s", "%InBOK_E", CSVSep)
		+ String.format("%8s%s", "%OutBOK_E", CSVSep)
		+ String.format("%8s%s", "%OutBNOTOK_E", CSVSep)//
		+ String.format("%8s%s", "AvgOutBDOK_E", CSVSep)
		+ String.format("%8s%s", "AvgOutBDNOTOK_E", CSVSep)
		+ String.format("%8s%s", "%InBOK_M", CSVSep)
		+ String.format("%8s%s", "%OutBOK_M", CSVSep)
		+ String.format("%8s%s", "%OutBNOTOK_M", CSVSep)//
		+ String.format("%8s%s", "AvgOutBDOK_M", CSVSep)
		+ String.format("%8s%s", "AvgOutBDNOTOK_M", CSVSep);
	/**
	 * OUTPUT_ROW is split in OUTPUT_ROW_GRAPH + OUTPUT_ROW_ALG_STATS
	 */
	static final String OUTPUT_ROW_GRAPH = "%31s" + CSVSep //name
	                                       + "%6d" + CSVSep //#nodes
	                                       + "%4d" + CSVSep //#contingents
	                                       + "%5d" + CSVSep//#edges
		;
	/**
	 * OUTPUT_ROW is split in OUTPUT_ROW_GRAPH + OUTPUT_ROW_ALG_STATS
	 */
	static final String OUTPUT_ROW_ALG_STATS = "%14E" + CSVSep // alg avgExe
	                                           + "%14E" + CSVSep // alg avgExe dev std
	                                           + "%14E" + CSVSep // ConjProbMass
	                                           + "%8.4f" + CSVSep // %InBoundOK_E
	                                           + "%8.4f" + CSVSep // %OutBoundOK_E
	                                           + "%8.4f" + CSVSep // %OutBoundNOTOK_E
	                                           + "%8.4f" + CSVSep // #OutBoundDDuraInOK_E
	                                           + "%8.4f" + CSVSep // #OutBoundDDuraInOK_E
	                                           + "%8.4f" + CSVSep // %InBoundOK_M
	                                           + "%8.4f" + CSVSep // %OutBoundOK_M
	                                           + "%8.4f" + CSVSep // %OutBoundNOTOK_M
	                                           + "%8.4f" + CSVSep // #OutBoundDDuraInOK_M
	                                           + "%8.4f" + CSVSep // #OutBoundDDuraInOK_M
		;
	/**
	 * Version
	 */
	static final String VERSIONandDATE = "1.0, July, 1 2024";
	/**
	 * Date formatter
	 */
	private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");

	/**
	 * Allows to check the execution time of algorithms giving a set of instances.
	 *
	 * @param args an array of {@link String} objects.
	 */
	public static void main(String[] args) {

		LOG.finest("Checker " + VERSIONandDATE + "\nStart...");
		System.out.println("Checker " + VERSIONandDATE + "\n" + getNow() + ": Start of execution.");
		final PSTNRTEBenchmarkRunner tester = new PSTNRTEBenchmarkRunner();

		if (!tester.manageParameters(args)) {
			return;
		}
		LOG.finest("Parameters ok!");
		if (tester.versionReq) {
			return;
		}

		/*
		 * To collect statistics w.r.t. the dimension of networks
		 */
		final GlobalStatistics globalStatistics = new GlobalStatistics();

		final RunMeter runMeter = new RunMeter(System.currentTimeMillis(), tester.instances.size(), 0);
		runMeter.printProgress(0);

		tester.output.println("*".repeat(79));
		tester.output.println("* Trial date: " + getNow());
		tester.output.println("*".repeat(79));
		tester.output.println(OUTPUT_HEADER);
		tester.output.flush();
		int nTaskSuccessfullyFinished = 0;
		for (final File file : tester.instances) {
			final boolean isOk = tester.worker(file, runMeter, globalStatistics);
			if (isOk) {
				nTaskSuccessfullyFinished++;
			}
		}
		final String msg = "Number of instances processed successfully over total: " + nTaskSuccessfullyFinished + "/" + tester.instances.size() + ".";
		LOG.info(msg);
		System.out.println("\n" + getNow() + ": " + msg);

		tester.output.printf(GLOBAL_HEADER);
		//Use one of the element in globalStatistics to extract all the possible globalStatisticsKeys

		for (final Object2ObjectMap.Entry<GlobalStatisticsKey, SummaryStatistics> entryNetworkEdges : globalStatistics.networkEdges.object2ObjectEntrySet()) {
			final GlobalStatisticsKey globalStatisticsKey = entryNetworkEdges.getKey();
			tester.output.printf(GLOBAL_HEADER_ROW, //
			                     Long.valueOf(entryNetworkEdges.getValue().getN()), //
			                     Integer.valueOf(globalStatisticsKey.getNodes()),//
			                     Integer.valueOf(globalStatisticsKey.getContingent()), //
			                     Double.valueOf(entryNetworkEdges.getValue().getMean()),//
			                     Double.valueOf(entryNetworkEdges.getValue().getStandardDeviation()),//
			                     Double.valueOf(globalStatistics.execTimeInSec.get(globalStatisticsKey).getMean()),//
			                     Double.valueOf(globalStatistics.execTimeInSec.get(globalStatisticsKey).getStandardDeviation()),//
			                     Double.valueOf(globalStatistics.probConjunctedProbMass.get(globalStatisticsKey).getMean()),//
			                     Double.valueOf(globalStatistics.probConjunctedProbMass.get(globalStatisticsKey).getStandardDeviation()),//
			                     Double.valueOf(globalStatistics.executionExit_E.get(globalStatisticsKey).getPct(ExitExecution.okInBound)),//
			                     Double.valueOf(globalStatistics.executionExit_E.get(globalStatisticsKey).getPct(ExitExecution.okOutBounds)),//
			                     Double.valueOf(globalStatistics.executionExit_E.get(globalStatisticsKey).getPct(ExitExecution.notOkOutBounds)),//
			                     Double.valueOf(globalStatistics.durationOutsideBoundsInOK_E.get(globalStatisticsKey).getMean()),//
			                     Double.valueOf(globalStatistics.durationOutsideBoundsInNOTOK_E.get(globalStatisticsKey).getMean()),//
			                     Double.valueOf(globalStatistics.executionExit_M.get(globalStatisticsKey).getPct(ExitExecution.okInBound)),//
			                     Double.valueOf(globalStatistics.executionExit_M.get(globalStatisticsKey).getPct(ExitExecution.okOutBounds)),//
			                     Double.valueOf(globalStatistics.executionExit_M.get(globalStatisticsKey).getPct(ExitExecution.notOkOutBounds)),//
			                     Double.valueOf(globalStatistics.durationOutsideBoundsInOK_M.get(globalStatisticsKey).getMean()),//
			                     Double.valueOf(globalStatistics.durationOutsideBoundsInNOTOK_M.get(globalStatisticsKey).getMean())//
			                    );//
			tester.output.println();
		}
		tester.output.printf("%n%n%n");
		tester.output.close();
	}

	/**
	 * @param offSet the offset to add to the lower bound for finding a new upper bound when upper bound is not finite.
	 *
	 * @return a strategy for ordinary node execution time.
	 */
	private static STNURTE.Strategy buildMiddlePointStrategy(int offSet) {

		/*
		 * It uses averageContingentRangeWidth to determine a middle exec value for nodes
		 * having ∞ as upper bound in their time window.
		 */
		final STNURTE.NodeAndExecutionTimeChoice strategy = candidates -> {
			LabeledNode first = null;
			TimeInterval firstTW = null;
			int minUpperBound = Constants.INT_POS_INFINITE;
			for (final STNURTE.NodeWithTimeInterval entry : candidates.nodes()) {
				final LabeledNode node = entry.node();
				final TimeInterval tw = entry.timeInterval();
				if (tw.getUpper() <= candidates.timeInterval().getUpper()) {
					first = node;
					firstTW = tw;
					break;
				} else {
					if (minUpperBound > tw.getUpper()) {
						minUpperBound = tw.getUpper();
						first = node;
						firstTW = tw;
					}
				}
			}
			if (minUpperBound == Constants.INT_POS_INFINITE) {
				//all candidates have an upper bound equals to +∞
				//select the first one and limit its upper bound using averageContingentRangeWidth
				@SuppressWarnings("OptionalGetWithoutIsPresent") final STNURTE.NodeWithTimeInterval entry = candidates.nodes().stream().findFirst().get();
				first = entry.node();
				firstTW = entry.timeInterval();
				final int lb = entry.timeInterval().getLower();
				firstTW.set(lb, lb + offSet);
				if (Debug.ON) {
					LOG.info("All candidates have upper bound = ∞. Selected the first node " + first + " with time window " + firstTW);
				}
			}
			final ObjectSet<LabeledNode> singleSet = new ObjectArraySet<>(1);
			singleSet.add(first);
			final int lowerBound = Math.max(firstTW.getLower(), candidates.timeInterval().getLower());
			final int upperBound = Math.min(firstTW.getUpper(), candidates.timeInterval().getUpper());
			final int exeTime = Constants.sumWithOverflowCheck(lowerBound, upperBound) / 2;
			return new STNURTE.NodeOccurrence(exeTime, singleSet);
		};

		return () -> strategy;
	}

	/**
	 * Builds a situation for the PSTN. Moreover, it collects the number of duration that are outside the bounds of the relative contingent link and the
	 * conjunct probability mass determined by the contingent bounds.
	 *
	 * @param pstn a network already initialized.
	 *
	 * @return a PSTNSituation if the network has some contingent links, null otherwise.
	 */
	private static PSTNSituation buildSituation(PSTN pstn) {
		if (pstn.getLowerCaseEdgesMap() == null) {
			//no contingent link
			LOG.info("There is no contingent link. Giving up!");
			return null;
		}
		final Object2IntMap<LabeledNode> situation = new Object2IntOpenHashMap<>(pstn.getLowerCaseEdgesMap().size());
		int durationsOutOfBounds = 0;
		double conjunctProbabilityMass = 1.0;

		double maxOutlierSpan = 0.0;
		for (final LabeledNode ctg : pstn.getLowerCaseEdgesMap().keySet()) {
			final LogNormalDistributionParameter logNormalParam = ctg.getLogNormalDistribution();
			if (logNormalParam == null) {
				continue;
			}
			final int rndValue = (int) logNormalParam.sample();
			situation.put(ctg, rndValue);
			//possible shift of activation timepoint
			final int shift = logNormalParam.getShift();
			//check if the value is outside of contingent approximated bounds.
			final int lowerBound = pstn.getLowerCaseEdgesMap().get(ctg).getLabeledValue();
			assert pstn.getUpperCaseEdgesMap() != null && pstn.getUpperCaseEdgesMap().get(ctg) != null;
			final int upperBound = (-pstn.getUpperCaseEdgesMap().get(ctg).getLabeledValue());
			if (lowerBound >= upperBound) {
				throw new IllegalStateException("Contingent ranges for " + ctg + " are wrong: [" + lowerBound + ", " + upperBound + "]");
			}
			if (rndValue < lowerBound || rndValue > upperBound) {
				LOG.info("The chosen duration for " + ctg + " is " + rndValue + ". It is outside the range [" + lowerBound + ", " + upperBound + "]");
				durationsOutOfBounds++;
				double diff;
				if (rndValue < lowerBound && (diff = lowerBound - rndValue) > maxOutlierSpan) {
					maxOutlierSpan = diff;
				}
				if (rndValue > lowerBound && (diff = rndValue - upperBound) > maxOutlierSpan) {
					maxOutlierSpan = diff;
				}
			}
			final double cdfValue = (logNormalParam.cumulativeProbability(upperBound) - logNormalParam.cumulativeProbability(lowerBound));
			if (-1E-4 < cdfValue && cdfValue < 1E-4) {
				throw new IllegalStateException(
					"Contingent ranges for " + ctg + ": [" + lowerBound + ", " + upperBound + "] determine a not significative cumulative distribution value "
					+ cdfValue + "\n Lognormale parameter: " + ctg.getLogNormalDistribution());
			}
			conjunctProbabilityMass *= cdfValue;
		}
		return new PSTNSituation(situation, maxOutlierSpan, durationsOutOfBounds, conjunctProbabilityMass);
	}

	/**
	 * @param situation a situation
	 *
	 * @return a strategy for contingent durations.
	 */
	private static STNURTE.Strategy buildSituationStrategy(@Nonnull Object2IntMap<LabeledNode> situation) {

		final Object2IntMap<LabeledNode> localSituation = new Object2IntOpenHashMap<>(situation);
		/*
		 * It is assumed that for each contingent node, the proper time window has value [executionTimeActivationTP, ∞]
		 * The candidates.timeInterval() is used only to guarantee that the chosen value is greater that the minimum
		 */
		final STNURTE.NodeAndExecutionTimeChoice strategy = candidates -> {
			final ObjectSet<LabeledNode> nodes = new ObjectLinkedOpenHashSet<>();
			if (candidates.nodes() == null) {
				return new STNURTE.NodeOccurrence(0, nodes);
			}
			final ExtendedPriorityQueue<LabeledNode> queue = new ExtendedPriorityQueue<>();
			for (final STNURTE.NodeWithTimeInterval entry : candidates.nodes()) {
				final int ctgTime = Constants.sumWithOverflowCheck(entry.timeInterval().getLower(), localSituation.getInt(entry.node()));
				if (ctgTime < candidates.timeInterval().getLower()) {
					throw new IllegalArgumentException(
						"The chosen time " + ctgTime + " for ctg " + entry.node() + " is before the lower bound of the time window " +
						candidates.timeInterval());
				}
				queue.insertOrUpdate(entry.node(), ctgTime);
			}
			assert queue.getFirstPriority() != null;
			AbstractObject2IntMap.BasicEntry<LabeledNode> entry = queue.extractFirstEntry();
			final int minTime = entry.getIntValue();
			nodes.add(entry.getKey());

			while (!queue.isEmpty()) {
				entry = queue.extractFirstEntry();
				if (entry.getIntValue() == minTime) {
					nodes.add(entry.getKey());
				} else {
					break;
				}
			}

			if (Debug.ON) {
				if (LOG.isLoggable(Level.FINER)) {LOG.finer("Environment chooses that " + Arrays.toString(nodes.toArray()) + " occur at time " + minTime);}
			}
			return new STNURTE.NodeOccurrence(minTime, nodes);
		};

		return () -> strategy;
	}

	/**
	 * @return current time in {@link #dateFormatter} format
	 */
	private static String getNow() {
		return dateFormatter.format(new Date());
	}

	/**
	 * @param value value in nanoseconds
	 *
	 * @return the value in seconds
	 */
	private static double nanoSeconds2Seconds(double value) {
		return value / 1E9;
	}

	/**
	 * Class for representing edge .
	 */
	private final Class<STNUEdgeInt> currentEdgeImplClass = STNUEdgeInt.class;
	/**
	 * The input file names. Each file has to contain a CSTN graph in GraphML format.
	 */
	@Argument(required = true, usage = "Input files. Each input file has to be an STNU graph in GraphML format.", metaVar = "STNU_file_names", handler = StringArrayOptionHandler.class)
	private String[] inputFiles;
	/**
	 *
	 */
	private List<File> instances;
	/**
	 * Parameter for asking how many times to execute a PSTN.
	 */
	@Option(name = "--numExecution", usage = "Number of time to re-execute a PSTN")
	private int nPSTNExecution = 1;
//	/**
//	 * Parameter for asking the execution strategy
//	 */
//	@Option(name = "--executionStrategy", usage = "Execution strategy for ordinary nodes.")
//	private STNURTE.StrategyEnum executionStrategy;

//	/**
//	 *
//	 */
//	@Option(name = "--save", usage = "Save all checked instances in dispatchable form.")
//	private boolean save;

//	/**
//	 * Parameter for asking timeout in sec.
//	 */
//	@Option(name = "--timeOut", usage = "Time in seconds.")
//	private int timeOut = 1800; // 20 min
	/**
	 * Output stream to outputFile
	 */
	private PrintStream output;
	/**
	 * Output file where to write the determined experimental execution times in CSV format.
	 */
	@Option(name = "-o", aliases = "--output", usage = "Output to this file in CSV format. If file is already present, data will be added.", metaVar = "outputFile")
	private File outputFile;
	/**
	 * Software Version.
	 */
	@Option(name = "-v", aliases = "--version", usage = "Version")
	private boolean versionReq;

	/**
	 * Execute the STNU graph as PSTN determining some statistics. It returns the string representing the ({@link #OUTPUT_ROW_ALG_STATS}) part to add to the row
	 * in the statistics file.
	 *
	 * @param pstn a network already initialized.
	 */
	private String executePSTNTest(@Nonnull PSTN pstn, @Nonnull String rowToWrite, //
	                               @Nonnull SummaryStatistics gExecTimeInSec,//
	                               @Nonnull SummaryStatistics gProbConjunctedProbMass,//
	                               @Nonnull Frequency gExecutionExit_E,//
	                               @Nonnull SummaryStatistics gDurationOutsideBoundsInOK_E,//
	                               @Nonnull SummaryStatistics gDurationOutsideBoundsInNOTOK_E,//
	                               @Nonnull Frequency gExecutionExit_M,//
	                               @Nonnull SummaryStatistics gDurationOutsideBoundsInOK_M,//
	                               @Nonnull SummaryStatistics gDurationOutsideBoundsInNOTOK_M//
	                              ) {

		String msg;
		boolean checkInterrupted = false;
		final String fileName = pstn.getG().getFileName().getName();
		PSTNSituation situation = null;
		final SummaryStatistics localExecTimeStat_E = new SummaryStatistics();
		final SummaryStatistics localDurationOutsideBoundsInOK_E = new SummaryStatistics();
		final SummaryStatistics localDurationOutsideBoundsInNOTOK_E = new SummaryStatistics();
		final Frequency localExitExe_E = new Frequency();
		final SummaryStatistics localDurationOutsideBoundsInOK_M = new SummaryStatistics();
		final SummaryStatistics localDurationOutsideBoundsInNOTOK_M = new SummaryStatistics();
		final Frequency localExitExe_M = new Frequency();

//		final STNURTE.Strategy middlePointStrategy = buildMiddlePointStrategy(pstn.getAvgContingentRangeWidth());


		STNURTE.RTEState status = null;
		for (int j = 0; j < nPSTNExecution && !checkInterrupted; j++) {
			LOG.info("Execution " + (j + 1) + "/" + nPSTNExecution + " for PSTN " + fileName);
			final STNURTE stnuRTE = new STNURTE(pstn.getG(), false);

			boolean rteExists = true;
			try {
				situation = buildSituation(pstn);
			} catch (IllegalArgumentException e) {
				msg = getNow() + ": " + fileName + " does not admit a situation. Details:" + e.getMessage() + "\nIgnored.";
				System.out.println(msg);
				LOG.severe(msg);
				checkInterrupted = true;
				continue;
			} catch (IllegalStateException e1) {
				msg = getNow() + ": " + fileName + " cannot be executed for the following unmanaged reason. Details:" + e1.getMessage() +
				      "\nIgnored.";
				LOG.severe(msg);
				checkInterrupted = true;
				continue;
			}
			if (situation == null) {
				msg = getNow() + ": " + fileName + " does not admit a situation";
				System.out.println(msg);
				LOG.severe(msg);
				checkInterrupted = true;
				continue;
			}
			LOG.info("Situation: " + situation);

			final STNURTE.Strategy middlePointStrategy = buildMiddlePointStrategy((int) (situation.maxOutlierSpan() * 2.5));

			if (situation.isWithinBounds()) {
				LOG.info("Execution OKinBounds: All contingent durations are within their bounds. Execution is not necessary.");
				localExitExe_E.addValue(ExitExecution.okInBound);
				localExitExe_M.addValue(ExitExecution.okInBound);
				gExecutionExit_E.addValue(ExitExecution.okInBound);
				gExecutionExit_M.addValue(ExitExecution.okInBound);
//				localExecTimeStat_E.addValue(1);
				continue;
			}

			final STNURTE.Strategy situationStrategy = buildSituationStrategy(situation.situation);

			//EARLY EXECUTION STRATEGY
			LOG.info("***\nTest with FIRST_NODE_EARLY_EXECUTION_STRATEGY\n***");
			try {
				status = stnuRTE.rte(middlePointStrategy, situationStrategy);
			} catch (IllegalStateException e) {
				msg = getNow() + ": " + fileName + " cannot be executed with the current situation. Details: " + e.getMessage()
				      + "\nContinue with another try!";
//				System.out.println(msg);
				LOG.info("EARLY Execution notOKOutBounds: " + msg);
				localExitExe_E.addValue(ExitExecution.notOkOutBounds);
				gExecutionExit_E.addValue(ExitExecution.notOkOutBounds);
				localDurationOutsideBoundsInNOTOK_E.addValue(situation.durationsOutOfBounds());
				rteExists = false;
			}

			if (rteExists) {
				LOG.info("EARLY Execution oKOutBounds.");
				localExitExe_E.addValue(ExitExecution.okOutBounds);
				gExecutionExit_E.addValue(ExitExecution.okOutBounds);
				localExecTimeStat_E.addValue(status.executionTimeRTEns.getMean());
				localDurationOutsideBoundsInOK_E.addValue(situation.durationsOutOfBounds());
			}

			//MIDDLE POINT EXECUTION STRATEGY
			LOG.info("***\nTest with FIRST_NODE_MIDDLE_EXECUTION_STRATEGY\n***");
			try {
				status = stnuRTE.rte(STNURTE.StrategyEnum.FIRST_NODE_MIDDLE_EXECUTION_STRATEGY, situationStrategy);
			} catch (IllegalStateException e) {
				msg =
					getNow() + ": " + fileName + " cannot be executed with the current situation. Details: " + e.getMessage() + "\nContinue with another try!";
//				System.out.println(msg);
				LOG.info("MIDDLE Execution notOKOutBounds: " + msg);
				localExitExe_M.addValue(ExitExecution.notOkOutBounds);
				gExecutionExit_M.addValue(ExitExecution.notOkOutBounds);
				localDurationOutsideBoundsInNOTOK_M.addValue(situation.durationsOutOfBounds());
				continue;
			}
			LOG.info("MIDDLE Execution oKOutBounds.");
			localExitExe_M.addValue(ExitExecution.okOutBounds);
			gExecutionExit_M.addValue(ExitExecution.okOutBounds);
//			localExecTimeStat_E.addValue(status.executionTimeRTEns.getSum());
			localDurationOutsideBoundsInOK_M.addValue(situation.durationsOutOfBounds());
		} // end for checking repetition for a single file

		if (checkInterrupted) {
			return "NOT EXECUTABLE";
		}
		final double localAvgExeInSec = nanoSeconds2Seconds(localExecTimeStat_E.getMean());
		final double localStdDevExeInSec = nanoSeconds2Seconds(localExecTimeStat_E.getStandardDeviation());

		assert situation != null;
		if (!Double.isNaN(localAvgExeInSec)) {
			gExecTimeInSec.addValue(localAvgExeInSec);
			gProbConjunctedProbMass.addValue(situation.conjunctProbabilityMass);
		}
		if (!Double.isNaN(localDurationOutsideBoundsInOK_E.getMean())) {
			gDurationOutsideBoundsInOK_E.addValue(localDurationOutsideBoundsInOK_E.getMean());
		}
		if (!Double.isNaN(localDurationOutsideBoundsInNOTOK_E.getMean())) {
			gDurationOutsideBoundsInNOTOK_E.addValue(localDurationOutsideBoundsInNOTOK_E.getMean());
		}
		if (!Double.isNaN(localDurationOutsideBoundsInOK_M.getMean())) {
			gDurationOutsideBoundsInOK_M.addValue(localDurationOutsideBoundsInOK_M.getMean());
		}
		if (!Double.isNaN(localDurationOutsideBoundsInNOTOK_M.getMean())) {
			gDurationOutsideBoundsInNOTOK_M.addValue(localDurationOutsideBoundsInNOTOK_M.getMean());
		}

		LOG.info(fileName + " has been executed: average execution time [s]: " + localAvgExeInSec);
		rowToWrite += String.format(OUTPUT_ROW_ALG_STATS,//
		                            localAvgExeInSec, //
		                            localStdDevExeInSec,//
		                            situation.conjunctProbabilityMass,//
		                            //EARLY
		                            localExitExe_E.getPct(ExitExecution.okInBound),//
		                            localExitExe_E.getPct(ExitExecution.okOutBounds),//
		                            localExitExe_E.getPct(ExitExecution.notOkOutBounds),//
		                            localDurationOutsideBoundsInOK_E.getMean(),//
		                            localDurationOutsideBoundsInNOTOK_E.getMean(),//
		                            //MIDDLE
		                            localExitExe_M.getPct(ExitExecution.okInBound),//
		                            localExitExe_M.getPct(ExitExecution.okOutBounds),//
		                            localExitExe_M.getPct(ExitExecution.notOkOutBounds),//
		                            localDurationOutsideBoundsInOK_M.getMean(),//
		                            localDurationOutsideBoundsInNOTOK_M.getMean()//
		                           );
		return rowToWrite;
	}

	/**
	 * Simple method to manage command line parameters using {@code args4j} library.
	 *
	 * @param args input arguments
	 *
	 * @return false if a parameter is missing, or it is wrong. True if every parameter is given in a right format.
	 */
	private boolean manageParameters(String[] args) {
		final CmdLineParser parser = new CmdLineParser(this);
		try {
			parser.parseArgument(args);
		} catch (CmdLineException e) {
			// if there's a problem in the command line, you'll get this exception. this will report an error message.
			System.err.println(e.getMessage());
			System.err.println("java -cp CSTNU-<version>.jar -cp it.univr.di.cstnu.DispatchabilityBenchmarkRunner [options...] arguments...");
			// print the list of available options
			parser.printUsage(System.err);
			System.err.println();

			// print option sample. This is useful some time
			// System.err.println("Example: java -jar Checker.jar" + parser.printExample(OptionHandlerFilter.REQUIRED) +
			// " <STNU_file_name0> <STNU_file_name1>...");
			return false;
		}

		if (versionReq) {
			System.out.print(
				getClass().getName() + " " + VERSIONandDATE + ". Academic and non-commercial use only.\n" + "Copyright © 2017-2020, Roberto Posenato");
			return true;
		}

		if (outputFile != null) {
			if (outputFile.isDirectory()) {
				System.err.println("Output file is a directory.");
				parser.printUsage(System.err);
				System.err.println();
				return false;
			}
			// filename has to end with .csv
			if (!outputFile.getName().endsWith(".csv")) {
				if (!outputFile.renameTo(new File(outputFile.getAbsolutePath() + ".csv"))) {
					final String m = "File " + outputFile.getAbsolutePath() + " cannot be renamed!";
					LOG.severe(m);
					System.err.println(m);
					return false;
				}
			}
			try {
				output = new PrintStream(new FileOutputStream(outputFile, true), true, StandardCharsets.UTF_8);
			} catch (IOException e) {
				System.err.println("Output file cannot be created: " + e.getMessage());
				parser.printUsage(System.err);
				System.err.println();
				return false;
			}
		} else {
			output = System.out;
		}

		final String suffix = "pstn";

		instances = new ArrayList<>(inputFiles.length);
		for (final String fileName : inputFiles) {
			final File file = new File(fileName);
			if (!file.exists()) {
				System.err.println("File " + fileName + " does not exit.");
				parser.printUsage(System.err);
				System.err.println();
				return false;
			}
			if (!file.getName().endsWith(suffix)) {
				System.err.println(
					"File " + fileName + " has not the right suffix associated to the suffix of the given network type (right suffix: " + suffix +
					"). Game over :-/");
				parser.printUsage(System.err);
				System.err.println();
				return false;
			}
			instances.add(file);
		}

		return true;
	}

	/**
	 * Loads the file and execute all the actions (specified as instance parameter) on the network represented by the file.
	 *
	 * @param file             input file
	 * @param runState         current state
	 * @param globalStatistics global statistics
	 *
	 * @return true if required task ends successfully, false otherwise.
	 */
	private boolean worker(@Nonnull File file,//
	                       @Nonnull RunMeter runState,//
	                       @Nonnull GlobalStatistics globalStatistics) {

		if (LOG.isLoggable(Level.FINER)) {
			LOG.finer("Loading " + file.getName() + "...");
		}
		final TNGraphMLReader<STNUEdge> graphMLReader = new TNGraphMLReader<>();
		final TNGraph<STNUEdge> graphToExecute;
		try {
			graphToExecute = graphMLReader.readGraph(file, currentEdgeImplClass);
		} catch (Exception e2) {
			final String msg = "File " + file.getName() + " cannot be parsed. Details: " + e2.getMessage() + ".\nIgnored.";
			LOG.warning(msg);
			System.out.println(msg);
			return false;
		}
		LOG.finer("...done!");
		//NODES and EDGES in the original graph. DO NOT MOVE such variable.
		final int nNodes = graphToExecute.getVertexCount();
		final int nEdges = graphToExecute.getEdgeCount();

		final PSTN pstn = new PSTN(graphToExecute);
		try {
			pstn.initAndCheck();
		} catch (Exception e) {
			final String msg = getNow() + ": " + file.getName() + " is not a not well-defined instance. Details:" + e.getMessage() + "\nIgnored.";
			System.out.println(msg);
			LOG.severe(msg);
			return false;
		}
		//Only now contingent node number is significative
		final int nContingents = graphToExecute.getContingentNodeCount();

		final GlobalStatisticsKey globalStatisticsKey = new GlobalStatisticsKey(nNodes, nContingents);

		SummaryStatistics gNetworkEdges = globalStatistics.networkEdges.get(globalStatisticsKey);
		if (gNetworkEdges == null) {
			gNetworkEdges = new SummaryStatistics();
			globalStatistics.networkEdges.put(globalStatisticsKey, gNetworkEdges);
		}
		gNetworkEdges.addValue(nEdges);

		SummaryStatistics gExecTimeInSec = globalStatistics.execTimeInSec.get(globalStatisticsKey);
		if (gExecTimeInSec == null) {
			gExecTimeInSec = new SummaryStatistics();
			globalStatistics.execTimeInSec.put(globalStatisticsKey, gExecTimeInSec);
		}

		SummaryStatistics gProbConjunctedProbMass = globalStatistics.probConjunctedProbMass.get(globalStatisticsKey);
		if (gProbConjunctedProbMass == null) {
			gProbConjunctedProbMass = new SummaryStatistics();
			globalStatistics.probConjunctedProbMass.put(globalStatisticsKey, gProbConjunctedProbMass);
		}

		Frequency gExecutionExit_E = globalStatistics.executionExit_E.get(globalStatisticsKey);
		if (gExecutionExit_E == null) {
			gExecutionExit_E = new Frequency();
			globalStatistics.executionExit_E.put(globalStatisticsKey, gExecutionExit_E);
		}

		SummaryStatistics gDurationOutsideBoundsInOK_E = globalStatistics.durationOutsideBoundsInOK_E.get(globalStatisticsKey);
		if (gDurationOutsideBoundsInOK_E == null) {
			gDurationOutsideBoundsInOK_E = new SummaryStatistics();
			globalStatistics.durationOutsideBoundsInOK_E.put(globalStatisticsKey, gDurationOutsideBoundsInOK_E);
		}

		SummaryStatistics gDurationOutsideBoundsInNOTOK_E = globalStatistics.durationOutsideBoundsInNOTOK_E.get(globalStatisticsKey);
		if (gDurationOutsideBoundsInNOTOK_E == null) {
			gDurationOutsideBoundsInNOTOK_E = new SummaryStatistics();
			globalStatistics.durationOutsideBoundsInNOTOK_E.put(globalStatisticsKey, gDurationOutsideBoundsInNOTOK_E);
		}

		Frequency gExecutionExit_M = globalStatistics.executionExit_M.get(globalStatisticsKey);
		if (gExecutionExit_M == null) {
			gExecutionExit_M = new Frequency();
			globalStatistics.executionExit_M.put(globalStatisticsKey, gExecutionExit_M);
		}

		SummaryStatistics gDurationOutsideBoundsInOK_M = globalStatistics.durationOutsideBoundsInOK_M.get(globalStatisticsKey);
		if (gDurationOutsideBoundsInOK_M == null) {
			gDurationOutsideBoundsInOK_M = new SummaryStatistics();
			globalStatistics.durationOutsideBoundsInOK_M.put(globalStatisticsKey, gDurationOutsideBoundsInOK_M);
		}

		SummaryStatistics gDurationOutsideBoundsInNOTOK_M = globalStatistics.durationOutsideBoundsInNOTOK_M.get(globalStatisticsKey);
		if (gDurationOutsideBoundsInNOTOK_M == null) {
			gDurationOutsideBoundsInNOTOK_M = new SummaryStatistics();
			globalStatistics.durationOutsideBoundsInNOTOK_M.put(globalStatisticsKey, gDurationOutsideBoundsInNOTOK_M);
		}


		String rowToWrite = String.format(OUTPUT_ROW_GRAPH, file.getName(), Integer.valueOf(nNodes), Integer.valueOf(nContingents), Integer.valueOf(nEdges));

//		final PSTN.PSTNCheckStatus status;
		LOG.info(getNow() + ": Executing PSTN: start.");
		rowToWrite = executePSTNTest(pstn, rowToWrite,//
		                             gExecTimeInSec,//
		                             gProbConjunctedProbMass,//
		                             gExecutionExit_E,//
		                             gDurationOutsideBoundsInOK_E,//
		                             gDurationOutsideBoundsInNOTOK_E,//
		                             gExecutionExit_M,//
		                             gDurationOutsideBoundsInOK_M,//
		                             gDurationOutsideBoundsInNOTOK_M//
		                            );
		LOG.info(getNow() + ": Executing PSTN: finished.");

		output.println(rowToWrite);
		output.flush();
		runState.printProgress();
		return !rowToWrite.contains("NOT EXECUTABLE");
	}
}