package org.openpixi.pixi.distributed.movement.boundary;

import org.openpixi.pixi.distributed.SharedData;
import org.openpixi.pixi.distributed.SharedDataManager;
import org.openpixi.pixi.physics.particles.Particle;
import org.openpixi.pixi.physics.force.Force;
import org.openpixi.pixi.physics.movement.boundary.*;
import org.openpixi.pixi.physics.solver.Solver;
import org.openpixi.pixi.physics.util.DoubleBox;
import org.openpixi.pixi.physics.util.Point;

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

/**
 * Maps the border and boundary regions to boundaries which should be applied.
 */
public class DistributedParticleBoundaries implements ParticleBoundaries {

	private BoundaryRegions boundaryRegions;
	private ParticleBoundary[] boundaryMap =
			new ParticleBoundary[BoundaryRegions.NUM_OF_REGIONS];

	private BorderRegions borderRegions;
	private List<List<BorderGate>> borderMap =
			new ArrayList<List<BorderGate>>(BorderRegions.NUM_OF_REGIONS);

	private ParticleBoundaryType boundaryType;
	private DoubleBox simulationArea;


	public ParticleBoundaryType getType() {
		return boundaryType;
	}


	public DistributedParticleBoundaries(
			DoubleBox simulationArea,
			DoubleBox innerArea,
			ParticleBoundaryType boundaryType,
			SharedDataManager sharedDataManager) {

		this.boundaryType = boundaryType;
		this.simulationArea = simulationArea;

		boundaryRegions = new BoundaryRegions(simulationArea);
		borderRegions = new BorderRegions(simulationArea, innerArea);

		for (int i = 0; i < BorderRegions.NUM_OF_REGIONS; ++i) {
			borderMap.add(new ArrayList<BorderGate>());
		}

		createBoundaryMap(boundaryType, sharedDataManager);
		createBorderMap(sharedDataManager);
	}


	private void createBoundaryMap(
			ParticleBoundaryType boundaryType, SharedDataManager sharedDataManager) {

		for (int region = 0; region < BoundaryRegions.NUM_OF_REGIONS; ++region) {
			SharedData sd = sharedDataManager.getBoundarySharedData(region);

			if (sd != null) {
				double xoffset = getXOffset(sharedDataManager.getBoundaryDirections(region));
				double yoffset = getYOffset(sharedDataManager.getBoundaryDirections(region));
				boundaryMap[region] = new BoundaryGate(xoffset, yoffset, sd);
			}
			else if (region == BoundaryRegions.X_CENTER + BoundaryRegions.Y_CENTER) {
				boundaryMap[region] = new EmptyBoundary(0, 0);
			}
			else {
				double xoffset = getXOffsetFromRegion(region);
				double yoffset = getYOffsetFromRegion(region);
				boundaryMap[region] = boundaryType.createBoundary(xoffset, yoffset);
			}

		}
	}


	private void createBorderMap(SharedDataManager sharedDataManager) {
		for (int region = 0; region < BorderRegions.NUM_OF_REGIONS; ++region) {
			List<SharedData> sharedDatas = sharedDataManager.getBorderSharedData(region);
			List<Point> directions = sharedDataManager.getBorderDirections(region);
			assert sharedDatas.size() == directions.size();

			for (int i = 0; i < sharedDatas.size(); ++i) {
				double xoffset = getXOffset(directions.get(i));
				double yoffset = getYOffset(directions.get(i));

				borderMap.get(region).add(new BorderGate(xoffset, yoffset, sharedDatas.get(i)));
			}
		}
	}


	/*
	 * The x and y offsets are necessary for
	 * correct particle position translation (in case of distributed boundary)
	 * or for correct particle reflection (in case of non-distributed boundary)
	 * The offsets depend on the direction of the neighbor (in case of distributed boundary)
	 * or on the region (in case of non-distributed boundary).
	 */

	private double getYOffset(Point direction) {
		return direction.y * simulationArea.ysize();
	}


	private double getXOffset(Point direction) {
		return direction.x * simulationArea.xsize();
	}

	private double getYOffsetFromRegion(int region) {
		return BoundaryRegions.getSign(region).y * simulationArea.ysize();
	}


	private double getXOffsetFromRegion(int region) {
		return BoundaryRegions.getSign(region).x * simulationArea.xsize();
	}


	public void changeType(ParticleBoundaryType boundaryType) {
		throw new UnsupportedOperationException(
				"The type of the boundary in distributed simulation can not be changed!");
	}


	public void applyOnParticleBoundingBox(
			Solver solver, Force force, Particle particle, double timeStep) {
		throw new UnsupportedOperationException(
				"In distributed boundaries we can not deduce particle's position " +
				"based on its bounding box as it would result in errors.");
	}


	public void applyOnParticleCenter(
			Solver solver, Force force, Particle particle, double timeStep) {
		int borderRegion = borderRegions.getRegion(particle.getX(), particle.getY());
		for (BorderGate bg: borderMap.get(borderRegion)) {
			bg.apply(solver, force, particle, timeStep);
		}

		// Boundary gates have to be called after border gates as they change the position
		// of the particle.
		int boundaryRegion = boundaryRegions.getRegion(particle.getX(), particle.getY());
		boundaryMap[boundaryRegion].apply(solver, force, particle, timeStep);
	}
}
