package mior.controler;


import java.io.IOException;
import java.util.Arrays;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import mcmas.core.MCMChrono;
import mior.model.IMiorModel;
import mior.model.ModelFactory;
import mior.view.MiorResultWriter;
import mior.view.MiorView;
import joptsimple.OptionParser;
import joptsimple.OptionSet;

public class MiorLauncher {
	
	private final ModelFactory factory;
	
	private int modelVersion;
	private boolean topologyOnly;
	private boolean guiEnabled;
	private String outputFile;
	private String prefix;
	
	private int nbRepetitions;
	private int nbSimulations;
	//private int nbKernels;
	
	private final static Logger logger = LoggerFactory.getLogger(MiorLauncher.class);
	
	public MiorLauncher() {
		this.modelVersion  = 4;
		this.factory       = new ModelFactory();
		this.topologyOnly  = false;
		this.guiEnabled    = false;
		this.outputFile    = "output.dat";
		this.prefix        = "";
		
		this.nbRepetitions     = 0;
		//this.nbKernels     = 0;
		this.nbSimulations     = 0;
	}
	
	public void setTopologyOnly(boolean topologyOnly) {
		this.topologyOnly = topologyOnly;
	}
	
	public boolean isTopologyOnly() {
		return topologyOnly;
	}
	
	public String getPrefix() {
		return prefix;
	}
	
	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}
	
	public int getModelVersion() {
		return modelVersion;
	}
	
	public void setModelVersion(int modelVersion) {
		this.modelVersion = modelVersion;
	}
	
	public void setBatchSize(int batchSize) {
		this.nbRepetitions = batchSize;
	}
	
	public int getBatchSize() {
		return nbRepetitions;
	}
	
	/*
	public void setNbKernels(int nbKernels) {
		this.nbKernels = nbKernels;
	}
	
	public int getNbKernels() {
		return nbKernels;
	}*/
	
	public void setTotalSize(int totalSize) {
		this.nbSimulations = totalSize;
	}
	
	public int getTotalSize() {
		return nbSimulations;
	}
	
	public String getOutputFile() {
		return outputFile;
	}
	
	public void setOutputFile(String outputFile) {
		this.outputFile = outputFile;
	}
	
	public boolean isGuiEnabled() {
		return guiEnabled;
	}
	
	public void setGuiEnabled(boolean guiEnabled) {
		this.guiEnabled = guiEnabled;
	}
	
	public ModelFactory getFactory() {
		return factory;
	}
	
	public void start(int nbMM, int nbOM, int scale) {
		logger.info("Simulation size: " + nbMM + "x" + nbOM + " (scale: " + scale + ")");
		IMiorModel model = factory.createModel(nbMM, nbOM, scale, modelVersion);
		model.addUpdateListener(new MiorResultWriter(outputFile));
		
		if (guiEnabled) {
			startGUI(model);
		}
		
		if ((nbRepetitions < 1 || nbSimulations < 1) && !guiEnabled) {
			throw new RuntimeException("No GUI and no simulation to execute");
		}
		
		System.err.println("# Running " + nbRepetitions + " time(s) the simulation");
		
		//System.out.println(guiEnabled);
		//System.out.println(nbKernels);
		long [] results = new long[nbRepetitions];
		final int groupSize = getFactory().getGroupSize();
		
		final int nbKernels = (nbSimulations % groupSize == 0 ? nbSimulations / groupSize : nbSimulations / groupSize + 1);
		final int actualTotalSize = nbKernels * groupSize;
		final double correctionFactor = (1.0d * nbSimulations) /  actualTotalSize;
		
		System.err.println("# " + correctionFactor);
		
		for (int i = 0; i < nbRepetitions; i++) {
			MCMChrono simChrono = new MCMChrono("Mior simulation");
			
			System.err.println("# Running " + nbKernels + " times(s) a kernel of size " + groupSize + " to complete the simulation");
			simChrono.start();
			
			for (int j = 0; j < nbKernels; j++) {
				
				if (topologyOnly) {
					model.reset();
					model.doTopology();
				} else {
					model.reset();
					model.doAutoLive();
				}
			}
			
			results[i] = (long) (simChrono.stop().getValue() * correctionFactor);
		}
		
		//System.out.println(Arrays.toString(results));
		if (nbRepetitions != 0) {
			printStats(results);
		}
		
		if (! guiEnabled) {
			model.release();
		}
	}
	
	private void startGUI(IMiorModel model) {
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (final Exception e) {
			e.printStackTrace();
		}
		
		final MiorView view = new MiorView(model, "MIOR Model version: " + modelVersion);
		
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				view.getFrame().setVisible(true);
			}

		});
	}
	
	private void printStats(long [] rawResults) {
		// Sort the results
		long [] results = rawResults.clone(); //Arrays.copyOf(rawResults, rawResults.length);
		Arrays.sort(results);
		
		// Remove the min and max if we have more than 5 values
		if (results.length >= 5) {
			results = Arrays.copyOfRange(results, 1, results.length - 1);
		}
		
		// Min and max time
		final long min = results[0];
		final long max = results[results.length - 1];
		
		// Algebric mean
		double totalTime = 0;
		
		for (long time : results) {
			totalTime += time;
		}
		
		final double mean = totalTime / results.length;
		
		// ET computation
		
		long totalET = 0;
		
		for (long time : results) {
			totalET += (time - mean) * (time - mean);
		}
		
		final double ET = Math.sqrt(totalET / results.length);
		
		// Display
		
		System.out.println("# Min Max Avg ET");
		System.out.println((prefix != null ? prefix + " " : "") + min + " " + max + " " + mean + " " + ET);
	}
	
	public static void main(String[] args) {
		final MiorLauncher simulation = new MiorLauncher();
		
		OptionParser parser = new OptionParser();
		
		parser.acceptsAll(Arrays.asList("o", "output"), "Output File")
				.withRequiredArg().ofType(String.class)
				.defaultsTo("output.dat");
		
		/*
		 * batchSize  : Number of time to repeat (and average results) the whole simulation
		 * totalSize  : Total number of MIOR simulations to execute
		 * kernelSize : Total number of MIOR to execute in the same kernel (par. implementations only)
		 */
		
		parser.acceptsAll(Arrays.asList("b", "batchSize", "r", "repeat"),
				"Number of time to repeat (for stats) the whole simulation")
				.withOptionalArg().ofType(Integer.class).defaultsTo(20);
		
		parser.acceptsAll(Arrays.asList("ts", "totalSize", "n", "nbSimulations"),
				"Total number of MIOR simulations to execute")
				.withRequiredArg().ofType(Integer.class).defaultsTo(0);
		
		parser.acceptsAll(Arrays.asList("ks", "kernelSize", "groupSize"),
				"Number of simulations to execute each kernel (PAR. IMPLEMENTATIONS ONLY)")
				.withRequiredArg().ofType(Integer.class);
		
		parser.acceptsAll(Arrays.asList("r", "random", "randomizePopulations"),
				"Enable randomization of Mior models").withOptionalArg()
				.ofType(Boolean.class).defaultsTo(true);
		
		parser.acceptsAll(Arrays.asList("c", "copy", "alwaysCopy"),
				"Always copy data between GPU and CPU").withOptionalArg()
				.ofType(Boolean.class).defaultsTo(true);
		
		parser.acceptsAll(Arrays.asList("j", "java"),
				"Use the Java implementation");
		
		parser.acceptsAll(Arrays.asList("s", "scale"),
				"Scale the model by the given factor").withRequiredArg()
				.ofType(Integer.class).defaultsTo(1);
		
		parser.accepts("mm", "Number of MM to use").withRequiredArg()
				.ofType(Integer.class).defaultsTo(38);
		
		parser.accepts("om", "Number of OM to use").withRequiredArg()
				.ofType(Integer.class).defaultsTo(310);
		
		parser.acceptsAll(Arrays.asList("v", "version"),
				"Use this OpenCL implementation version").withRequiredArg()
				.ofType(Integer.class).defaultsTo(5);
		
		parser.acceptsAll(Arrays.asList("p", "prefix"),
				"Prefix to use for output").withRequiredArg()
				.ofType(String.class);
		
		parser.acceptsAll(Arrays.asList("t", "topoOnly"),
				"Execute only the topology step").withOptionalArg()
				.ofType(Boolean.class).defaultsTo(false);
		
		parser.acceptsAll(Arrays.asList("g", "gui"), "Enable GUI")
				.withOptionalArg().ofType(Boolean.class).defaultsTo(true);
		
		parser.acceptsAll(Arrays.asList("h", "help"), "Print this help");
		
		OptionSet options = parser.parse(args);
		
		/**
		 * HELP
		 */
		
		if (options.has("help")) {
			try {
				parser.printHelpOn(System.out);
				return;
			} catch (IOException e) {
				System.err.println("Failed to print help to standard output");
				e.printStackTrace();
			}
		}
		
		/**
		 * PARALLELIZATION SETTINGS
		 */
		
		if (options.has("batchSize")) {
			simulation.getFactory().setBatchModeEnabled(true);
			simulation.setBatchSize((Integer) options.valueOf("batchSize"));
		}

		
		if (options.has("totalSize")) {
			int totalSize = (Integer) options.valueOf("totalSize");
			simulation.setTotalSize(totalSize);
			
			if (options.has("kernelSize")) {
				int kernelSize = (Integer) options.valueOf("kernelSize");
				
				if (kernelSize < 1) {
					throw new RuntimeException("kernelSize must be a divider of the total number of simulations (" + totalSize  + ")");
				}
				
				//simulation.setNbKernels(totalSize % kernelSize == 0 ? totalSize / kernelSize : (totalSize / kernelSize) + 1);
				simulation.getFactory().setGroupSize(kernelSize);
			} else {
				//simulation.setNbKernels(totalSize);
				simulation.getFactory().setGroupSize(1);
			}
		}
		
		if (options.has("blockSize")) {
			simulation.getFactory().setBlockSize((Integer) options.valueOf("blockSize"));
		}
		
		if (options.has("random")) {
			final boolean random = (Boolean) options.valueOf("random");
			simulation.getFactory().setRandomEnabled(random);
		}
		
		if (options.has("alwaysCopy")) {
			final boolean copy = (Boolean) options.valueOf("alwaysCopy");
			simulation.getFactory().setBatchModeEnabled(!copy);
		}
		
		/**
		 * MODEL VERSION SETTINGS
		 */
		
		if (options.has("java")) {
			if (! options.has("version")) {
				simulation.setModelVersion(0);
			} else {
				System.err.println("Error: Java and OpenCL implementation can't be used at the same time.");
				System.exit(1);
			}
		} else {
			simulation.setModelVersion((Integer) options.valueOf("version"));
		}
		
		/**
		 * OTHER PARAMETERS
		 */
		
		if (options.has("gui")) {
			simulation.setGuiEnabled((Boolean) options.valueOf("gui"));
		} else if (simulation.getFactory().getGroupSize() == 0) {
			simulation.setGuiEnabled(true);
		}
		
		simulation.setTopologyOnly((Boolean) options.valueOf("topoOnly"));
		simulation.setOutputFile((String) options.valueOf("output"));
		simulation.setPrefix((String) options.valueOf("prefix"));
		
		final int nbMM = (Integer) options.valueOf("mm");
		final int nbOM = (Integer) options.valueOf("om");
		final int scale = (Integer) options.valueOf("scale");
		
		simulation.start(nbMM, nbOM, scale);
	}

}
