package mior.model;

import java.util.Arrays;
import java.util.BitSet;

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 mior.model.dist.IMiorDistribution;

import org.jocl.Pointer;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class OCLParaCSRModel4 extends AbstractMiorModel {
	
	/**
	 * Java view
	 */
	
	private final int nbMM;
	private final int nbOM;
	
	private final MiorMM [] mmList;
	private final MiorOM [] omList;
	private final MiorWorld [] worlds;
	
	/**
	 * OpenCL implementation
	 */
	final MCMContext context;
	final MCMCommandQueue queue;
	final MCMProgram program;
	
	final MCMKernel topoKernel;
	final MCMKernel simulateKernel;
	final MCMKernel autoliveKernel;
	
	final MCMMem mmMem;
	final MCMMem omMem;
	final MCMMem worldsMem;
	
	final MCMMem mmOffsetsMem;
	final MCMMem omOffsetsMem;
	
	MCMMem mmCSRMem;
	MCMMem omCSRMem;
	
	final MCMMem partsMem;
	
	private final int [] mmOffsets;
	private final int [] omOffsets;
	
	private int [] mmCSR;
	private int [] omCSR;
	private final int [] parts;
	
	private final int nbSim;
	private int blockSize;
	
	private final static Logger logger = LoggerFactory.getLogger(OCLParaCSRModel4.class);
	private final Slf4JStopWatch watch = new Slf4JStopWatch(logger);
	
	private final static String MODEL_SOURCE = "kernels/mior_model_multisim4.cl";
	private final IMiorDistribution dist;
	
	public OCLParaCSRModel4(int onbMM, int onbOM, int nbSim, IMiorDistribution dist) {
		this.dist = dist;
		System.out.println(dist);
		
		//final double factor = dist.getMax() / ((double) onbOM);
		this.nbOM = (int) (dist.getMaxFactor() * dist.getMeanOM());
		this.nbMM = (int) (dist.getMaxFactor() * dist.getMeanMM());
		
		this.nbSim = nbSim;
		this.blockSize = Math.max(nbOM, nbMM);
		
		// Allocate Java MIOR structures
		this.mmList       = new MiorMM[nbSim * nbMM];
		this.omList       = new MiorOM[nbSim * nbOM];
		this.worlds       = new MiorWorld[nbSim];
		
		this.mmOffsets    = new int[nbSim * nbMM];
    this.omOffsets    = new int[nbSim * nbOM];
		//this.mmCSR        = allocateStorage(nbSim * nbOM, nbMM);
		//this.omCSR        = allocateStorage(nbSim * nbMM, nbOM);
		this.parts        = new int[nbSim * nbMM * nbOM];
		
		MiorUtils.initWorldArray(worlds, dist);
		MiorUtils.initRandomMMArray(mmList, worlds[0].width);
		MiorUtils.initRandomOMArray(omList, worlds[0].width);
		
		// OpenCL allocations
		watch.start("OpenCL setup");
		
		this.context = new MCMContext();
		this.queue = context.createCommandQueue(MCMCommandQueueProperty.ENABLE_PROFILING);
		
		watch.stop();
		
		System.out.println("NB_MM: " + nbMM + ", NB_OM: " + nbOM);
		watch.start("Program compilation");
		this.program = MCMUtils.compileFile(context, MODEL_SOURCE, " -DNB_MM=" + nbMM + " -DNB_OM=" + nbOM);// + " -g -O0");
		watch.stop();
		
		watch.start("Kernel allocation");
		
		this.topoKernel = program.createKernel("topology");
		this.simulateKernel = program.createKernel("simulate");
		this.autoliveKernel = program.createKernel("autolive");
		
		watch.stop();
		watch.start("Memory allocation");
		
		this.mmMem = context.newBuffer().Using(mmList).b();
		this.omMem = context.newBuffer().Using(omList).b();
		this.worldsMem = context.newBuffer().Using(worlds).b();
		this.mmOffsetsMem = context.newBuffer().Size(nbSim * nbMM).b();
		this.omOffsetsMem = context.newBuffer().Size(nbSim * nbOM).b();
		
		//this.mmCSRMem = context.newBuffer().Using(mmCSR).b();
		//this.omCSRMem = context.newBuffer().Using(omCSR).b();
		this.partsMem = context.newBuffer().Using(new int[parts.length]).b();
		
		watch.stop();
	}
	
	/*private int[] allocateStorage(int nbList, int listSize) {
		return new int[nbList * (listSize + 1)];
	}*/

	@Override
	public void setBlockSize(int blockSize) {
		this.blockSize = blockSize;
	}
	
	@Override
	public int getBlockSize() {
		return blockSize;
	}
	
	
	@Override
	public int getNbSimulations() {
		return nbSim;
	}
	
	@Override
	protected void resetImpl() {
		// Initialise models
		/*for (int i = 0; i < nbSim; i++) {
			this.worlds[i] = new MiorWorld(nbMM, nbOM);
		}*/
		
		MiorUtils.initWorldArray(worlds, dist);
		MiorUtils.initRandomMMArray(mmList, worlds[0].width);
		MiorUtils.initRandomOMArray(omList, worlds[0].width);
		
		if (mmCSR != null) Arrays.fill(mmCSR, 0);
		if (omCSR != null) Arrays.fill(omCSR, 0);
		Arrays.fill(parts, 0);
		
		queue.enqueueWriteBuffer(mmMem, mmList, 0, mmMem.getSize());
		queue.enqueueWriteBuffer(omMem, omList, 0, omMem.getSize());
		queue.enqueueWriteBuffer(worldsMem, worlds, 0, worldsMem.getSize());
		
		if (mmCSR != null) { 
			queue.enqueueWriteBuffer(mmCSRMem, Pointer.to(mmCSR), 0, mmCSRMem.getSize());
		}
		if (omCSR != null) {
			queue.enqueueWriteBuffer(omCSRMem, Pointer.to(omCSR), 0, omCSRMem.getSize());
		}
		queue.enqueueWriteBuffer(partsMem, Pointer.to(parts), 0, partsMem.getSize());
		queue.finish();
	}
	
	@Override
	protected void doTopologyImpl() {
		troveTopology();
		
		queue.enqueueWriteBuffer(mmOffsetsMem, Pointer.to(mmOffsets), 0, mmOffsetsMem.getSize());
		queue.enqueueWriteBuffer(mmOffsetsMem, Pointer.to(mmOffsets), 0, mmOffsetsMem.getSize());
		this.mmCSRMem = context.newBuffer().Using(mmCSR).b();
		this.omCSRMem = context.newBuffer().Using(omCSR).b();
		queue.finish();
	}
	
	protected void doTopologyImplOld() {
		// System.out.println(topoKernel.getArgumentsNumber());
		topoKernel.setArguments(mmMem, omMem, worldsMem, mmCSRMem, omCSRMem); // associationsMem
		
		MCMEvent event = queue.enqueueKernel(topoKernel, 2,
				new long[] { nbSim * nbMM , nbOM} // Global size
		);
		
		/*OCLEvent event = queue.enqueueKernel(topoKernel, 2,
				new long[] { nbMM , nbSim * nbOM}, // Global size
				new long[] { 1, nbOM }
		);*/
		
		MCMEvent.waitFor(event);
		MCMUtils.printEventStats("topology", event);
		
		if (! isBatchModeEnabled()) {
			queue.blockingReadBuffer(mmCSRMem, Pointer.to(mmCSR), 0, mmCSRMem.getSize());
			queue.blockingReadBuffer(omCSRMem, Pointer.to(omCSR), 0, omCSRMem.getSize());
			//System.out.println(Arrays.toString(mmCSR));
			//queue.blockingReadBuffer(associationsMem, Pointer.to(associations), 0, associationsMem.getSize());
		}
	}
	
	@Override
	protected void doLiveImpl() {
		//throw new UnsupportedOperationException();
		simulateKernel.setArguments(mmMem, omMem, worldsMem, mmOffsetsMem, omOffsetsMem,  mmCSRMem, omCSRMem, partsMem);
		
		if (blockSize < Math.max(nbOM, nbMM)) {
			throw new RuntimeException("blockSize (" + blockSize + ") is too small to execute the simulation");
		}
		
		MCMEvent event = queue.enqueue1DKernel(simulateKernel, nbSim * blockSize, blockSize);
		
		MCMEvent.waitFor(event);
		MCMUtils.printEventStats("simulate", event);
		
		if (! isBatchModeEnabled()) {
			queue.blockingReadBuffer(mmMem, mmList, 0, mmMem.getSize());
			queue.blockingReadBuffer(omMem, omList, 0, omMem.getSize());
			queue.blockingReadBuffer(worldsMem, worlds, 0, worldsMem.getSize());
			System.out.println("copy");
		}
	}
	
	@Override
	public void doAutoLive() {
		resetImpl();
		doTopologyImpl();
		
		if (blockSize < Math.max(nbOM, nbMM)) {
			throw new RuntimeException("blockSize (" + blockSize + ") is too small to execute the simulation");
		}
		
		autoliveKernel.setArguments(mmMem, omMem, worldsMem,  mmOffsetsMem, omOffsetsMem, mmCSRMem, omCSRMem, partsMem);
		
		if (! isBatchModeEnabled()) {
				int CO2 = -1;
				
				while (CO2 == -1 || CO2 != worlds[0].CO2) {
					CO2 = worlds[0].CO2;
					System.out.println("CO2 = " + CO2);
					doLive();
				}
				
		} else { 
			autoliveKernel.setArguments(mmMem, omMem, worldsMem, mmCSRMem, omCSRMem, partsMem);
			
			MCMEvent event = queue.enqueue1DKernel(autoliveKernel, nbSim * blockSize, blockSize);
			
			MCMEvent.waitFor(event);
			MCMUtils.printEventStats("autolive", event);
		}
		
		onSimulationFinished();
	}
	
	@Override
	protected void onSimulationFinished() {
		
		queue.blockingReadBuffer(mmMem, mmList, 0, mmMem.getSize());
		queue.blockingReadBuffer(omMem, omList, 0, omMem.getSize());
		queue.blockingReadBuffer(worldsMem, worlds, 0, worldsMem.getSize());
		//queue.blockingReadBuffer(associationsMem, Pointer.to(associations), 0, associationsMem.getSize());
		queue.blockingReadBuffer(partsMem, Pointer.to(parts), 0, partsMem.getSize());
		
		super.onSimulationFinished();
	}
	
	@Override
	protected void releaseImpl() {
		partsMem.release();
		
		omCSRMem.release();
		mmCSRMem.release();
		//associationsMem.release();
		
		worldsMem.release();
		omMem.release();
		mmMem.release();
		
		autoliveKernel.release();
		simulateKernel.release();
		topoKernel.release();
		
		program.release();
		queue.release();
		context.release();
	}

	@Override
	public MiorWorld getWorld() {
		return worlds[0];
	}

	@Override
	public MiorOM[] getOMList() {
		return Arrays.copyOfRange(omList, 0, nbOM);
	}

	@Override
	public MiorMM[] getMMList() {
		return Arrays.copyOfRange(mmList, 0, nbMM);
	}

	@Override
	public boolean isAccessible(int iMM, int iOM) {
		return false;
		//return associations[iMM * nbOM + iOM] != -1;
	}

	@Override
	public boolean isAccessible(int iMM, int iOM, int iSim) {
		return false;
		//return associations[iSim * nbMM * nbOM + iMM * nbOM + iOM] != -1;
	}
	
	private void troveTopology() {
		//int[] mmOffsets = new int[nbSim * nbMM];
		//int[] omOffsets = new int[nbSim * nbOM];
		int[] mmSizes = new int[nbSim * nbMM];
		int[] omSizes = new int[nbSim * nbOM];
		DynamicArray mmCSR = new DynamicArray();
		DynamicArray omCSR = new DynamicArray();
		BitSet bitmap = new BitSet();
		
		for (int iSim = 0; iSim < nbSim; iSim++) {
			final MiorWorld world = worlds[iSim];
			
			for (int iMM = 0; iMM < world.nbMM; iMM++) {			
				
				for (int iOM = 0; iOM < world.nbOM; iOM++) {
					final float dx = omList[iSim * nbOM + iOM].x - mmList[iSim * nbMM + iMM].x;
					final float dy = omList[iSim * nbOM + iOM].y - mmList[iSim * nbMM + iMM].y;
					final double distance = Math.sqrt(dx * dx + dy * dy);
					
					if (distance <= world.RA) {
						mmSizes[iSim * nbMM + iMM]++;
						omSizes[iSim * nbOM + iOM]++;
						bitmap.set(iSim * nbOM * nbMM + iMM * nbOM + iOM);
						//System.out.println("Store link " + iMM + " - " + iOM);
					}
				}
			}
		}
		
		for (int i = 1; i < nbSim * nbMM; i++) {
			mmOffsets[i] = mmOffsets[i - 1] + mmSizes[i - 1] + 1;
			mmCSR.set(mmOffsets[i], 0);
		}
		
		for (int i = 1; i < nbSim * nbOM; i++) {
			omOffsets[i] = omOffsets[i - 1] + omSizes[i - 1] + 1;
			omCSR.set(omOffsets[i], 0 /*omSizes[i]*/);
		}
		
		for (int iSim = 0; iSim < nbSim; iSim++) {
			final MiorWorld world = worlds[iSim];
			
			for (int iMM = 0; iMM < world.nbMM; iMM++) {
				final int mmIndex = iSim * nbMM + iMM;
				
				for (int iOM = 0; iOM < world.nbOM; iOM++) {
					final int omIndex = iSim * nbOM + iOM;
					
					if (bitmap.get(iSim * nbOM * nbMM + iMM * nbOM + iOM)) {
						list_add(omCSR, omOffsets[omIndex], mmIndex);
						list_add(mmCSR, mmOffsets[mmIndex], omIndex);
						//System.out.println("Add link " + iMM + " - " + iOM);
					}
				}
			}
		}
		
		this.mmCSR = mmCSR.toArray();
		this.omCSR = omCSR.toArray();
		
		/*
		System.out.println("nbSim: " + nbSim);
		System.out.println("mmSizes: " + Arrays.toString(mmSizes));
		System.out.println("mmOffsets: " + Arrays.toString(mmOffsets));
		System.out.println("mmCSR: " + Arrays.toString(this.mmCSR));
		System.out.println("omSizes: " + Arrays.toString(omSizes));
		System.out.println("omOffsets: " + Arrays.toString(omOffsets));
		System.out.println("omCSR: " + Arrays.toString(this.omCSR));*/
	}
	
	private void list_add(DynamicArray list, int index, int val) {
		final int offset = list.get(index);
		list.set(offset + 1 + index, val);
		list.set(index, offset + 1);
	}
	
}
