using HPJ.Simulation;
using HPJ.Simulation.Enums;
using HPJ.Simulation.Map;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace HPJ.Presentation.Obstacle
{   
    /// <summary>
    /// The Base class for all navigation obstacles
    /// </summary>
    public abstract class NavigationObstacle : MonoBehaviour
    {
        #region Unity

        // Start is called before the first frame update
        public virtual void Start()
        {
            if (!NavigationManager.Instance.Initialized)
            {
                Invoke(nameof(Start), 01f);
                return;
            }
            UpdatePosition(transform.position, true);
            NavigationManager.Instance?.AddObstacle(this);
        }

        public virtual void OnDestroy()
        {
            NavigationManager.Instance?.RemoveObstacle(this);
        }

        #endregion

        #region Obstacle

        /// <summary>
        /// Does the obstacle update automatically through the built in systems
        /// </summary>
        [SerializeField]
        protected bool _automaticUpdate = true;
        public bool AutomaticUpdate { get { return _automaticUpdate; } set { _automaticUpdate = value; } }
        /// <summary>
        /// The Height difference from the map to count as being on top of the map
        /// </summary>
        [SerializeField, Tooltip("The Height from the transform y position to the ground that this counts as an obstacle")]
        protected float _checkMapHeight = 1f;
        public float CheckMapHeight { get { return _checkMapHeight; } set { _checkMapHeight = value; } }
        /// <summary>
        /// The tile type the obstacle changes the tiles beneath it too
        /// </summary>
        [SerializeField]
        protected TileTypes _obstacleTileType = TileTypes.Obstacle;
        public TileTypes ObstacleTileType { get { return _obstacleTileType; } }

        /// <summary>
        /// The Update cycle of the obstacle
        /// </summary>
        public virtual void UpdateObstacle()
        {
            if (_automaticUpdate)
            {
                UpdatePosition(transform.position);
            }
        }

        /// <summary>
        /// The tiles and map relationship data with this obstacle
        /// </summary>
        public Dictionary<MapSet, MapObstacleRelationshipData> MapTileRelationships { get; protected set; }
        /// <summary>
        /// Checks to see if the new position changes and if so it changes the mesh to block out the new corresponding tiles
        /// </summary>
        /// <param name="NewPosition"></param>
        /// <param name="ForceCheck"></param>
        public abstract void UpdatePosition(Vector3 NewPosition, bool ForceCheck = false);

        /// <summary>
        /// What happens the the obstacle mesh position changes
        /// </summary>
        /// <param name="NewPosition"></param>
        /// <returns></returns>
        protected abstract bool PositionChanged(Vector3 NewPosition);

        /// <summary>
        /// Rotates the obstacle based on the desired Rotation. Basic NavigationObstacle will only swap the object size x and y values. Inherit to ovverride this behaviour
        /// </summary>
        /// <param name="Rotation"></param>
        public abstract void Rotate(Quaternion Rotation);

        /// <summary>
        /// Revalidates the tiles of the obstacle in case another obstacle travels over top of this one
        /// </summary>
        public abstract void RevalidateTiles();

        #endregion
    }

    [Serializable]
    public struct PositionSquareRect
    { 
        public Vector3 Position;
        public Vector2 Size;
        public Vector3[] Corners;

        public PositionSquareRect(Vector3 position, Vector2 size)
        {
            Position = position;
            Size = size;
            Corners = new Vector3[4];

            Corners[0] = Position + new Vector3(Size.x / 2, 0, Size.y / 2);
            Corners[1] = Position + new Vector3(Size.x / 2, 0, -Size.y / 2);
            Corners[2] = Position + new Vector3(-Size.x / 2, 0, -Size.y / 2);
            Corners[3] = Position + new Vector3(-Size.x / 2, 0, Size.y / 2);
        }

        public PositionSquareRect(Vector3 position, Vector3 LowerBounds, Vector3 HigherBounds)
        {
            Position = position;
            Size = new Vector2(HigherBounds.x - LowerBounds.x, HigherBounds.z - LowerBounds.z);
            Corners = new Vector3[4];
            LowerBounds.y = 0;
            HigherBounds.y = 0;

            Corners[0] = HigherBounds;
            Corners[1] = new Vector3(HigherBounds.x, 0, LowerBounds.z);
            Corners[2] = LowerBounds;
            Corners[3] = new Vector3(LowerBounds.x, 0, HigherBounds.z);
        }

        public void UpdatePosition(Vector3 position)
        {
            Position = position;

            Corners[0] = Position + new Vector3(Size.x / 2, 0, Size.y / 2);
            Corners[1] = Position + new Vector3(Size.x / 2, 0, -Size.y / 2);
            Corners[2] = Position + new Vector3(-Size.x / 2, 0, -Size.y / 2);
            Corners[3] = Position + new Vector3(-Size.x / 2, 0, Size.y / 2);
        }

        public void UpdateSize(Vector2 newSize)
        {
            Size = newSize;

            Corners[0] = Position + new Vector3(Size.x / 2, 0, Size.y / 2);
            Corners[1] = Position + new Vector3(Size.x / 2, 0, -Size.y / 2);
            Corners[2] = Position + new Vector3(-Size.x / 2, 0, -Size.y / 2);
            Corners[3] = Position + new Vector3(-Size.x / 2, 0, Size.y / 2);
        }

        public bool InBounds(Vector3 Point)
        {
            if (Point.x > Corners[0].x || Point.x < Corners[2].x || Point.z > Corners[0].z || Point.z < Corners[2].z)
            {
                return false;
            }

            return true;
        }
    }

    [Serializable]
    public class MapObstacleRelationshipData
    {
        public MapSet Map;
        public NavigationObstacle Obstacle; 
        public Dictionary<IntVector3, ObstacleData> RelatedTilesData;
        public HashSet<IntVector3> ValidatedTiles;
        public HashSet<IntVector3> InvalidatedTiles;
        public HashSet<IntVector3> AddedTiles;
        public TileTypes ObstacleTileType;

        public MapObstacleRelationshipData(MapSet map, NavigationObstacle obstacle, TileTypes ObstacleType)
        {
            ValidatedTiles = new HashSet<IntVector3>();
            InvalidatedTiles = new HashSet<IntVector3>();
            AddedTiles = new HashSet<IntVector3>();
            RelatedTilesData = new Dictionary<IntVector3, ObstacleData>();
            Map = map;
            Obstacle = obstacle;
            ObstacleTileType = ObstacleType;
        }

        public void UnvalidateTiles()
        {
            InvalidatedTiles.Clear();
            ValidatedTiles.Clear();
            AddedTiles.Clear();
            foreach (ObstacleData Data in RelatedTilesData.Values)
            {
                InvalidatedTiles.Add(Data.TileIndexKey);
                Data.UnValidate();
            }
        }

        public void AddOrValidateTile(IntVector3 tileIndex)
        {
            if (RelatedTilesData.ContainsKey(tileIndex))
            {
                RelatedTilesData[tileIndex].Validate();
                InvalidatedTiles.Remove(tileIndex);
                ValidatedTiles.Add(tileIndex);
            }
            else
            {
                ObstacleData NewTile = new ObstacleData(tileIndex);
                RelatedTilesData.Add(tileIndex, NewTile);
                AddedTiles.Add(tileIndex);
            }
        }

        public void SortRelatedTileData()
        {
            foreach(IntVector3 InvalidatedTile in InvalidatedTiles)
            {
                RelatedTilesData.Remove(InvalidatedTile);
            }
        }

        public ChangeMapJob GenerateMapChangeJob()
        {
            if (AddedTiles.Count == 0 && InvalidatedTiles.Count == 0)
            {
                //Debug.Log("No Tiles Changed");
                return null;
            }

            ChangeMapJob MapChange = new ChangeMapJob();

            // Set to Obstacle type
            foreach (IntVector3 Tile in AddedTiles)
            {
                MapChange.AddTileChange(Tile.Vector2(), Obstacle.ObstacleTileType);
            }

            // Set back to default
            foreach (IntVector3 Tile in InvalidatedTiles)
            {
                MapChange.AddTileChange(Tile.Vector2(), (TileTypes)Map.SetSettings.MapSettings.MapInfo.SavedTiles[Tile.x, Tile.y]);
            }
            //Debug.Log($"Tiles To Change {MapChange.TilesToBeChanged.Count} -> Added {AddedTiles.Count} -> Removed {InvalidatedTiles.Count}");

            return MapChange;
        }

        internal void Revalidate()
        {
            ChangeMapJob MapChange = new ChangeMapJob();

            bool ChangedTiles = false;
            foreach (IntVector3 Tile in ValidatedTiles)
            {
                if (Map.GetTileType(Tile.Vector2()) != ObstacleTileType)
                {
                    ChangedTiles = true;
                    MapChange.AddTileChange(Tile.Vector2(), ObstacleTileType);
                }
            }

            if (ChangedTiles)
            {
                Map.ChangeMap(MapChange);
            }
        }
    }
}