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

public class PlaneGrid : MonoBehaviour {


	string stringMap =  "A        |"
					  + ".. UUU ..|"
					  + "L. UUU .R|"
					  + "LL ... RR|"
					  + "LL ... RR|"
					  + "LL ... RR|"
					  + "LL ... RR|"
					  + "L. DDD .R|"
					  + ".. DDD ..|"
					  ;
	string stringEntsTutorial
					  = ".        |"
					  + ".R R.. .2|"
					  + ".. ... ..|"
					  + ".. .R. ..|"
					  + "-. .-. ..|"
					  + "S. -2. ..|"
					  + ".. .2. .-|"
					  + ".. ... ..|"
					  + "-. .RR .2|"
					  ;

	Dictionary<char, Color> _colors = new Dictionary<char, Color> {
		  { 'A', Color.white }
		, { 'L', Color.black } 
		, { 'R', Color.red } 
		, { 'U', Color.cyan }
		, { 'D', Color.magenta }
	};

	Dictionary<char, Vector2> _steerings = new Dictionary<char, Vector2> {
		  { 'L', new Vector2(-1f ,  0f) }
		, { 'R', new Vector2( 1f ,  0f) }
		, { 'U', new Vector2( 0f ,  1f) }
		, { 'D', new Vector2( 0f , -1f) }
	};

	public Entity prefabEntRegular;
	public Entity prefabEntBro;
	public Entity prefabEntBaby;
	public Entity prefabEntSalaryman;

	public Seat prefabSeat;
	public Seat[,] map;
	public Vector3 seatDistance = Vector3.one;
	public Vector3 entityOffset = new Vector3(0f, 0.45f, -0.1f);
	public Camera cam;
	public Seat[] seatsSteerable;


	//class Automaton {
	//	public int x, z;
	//	public int steps;
	//	public int ttl = 1;
	//	public int value;
	//	public List<Vector2Int> directions;
	//}

	public List<AutomataState> automata = new List<AutomataState>();
	Vector2Int[] dirListRipple = new [] {
		new Vector2Int(-1, 0)
		, new Vector2Int(1, 0)
		, new Vector2Int(0, 1)
		, new Vector2Int(0, -1)
		};

	bool InsidePlaneCoords(int x, int z) {
		return 0<=x && x<map.GetLength(0) && 0<=z && z<map.GetLength(1);
	}

	void StepAutomata(AutomataState state) {
		if(state.ttl-- <= 0)
			return;
			
		//BFS step
		int count = state.pending.Count;
		while(count-- != 0) { 
			var curr = state.pending.Dequeue();
			curr.karma += state.karmaDelta;
			if(curr.entity) {
				var actor = curr.entity.GetComponent<CharBase>();
				if(actor)
					actor.OnRippleTouch(state);
			}

			state.visited.Add(curr);
				
			foreach(var dir in dirListRipple) {
				int nx = curr.x + dir.x;
				int nz = curr.z + dir.y;	//y is z-offset (ok)
				if(!InsidePlaneCoords(nx, nz))
					continue;
				var nseat = map[nx, nz];
				if(!state.visited.Contains(nseat)) {
					state.visited.Add(nseat);
					state.pending.Enqueue(nseat);
				}
			}
		}		
	}

	public float karmaSteerRollMult = 1f;
	public float karmaSteerPitchMult = 1f;
	public float karmaLerp = 1f;
	Vector2 steerTarget;
	void RefreshSteer() {
		var steer = Vector2.zero;
		var sseats = Vector2Int.zero;
		foreach(var seat in seatsSteerable) {
			 steer += seat.karma * seat.steer;
			 if(seat.steer.x != 0) sseats.x++;
			 if(seat.steer.y != 0) sseats.y++;
		}
		steerTarget.x = -karmaSteerRollMult * steer.x / sseats.x;
		steerTarget.y = -karmaSteerPitchMult * steer.y / sseats.y;
	}

	void RefreshKarma() {
		var entities = new List<Entity>();
		for(int x = 0; x < map.GetLength(0); x++) {
			for(int z = 0; z < map.GetLength(1); z++) {
				var seat = map[x, z];
				seat.karma = 0;
				if(seat.entity)
					entities.Add(seat.entity);
			}
		}
		for(int i = automata.Count-1; i >= 0; i--) {
			var a = automata[i];
			StepAutomata(a);
			if(a.ttl <= 0)
				automata.RemoveAt(i);
		}
		foreach(var ent in entities) {
			ent.seat.karma += ent.effectiveMood;
		}
		for(int x = 0; x < map.GetLength(0); x++) {
			for(int z = 0; z < map.GetLength(1); z++) {
				var seat = map[x, z];
				seat.colorFloor = Color.HSVToRGB(
					seat.karma < 0f ? 0f : 0.75f
					, Mathf.Clamp01(-seat.karma/4f)
					, 0.75f
					);
				seat.lblNumber.text = seat.karma.ToString("+0;-#;");

				if(seat.entity)
					entities.Add(seat.entity);
			}
		}
	}

	void OnBeat(int absBeat) {
		//TODO: entities step
	}

	void OnPostPulse(int absPulse, int relPulse) {
		RefreshKarma();
		RefreshSteer();
	}

	void OnDestroy() {
		Game.instance.OnPostPulse -= OnPostPulse;
		Game.instance.OnBeat-= OnBeat;
	}

	public void AddEntity(Entity ent, Seat seat) {
		ent.transform.parent = transform.parent;
		seat.entity = ent;
		ent.seat = seat;
		ent.transform.localPosition = seat.transform.localPosition + entityOffset;
		ent.transform.localRotation = Quaternion.identity;
	}

	// Use this for initialization
	void Start () {
		var data = stringMap.Split(new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries).Select(line => line.ToArray()).ToArray();
		var dataEnts = stringEntsTutorial.Split(new[] { '|' }, System.StringSplitOptions.RemoveEmptyEntries).Select(line => line.ToArray()).ToArray();
		Debug.Assert(data.All(arr => arr.Length == data[0].Length));

		Game.instance.OnPostPulse += OnPostPulse;
		Game.instance.OnBeat += OnBeat;

		map = new Seat[data[0].Length, data.Length];
		for(int x = 0; x < map.GetLength(0); x++) {
			for(int z = 0; z < map.GetLength(1); z++) {
				var chr = data[z][x];
				var entChr = dataEnts[z][x];
				var seat = Instantiate(prefabSeat, transform);

				//for convenience:
				seat.x = x;
				seat.z = z;
				seat.grid = this;

				seat.colorFloor = new Color(0.5f * (float)x / map.GetLength(0), 0.05f, 0.5f * (float)z / map.GetLength(1));
				seat.name = string.Format("Seat[{0},{1}]", x, z);
				map[x, z] = seat;
				seat.transform.localPosition = new Vector3(
						(x - map.GetLength(0) / 2f) * seatDistance.x
					, seatDistance.y
					, (z - map.GetLength(1) / 2f) * seatDistance.z
					);

				seat.hallway = chr == ' ';
				seat.usable = chr != 'A' && chr != ' ';

				Entity ent = null;
				switch(entChr) {
					case 'S':
						ent = Instantiate(prefabEntSalaryman);
						ent.personalityMood = 0;
						break;
					case 'R':
						ent = Instantiate(prefabEntRegular);
						ent.personalityMood = 0;
						break;
					case '-':
						ent = Instantiate(prefabEntRegular);
						ent.personalityMood = -1f;
						break;
					case '2':
						ent = Instantiate(prefabEntRegular);
						ent.personalityMood = -2f;
						break;
					case '+':
						ent = Instantiate(prefabEntRegular);
						ent.personalityMood = 1f;
						break;
				}
				if(ent) {
					AddEntity(ent, seat);
				}

				Color color;
				if(_colors.TryGetValue(chr, out color))
					seat.colorSeat = color;
				Vector2 steer;
				if(_steerings.TryGetValue(chr, out steer))
					seat.steer = steer;
			}
		}

		seatsSteerable = map.OfType<Seat>().Where(s => s.steer != Vector2.zero).ToArray();
		Debug.Assert(seatsSteerable.Length > 0);

		//Testing:
		//var a = new AutomataState();
		//a.ttl = 5;
		//a.pending.Enqueue(map[4, 5]);
		//automata.Add(a);
		//a = new AutomataState();
		//a.ttl = 4;
		//a.pending.Enqueue(map[2, 3]);
		//automata.Add(a);
	}

	public AutomataState AddRipple(int x, int z, int ttl, CharBase instigator) {
		var a = new AutomataState();
		if(!InsidePlaneCoords(x, z)) {
			Debug.LogWarning("AddRipple invalid coords");
			return a;
		}
		a.ttl = ttl;
		a.instigator = instigator;
		a.pending.Enqueue(map[x, z]);
		automata.Add(a);
		return a;
	}

	Entity dragging;

	public void OnMouseDownSeat(Seat seat) {
		if(seat.entity) {
			
		}
	}


	// Update is called once per frame
	void Update () {
		Game.instance.plane.steerRoll =
			Mathf.Lerp(
				Game.instance.plane.steerRoll
				, steerTarget.x
				, karmaLerp * Time.deltaTime
				);
		Game.instance.plane.steerAltitude = Mathf.Lerp(
				Game.instance.plane.steerAltitude
				, steerTarget.y
				, karmaLerp * Time.deltaTime
				);


		var ray = cam.ScreenPointToRay(Input.mousePosition);
		
		var mask = LayerMask.GetMask(dragging == null ? new[] { "InteriorEntity" } : new[] { "Interior" });
		RaycastHit hit;
		if(Physics.Raycast(ray, out hit, 1000f, mask)) {
			Debug.DrawLine(hit.point + hit.normal, hit.point + hit.normal + Vector3.right);
			Debug.DrawLine(hit.point + hit.normal, hit.point + hit.normal + Vector3.forward);
			//Debug.Log(hit.transform.name);
			if(dragging) {
				dragging.transform.position = hit.point - ray.direction;
				//FIXME: goes through seats (solvable by taller colliders, but then raycasts will hit them when
				//		 pointing to another seat
				//dragging.transform.localPosition += entityOffset;

				if(Input.GetMouseButtonUp(0)) {
					var seat = hit.transform.GetComponent<Seat>();
					if(seat != null && seat.usable && seat.entity == null) {
						if(dragging.seat)
							dragging.seat.entity = null;	//empty
						seat.entity = dragging;			//fill
						dragging.seat = seat;			//remember new
					}
					//TODO: target seat, walk towards it
					if(dragging.seat != null) {
						dragging.transform.localPosition = dragging.seat.transform.localPosition + entityOffset;
					}
					dragging = null;
				}
			}
			else {
				var ent = hit.transform.GetComponent<Entity>();
				if(ent != null) {
					ent.LoadInfo();
					if(Input.GetMouseButton(0)) {
						dragging = ent;
					}
				}
			}
		}
	}
}
