using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using FluidTest1;

public class FluidSimulation : MonoBehaviour
{
	public const int MAX_PARTICLES = 1000;
	public const int MAX_NEIGHBORS = 75;
	public const float RADIUS = 0.6f;
	public const float VISCOSITY = 0.004f;
	public const float IDEAL_RADIUS = 50f;
	public const float MULTIPLIER = IDEAL_RADIUS / RADIUS;

	public const float IDEAL_RADIUS_SQ = IDEAL_RADIUS * IDEAL_RADIUS;

	public const float CELL_SIZE = 0.5f;

	private int _numActiveParticles = 0;
	private MyParticle[] _liquid;
	private List<int> _activeParticles;
	private Vector2[] _delta;
	private Vector2[] _scaledPositions;
	private Vector2[] _scaledVelocities;

	private Dictionary<int, Dictionary<int, List<int>>> _grid;

	private GameObject	_sprite;

	public void Init (GameObject sprite)
	{
		_sprite = sprite;

		_activeParticles = new List<int>(MAX_PARTICLES);
		_liquid = new MyParticle[MAX_PARTICLES];
		for (int i = 0; i < MAX_PARTICLES; i++)
		{
			_liquid[i] = new MyParticle(Vector2.zero, Vector2.zero, false);
			_liquid[i].index = i;
		}

		_delta = new Vector2[MAX_PARTICLES];

		_scaledPositions = new Vector2[MAX_PARTICLES];
		_scaledVelocities = new Vector2[MAX_PARTICLES];

		_grid = new Dictionary<int, Dictionary<int, List<int>>>();
	}

	public void createParticle(Vector3 screenPosition, int numParticlesToSpawn = 4)
	{
		IEnumerable<MyParticle> inactiveParticles = from particle in _liquid
			where particle.alive == false
				select particle;
		inactiveParticles = inactiveParticles.Take(numParticlesToSpawn);
		
		foreach (MyParticle particle in inactiveParticles)
		{
			if (_numActiveParticles < MAX_PARTICLES)
			{
				Vector2 jitter = new Vector2(Random.Range(-1f, 1f), Random.Range(-0.5f, 0.5f));
				
				particle.position = new Vector2(screenPosition.x, screenPosition.y) + jitter;
				particle.velocity = Vector2.zero;

				particle.ci = getGridX(particle.position.x);
				particle.cj = getGridY(particle.position.y);

				if(particle.sprite == null)
				{
					GameObject newSprite = Instantiate(_sprite) as GameObject;
					newSprite.transform.parent = _sprite.transform.parent;
					particle.Visualize(newSprite);
					Vector3 worldPosition = Camera.main.ScreenToWorldPoint(new Vector3(particle.position.x, particle.position.y, 0));
					worldPosition.Scale(new Vector3(1, 1, 0));
               		particle.sprite.transform.position = worldPosition;
				}

				particle.alive = true;
				
				// Create grid cell if necessary
				if (!_grid.ContainsKey(particle.ci))
					_grid[particle.ci] = new Dictionary<int, List<int>>();
				if (!_grid[particle.ci].ContainsKey(particle.cj))
					_grid[particle.ci][particle.cj] = new List<int>();
				_grid[particle.ci][particle.cj].Add(particle.index);
				
				_activeParticles.Add(particle.index);
				_numActiveParticles++;
			}
		}
	}

	public void applyLiquidConstraints(float deltaTime)
	{

		// Prepare simulation
		for (int i = 0; i < _numActiveParticles; i++)
		{
			int index = _activeParticles[i];
			MyParticle particle = _liquid[index];

			// Find neighbors
			findNeighbors(particle);

			// Scale positions and velocities
			_scaledPositions[index] = particle.position * MULTIPLIER;
			_scaledVelocities[index] = particle.velocity * MULTIPLIER;
			
			// Reset deltas
			_delta[index] = Vector2.zero;

			// Calculate pressure
			float p = 0.0f;
			float pnear = 0.0f;
			for (int a = 0; a < particle.neighborCount; a++)
			{
				Vector2 relativePosition = _scaledPositions[particle.neighbors[a]] - _scaledPositions[index];
				float distanceSq = relativePosition.sqrMagnitude;
				
				//within idealRad check
				if (distanceSq < IDEAL_RADIUS_SQ)
				{
					particle.distances[a] = (float)Mathf.Sqrt(distanceSq);
					//if (particle.distances[a] < Settings.EPSILON) particle.distances[a] = IDEAL_RADIUS - .01f;
					float oneminusq = 1.0f - (particle.distances[a] / IDEAL_RADIUS);
					p = (p + oneminusq * oneminusq);
					pnear = (pnear + oneminusq * oneminusq * oneminusq);
				}
				else
				{
					particle.distances[a] = float.MaxValue;
				}
			}

			// Apply forces
			float pressure = (p - 5f) / 2.0f; //normal pressure term
			float presnear = pnear / 2.0f; //near particles term
			Vector2 change = Vector2.zero;
			for (int a = 0; a < particle.neighborCount; a++)
			{
				Vector2 relativePosition = _scaledPositions[particle.neighbors[a]] - _scaledPositions[index];
				
				if (particle.distances[a] < IDEAL_RADIUS)
				{
					float q = particle.distances[a] / IDEAL_RADIUS;
					float oneminusq = 1.0f - q;
					float factor = oneminusq * (pressure + presnear * oneminusq) / (2.0F * particle.distances[a]);
					Vector2 d = relativePosition * factor;
					Vector2 relativeVelocity = _scaledVelocities[particle.neighbors[a]] - _scaledVelocities[index];
					
					factor = VISCOSITY * oneminusq * deltaTime;
					d -= relativeVelocity * factor;
					_delta[particle.neighbors[a]] += d;
					change -= d;
				}
			}
			_delta[index] += change;
		}

		// Move particles
		for (int i = 0; i < _numActiveParticles; i++)
		{
			int index = _activeParticles[i];
			MyParticle particle = _liquid[index];
			
			particle.position += _delta[index] / MULTIPLIER;
			particle.velocity += _delta[index] / (MULTIPLIER * deltaTime);

			Vector3 worldPosition = Camera.main.ScreenToWorldPoint(new Vector3(particle.position.x, particle.position.y, 0));
			worldPosition.Scale(new Vector3(1, 1, 0));
			particle.sprite.transform.position = worldPosition;

			// Update particle cell
			int x = getGridX(particle.position.x);
			int y = getGridY(particle.position.y);
			
			if (particle.ci == x && particle.cj == y)
			{
				continue;
			}
			else
			{
				_grid[particle.ci][particle.cj].Remove(index);
				
				if (_grid[particle.ci][particle.cj].Count == 0)
				{
					_grid[particle.ci].Remove(particle.cj);
					
					if (_grid[particle.ci].Count == 0)
					{
						_grid.Remove(particle.ci);
					}
				}
				
				if (!_grid.ContainsKey(x))
					_grid[x] = new Dictionary<int, List<int>>();
				if (!_grid[x].ContainsKey(y))
					_grid[x][y] = new List<int>(20);
				
				_grid[x][y].Add(index);
				particle.ci = x;
				particle.cj = y;
			}
		}
	}

	private void findNeighbors(MyParticle particle)
	{
		particle.neighborCount = 0;
		Dictionary<int, List<int>> gridX;
		List<int> gridY;
		
		for (int nx = -1; nx < 2; nx++)
		{
			for (int ny = -1; ny < 2; ny++)
			{
				int x = particle.ci + nx;
				int y = particle.cj + ny;
				if (_grid.TryGetValue(x, out gridX) && gridX.TryGetValue(y, out gridY))
				{
					
					for (int a = 0; a < gridY.Count; a++)
					{
						if (gridY[a] != particle.index)
						{
							particle.neighbors[particle.neighborCount] = gridY[a];
							particle.neighborCount++;
							
							if (particle.neighborCount >= MAX_NEIGHBORS)
								return;
						}
					}
				}
			}
		}
	}
	
	private int getGridX(float x) { return (int)Mathf.Floor(x / CELL_SIZE); }
	private int getGridY(float y) { return (int)Mathf.Floor(y / CELL_SIZE); }
}


