package collembola;

import java.net.MalformedURLException;
import java.net.URL;

import joptsimple.OptionParser;
import joptsimple.OptionSet;
import mcmas.core.MCMChrono;
import mcmas.core.MCMCommandQueue;
import mcmas.core.MCMCommandQueueProperty;
import mcmas.core.MCMContext;
import mcmas.core.MCMEvent;
import mcmas.core.MCMKernel;
import mcmas.core.MCMMem;
import mcmas.core.MCMProgram;
import mcmas.core.MCMUtils;

import org.jocl.CL;
import org.jocl.Pointer;

import collembola.model.CollemLoader;
import collembola.model.CollemModel;

public class CollemSimulation {
	
	private final CollemModel model;
	
	private MCMContext context;
	private final MCMCommandQueue queue;
	private final MCMProgram program;
	
	//private final OCLKernel kernel;
	private final MCMKernel reproduceKernel;
	private final MCMKernel newArrivalsKernel;
	private final MCMKernel diffuseKernel;
	private final MCMKernel deathKernel;
	
	//private final OCLMem patchesMem;
	//private final OCLMem terrainsMem;
	
	//private final IntBuffer patchesPopulations;
	//private final IntBuffer patchesOverflows;
	//private final IntBuffer patchesOwners;
	//private final IntBuffer patchesTypes;
	//private final IntBuffer plotsPopulations;
	//private final IntBuffer plotSurface;
	//private final CollemWorld world;
	
	private final int [] patchesPopulations;
	private final int [] newPopulations;
	private final int [] patchesOverflows;
	private final int [] plotsPopulations;
	
	private final MCMMem patchesPopulationsMem;
	private final MCMMem patchesOverflowsMem;
	private final MCMMem patchesOwnersMem;
	private final MCMMem patchesTypesMem;
	
	private final MCMMem newPopulationsMem;
	
	private final MCMMem plotsPopulationsMem;
	private final MCMMem plotsSurfacesMem;
	
	private final MCMMem worldMem;
	
	public CollemSimulation(CollemModel model) {
		this(model, new MCMContext());
	}
	
	public CollemSimulation(CollemModel model, MCMContext context) {
		this.model = model;
		
		this.context = context;
		this.queue = context.createCommandQueue(MCMCommandQueueProperty.ENABLE_PROFILING);
		
		//this.patchesMem = context.newBuffer().Using(model.getPatches()).b();
		//this.terrainsMem = context.newBuffer().Using(model.getTerrains()).b();
		//this.populationsMem = context.newBuffer().Using(model.getPopulations()).b();
		//this.worldMem = context.newBuffer().Using(model.getWorld()).b();
		
		this.program = MCMUtils.compileFile(context, "kernels/collembola.cl",
				" -DWIDTH=" + model.getWidth() + " -DHEIGHT=" + model.getHeight());
		//this.kernel = program.createKernel("collembola");
		
		//this.kernel = program.createKernel("diffusion");
		this.reproduceKernel = program.createKernel("reproduction");
		this.newArrivalsKernel = program.createKernel("newArrivals");
		this.diffuseKernel = program.createKernel("diffusion");
		this.deathKernel = program.createKernel("death");
		
		/*this.patchesPopulations = new int[(model.getHeight() + 2) * (model.getWidth() + 2)];
		this.newPopulations = new int[(model.getHeight() + 2) * (model.getWidth() + 2)];
		this.patchesOverflows = new int[model.getHeight() * model.getWidth()];*/
		
		this.patchesPopulations = model.getPatchesPopulations();
		//this.patchesPopulations = new int[model.getHeight() * model.getWidth()];
		this.newPopulations = new int[model.getHeight() * model.getWidth()];
		this.patchesOverflows = new int[(model.getHeight() + 2) * (model.getWidth() + 2)];
		
		for (int i = 0; i < model.getWorld().width; i++) {
			this.patchesPopulations[model.getHeight() / 2 * model.getWidth() + i] = 100;
		}
		//this.patchesPopulations[8 * 8] = 1000;
		//this.patchesPopulations[8 * 8] = 1000;
		
		this.plotsPopulations = new int[model.getPlotsSurfaces().length];
		
		this.patchesPopulationsMem = context.newBuffer().Using(patchesPopulations).b();
		this.newPopulationsMem = context.newBuffer().Using(newPopulations).b();
		
		this.patchesOwnersMem = context.newBuffer().Using(model.getPatchesOwners()).b();
		this.patchesTypesMem = context.newBuffer().Using(model.getPatchesTypes()).b();
		this.patchesOverflowsMem = context.newBuffer().Using(patchesOverflows).b();
		
		this.plotsSurfacesMem = context.newBuffer().Using(model.getPlotsSurfaces()).b();
		this.plotsPopulationsMem = context.newBuffer().Using(plotsPopulations).b();
		this.worldMem = context.newBuffer().Using(model.getWorld()).b();
	}
	
	/*
	public void prepare() {
		int [] terrains = model.getTerrains();
		
		System.out.println(Arrays.toString(model.getPopulations()));
		
		//kernel.setArguments(patchesMem, model.getWidth());
		kernel.setArguments(populationsMem, terrainsMem, model.getWidth(), model.getHeight());
		queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() });
		
		queue.blockingReadBuffer(terrainsMem, Pointer.to(terrains), 0, terrainsMem.getSize());
		
		
		System.out.println(Arrays.toString(terrains));
		
		queue.enqueueCopyBuffer(terrainsMem, populationsMem, 0, 0, terrainsMem.getSize());
		queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() });
		queue.blockingReadBuffer(terrainsMem, Pointer.to(terrains), 0, terrainsMem.getSize());
		
		//System.out.println(Arrays.toString(model.getPatches()));
		//System.out.println(Arrays.toString(terrains));
		
		System.out.println(Arrays.toString(terrains));
		
		queue.enqueueCopyBuffer(terrainsMem, populationsMem, 0, 0, terrainsMem.getSize());
		queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() });
		queue.blockingReadBuffer(terrainsMem, Pointer.to(terrains), 0, terrainsMem.getSize());
		
		//System.out.println(Arrays.toString(model.getPatches()));
		//System.out.println(Arrays.toString(terrains));
		
		System.out.println(Arrays.toString(terrains));
		
		queue.enqueueCopyBuffer(terrainsMem, populationsMem, 0, 0, terrainsMem.getSize());
		queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() });
		queue.blockingReadBuffer(terrainsMem, Pointer.to(terrains), 0, terrainsMem.getSize());
		
		//System.out.println(Arrays.toString(model.getPatches()));
		//System.out.println(Arrays.toString(terrains));
		
		System.out.println(Arrays.toString(terrains));
		
		queue.enqueueCopyBuffer(terrainsMem, populationsMem, 0, 0, terrainsMem.getSize());
		queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() });
		queue.blockingReadBuffer(terrainsMem, Pointer.to(terrains), 0, terrainsMem.getSize());
		
		//System.out.println(Arrays.toString(model.getPatches()));
		//System.out.println(Arrays.toString(terrains));
		
		System.out.println(Arrays.toString(terrains));
	}*/
	
	public void run() {
		newArrivalsKernel.setArguments(worldMem, patchesPopulationsMem, patchesOwnersMem, plotsPopulationsMem, plotsSurfacesMem);
		reproduceKernel.setArguments(worldMem, patchesPopulationsMem, patchesOwnersMem, plotsPopulationsMem);
		diffuseKernel.setArguments(worldMem, patchesPopulationsMem, patchesOverflowsMem, newPopulationsMem);
		deathKernel.setArguments(worldMem, patchesPopulationsMem, patchesTypesMem);
		
		MCMEvent newArrivalFinished, reproduceFinished, diffuseFinished, copyFinished, deathFinished;
		
		int i = 1;
		
		MCMChrono totalTime = new MCMChrono("totalTime");
		totalTime.start();
		
		while (true) {
			newArrivalFinished = enqueueKernel(newArrivalsKernel);
			reproduceFinished = enqueueKernel(reproduceKernel, newArrivalFinished);
			diffuseFinished = enqueueKernel(diffuseKernel, reproduceFinished);
			copyFinished = queue.enqueueCopyBuffer(newPopulationsMem, patchesPopulationsMem, 0L, 0L, patchesPopulationsMem.getSize(), diffuseFinished);
			MCMEvent.waitFor(copyFinished);
			deathFinished = enqueueKernel(deathKernel, copyFinished);
			
			queue.blockingReadBuffer(plotsPopulationsMem, Pointer.to(plotsPopulations), 0, plotsPopulationsMem.getSize(), deathFinished);
			queue.blockingReadBuffer(patchesPopulationsMem, Pointer.to(patchesPopulations), 0, patchesPopulationsMem.getSize(), deathFinished);
			queue.blockingReadBuffer(patchesOverflowsMem, Pointer.to(patchesOverflows), 0, patchesOverflowsMem.getSize(), deathFinished);
			/*queue.flush();
			queue.finish();*/
			
			/*OCLUtils.printEventStats("newArrivals", newArrivalFinished);
			OCLUtils.printEventStats("reproduction", reproduceFinished);
			OCLUtils.printEventStats("diffusion", diffuseFinished);
			OCLUtils.printEventStats("swapBuffers", copyFinished);
			OCLUtils.printEventStats("death", deathFinished);*/
						
			// FIXME
			//CollemUtils.savePicture(patchesPopulations, model.getWidth(), "pop", i);
			//CollemUtils.savePicture(patchesOverflows, model.getWidth() + 2, "over", i);
			i++;
			
			if (i == 100) break;
		}
		
		//queue.flush();
		queue.finish();
		
		System.out.println("For " + i + "iterations," + totalTime.stop());
	}

	public void release() {
		worldMem.release();
		
		patchesOverflowsMem.release();
		patchesTypesMem.release();
		patchesOwnersMem.release();
		patchesPopulationsMem.release();
		newPopulationsMem.release();
		
		plotsPopulationsMem.release();
		plotsSurfacesMem.release();
		
		newArrivalsKernel.release();
		reproduceKernel.release();
		diffuseKernel.release();
		deathKernel.release();
		
		program.release();
		queue.release();
		context.release();
	}
	
	public static void main(String[] args) throws MalformedURLException {
		CollemModel model = CollemLoader.load(256, 256, new URL("file://data/site4.shp"));
		model.getWorld().growthRate = 3f;
		model.getWorld().diffusionThreshold = 0f;
		model.getWorld().diffusionRate = 0.5f;
		
		OptionParser parser = new OptionParser();
		
		parser.accepts("cpu", "Force OpenCL CPU mode");
		
		OptionSet options = parser.parse(args);
		
		MCMContext context = (options.has("cpu") ? new MCMContext(CL.CL_DEVICE_TYPE_CPU) : new MCMContext());
		
		//model.savePicture();
		CollemSimulation sim = new CollemSimulation(model, context);
		
		//System.out.println(Arrays.toString(model.getPatchesOwners()));
		//CollemUtils.savePicture(model.getPatchesOwners(), model.getWidth(), "owners", 0);
		//CollemUtils.savePicture(sim.patchesPopulations, model.getWidth(), "pop", 0);
		sim.run();
	}
	
	private MCMEvent enqueueKernel(MCMKernel kernel, MCMEvent... events) {
		MCMEvent e = queue.enqueueKernel(kernel, 2, new long[] { model.getHeight(),  model.getWidth() }, events);
		//queue.flush();
		//queue.finish();-
		return e;
	}
	
}
