package net.beadsproject.touch;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import math.geom2d.Point2D;
import net.beadsproject.touch.perform.PerformNode;


/**
 * Particle Simulation Engine.
 * 
 * @author ben
 *
 */
public class ParticleSimulator {

	/**
	 * A particle in the simulation.
	 * Each particle corresponds to a perform node. 
	 * 
	 * @author ben
	 *
	 */
	@SuppressWarnings("serial")
	public static class Particle extends Point2D 
	{
		public double radius;
		public PerformNode pn;
		private Map<Particle,Integer> treeDistanceMap;
		
		public Particle(Point2D p)
		{
			super(p);
			treeDistanceMap = new HashMap<Particle,Integer>();
		}
		
		public Particle(HVLayout.HVPoint hvp)
		{
			setLocation(hvp);
			radius = hvp.radius;
			pn = hvp.pn;
			treeDistanceMap = new HashMap<Particle,Integer>();			
		}
		
		public void clearTreeDistMap()
		{
			treeDistanceMap.clear();
		}
		
		public void setTreeDistTo(Particle p, int d)
		{
			treeDistanceMap.put(p,d);
		}
		
		public int treeDistTo(Particle p)
		{
			Integer i = treeDistanceMap.get(p);
			if (i==null) return Integer.MAX_VALUE;
			else return i;
		}
		
		/**
		 * Does this particle contain point p?
		 */
		public boolean contains(Point2D p)
		{
			double d = p.distanceSq(this);
			if (d < radius*radius)
			{
				return true;
			}
			else
				return false;
		}
		
		public static float overlap(Particle a, Particle b)
		{
			return (float) (a.distance(b) - (a.radius+b.radius));
		}
	}
	
	List<Particle> particles;
	static private final double width = 1, height = 1;
		
	public ParticleSimulator()
	{
		particles = new LinkedList<Particle>();
	}	
	
	public void addHVPoints(List<HVLayout.HVPoint> hvpoints)
	{
		for(HVLayout.HVPoint hvp: hvpoints)
			particles.add(new Particle(hvp));		
	}	
	
	public void addParticle(Particle p)
	{
		for(Particle q: particles)
		{
			q.setTreeDistTo(p, PerformNode.getTreeDistance(q.pn, p.pn));
			p.setTreeDistTo(q, q.treeDistTo(p));			
		}
		particles.add(p);
		
	}
	
	public List<Particle> getParticles()
	{
		return particles;
	}
	
	public void computeTreeDistance()
	{
		// cache tree dist
		// for each overlapping particle, move it one "step" away, i.e., a little bit
		ListIterator<Particle> it = particles.listIterator();
		while(it.hasNext())
		{
			Particle p = it.next();
			p.clearTreeDistMap();
			
			ListIterator<Particle> it2 = particles.listIterator();
			while(it2.hasNext())
			{
				Particle q = it2.next();
				if (p==q) continue;
				
				p.setTreeDistTo(q, PerformNode.getTreeDistance(p.pn, q.pn));				
			}
		}
	}
	
	public void step(float dt)
	{
		updateParticles(dt);
	}
	
	private void updateParticles(float dt)
	{
		//updateParticleTreeAttraction(dt);
		updateParticlesExponential(dt);
		updateParticlesBoundary(dt);
	}	
	
	private void updateParticleTreeAttraction(float dt)
	{
		// perform an attraction step between all pairs...
		// for each overlapping particle, move it one "step" away, i.e., a little bit
		ListIterator<Particle> it = particles.listIterator();
		while(it.hasNext())
		{
			Particle p = it.next();
			ListIterator<Particle> it2 = particles.listIterator();
			while(it2.hasNext())
			{
				Particle q = it2.next();
				if (p==q) continue;
				
				int treedist = p.treeDistTo(q);
				if (treedist<=2)
				{				
					Point2D v = p.minus(q);
					double mag = v.distance(0,0);				
					// force should be proportional to the distance from the centroid
					v = v.scale(-0.00001*dt*mag); // let the force of repulsion equal the overlapsquared
					p.setLocation(p.plus(v));
					q.setLocation(q.minus(v));
				}
			}
		}
	}		
	
	public void updateParticlesExponential(float dt)
	{
		// for each overlapping particle, move it one "step" away, i.e., a little bit
		ListIterator<Particle> it = particles.listIterator();
		while(it.hasNext())
		{
			Particle p = it.next();
			ListIterator<Particle> it2 = particles.listIterator();
			while(it2.hasNext())
			{
				Particle q = it2.next();
				if (p==q) continue;
				
				float o = Particle.overlap(p,q);
				if (o<0)
				{
					// p and q away from each other
					final double PERTURB = 0.02;
					Point2D v = p.minus(q);
					double lv = v.distance(0,0);
					if (lv < PERTURB)
					{
						// then perturb each position randomly						
						p.setLocation(p.plus(new Point2D(PERTURB*Math.random() - PERTURB/2,PERTURB*Math.random() - PERTURB/2)));
						q.setLocation(q.plus(new Point2D(PERTURB*Math.random() - PERTURB/2,PERTURB*Math.random() - PERTURB/2)));						
					}
					else
					{	
						float strength = (float) (-o/lv); // between 0 and 1
						v = v.scale(dt*strength*strength/lv); // let the force of repulsion equal the overlapsquared
						p.setLocation(p.plus(v));
						q.setLocation(q.minus(v));
					}
				}
			}
		}
	}
	
	void updateParticlesBoundary(float dt)
	{
		// for each overlapping particle, move it one "step" away, i.e., a little bit
		ListIterator<Particle> it = particles.listIterator();
		while(it.hasNext())
		{
			Particle p = it.next();		
		
			// also force the boundary constraint
			final double BOUNDFORCE = 0.001;
			if ((p.x-p.radius) < 0)
			{
				p.x += BOUNDFORCE*dt*(p.x-p.radius)*(p.x-p.radius);
			}
			else if ((p.x+p.radius) > width)
			{
				p.x -= BOUNDFORCE*dt*((width-p.x) - p.radius)*((width-p.x) - p.radius);
			}
			
			if ((p.y - p.radius) < 0)
			{
				p.y += BOUNDFORCE*dt*(p.y-p.radius)*(p.y-p.radius);	
			}
			else if ((p.y+p.radius) > height)
			{
				p.y -= BOUNDFORCE*dt*((height-p.y) - p.radius)*((height-p.y) - p.radius);
			}
			
		}
	}	
}
