package org.openpixi.pixi.physics.util;

import org.openpixi.pixi.physics.grid.Cell;
import org.openpixi.pixi.physics.grid.Grid;
import org.openpixi.pixi.physics.particles.Particle;

import java.util.List;

/**
 * Compares the results of two simulations.
 * In case of failure throws ComparisonFailedException.
 */
public class ResultsComparator {

	private static final Double TOLERANCE = 1e-10;

	private static final int NO_STEP_TRACKING = -1;
   	private int stepNo = NO_STEP_TRACKING;


	public ResultsComparator() {

	}


	/**
	 * In case of failure outputs also the step number in which the failure occurred.
	 */
	public ResultsComparator(int stepNo) {
		this.stepNo = stepNo;
	}


	public void compare(
			List<Particle> expectedParticles, List<Particle> actualParticles,
			Grid expectedGrid, Grid actualGrid) {

		compareParticleLists(expectedParticles, actualParticles, TOLERANCE);
		compareGrids(expectedGrid, actualGrid, TOLERANCE);
	}


	private void compareParticleLists(
			List<Particle> expectedParticles, List<Particle> actualParticles, double tolerance) {

		if (expectedParticles.size() < actualParticles.size()) {
			fail("There are more actual particles than expected!");
		}
		if (expectedParticles.size() > actualParticles.size()) {
			fail("There are less actual particles than expected!");
		}

		for (Particle p: expectedParticles) {
			if (!findParticle(p, actualParticles, tolerance)) {
				fail("Could not find particle " + p + " in the list of actual particles!");
			}
		}
	}


	private boolean findParticle(
			Particle p, List<Particle> particles, Double tolerance) {
		boolean retval = false;
		for (Particle p2: particles) {
			if (compareParticles(p, p2, tolerance)) {
				return true;
			}
		}
		return retval;
	}


	/**
	 * Compares just the position.
	 */
	private boolean compareParticles(Particle p1, Particle p2, Double tolerance) {
		if (Math.abs(p1.getX() - p2.getX()) > tolerance) {
			return false;
		}
		if (Math.abs(p1.getY() - p2.getY()) > tolerance) {
			return false;
		}
		return true;
	}


	private void compareGrids(Grid expectedGrid, Grid actualGrid, double tolerance) {
		if (expectedGrid.getNumCellsX() < actualGrid.getNumCellsX()) {
			fail("Actual grid is larger in X direction!");
		}
		if (expectedGrid.getNumCellsX() > actualGrid.getNumCellsX()) {
			fail("Actual grid is smaller in X direction!");
		}
		if (expectedGrid.getNumCellsY() < actualGrid.getNumCellsY()) {
			fail("Actual grid is larger in Y direction!");
		}
		if (expectedGrid.getNumCellsY() > actualGrid.getNumCellsY()) {
			fail("Actual grid is smaller in Y direction!");
		}

		/*
		 * We only compare the cells up till interpolation radius
		 * (ie. we do not compare the last row and column of the cells).
		 * The last row and column would be different in periodic boundaries because
		 * in the non distributed simulation these rows and cells point inside of the grid,
		 * whereas in distributed simulation they are not exchanged since they are not needed
		 * for any interpolation.
		 */
		int differences = 0;
		for (int x = -Grid.INTERPOLATION_RADIUS;
		     x < expectedGrid.getNumCellsX() + Grid.INTERPOLATION_RADIUS; ++x) {
			for (int y = -Grid.INTERPOLATION_RADIUS;
			     y < expectedGrid.getNumCellsY() + Grid.INTERPOLATION_RADIUS; ++y) {

				Cell expectedCell = expectedGrid.getCell(x,y);
				Cell actualCell = actualGrid.getCell(x,y);
				if (!compareCells(expectedCell, actualCell, tolerance)) {
					++differences;
					System.out.println(" -> differences in cell: " + x + "," + y);
				}
			}
		}

		if (differences > 0) {
			fail(String.format("%d cells were different!", differences));
		}
	}


	private boolean compareCells(Cell cellA, Cell cellB, double tolerance) {
		boolean ok = true;
		double difference = Math.abs(cellA.getEx() - cellB.getEx());
		if (difference > tolerance) {
			printSingleMemberDifference("ex", difference);
			ok = false;
		}
		difference = Math.abs(cellA.getEy() - cellB.getEy());
		if (difference > tolerance) {
			printSingleMemberDifference("ey", difference);
			ok = false;
		}
		difference = Math.abs(cellA.getBz() - cellB.getBz());
		if (difference > tolerance) {
			printSingleMemberDifference("bz", difference);
			ok = false;
		}
		difference = Math.abs(cellA.getBzo() - cellB.getBzo());
		if (difference > tolerance) {
			printSingleMemberDifference("bzo", difference);
			ok = false;
		}
		difference = Math.abs(cellA.getJx() - cellB.getJx());
		if (difference > tolerance) {
			printSingleMemberDifference("jx", difference);
			ok = false;
		}
		Math.abs(cellA.getJy() - cellB.getJy());
		if (difference > tolerance) {
			printSingleMemberDifference("jy", difference);
			ok = false;
		}
		return ok;
	}


	private void printSingleMemberDifference(String member, double difference) {
		System.out.print(String.format("%s (%.6f) ", member,  difference));
	}


	private void fail(String msg) {
		StringBuilder finalMsg = new StringBuilder(msg);
		if (stepNo != NO_STEP_TRACKING) {
			finalMsg.append(" STEP NUMBER: " + stepNo);
		}
		finalMsg.append(", COMPARISON FAILED !!! ");
		throw new ComparisonFailedException(finalMsg.toString());
	}


	/**
	 * Used in debugging for fields comparison.
	 */
	private void printFields(Grid grid) {
		for (int y = -Grid.EXTRA_CELLS_BEFORE_GRID;
		     y < grid.getNumCellsY() + Grid.EXTRA_CELLS_AFTER_GRID; ++y) {
			for (int x = -Grid.EXTRA_CELLS_BEFORE_GRID;
			     x < grid.getNumCellsX() + Grid.EXTRA_CELLS_AFTER_GRID; ++x) {

				System.out.print(String.format("[%9.6f,%9.6f] ",
						grid.getCell(x, y).getEx(), grid.getCell(x, y).getEy()));
			}
			System.out.println();
		}
	}


	/**
	 * Used in debugging for currents comparison.
	 */
	private void printCurrents(Grid grid) {
		for (int y = -Grid.EXTRA_CELLS_BEFORE_GRID;
		     y < grid.getNumCellsY() + Grid.EXTRA_CELLS_AFTER_GRID; ++y) {
			for (int x = -Grid.EXTRA_CELLS_BEFORE_GRID;
			     x < grid.getNumCellsX() + Grid.EXTRA_CELLS_AFTER_GRID; ++x) {

				System.out.print(String.format("[%.6f,%.6f] ",
						grid.getCell(x, y).getJx(), grid.getCell(x, y).getJy()));
			}
			System.out.println();
		}
	}
}
