/* THIS PUBLIC DOMAIN SOFTWARE WAS PRODUCED BY AN EMPLOYEE OF U.S. GOVERNMENT 
 * AS PART OF THEIR OFFICIAL DUTIES.
 */

package fractalRabbitGenerator;

import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;

import simulators.AgoraphobicPoints;
import simulators.Retropreferential;
import simulators.SporadicReporter;
import utilities.EuclideanPoint;
import utilities.PoissonVariate;

/**
 * Invokes all three generators in the <code>simulators</code> package using
 * parameters supplied in input file, constructs synthetic waypoints, and writes
 * them to CSV.
 * <p>
 * Parameters are entered via a CSV file.
 * <p>
 * Reference: R. W. R. Darling, "Retro-preferential Stochastic Mobility Models
 * on Random Fractals Under Sporadic Observations", <a href =
 * "https://www.researchgate.net/publication/340741639_Retro-preferential_Stochastic_Mobility_Models_on_Random_Fractals_Under_Sporadic_Observations">
 * DOI: 10.13140/RG.2.2.15267.40489</a>, 2018
 * 
 * 
 * @author rwdarli
 * @since 4.15.2022
 */
public class MainClassFR {
	/*
	 * Three tiers of the FRACTALRABBIT simulator
	 */
	private AgoraphobicPoints app;
	private Retropreferential rpp;
	private SporadicReporter spore;
	private List<List<Integer>> trajectoryList;
	private Map<Integer, Integer> trajectoryAssignment; // traveler to trajectory number
	private List<Integer> coTravellers;
	private Map<Integer, List<Double>> reportTimesAssignment; // traveler to list of report times
	private Map<Integer, List<Integer>> reportPlacesAssignment; // traveler to list of point references (at given times)

	/**
	 * 
	 * @param n            # points in Agoraphobic point process
	 * @param d            ambient dimension of points generated by Agoraphobic
	 *                     point process, e.g. 2
	 * @param h            fractal dimension for AgoraphobicPoints, e.g. 1.3
	 * @param theta        non-negative double controlling rate at which additional
	 *                     clumps form in Agoraphobic Point Process
	 * @param exponent     negative double to convert distances to entries in
	 *                     Retro-preferential matrix
	 * @param maxKmPerHour speed bound for travelers
	 * @param kmper1       conversion between kilometers and abstract distance units
	 *                     for model
	 */
	public MainClassFR(int n, int d, double h, double theta, double exponent, double maxKmPerHour, double kmper1) {
		this.app = new AgoraphobicPoints(n, d, h, theta);
		this.app.generatePoints();
		EuclideanPoint[] euclidPoints = new EuclideanPoint[app.getPoints().size()];
		Arrays.setAll(euclidPoints, j -> new EuclideanPoint(app.getPoints().get(j)));
		this.rpp = new Retropreferential(euclidPoints, exponent);
		this.spore = new SporadicReporter(euclidPoints, maxKmPerHour, kmper1);
		this.trajectoryList = new ArrayList<List<Integer>>();
		this.coTravellers = new ArrayList<Integer>();
		/*
		 * The following three maps all have the SAME key set, i.e. shuffled travelers
		 */
		this.trajectoryAssignment = new HashMap<Integer, Integer>(); // assigns traveler to trajectory
		this.reportTimesAssignment = new HashMap<Integer, List<Double>>(); // assigns traveler to times
		this.reportPlacesAssignment = new HashMap<Integer, List<Integer>>(); // assigns traveler to places
	}

	/**
	 * Assign travelers to trajectories, with randomization
	 * 
	 * @param numCoTravelers small number, such as five, of the travelers who will
	 *                       follow asynchronously the same trajectory
	 * @param numTravelers   total number of travelers
	 */
	public void travelerConfigure(int numCoTravelers, int numTravelers) {
		Integer[] travelerIDs = new Integer[numTravelers];
		Arrays.setAll(travelerIDs, i -> i);
		/*
		 * Shuffle the traveler IDs, and assign the first #(numCoTravelers) to first
		 * trajectory
		 */
		List<Integer> travelerShuffle = Arrays.asList(travelerIDs);
		Collections.shuffle(travelerShuffle);
		// All the co-travelers are assigned to trajectory 0, and their labels are
		// recorded
		for (int t = 0; t < numCoTravelers; t++) {
			this.trajectoryAssignment.put(travelerShuffle.get(t), 0);
			this.coTravellers.add(travelerShuffle.get(t));
		}
		// Other travelers are assigned to trajectories 1 through numTrajectories - 1
		for (int t = numCoTravelers; t < numTravelers; t++) {
			this.trajectoryAssignment.put(travelerShuffle.get(t), t - numCoTravelers + 1);
		}
	}

	/**
	 * The main class accepts input parameters, generates synthetic mobility data,
	 * and write the output to the specified destination file.
	 * 
	 * @param args[0] in Path to input file parameters.csv, containing seven principal
	 *                parameters.
	 * @param args[1] is Path to an output file
	 *
	 *                Produces an output csv file where each line has the format -
	 *                identifier (integer), - time (days), - latitude, - longitude
	 *                <p>
	 *                or else - identifier (integer), - time (days), - x (km), - y
	 *                (km)
	 *                <p>
	 *                Here identifier refers to a specific traveler.
	 * 
	 */
	public static void main(String[] args) throws IOException {

		try {
			Reader inputParameterFile = new FileReader(args[0]);
			String csvOutputFile = args[1];
			Iterable<CSVRecord> records = CSVFormat.RFC4180.parse(inputParameterFile);
			List<String> par = new ArrayList<>(); // parameters as strings
			for (CSVRecord record : records) {
				System.out.println(record.get(0) + " has value " + record.get(1));
				par.add(record.get(1).replaceAll("\\s", "")); // remove whitespace
			}
			/*
			 * Small number of parameters which will often change
			 */
			int dimension = Integer.parseInt(par.get(0));
			double h = Double.parseDouble(par.get(1)); // fractal dimension for AgoraphobicPoints
			int n = Integer.parseInt(par.get(2));// # points in Agoraphobic points process
			int numTravelers = Integer.parseInt(par.get(3));
			int numCoTravelers = Integer.parseInt(par.get(4));
			double days = Double.parseDouble(par.get(5)); // Sporadic Reporter - duration in days
			double countMean = Double.parseDouble(par.get(6)); // Sporadic Reporter - # reports - mean value

			/*
			 * Large number of parameters which seldom change
			 */

			double theta = 0.75; // restart rate for AgoraphobicPoints
			double exponent = -2.0; // convert distances to Retro-preferential matrix
			double speedbound = 50.0; // Sporadic Reporter - units per day
			double kilometersPerUnit = 200.0; // Sporadic Reporter - # km per one distance unit for points
			/*
			 * Instantiate generator with these parameters. Here the points are generated.
			 */
			MainClassFR frg = new MainClassFR(n, dimension, h, theta, exponent, speedbound, kilometersPerUnit);
			System.out.println(n + " points generated for fractal dimension " + h);
			/*
			 * Generate the trajectories (points exist already). Each trajectory needs a
			 * random phi, random # of steps, random start point,
			 */
			double phiMean = 10.0; // Retro-preferential exploration - mean value
			PoissonVariate phiRandom = new PoissonVariate(phiMean);
			double stepsMean = 100.0; // Retro-preferential trajectory length - mean value
			PoissonVariate stepsRandom = new PoissonVariate(stepsMean);
			Random g = new Random();
			/*
			 * Set up co-travel structure
			 */
			frg.travelerConfigure(numCoTravelers, numTravelers);
			int numTrajectories = numTravelers - numCoTravelers + 1;// all co-travelers use same trajectory
			// Loop through trajectories
			for (int t = 0; t < numTrajectories; t++) {
				// new trajectory with Poisson phi, Poisson # of steps, uniform random start
				// point
				frg.trajectoryList.add(frg.rpp.trajectory(phiRandom.generate(), stepsRandom.generate(), g.nextInt(n)));
			}

			/*
			 * Sporadic reporting parameters
			 */
			double delta = 0.001; // Sporadic Reporter - Pareto cutoff
			double alpha = -1.5; // Sporadic Reporter - Pareto tail
			PoissonVariate countRandom = new PoissonVariate(countMean);

			System.out.println(
					numTrajectories + " trajectories generated, in which travelers assigned to same trajectory are: ");
			for (Integer p : frg.coTravellers) {
				System.out.print(p + ", ");
			}
			System.out.println();
			/*
			 * Loop through travelers, attaching trajectory to each. p < # travelers,
			 * defined above as (numTrajectories - 1 + numCoTravelers) Co-travelers will
			 * receive same trajectory.
			 * 
			 * ERROR HERE!
			 */
			int currentTrajectory;
			for (Integer p : frg.trajectoryAssignment.keySet()) {
				currentTrajectory = frg.trajectoryAssignment.get(p); // trajectory # for traveler p
				frg.spore.embedTrajectory(frg.trajectoryList.get(currentTrajectory)); // embed in continuous time
				frg.spore.generateReports(frg.trajectoryList.get(currentTrajectory), countRandom.generate(), delta,
						alpha, days); // generate sporadic reports for this trajectory
				frg.reportTimesAssignment.put(p, List.copyOf(frg.spore.getReportTimes())); // tag p with Report Times
				frg.reportPlacesAssignment.put(p, List.copyOf(frg.spore.getReportPlaces())); // tag p with Report Places
			}
			/*
			 * Check that the trajectories differ , by inspecting first element of each -
			 * FAILED!
			 */
			for (Integer p : frg.reportTimesAssignment.keySet()) {
				System.out.println(
						"First report for traveler " + p + " at time " + frg.reportTimesAssignment.get(p).get(0)
								+ " is place " + frg.reportPlacesAssignment.get(p).get(0));
			}
			System.out.println("Reports have been generated for " + numTravelers + " travelers.");
			/*
			 * Need only format the data as (traveler ID, time, x in km, y in km), and write
			 * to csv using Apache Commons CSV
			 */
			// CSVFormat.Builder.create();
			try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(csvOutputFile + "XY.csv"));
					CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
				csvPrinter.printRecord("ID", "Days", "x(km)", "y(km)"); // header
				double x, y, time;
				int place;
				int lines = 0;
				// Write to file ERROR - repetitions of one report set!

				for (Integer p : frg.reportTimesAssignment.keySet()) {
					for (int t = 0; t < frg.reportTimesAssignment.get(p).size(); t++) {
						time = frg.reportTimesAssignment.get(p).get(t);
						place = frg.reportPlacesAssignment.get(p).get(t);
						if (t == 0) {
							System.out.println("First report for traveler " + p + " is point "
									+ frg.reportPlacesAssignment.get(p).get(t) + " at time "
									+ frg.reportTimesAssignment.get(p).get(t));
						}
						x = kilometersPerUnit * frg.app.getPoints().get(place)[0];
						y = kilometersPerUnit * frg.app.getPoints().get(place)[1];
						csvPrinter.printRecord(p, time, x, y);
						lines++;
					}
				}

				System.out.println("CSV file created with " + lines + " lines, called " + csvOutputFile + "XY.csv");
				csvPrinter.flush();
				writer.flush();
				writer.close();
			}

			catch (IOException ex) {
				ex.printStackTrace();
			}

			/*
			 * SECOND VERSION! (traveler ID, time, place ID), and write to csv using Apache
			 * Commons CSV
			 */

			try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(csvOutputFile + "PLACES.csv"));
					CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
				csvPrinter.printRecord("Traveler ID", "Days", "Place ID");
				int lines = 0;
				// Write to file ERROR - repetitions of one report set!

				for (Integer p : frg.reportTimesAssignment.keySet()) {
					for (int t = 0; t < frg.reportTimesAssignment.get(p).size(); t++) {
						csvPrinter.printRecord(p, frg.reportTimesAssignment.get(p).get(t),
								frg.reportPlacesAssignment.get(p).get(t));
						lines++;
					}
				}
				System.out.println(
						"CSV Places file created with " + lines + " lines, called " + csvOutputFile + "PLACES.csv");
				csvPrinter.flush();
				writer.flush();
				writer.close();
			}

		} catch (

		IOException ex) {
			System.out.println(ex.toString());
			System.out.println("Could not find parameter file.");
		}

	}

}
