/*
 * OpenPixi - Open Particle-In-Cell (PIC) Simulator
 * Copyright (C) 2012  OpenPixi.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.openpixi.pixi.physics;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.FileWriter;
import org.openpixi.pixi.physics.collision.algorithms.CollisionAlgorithm;
import org.openpixi.pixi.physics.collision.detectors.Detector;
import org.openpixi.pixi.physics.fields.PoissonSolver;
import org.openpixi.pixi.physics.force.CombinedForce;
import org.openpixi.pixi.physics.force.SimpleGridForce;
import org.openpixi.pixi.physics.grid.Grid;
import org.openpixi.pixi.physics.grid.Interpolation;
import org.openpixi.pixi.physics.grid.LocalInterpolation;
import org.openpixi.pixi.physics.movement.ParticleMover;
import org.openpixi.pixi.physics.movement.boundary.ParticleBoundaries;
import org.openpixi.pixi.physics.movement.boundary.SimpleParticleBoundaries;
import org.openpixi.pixi.physics.particles.Particle;
import org.openpixi.pixi.physics.util.DoubleBox;

import java.util.ArrayList;
import java.util.List;

public class Simulation {

	/**
	 * Timestep
	 */
	public double tstep;
	/**
	 * Width of simulated area
	 */
	private double width;
	/**
	 * Height of simulated area
	 */
	private double height;
	private double speedOfLight;
	private double eps0;
	private double mu0;
	/**
	 * Number of iterations in the non-interactive simulation.
	 */
	private int iterations;
	/**
	 * Total number of steps simulated so far.
	 */
	public int tottime;
	/**
	 * Total number of steps between spectral measurements.
	 */
	public int specstep;
	/**
	 * File path to output files.
	 */
	private String filePath;
	/**
	 * Contains all Particle2D objects
	 */
	public ArrayList<Particle> particles;
	public CombinedForce f;
	private ParticleMover mover;
	/**
	 * Grid for dynamic field calculation
	 */
	public Grid grid;
	public Detector detector;
	public CollisionAlgorithm collisionalgorithm;
	/**
	 * We can turn on or off the effect of the grid on particles by adding or
	 * removing this force from the total force.
	 */
	private SimpleGridForce gridForce = new SimpleGridForce();
	private boolean usingGridForce = false;
	public boolean relativistic = false;
	private ParticleGridInitializer particleGridInitializer = new ParticleGridInitializer();
	private Interpolation interpolation;
	/**
	 * solver for the electrostatic poisson equation
	 */
	private PoissonSolver poisolver;

	public Interpolation getInterpolation() {
		return interpolation;
	}
	
	public int getIterations() {
		return iterations;
	}

	public double getWidth() {
		return width;
	}

	public double getHeight() {
		return height;
	}

	public double getSpeedOfLight() {
		return speedOfLight;
	}

	public ParticleMover getParticleMover() {
		return mover;
	}

	/**
	 * Constructor for non distributed simulation.
	 */
	public Simulation(Settings settings) {
		tstep = settings.getTimeStep();
		width = settings.getSimulationWidth();
		height = settings.getSimulationHeight();
		speedOfLight = settings.getSpeedOfLight();
		iterations = settings.getIterations();
		tottime = 0;
		specstep = settings.getSpectrumStep();
		filePath = settings.getFilePath();
		relativistic = settings.getRelativistic();
		eps0 = settings.getEps0();
		mu0 = settings.getMu0();

		// TODO make particles a generic list
		particles = (ArrayList<Particle>) settings.getParticles();
		f = settings.getForce();

		ParticleBoundaries particleBoundaries = new SimpleParticleBoundaries(
				new DoubleBox(0, width, 0, height),
				settings.getParticleBoundary());
		mover = new ParticleMover(
				settings.getParticleSolver(),
				particleBoundaries,
				settings.getParticleIterator());

		grid = new Grid(settings);
		if (settings.useGrid()) {
			turnGridForceOn();
		} else {
			turnGridForceOff();
		}

		poisolver = settings.getPoissonSolver();
		interpolation = new LocalInterpolation(
				settings.getInterpolator(), settings.getParticleIterator());
		particleGridInitializer.initialize(interpolation, poisolver, particles, grid);

		detector = settings.getCollisionDetector();
		collisionalgorithm = settings.getCollisionAlgorithm();

		prepareAllParticles();
		
		clearFile();
	}

	/**
	 * Constructor for distributed simulation. Expects settings specific to the
	 * local node => the simulation width and height as well as the number of
	 * cells in y and x direction must pertain to local simulation not to the
	 * global simulation. (No need to set poison solver and run
	 * ParticleGridInitializer as it was already run on the master node).
	 */
	public Simulation(Settings settings,
			Grid grid,
			List<Particle> particles,
			ParticleBoundaries particleBoundaries,
			Interpolation interpolation) {

		this.tstep = settings.getTimeStep();
		this.width = settings.getSimulationWidth();
		this.height = settings.getSimulationHeight();
		this.speedOfLight = settings.getSpeedOfLight();
		this.iterations = settings.getIterations();
		this.tottime = 0;
		this.specstep = settings.getSpectrumStep();
		this.filePath = settings.getFilePath();
		this.relativistic = settings.getRelativistic();
		this.eps0 = settings.getEps0();
		this.mu0 = settings.getMu0();

		this.particles = (ArrayList<Particle>) particles;
		f = settings.getForce();

		mover = new ParticleMover(
				settings.getParticleSolver(),
				particleBoundaries,
				settings.getParticleIterator());

		this.grid = grid;
		if (settings.useGrid()) {
			turnGridForceOn();
		} else {
			turnGridForceOff();
		}

		this.interpolation = interpolation;

		detector = settings.getCollisionDetector();
		collisionalgorithm = settings.getCollisionAlgorithm();

		prepareAllParticles();
		
		clearFile();
	}

	public void turnGridForceOn() {
		if (!usingGridForce) {
			f.add(gridForce);
			usingGridForce = true;
		}
        if(!f.forces.contains(gridForce)){
            f.add(gridForce);
        }
	}

	public void turnGridForceOff() {
		if (usingGridForce) {
			f.remove(gridForce);
			usingGridForce = false;
		}
	}

	/**
	 * Runs the simulation in steps. (for interactive simulations)
	 */
	public void step() throws FileNotFoundException,IOException {

		if (continues()) {
			// Only write to file while simulation continues.
			writeToFile(tstep*tottime);
			if( (tottime % specstep) == 0) writeSpecFile(tottime);
		}
		particlePush();
		detector.run();
		collisionalgorithm.collide(detector.getOverlappedPairs(), f, mover.getSolver(), tstep);
		interpolation.interpolateToGrid(particles, grid, tstep);
		grid.updateGrid(tstep);
		interpolation.interpolateToParticle(particles, grid);

		tottime++;
	}

	/**
	 * Whether the simulation should continue.
	 * @return
	 */
	public boolean continues() {
		return tottime <= iterations;
	}

	/**
	 * Runs the entire simulation at once. (for non-interactive simulations)
	 */
	public void run() throws FileNotFoundException,IOException {
		while (continues()) {
			step();
		}
	}

	/**
	 * Checks if the files are already existent and deletes them.
	 */
	public void clearFile() {
		File particlesfile = getOutputFile("particles_seq.txt");
		boolean fileExists1 = particlesfile.exists();
		if(fileExists1 == true) {
			particlesfile.delete();
		}

		File gridfile = getOutputFile("cells_seq.txt");
		boolean fileExists2 = gridfile.exists();
		if(fileExists2 == true) {
			gridfile.delete();
		}
	}

	/**
	 * Get output file in correct subdirectory.
	 * Create subdirectories if necessary.
	 * @param filename
	 * @return file
	 */
	public File getOutputFile(String filename) {
		// Default output path is
		// 'output/' + filePath + '/' + filename
		File fullpath = new File("output");
		if(!fullpath.exists()) fullpath.mkdir();

		fullpath = new File(fullpath, filePath);
		if(!fullpath.exists()) fullpath.mkdir();

		return new File(fullpath, filename);
	}

	/**
	 * Write the results to a txt file
	 */
	public void writeToFile(double time) throws IOException {
		//PrintWriter pw = new PrintWriter(new File("particles_seq.txt"));
		
		File file = getOutputFile("particles_seq.txt");
		FileWriter pw = new FileWriter(file, true);
		double kinetic = 0;
		double kineticTotal = 0;
		RelativisticVelocity relvelocity = new RelativisticVelocity(1);
		
		if(time == 0) {
			pw.write("#time \t x \t y \t vx \t vy \t kinetic \t Ex \t Ey \t Bz");
			pw.write("\n");
		} else {}
		
		pw.write(time + "\t");
		
		for (int i = 0; i < particles.size(); i++) {
			pw.write(particles.get(i).getX() + "\t");
			pw.write(particles.get(i).getY() + "\t");
			//pw.write(particles.get(i).getRadius() + "\n");
			pw.write(particles.get(i).getVx() + "\t");
			pw.write(particles.get(i).getVy() + "\t");
			pw.write(relvelocity.calculateGamma(particles.get(i)) + "\t");
			if(relativistic == false) {kinetic = particles.get(i).getMass()*(particles.get(i).getVx() * particles.get(i).getVx() + particles.get(i).getVy()*particles.get(i).getVy())/2;}
			else {kinetic = Math.sqrt(particles.get(i).getMass()*particles.get(i).getMass()*( particles.get(i).getVx() * particles.get(i).getVx() + particles.get(i).getVy()*particles.get(i).getVy() + 1) ); }
			pw.write(kinetic + "\t");
			kineticTotal += kinetic;
			pw.write(particles.get(i).getEx() + "\t");
			pw.write(particles.get(i).getEy() + "\t");
			pw.write(particles.get(i).getBz() + "\t");
			/*pw.write(particles.get(i).getAx() + "\n");
			pw.write(particles.get(i).getAy() + "\n");
			pw.write(particles.get(i).getMass() + "\n");
			pw.write(particles.get(i).getCharge() + "\n");
			pw.write(particles.get(i).getPrevX() + "\n");
			pw.write(particles.get(i).getPrevY() + "\n");
			pw.write(particles.get(i).getPrevPositionComponentForceX() + "\n");
			pw.write(particles.get(i).getPrevPositionComponentForceY() + "\n");
			pw.write(particles.get(i).getPrevTangentVelocityComponentOfForceX() + "\n");
			pw.write(particles.get(i).getPrevTangentVelocityComponentOfForceY() + "\n");
			pw.write(particles.get(i).getPrevNormalVelocityComponentOfForceX() + "\n");
			pw.write(particles.get(i).getPrevNormalVelocityComponentOfForceY() + "\n");
			pw.write(particles.get(i).getPrevBz() + "\n");
			pw.write(particles.get(i).getPrevLinearDragCoefficient() + "\n");*/
		}
		pw.write("\n");
		
		pw.close();

		file = getOutputFile("cells_seq.txt");
		//pw = new PrintWriter(file);
		pw = new FileWriter(file, true);
		
		pw.write(time + "\t");
		
		double SumRho = 0;
		double SumJx = 0;
		double SumJy = 0;
		double fieldEnergy = 0;
		double GaussLaw = 0;
		//int NumPoints = grid.getNumCellsX()*grid.getNumCellsY();
		
		for (int i = 0; i < grid.getNumCellsX(); i++) {
			for (int j = 0; j < grid.getNumCellsY(); j++) {
				
				SumRho += grid.getCells()[i][j].getRho();
				SumJx += grid.getCells()[i][j].getJx();
				SumJy += grid.getCells()[i][j].getJy();
				fieldEnergy += ( grid.getCells()[i][j].getBz()*grid.getCells()[i][j].getBz() + grid.getCells()[i][j].getEx()*grid.getCells()[i][j].getEx() + grid.getCells()[i][j].getEy()*grid.getCells()[i][j].getEy())/2;
				GaussLaw += (grid.getEx((i+1)%grid.getNumCellsX(), j) - grid.getEx(i, j)) / grid.getCellWidth() +
						(grid.getEy(i, (j+1)%grid.getNumCellsY()) - grid.getEy(i, j)) / grid.getCellHeight() - grid.getRho(i,j)*4*Math.PI;
				/*pw.write(grid.getCells()[i][j].getJx() + "\n");
				pw.write(grid.getCells()[i][j].getJy() + "\n");
				pw.write(grid.getCells()[i][j].getRho() + "\n");
				pw.write(grid.getCells()[i][j].getPhi() + "\n");
				pw.write(grid.getCells()[i][j].getEx() + "\n");
				pw.write(grid.getCells()[i][j].getEy() + "\n");
				pw.write(grid.getCells()[i][j].getBz() + "\n");
				pw.write(grid.getCells()[i][j].getBzo() + "\n");*/
			}
		}
		pw.write(kineticTotal + "\t");
		pw.write(fieldEnergy + "\t");
		pw.write(SumRho + "\t");
		pw.write(SumJx + "\t");
		pw.write(SumJy + "\t");
		pw.write(GaussLaw + "\t");
		pw.write(grid.getEy(grid.getNumCellsX()/2, grid.getNumCellsY()/2) + "\t");
		pw.write(grid.getBz(grid.getNumCellsX()/2, grid.getNumCellsY()/2) + "\t");
		
		pw.write("\n");
		
		pw.close();
		
	}

	public void writeSpecFile(int time) throws FileNotFoundException {
		File file = getOutputFile("spec" + time + ".txt");
		PrintWriter sw = new PrintWriter(file);
		
		for (int i = 0; i < particles.size(); i++) {
			sw.write(i + "\t");
			sw.write(particles.get(i).getX() + "\t");
			sw.write(particles.get(i).getY() + "\t");
			sw.write(particles.get(i).getVx() + "\t");
			sw.write(particles.get(i).getVy() + "\t");
			sw.write("\n");
		}
		
		sw.close();

		file = getOutputFile("snapshot" + time + ".txt");
		PrintWriter snap = new PrintWriter(file);

		for (int i = 0; i < grid.getNumCellsXTotal(); i++) {
			for (int j = 0; j < grid.getNumCellsYTotal(); j++) {
				
				snap.write(i + "\t");
				snap.write(j + "\t");
				snap.write(grid.getCells()[i][j].getEy() + "\t");
				snap.write(grid.getCells()[i][j].getBz() + "\t");
				snap.write("\n");
				
			}
		}
		
		snap.close();
		
	}
	
	public void particlePush() {
		mover.push(particles, f, tstep);
	}

	public void prepareAllParticles() {
		mover.prepare(particles, f, tstep);
	}

	public void completeAllParticles() {
		mover.complete(particles, f, tstep);
	}
}
