﻿using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Gamelogic.Grids.Examples
{
	public class HighlightPointsInRangeGridBehaviour : GridBehaviour<RectPoint>
	{
		public PathMode pathMode;
		public float range;
		public Gradient cellMovementCostColor;
		public Gradient cellDistanceColor;
		public Color obstacleColor;

		public SpriteCell pathPrefab;
		public GameObject pathRoot;

		private RectGrid<WalkableCell> walkableGrid;
		private RectPoint start;
		private RectGrid<SpriteCell> pathGrid;
		private Dictionary<RectPoint, float> pointsInRange;

		//We use this map instead of the Map provided by the GridBehviour
		//for Euclidean distance calculations so that the scale is the same 
		//as for the other distance metrics. Normally, you can use the same map
		//as the one you use for the grid.
		//
		//We could alnternatively have scaled the value returned by the distance function.
		//This is lsightly neater, and more robust against changes of the grid and map.
		private IMap<RectPoint> distanceMap;

		public override void InitGrid()
		{
			//Using cell dimensions of Vector2.one means two orthogonal neighbors are 
			//1 unit apart, similar to the other metrics.
			distanceMap = new RectMap(Vector2.one);

			//We create a copy of the grid which has a more convenient type signature.
			//We cast all values to Walkable cell, and the entire grid to a RectGrid.
			walkableGrid = (RectGrid<WalkableCell>) Grid.CastValues<WalkableCell, RectPoint>();
			walkableGrid.Apply(SetupWalkableCell);

			if (Application.isPlaying)
			{
				pathRoot.transform.DestroyChildren();
			}
			else
			{
				pathRoot.transform.DestroyChildrenImmediate();
			}

			start = RectPoint.Zero;

			//We create a duplicate grid to keep the path nodes
			pathGrid = (RectGrid<SpriteCell>) Grid.CloneStructure<SpriteCell, RectPoint>(InstantiatePathCell);

			UpdateRange();
		}

		public void Update()
		{
			if (Input.GetKeyDown(KeyCode.UpArrow))
			{
				SetNewStartPoint(start + RectPoint.North);
			}

			if (Input.GetKeyDown(KeyCode.DownArrow))
			{
				SetNewStartPoint(start + RectPoint.South);
			}

			if (Input.GetKeyDown(KeyCode.RightArrow))
			{
				SetNewStartPoint(start + RectPoint.East);
			}

			if (Input.GetKeyDown(KeyCode.LeftArrow))
			{
				SetNewStartPoint(start + RectPoint.West);
			}
		}

		public void OnLeftClick(RectPoint clickedPoint)
		{
			ToggleCellWalkability(clickedPoint);
			UpdateRange();
		}

		public void OnRightClick(RectPoint clickedPoint)
		{
			SetNewStartPoint(clickedPoint);

			UpdateRange();
		}

		private void SetupWalkableCell(WalkableCell cell)
		{
			cell.IsWalkable = true;
			var movementCost = 1 + Random.value*2;
			cell.MovementCost = movementCost;
			cell.Color = cellMovementCostColor.Evaluate((movementCost - 1)/2);
		}

		private SpriteCell InstantiatePathCell(RectPoint point)
		{
			var cell = Instantiate(pathPrefab);

			cell.transform.parent = pathRoot.transform;
			cell.transform.localPosition = Map[point];
			cell.transform.localScale = Vector3.one*0.5f;
			cell.name = "P";
			cell.Color = cellDistanceColor.Evaluate(1);
			cell.gameObject.SetActive(false);

			return cell;
		}

		public Dictionary<RectPoint, float> GetGridPath()
		{
			var cost = Algorithms.GetPointsInRangeCost(
				walkableGrid,
				start,
				c => c.IsWalkable,
				(p, q) => 1,
				range);

			return cost;
		}

		public Dictionary<RectPoint, float> GetEuclideanPath()
		{
			var cost = Algorithms.GetPointsInRangeCost(
				walkableGrid,
				start,
				c => c.IsWalkable,
				EuclideanDistance,
				range);

			return cost;
		}

		public Dictionary<RectPoint, float> GetWeightedPath()
		{
			var cost = Algorithms.GetPointsInRangeCost(
				walkableGrid,
				start,
				c => c.IsWalkable,
				GetMovementCost,
				range);

			return cost;
		}

		private void UpdateRange()
		{
			if (pointsInRange != null)
			{
				foreach (var point in pointsInRange.Keys)
				{
					pathGrid[point].gameObject.SetActive(false);
				}
			}

			switch (pathMode)
			{
				case PathMode.GridPath:
					pointsInRange = GetGridPath();
					break;
				case PathMode.EuclideanPath:
					pointsInRange = GetEuclideanPath();
					break;
				case PathMode.WeightedPath:
				default:
					pointsInRange = GetWeightedPath();
					break;
			}

			if (pointsInRange == null)
			{
				return; //then there is no path between the start and goal.
			}

			var maxCost = pointsInRange.Values.Max();

			foreach (var point in pointsInRange.Keys)
			{
				var cell = pathGrid[point];
				cell.gameObject.SetActive(true);
				cell.Color = cellDistanceColor.Evaluate(pointsInRange[point]/maxCost);
			}
		}

		private void SetNewStartPoint(RectPoint point)
		{
			if (walkableGrid.Contains(point) && walkableGrid[point].IsWalkable)
			{
				start = point;

				UpdateRange();
			}
		}

		private void ToggleCellWalkability(RectPoint selectedPoint)
		{
			var cell = walkableGrid[selectedPoint];

			cell.IsWalkable = !cell.IsWalkable;
			cell.Color = cell.IsWalkable ? cellMovementCostColor.Evaluate((cell.MovementCost - 1)/2) : obstacleColor;
		}

		//Returns the Euclidean distance (in world units)
		//between the given grid points
		private float EuclideanDistance(RectPoint p, RectPoint q)
		{
			float dX = distanceMap[p].x - distanceMap[q].x;
			float dY = distanceMap[p].y - distanceMap[q].y;

			float distance = Mathf.Sqrt(dX*dX + dY*dY);

			return distance;
		}

		/**
		Gets the cost of moving between the cells at the given grid points, 
		asuming cells are neighbors.
	*/

		private float GetMovementCost(RectPoint p1, RectPoint p2)
		{
			return (walkableGrid[p1].MovementCost +
			        walkableGrid[p2].MovementCost)/2;
		}
	}
}