package org.openpixi.pixi.distributed;

import org.openpixi.pixi.distributed.ibis.IbisRegistry;
import org.openpixi.pixi.distributed.ibis.WorkerToWorker;
import org.openpixi.pixi.physics.GeneralBoundaryType;
import org.openpixi.pixi.physics.particles.Particle;
import org.openpixi.pixi.physics.grid.Grid;
import org.openpixi.pixi.physics.movement.boundary.ParticleBoundaries;
import org.openpixi.pixi.physics.util.IntBox;
import org.openpixi.pixi.physics.util.Point;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Holds shared data for each boundary and border region.
 * Provides access to the shared data and operations upon the shared data.
 *
 * The shared data for a specific neighbor is created according to demand on the fly.
 */
public class SharedDataManager {

	/** Maps region to neighbor. */
	private NeighborMap neighborMap;

	/** Maps neighbor to SharedData. */
	private Map<Integer, SharedData> sharedData = new HashMap<Integer, SharedData>();

	private IbisRegistry registry;

	private Thread particleExchangeThread;


	public SharedDataManager(
			int thisWorkerID,
			IntBox[] partitions,
	        IntBox globalSimArea,
	        GeneralBoundaryType boundaryType,
	        IbisRegistry registry) {

		this.registry = registry;
		this.neighborMap = new NeighborMap(thisWorkerID, partitions, globalSimArea, boundaryType);
	}


	//----------------------------------------------------------------------------------------------
	// Methods required for initialization of distributed simulation
	//----------------------------------------------------------------------------------------------


	public SharedData getBoundarySharedData(int boundaryRegion) {
		int neighbor = neighborMap.getBoundaryNeighbor(boundaryRegion);
		if (neighbor != NeighborMap.NO_NEIGHBOR) {
			return getSharedData(neighbor);
		}
		else {
			return null;
		}
	}


	public Point getBoundaryDirections(int boundaryRegion) {
		return neighborMap.getBoundaryNeighborsDirections(boundaryRegion);
	}


	public List<SharedData> getBorderSharedData(int borderRegion) {
		List<SharedData> retval = new ArrayList<SharedData>();
		int[] neighbors = neighborMap.getBorderNeighbors(borderRegion);
		for (int neighbor: neighbors) {
			if (neighbor != NeighborMap.NO_NEIGHBOR) {
				retval.add(getSharedData(neighbor));
			}
		}
		return retval;
	}


	public List<Point> getBorderDirections(int borderRegion) {
		List<Point> retval = new ArrayList<Point>();
		Point[] directions = neighborMap.getBorderNeighborsDirections(borderRegion);
		if (directions != null) {
			for (Point direction: directions) {
				if (direction != null) {
					retval.add(direction);
				}
			}
		}
		return retval;
	}


	/**
	 * If the SharedData for the given neighbor does not exist it is created on the fly.
	 */
	private SharedData getSharedData(int neighbor) {
		if (!sharedData.containsKey(neighbor)) {
			sharedData.put(
					neighbor,
					new SharedData(new WorkerToWorker(registry, neighbor)));
		}
		return sharedData.get(neighbor);
	}


	public void setParticleBoundaries(ParticleBoundaries particleBoundaries) {
		for (SharedData sd: sharedData.values()) {
			sd.setParticleBoundaries(particleBoundaries);
		}
	}


	public void setGrid(Grid grid) {
		for (SharedData sd: sharedData.values()) {
			sd.setGrid(grid);
		}
	}


	public void initializeCommunication() {
		for (SharedData sd: sharedData.values()) {
			sd.initializeCommunication();
		}
	}


	//----------------------------------------------------------------------------------------------
	// Methods required during distributed simulation
	//----------------------------------------------------------------------------------------------


	/**
	 * The exchange of particles can last some time as we have to wait for the arriving particles
	 * before we send the border particles.
	 * As we do not want to stall the calling thread,
	 * we start the exchange of particles in a new thread.
	 */
	public void startExchangeOfParticles() {
		particleExchangeThread = new Thread(new Runnable() {
			public void run() {
				for (SharedData sd: sharedData.values()) {
					sd.sendLeavingParticles();
				}

				// Before we send the border particles we have to wait
				// for all the arriving particles as they are as well border particles.
				waitForArrivingParticles();
				for (SharedData sd: sharedData.values()) {
					sd.sendBorderParticles();
				}
			}
		});
		particleExchangeThread.start();
	}


	public List<Particle> getArrivingParticles() {
		List<Particle> arrivingParticles = new ArrayList<Particle>();
		for (SharedData sd: sharedData.values()) {
			arrivingParticles.addAll(sd.getArrivingParticles());
		}
		return arrivingParticles;
	}


	public List<Particle> getGhostParticles() {
		List<Particle> ghostParticles = new ArrayList<Particle>();
		for (SharedData sd: sharedData.values()) {
			ghostParticles.addAll(sd.getGhostParticles());
		}
		return ghostParticles;
	}


	public List<Particle> getLeavingParticles() {
		List<Particle> leavingParticles = new ArrayList<Particle>();
		for (SharedData sd: sharedData.values()) {
			leavingParticles.addAll(sd.getLeavingParticles());
		}
		return leavingParticles;
	}


	public void exchangeCells() {
		for (SharedData sd: sharedData.values()) {
			sd.sendBorderCells();
		}
	}


	public void waitForGhostCells() {
		for (SharedData sd: sharedData.values()) {
			sd.waitForGhostCells();
		}
	}


	private void waitForArrivingParticles() {
		for (SharedData sd: sharedData.values()) {
			sd.waitForArrivingParticles();
		}
	}


	public void cleanUpCellCommunication() {
		for (SharedData sd: sharedData.values()) {
			sd.cleanUpCellCommunication();
		}
	}


	public void cleanUpParticleCommunication() {
		// Wait for the exchange of particles to finish
		// (particularly wait for finishing the sending of border particles).
		// Otherwise, we can end up concurrently modifying the border particles
		// (this thread cleans up the list of border particles).
		try {
			particleExchangeThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}

		for (SharedData sd: sharedData.values()) {
			sd.cleanUpParticleCommunication();
		}
	}


	public void close() {
		for (SharedData sd: sharedData.values()) {
			sd.closeSendPorts();
		}
		for (SharedData sd: sharedData.values()) {
			sd.closeReceivePorts();
		}
	}
}
