package net.beadsproject.touch.old;

import java.awt.Color;
import java.awt.MouseInfo;
import java.awt.Point;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import math.geom2d.Point2D;
import megamu.mesh.MPolygon;
import megamu.mesh.Voronoi;
import net.beadsproject.beads.core.AudioContext;
import net.beadsproject.beads.core.UGen;
import net.beadsproject.touch.SurfacePlayer;
import net.beadsproject.touch.event.EnterEvent;
import net.beadsproject.touch.event.Event;
import net.beadsproject.touch.event.EventListener;
import net.beadsproject.touch.event.ExitEvent;
import net.beadsproject.touch.event.SwitchEvent;
import net.beadsproject.touch.examples.Example;
import net.beadsproject.touch.perform.PerformNode;
import net.beadsproject.touch.surface.PixelSurface;
import processing.core.PApplet;
import processing.core.PGraphics;


/**
 * Shows voronoi version.
 * 
 * @author ben
 */
public class ParticleHierarchyTestWithVoronoi extends PApplet {	
	static int NUM_PARTICLES; // determined below
	
	// particle physics models
	static final int MODE_EXPONENTIAL = 2;
	static final int MODE = MODE_EXPONENTIAL;	
	
	Voronoi voronoi;

	static class Particle
	{
		public Point2D position; 
		public float radius;
		public PerformNode pn;
		
		// depre
		public int group;
		
		// cache the tree distance to each other particle
		public Map<Particle,Integer> treeDistanceMap;
		
		public Particle(Point2D pos, float rad)
		{
			position = pos; radius = rad;
			treeDistanceMap = new HashMap<Particle,Integer>();
		}
		public static float overlap(Particle a, Particle b)
		{
			return (float) (a.position.distance(b.position) - (a.radius+b.radius));
		}
	}
	
	List<Particle> particles;
	Map<PerformNode,Particle> pnToParticleMap;
	
	List<Set<Particle>> particleGroups;
	Color[] groupColours;
	
	boolean shouldIUpdateParticles = false;
	
	boolean hasBeenBaked = false;
	PixelSurface ps;
	
	PGraphics tempImg;
	
	float ox, oy; // old mousex, mousey;
	SurfacePlayer sp; // the user
	
	public void setup()
	{
		size(800,800);		
		ellipseMode(PApplet.CENTER);
		smooth();		
		frameRate(50);
		sp = new SurfacePlayer();
		
		pnToParticleMap = new HashMap<PerformNode,Particle>();
		
		// String filename = "D:\\audio\\crash test audio\\chopped live sounds\\Bongos\\BONGOROL.WAV";
		// String filename = "D:\\Music\\Alog\\Amateur\\02 A Throne For The Common Man.mp3";
		// String filename = "D:\\Music\\gammaBrosTheme.mp3";
		/*
		NUM_PARTICLES = 200;
		final int DEPTH = 2;
		final int NARY = 3;
		
		PerformTree pt = ExampleTree.exampleScatterPartsTreeUniform(DEPTH,NARY,(int) (NUM_PARTICLES/(Math.pow(DEPTH,NARY))));
		// PerformTree pt = ExampleTree.exampleScatterPartsTree(filename, NUM_PARTICLES);
		pt.computeUniqueValues();
		*/
		
		//BEN'S EXAMPLE
//		String filename = "D:\\Music\\Alog\\Amateur\\02 A Throne For The Common Man.mp3";
//		String filename = "../audio/flex.wav";
//		PerformTree pt = ExampleTree.exampleScatterPartsTree(filename,100);
		
		//OLLIE'S EXAMPLE
		AudioContext ac = new AudioContext();
		PerformNode pt = Example.example1();
//		PerformTree pt = ExampleTree3.example1(ac);
//		setupAudioRateMousePoller(ac);
		
		pt.computeUniqueValue(0,1);
		NUM_PARTICLES = pt.getRoot().totalNumDescendents();		
		
		System.out.println(pt.toString());
		loadTree(pt);		
		buildVoronoi();
	}
	
	public void draw()
	{
		background(225);		
		
		if (!hasBeenBaked)
		{		
			if (shouldIUpdateParticles)
				updateParticles(0.01f);
			
			/*
			MPolygon[] regions = voronoi.getRegions();                          
			int index = 0;
			noStroke();
			for(Particle p: particles)
			{
				Color c = new Color(Color.HSBtoRGB(p.pn.uniqueValue(), 0.75f, 0.75f));
				fill(c.getRed(),c.getGreen(),c.getBlue(),150);			
				regions[index].draw(this);
				
				fill(0);			
				ellipse((float)p.position.x,(float)p.position.y,5,5);
				
				index++;
			}
			
			stroke(255);
			strokeWeight(500.f/NUM_PARTICLES);
			
			float[][] myEdges = voronoi.getEdges();
			for(int i=0; i<myEdges.length; i++)
			{
				float startX = myEdges[i][0];
				float startY = myEdges[i][1];
				float endX = myEdges[i][2];
				float endY = myEdges[i][3];
				line( startX, startY, endX, endY );
			}*/
			
			for(Particle p: particles)
			{
				/*
				stroke(255);
				strokeWeight(500.f/NUM_PARTICLES);			
				Color c = new Color(Color.HSBtoRGB(p.pn.uniqueValue(), 0.6f, 0.8f));			
				fill(c.getRed(),c.getGreen(),c.getBlue(),150);
				ellipse((float)p.position.x,(float)p.position.y,2*p.radius,2*p.radius);
				*/
				noStroke();
				Color c = new Color(Color.HSBtoRGB(p.pn.uniqueValue(), 0.75f, 0.75f));
				fill(c.getRed(),c.getGreen(),c.getBlue());
				ellipse((float)p.position.x,(float)p.position.y,5,5);
			}
		}
		else // show baked mode
		{
			background(0);
			noFill();
			//image(tempImg,0,0);
			ps.draw(this);
		}
		
		
		
		
		/*
		
		for(Particle p: particles)
		{			
			if (p.pn.getChildren().isEmpty())
			{			
				// draw a line to the centroid of all pn's siblings
				List<PerformNode> childlessSiblings = new LinkedList<PerformNode>();
				// filter to choose only those that have no children themself
				List<PerformNode> siblings = p.pn.getSiblings();
				if (siblings!=null)
				{
					for(PerformNode s: p.pn.getSiblings())
					{
						if (s.getChildren().isEmpty())
							childlessSiblings.add(s);
					}
					
					if (childlessSiblings!=null)
					{
						Point2D centroid = new Point2D(p.position);
						for(PerformNode pn: childlessSiblings)
						{
							Particle s = pnToParticleMap.get(pn);
							if (s!=null)
							{
								centroid = centroid.plus(s.position);
							}
						}
						centroid = centroid.scale(1.f/(1+childlessSiblings.size()));
					
						stroke(0,150);
						strokeWeight(2);
						line((float)p.position.x,(float)p.position.y,
								(float)centroid.x,(float)centroid.y);				
						
					}
				}
			}
		}
		*/
		
	}
	
	public void keyPressed()
	{
		if (key==' ' && !hasBeenBaked)
		{
			// bake the surface and render it
			hasBeenBaked = true;
			ps = new PixelSurface(width,height);
			
			// render the surface using the index of the perform node as the color...
			int index = 0;
			PerformNode[] pns = new PerformNode[particles.size()];
			for(Particle p: particles)			
			{
				pns[index] = p.pn;
				index++;
			}
			
			tempImg = this.createGraphics(width,height,P2D);
			MPolygon[] regions = voronoi.getRegions();
			tempImg.beginDraw();
			tempImg.background(0);
			// tempImg.backgroundColor = 0;			
			tempImg.noStroke();
			index = 0;
			for(PerformNode pn: pns)
			{
				//tempImg.fill(index+1);
				int col  = index + 1;
				tempImg.fill((col >> 16)&0xFF,(col >> 8)&0xFF,col&0xFF);
				
				//tempImg.fill(index);
				regions[index].draw(tempImg);
				index++;
			}
			
			if (index > (256*256*256))
			{
				System.err.println("Error, too many regions! Has to be less than 256^3 atm.\nContinuing anyway..");
			}
			tempImg.endDraw();
			
			tempImg.loadPixels();
			for(int i=0;i<tempImg.pixels.length;i++)
			{
				int r = i/tempImg.width;
				int c = i%tempImg.width;
				int pni = tempImg.pixels[i];
				
				int rd = (pni >> 16) & 0xFF;
				int gr = (pni >> 8) & 0xFF;
				int bl = pni & 0xFF;
				
				int in = (int)((rd << 16) | (gr << 8) | bl) - 1; 

				//Ollie - seems like there be something wrong here
				//Ben - fixed
				if (in<0)
					ps.set(c,r,null);
				else
					ps.set(c,r,pns[in]);
			}			
			tempImg.updatePixels();
			
			ps.bake(this);			
			
			ps.setEventListener(el);
		}
	}
		
	public void updateParticles(float dt)
	{
		updateParticleTreeAttraction(dt);
		updateParticlesExponential(dt);
		updateParticlesBoundary(dt);
		
		buildVoronoi();
	}	
	
	public 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.treeDistanceMap.get(q);
				if (treedist<=1)
				{				
					Point2D v = p.position.minus(q.position);
					double mag = v.distance(0,0);				
					// force should be proportional to the distance from the centroid
					v = v.scale(-0.000001*dt*mag); // let the force of repulsion equal the overlapsquared
					p.position = p.position.plus(v);
					q.position = q.position.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
					
					if (p.position.distanceSq(q.position) < 2)
					{
						// then perturb each position randomly
						final double PERTURB = 2;
						p.position = p.position.plus(new Point2D(PERTURB*Math.random() - PERTURB/2,PERTURB*Math.random() - PERTURB/2));
						q.position = q.position.plus(new Point2D(PERTURB*Math.random() - PERTURB/2,PERTURB*Math.random() - PERTURB/2));						
					}
					else
					{
						Point2D v = p.position.minus(q.position);
						v = v.scale((o*o)*dt/v.distance(0,0)); // let the force of repulsion equal the overlapsquared
						p.position = p.position.plus(v);
						q.position = q.position.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.2;
			if ((p.position.x-p.radius) < 0)
			{
				p.position.x += BOUNDFORCE*dt*(p.position.x-p.radius)*(p.position.x-p.radius);
			}
			else if ((p.position.x+p.radius) > width)
			{
				p.position.x -= BOUNDFORCE*dt*((width-p.position.x) - p.radius)*((width-p.position.x) - p.radius);
			}
			
			if ((p.position.y - p.radius) < 0)
			{
				p.position.y += BOUNDFORCE*dt*(p.position.y-p.radius)*(p.position.y-p.radius);	
			}
			else if ((p.position.y+p.radius) > height)
			{
				p.position.y -= BOUNDFORCE*dt*((height-p.position.y) - p.radius)*((height-p.position.y) - p.radius);
			}
			
		}
	}	
	
	
	void loadTree(PerformNode pt)
	{
		particles = new LinkedList<Particle>();
		
		PerformNode root = pt.getRoot();
		if (root==null) return;
		BRV_addPNAndChildren(root,width/2,height/1,width/1);
		//vs.buildPointsAndTheirPolys();
		
		// 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();
			ListIterator<Particle> it2 = particles.listIterator();
			while(it2.hasNext())
			{
				Particle q = it2.next();
				if (p==q) continue;
				
				p.treeDistanceMap.put(q, PerformNode.getTreeDistance(p.pn, q.pn));				
			}
		}
	}
	
	void BRV_addPNAndChildren(PerformNode pn, double x, double y, double r)
	{
		List<PerformNode> children = pn.getChildren();
		int num = children.size();
		// add self 
		// addPerformNode(pn,x,y);
		if (num==0)
		{
			addPerformNode(pn, x, y);
		}
		if (num==1)
		{
			// add sole child just above it
			PerformNode child = children.get(0);
			BRV_addPNAndChildren(child, x, y+r/2, (float) (r*0.8));
		}
		else
		{
			// distribute the circles equally around the center
			double dtheta = 2.0*Math.PI/num;
			double theta = 0;
			double distFromCenter = r * .5;
	
			// compute the maximum radius the subcircles can be
			double radius = Math.min(r-distFromCenter,distFromCenter*Math.sin(dtheta/2));
			for(PerformNode child: children)
			{
				double cx = distFromCenter*Math.cos(theta) + x;
				double cy = distFromCenter*Math.sin(theta) + y;
				theta += dtheta;	
	
				BRV_addPNAndChildren(child, cx, cy, radius);				
			}			
		}		
	}
	
	void addPerformNode(PerformNode pn, double x, double y)
	{
		final float minRadius = (float) (width/(2.0*Math.sqrt(NUM_PARTICLES)));
		final float maxRadius = (float) (width/(1.5*Math.sqrt(NUM_PARTICLES)));
		float radius = (float)(Math.random()*(maxRadius-minRadius) + minRadius);		
		
		Particle p = new Particle(new Point2D(x,y), radius);
		p.pn = pn;		
		particles.add(p);
		pnToParticleMap.put(p.pn,p);
	}
		
	public void buildVoronoi()
	{
		// parse data appropriately
		float[][] pts = new float[particles.size()][2];
		int index = 0;
		for(Particle p: particles)
		{
			pts[index][0] = (float) p.position.x;
			pts[index][1] = (float) p.position.y;
			index++;
		}
		
		voronoi = new Voronoi(pts);
	}
	
	public void setupAudioRateMousePoller(AudioContext ac) {
		UGen mousePoller = new UGen(ac) {
			public void calculateBuffer() {
				if(mousePressed) {
					Point p = MouseInfo.getPointerInfo().getLocation();
					didMouseDragged(p.x, p.y);
				}
			}
		};
		ac.out.addDependent(mousePoller);
	}
	
	public void mouseDragged()
	{
		didMouseDragged(mouseX, mouseY);
	}

	
	int count = 0;
	long previousTime = 0;
	
	public void didMouseDragged(int mouseX, int mouseY)
	{
		if (hasBeenBaked)
		{
			float nx = mouseXToWorldX(mouseX);
			float ny = mouseYToWorldY(mouseY);
			
			// notify surface
			// cs.mouseMoved(ox,oy,nx,ny);
			ps.playerMoved(sp,ox,oy,nx,ny);
			
			// update old position
			ox = nx;
			oy = ny;
			
			long newTime = System.currentTimeMillis();
//			System.out.println("drag " + count++ + " " + (int)(newTime - previousTime));
			previousTime = newTime;
		}
	}
	
	public void mouseReleased()
	{
		if (hasBeenBaked)
		{
			float nx = mouseXToWorldX(mouseX);
			float ny = mouseYToWorldY(mouseY);
			ps.playerReleased(sp,nx,ny);		
			ox = nx;
			oy = ny;
		}
	}
	
	public void mousePressed()
	{
		if (!hasBeenBaked)
		{
			shouldIUpdateParticles = !shouldIUpdateParticles;
		}
		else
		{
			float nx = mouseXToWorldX(mouseX);
			float ny = mouseYToWorldY(mouseY);
			ps.playerPressed(sp,nx,ny);
			ox = nx;
			oy = ny;
		}
	}
	
	
	public float mouseXToWorldX(int x)
	{
		return (float) (1.0*x/width);
	}
	
	public float mouseYToWorldY(int y)
	{
		return (float) (1.0*y/height);
	}
	
}
