package preypredator;
import it.unimi.dsi.fastutil.ints.IntArrayList;

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

import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import joptsimple.OptionParser;
import joptsimple.OptionSet;

public class PreyPredator2 {
	
	private final static Logger logger = LoggerFactory.getLogger(PreyPredator2.class);
	private final StopWatch watch = new Slf4JStopWatch();
	private final StopWatch runWatch = new Slf4JStopWatch();
	
	//public static final int WIDTH = 1000;
	//public static final int HEIGHT = 1000;
	
	public static final int GRASS_MIN = 0;
	public static final int GRASS_MAX = 100;
	public static final int GRASS_ORIG = 20;
	public static final float GRASS_FACT = 1.0f;
	public static final int   GRASS_GROWTH = 10;
	
	public static final int PREY_ORIG = 10;
	//public static final int PREY_RADIUS = 20;
	public static final int PREY_MAX = 50;
	
	public static final int PRED_ORIG = 10;
	//public static final int PRED_RADIUS = 20;
	public static final int PRED_MAX = 100;
	
	// Attributes
	
	private final PPGrid grass;
	private final PPGrid preys;
	private final PPGrid preds;
	
	
	// Temporary buffers used in each step
	
	private final IntArrayList preysX;
	private final IntArrayList preysY;
	private final IntArrayList predsX;
	private final IntArrayList predsY;
	
	private final int nbPreds;
	private final int nbPreys;
	private final int range;
	private final PPRuntime runtime;
	
	//OCLChrono chrono = new OCLChrono("preypredator");
	
	private final int size;
	
	public PreyPredator2(int size, int nbPreys, int nbPredators, int range, PPRuntime runtime) {
		this.grass = new PPGrid(size, size);
		this.preys = new PPGrid(size, size);
		this.preds = new PPGrid(size, size);
		
		this.size    = size;
		this.nbPreds = nbPredators;
		this.nbPreys = nbPreys;
		this.range   = range;
		
		this.preysX = new IntArrayList();
		this.preysY = new IntArrayList();
		this.predsX = new IntArrayList();
		this.predsY = new IntArrayList();
		
		this.runtime = runtime; 
	}	
	
	public void setup() {
		watch.start("pp_setup");
		initGrid(grass.getStorage(), (int) (grass.getStorage().length * 0.8), GRASS_ORIG);
		initGrid(preys.getStorage(), nbPreys, PREY_ORIG);
		initGrid(preds.getStorage(), nbPreds, PRED_ORIG);
		watch.stop();
	}
	
	public void update_positions() {
		//chrono.start("update_positions");
		
		preysX.clear();
		preysY.clear();
		predsX.clear();
		predsY.clear();
		
		for (int j = 0; j < size; j++) {
			for (int i = 0; i < size; i++) {
				if (preys.get(i, j) > 0) {
					preysX.add(i);
					preysY.add(j);
				}
				
				if (preds.get(i, j) > 0) {
					predsX.add(i);
					predsY.add(j);
				}
			}
		}
		
		//System.out.println(chrono.stop());
	}
	
	public void step() {
		watch.start("pp_step");
		//OCLChrono stepChrono = new OCLChrono("step");
		//stepChrono.start();
		
		update_positions();
		
		runWatch.start("pp_grass");
		runtime.growGrass(grass, GRASS_FACT, GRASS_GROWTH, GRASS_MIN, GRASS_MAX);
		runWatch.stop();
		
		int[] newPreysX = new int[preysX.size()];
		int[] newPreysY = new int[preysY.size()];
		int[] newPredsX = new int[predsX.size()];
		int[] newPredsY = new int[predsY.size()];
		
		runWatch.start("pp_move_preys");
		if (preysX.size() > 0) {
			
			runtime.selectMaxTarget(grass, range,
					preysX.toIntArray(), preysY.toIntArray(),
					newPreysX, newPreysY);
		}
		runWatch.stop();
		
		/*
		for (int i = 0; i < preysX.size(); i++) {
			System.out.println("Prey: (" + preysX.get(i) + ", " + preysY.get(i) + ") => (" +
					newPreysX[i] + ", " + newPreysY[i] + ")");	
		}*/
		
		// PREYS
		
		for (int i = 0; i < preysX.size(); i++) {
			final int oldPos = preys.offset(preysX.get(i), preysY.get(i));
			final int newPos = preys.offset(newPreysX[i], newPreysY[i]);
			
			// Check an eventual conflict with another prey
			if (preys.get(newPos) > 0) {
				continue;
			}
			
			// Movement
			preys.set(newPos, preys.get(oldPos));
			preys.set(oldPos,  0);
			
			// Eating
			if (grass.get(newPos) >= 0) {
				preys.set(newPos, clamp(preys.get(newPos) + grass.get(newPos), 0, PREY_MAX));
				grass.set(newPos, 0);
			}
			
			// Reproduction
			if (preys.get(newPos) == PREY_MAX) {
				preys.set(oldPos, PREY_ORIG);
			}
		}
		
		runWatch.start("pp_move_preds");
		if (predsX.size() > 0) {
			int [] predsXbis = predsX.toIntArray();
			int [] predsYbis = predsY.toIntArray();
			
			runtime.selectMaxTarget(preys, (int) Math.round(range * 1.5),
					predsXbis, predsYbis,
					newPredsX, newPredsY);
		}
		runWatch.stop();
		
		/*for (int i = 0; i < predsX.size(); i++) {
			System.out.println("Pred: (" + predsX.get(i) + ", " + predsY.get(i) + ") => (" +
					newPredsX[i] + ", " + newPredsY[i] + ")");	
		}*/
		
		// PREDATOR
		
		for (int i = 0; i < predsX.size(); i++) {
			final int pos = predsY.get(i) * size + predsX.get(i);
			final int newPos = newPredsY[i] * size + newPredsX[i];
			
			// Check an eventual conflict with another predator
			if (preds.get(newPos) > 0) {
				continue;
			}
			
			// Movement
			preds.set(newPos, preds.get(pos));
			preds.set(pos, 0);
			
			// Eating
			if (preys.get(newPos) >= 0) {
				preds.set(newPos, clamp(preds.get(newPos) + preys.get(newPos), 0, PRED_MAX));
				preys.set(newPos, 0);
			}
			
			// Reproduction
			if (preds.get(newPos) == PRED_MAX) {
				preds.set(pos, PRED_ORIG);
			}
		}
		
		/*System.out.println(grass);
		System.out.println(preys);*/
		//System.out.println(preds);
		//logger.info(stepChrono.stop().toString());
		watch.stop();
	}
	
	private void initGrid(int[] grid, int nbCells, int value) {
		int remainingCells = nbCells;
		Random rng = new Random();
		
		while (remainingCells > 0) {
			final int pos = rng.nextInt(grid.length);
			
			if (grid[pos] == 0) {
				grid[pos] = value;
				remainingCells--;
			}
		}
	}
	
	// Helpers
	private int clamp(int value, int min, int max) {
		return Math.max(min, Math.min(value, max));
	}
	
	public static void main(String[] args) {
		OptionParser parser = new OptionParser();
		
		parser.acceptsAll(Arrays.asList("g", "gpu", "opencl"), "Enable OpenCL support")
		.withOptionalArg().ofType(Boolean.class).defaultsTo(true);
		
		parser.acceptsAll(Arrays.asList("h", "help"), "Print this help");
		
		parser.accepts("size", "Size of the grid (side)").withRequiredArg()
		.ofType(Integer.class).defaultsTo(1000);
		
		parser.accepts("preys", "Number of Preys to use").withRequiredArg()
		.ofType(Integer.class).defaultsTo(10000);

		parser.accepts("preds", "Number of Predators to use").withRequiredArg()
		.ofType(Integer.class).defaultsTo(5000);
		
		parser.accepts("range", "Range to use for preys (x 1.5 for predators)").withRequiredArg()
		.ofType(Integer.class).defaultsTo(100);

		OptionSet options = parser.parse(args);
		
		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();
			}
		}
		
		PPRuntime runtime;
		
		if ((Boolean) (options.valueOf("gpu"))) {
			logger.info("Use OpenCL runtime on first available GPU");
			runtime = new PPRuntimeGPU();
		} else {
			logger.info("Use Java Runtime");
			runtime = new PPRuntimeCPU();
		}
		
		int size = (Integer) options.valueOf("size");
		int nbPreys = (Integer) options.valueOf("preys");
		int nbPreds = (Integer) options.valueOf("preds");
		int range = (Integer) options.valueOf("range");
		
		logger.info(
				"size: " + size + ", nbPreys: " + nbPreys + ", nbPreds: " + nbPreds + ", range: " + range
		);
		
		//PPRuntime runtime = new PPRuntimeGPU(ContextType.ALL);
		//PPRuntime runtime = new PPRuntimeCPU();
		PreyPredator2 simulation = new PreyPredator2(size, nbPreys, nbPreds, range, runtime);
		simulation.setup();
		
		for (int i = 0; i < 50; i++) {
			simulation.step();
		}
	}

}