using HPJ.Simulation.Enums;
using HPJ.Simulation.Pathing;
using HPJ.Simulation.Utilities;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace HPJ.Simulation.Map
{
    /// <summary>
    /// The Map that represents a area with square tiles
    /// </summary>
    public class Map
    {
        protected string _name;
        /// <summary>
        /// The Name of the Map
        /// </summary>
        public string Name { get { return _name; } }

        protected byte[,] _tiles;
        /// <summary>
        /// A 2D Array of the tiles of the Map in bytes
        /// </summary>
        public byte[,] Tiles { get { return _tiles; } }

        public MapSettings _settings;
        /// <summary>
        /// The Map Settings
        /// </summary>
        public MapSettings Settings { get { return _settings; } }

        /// <summary>
        /// If the Map has been Initialized
        /// </summary>
        public bool Initialized { get; protected set; }

        /// <summary>
        /// If the Map logic is currently running
        /// </summary>
        public bool Running { get; protected set; }

        /// <summary>
        /// If the Map uses agent avoidence
        /// </summary>
        public bool AgentAvoidence { get; protected set; }

        /// <summary>
        /// If the map is currently completing navigation job requests
        /// </summary>
        public bool CompletingNavigationJobs { get; protected set; }

        /// <summary>
        /// If the map is currently completing navigation map change requests
        /// </summary>
        public bool CompletingMapChanges { get; protected set; }

        /// <summary>
        /// If the map is currently completing tile ownership claims
        /// </summary>
        public bool CompletingClaims { get; protected set; }
        /// <summary>
        /// If the map is currently completing tile disownment claims
        /// </summary>
        public bool CompletingDisownment { get; protected set; }

        protected Dictionary<string, BytePointMap> _pointMaps;
        /// <summary>
        /// The Byte point maps used by the JPS pathfinding for each valid tile types
        /// </summary>
        public Dictionary<string, BytePointMap> PointMaps { get { return _pointMaps; } }

        public List<MapBridge> _bridges;
        /// <summary>
        /// A List of all the bridges connected to this Map
        /// </summary>
        public List<MapBridge> Bridges { get { return _bridges; } }

        public List<MapSeam> _seams;
        /// <summary>
        /// A List of all the seams connected to this Map
        /// </summary>
        public List<MapSeam> Seams { get { return _seams; } }

        /// <summary>
        /// The map copy that keeps record of tile ownership by agent
        /// </summary>
        public AgentMap AgentOwnershipMap { get; protected set; }

        /// <summary>
        /// The Parrallel options used by the map for the multithreading of each Unit that wants pathfinding
        /// </summary>
        protected ParallelOptions _parallelOptions;

        public Map(MapSettings mapSettings, string MapName, int DegreeOfParallelism, bool CreateAgentMap)
        {
            _name = MapName;
            _settings = mapSettings;
            _bridges = new List<MapBridge>();
            _requestedJobs = new List<NavigationJob>();
            _requestedMapChanges = new List<ChangeMapJob>();
            _agentClaimRequests = new List<TileOwnershipClaim>();
            _agentDisownmentClaimRequests = new List<DisownTileOwnership>();
            _savedPaths = new Dictionary<PathingInfo, NavigationPath>();
            _needToBeUpdatedPaths = new List<NavigationPath>();
            _parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = DegreeOfParallelism };
            AgentAvoidence = CreateAgentMap;

            if (CreateAgentMap)
            {
                AgentOwnershipMap = new AgentMap(this);
            }
        }

        /// <summary>
        /// The Debug callback to Unity
        /// </summary>
        public Action<string, SimulationDebugLog> DebugLogCallback;

        /// <summary>
        /// Initialized the Map
        /// </summary>
        /// <param name="debugLogCallback"></param>
        public void Initialize(Action<string, SimulationDebugLog> debugLogCallback)
        {
            DebugLogCallback = debugLogCallback;
            Running = true;

            try
            {
                _pointMaps = new Dictionary<string, BytePointMap>();

                _settings.MapInfo.MakeSureSizeIsRight();
                _tiles = new byte[_settings.MapWidth, _settings.MapHeight];
                // Create Tiles
                for (int x = 0; x < _settings.MapWidth; x++)
                {
                    for (int y = 0; y < _settings.MapHeight; y++)
                    {
                        _tiles[x, y] = _settings.MapInfo.SavedTiles[x, y];
                    }
                }
                Initialized = true;

                _pointMaps.Add(_settings.DefaultTraversaleTiles.TilesToString(), new BytePointMap(_tiles, _settings.DefaultTraversaleTiles, Settings.MapWidth, Settings.MapHeight, Settings.CornerCutting));
                DebugLogCallback?.Invoke($"{_name} Map Initialized", SimulationDebugLog.Log);
            }
            catch (Exception ex)
            {
                Log($"({_settings.MapWidth},{_settings.MapHeight}) vs ({_settings.MapInfo.Width},{_settings.MapInfo.Height}) vs ({_settings.MapInfo.SavedTiles.GetLength(0)},{_settings.MapInfo.SavedTiles.GetLength(1)}) -> {_settings.MapInfo.SavedTiles.Length}, P{_settings.MapInfo.SavedTiles.LongLength}, {_settings.MapInfo.SavedTiles.Rank}");
                DebugLogCallback?.Invoke($"{ex.Message}: {ex.StackTrace}", SimulationDebugLog.Error);
            }
        }

        public void OnDestroy()
        {
            Running = false;
        }

        /// <summary>
        /// Debug.Log 
        /// </summary>
        /// <param name="Log"></param>
        public void Log(string Log)
        {
            DebugLogCallback?.Invoke(Log, SimulationDebugLog.Log);
        }

        /// <summary>
        /// Debug.Warning 
        /// </summary>
        /// <param name="Log"></param>
        public void LogWarning(string Log)
        {
            DebugLogCallback?.Invoke(Log, SimulationDebugLog.Warning);
        }

        /// <summary>
        /// Debug.Error 
        /// </summary>
        /// <param name="Log"></param>
        public void LogError(string Log)
        {
            DebugLogCallback?.Invoke(Log, SimulationDebugLog.Error);
        }

        /// <summary>
        /// Gets or Generates a Byte point map based on a set of traversable tiles types
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        internal BytePointMap GetBytePointMap(NavigationPath path)
        {
            lock (_pointMaps)
            {
                if (!_pointMaps.ContainsKey(path.Info.TraversableTilesKey))
                {
                    _pointMaps.Add(path.Info.TraversableTilesKey, new BytePointMap(_tiles, path.Info.TraversableTilesKey.StringToTilesArray(), Settings.MapWidth, Settings.MapHeight, Settings.CornerCutting));
                }
            }

            return _pointMaps[path.Info.TraversableTilesKey];
        }

        /// <summary>
        /// Gets the movement speed for a specific tile type on this Map
        /// </summary>
        /// <param name="TileType"></param>
        /// <returns></returns>
        internal byte GetTileSpeedData(TileTypes TileType)
        {
            for (int i = 0; i < Settings.TileData.Count; i++)
            {
                TileData Data = Settings.TileData[i];
                if (Data.Type == TileType)
                {
                    return Data.Speed;
                }
            }

            return 0;
        }

        /// <summary>
        /// A List of the Requested Navigation Jobs
        /// </summary>
        protected List<NavigationJob> _requestedJobs;

        /// <summary>
        /// A List of the Requested Map Changes
        /// </summary>
        protected List<ChangeMapJob> _requestedMapChanges;

        /// <summary>
        /// A List of the Requested Tile Ownership claims
        /// </summary>
        protected List<TileOwnershipClaim> _agentClaimRequests;

        /// <summary>
        /// A List of the Requested Tile Disownment claims
        /// </summary>
        protected List<DisownTileOwnership> _agentDisownmentClaimRequests;

        /// <summary>
        /// A List of all the saved paths that need to be updated because they were invalidated or are new
        /// </summary>
        protected List<NavigationPath> _needToBeUpdatedPaths;

        /// <summary>
        /// A Dictionary of all the saved paths on this Map
        /// </summary>
        protected Dictionary<PathingInfo, NavigationPath> _savedPaths;

        /// <summary>
        /// The Update function of the map each update tick
        /// </summary>
        public void Update()
        {
            try
            {
                if (!Initialized)
                {
                    return;
                }

                // Update Claims
                lock (_agentDisownmentClaimRequests)
                {
                    if (_agentDisownmentClaimRequests.Count > 0)
                    {
                        CompletingDisownment = true;
                        for (int i = 0; i < _agentDisownmentClaimRequests.Count; i++)
                        {
                            Disown(_agentDisownmentClaimRequests[i]);
                        }

                        CompletingDisownment = false;
                        _agentDisownmentClaimRequests.Clear();
                    }
                }
                lock (_agentClaimRequests)
                {
                    if (_agentClaimRequests.Count > 0)
                    {
                        CompletingClaims = true;
                        for (int i = 0; i < _agentClaimRequests.Count; i++)
                        {
                            Claim(_agentClaimRequests[i]);
                        }

                        CompletingClaims = false;
                        _agentClaimRequests.Clear();
                    }
                }

                // Update Map
                lock (_requestedMapChanges)
                {
                    if (_requestedMapChanges.Count > 0)
                    {
                        CompletingMapChanges = true;
                        for (int i = 0; i < _requestedMapChanges.Count; i++)
                        {
                            ChangeMap(_requestedMapChanges[i]);
                        }

                        CompletingMapChanges = false;
                        _requestedMapChanges.Clear();
                    }
                }


                // Complete Navigation Jobs
                lock (_requestedJobs)
                {
                    if (_requestedJobs.Count > 0)
                    {
                        CompletingNavigationJobs = true;
                        lock (_savedPaths)
                        {
                            if (AgentAvoidence)
                            {
                                // Find out the paths that need to be recalculated
                                for (int i = 0; i < _requestedJobs.Count; i++)
                                {
                                    NavigationJob Job = _requestedJobs[i];
                                    NavigationPath Path;

                                    Path = new NavigationPath(Job.Info, this);
                                    _needToBeUpdatedPaths.Add(Path);
                                    Path.AgentID = Job.AgentID;

                                    Job.SetNewCurrentPath(Path);
                                }
                            }
                            else
                            {
                                // Find out the paths that need to be recalculated
                                for (int i = 0; i < _requestedJobs.Count; i++)
                                {
                                    NavigationJob Job = _requestedJobs[i];
                                    NavigationPath Path;

                                    if (_savedPaths.ContainsKey(Job.Info))
                                    {
                                        Path = _savedPaths[Job.Info];
                                        if (!Path.ValidPath || Job.RepathSavedPath)
                                        {
                                            Path.Path.Clear();
                                            // In Case the Path is already being pathed
                                            if (!_needToBeUpdatedPaths.Contains(Path))
                                            {
                                                _needToBeUpdatedPaths.Add(Path);
                                            }
                                        }
                                    }
                                    else
                                    {
                                        Path = new NavigationPath(Job.Info, this);
                                        _savedPaths.Add(Job.Info, Path);
                                        _needToBeUpdatedPaths.Add(Path);
                                    }

                                    Job.SetNewCurrentPath(Path);
                                }
                            }
                        }

                        // Recalculate Path
                        Parallel.ForEach(_needToBeUpdatedPaths, _parallelOptions, CompletePathfind);

                        // Complete the pathfinding
                        //Parallel.ForEach(_requestedJobs, _parallelOptions, FinishPathfinding);
                        for(int i = _requestedJobs.Count - 1; i >= 0; i--)
                        {
                            _requestedJobs[i].PathfindingComplete();
                        }

                        _needToBeUpdatedPaths.Clear();
                        _requestedJobs.Clear();
                        CompletingNavigationJobs = false;
                    }
                }
            }
            catch (Exception ex)
            {
                LogError($"Map Error {ex.Message}: {ex.StackTrace}");
            }
        }

        /// <summary>
        /// A Check to see if the navigation job can be run on this map
        /// </summary>
        /// <param name="job"></param>
        /// <returns></returns>
        internal bool JobCompatible(NavigationJob job)
        {
            if (!job.TraverableTileTypes.Contains(GetTileType(job.Info.PointA)) || !job.TraverableTileTypes.Contains(GetTileType(job.Info.PointB)))
            {
                return false;
            }
            return PointInMap(job.Info.PointA) && PointInMap(job.Info.PointB);
        }

        /// <summary>
        /// Checks to see if this point is valid on this map
        /// </summary>
        /// <param name="WorldPosition"></param>
        /// <returns></returns>
        public bool PointInMap(IntVector3 WorldPosition)
        {
            return GetTileType(WorldPosition) != TileTypes.OutOfBounds;
        }

        /// <summary>
        /// Checks to see if this point is valid on this map
        /// </summary>
        /// <param name="WorldPosition"></param>
        /// <returns></returns>
        public bool PointInMap(IntVector2 TilePosition)
        {
            return GetTileType(TilePosition) != TileTypes.OutOfBounds;
        }

        /// <summary>
        /// Adds a Map change Request for this map
        /// </summary>
        /// <param name="MapChange"></param>
        public void AddMapChange(ChangeMapJob MapChange)
        {
            lock (_requestedMapChanges)
            {
                _requestedMapChanges.Add(MapChange);
            }
        }

        /// <summary>
        /// Adds a agent tile request claim for this map
        /// </summary>
        /// <param name="Claim"></param>
        public void AddClaim(TileOwnershipClaim Claim)
        {
            lock (_agentClaimRequests)
            {
                _agentClaimRequests.Add(Claim);
            }
        }

        /// <summary>
        /// Adds a agent tile disownment claim for this map
        /// </summary>
        /// <param name="Claim"></param>
        public void AddDisownmentClaim(DisownTileOwnership Claim)
        {
            lock (_agentDisownmentClaimRequests)
            {
                _agentDisownmentClaimRequests.Add(Claim);
            }
        }

        /// <summary>
        /// Changes the map tiles based on the map change requests
        /// </summary>
        /// <param name="MapChange"></param>
        protected void ChangeMap(ChangeMapJob MapChange)
        {
            for (int i = 0; i < MapChange.TilesToBeChanged.Count; i++)
            {
                (IntVector2, TileTypes) TileChange = MapChange.TilesToBeChanged[i];
                SetTile(TileChange.Item1, TileChange.Item2);

                foreach(BytePointMap ByteMap in _pointMaps.Values)
                {
                    ByteMap.ChangeTile(TileChange.Item1, TileChange.Item2);
                }
            }
        }

        /// <summary>
        /// Agent claims a tile
        /// </summary>
        /// <param name="Claim"></param>
        protected void Claim(TileOwnershipClaim Claim)
        {
            AgentOwnershipMap.ClaimTile(Claim.RequestTile, Claim.OwnerID);
        }

        /// <summary>
        /// Agent disowns a claims to a tile
        /// </summary>
        /// <param name="Claim"></param>
        protected void Disown(DisownTileOwnership Claim)
        {
            AgentOwnershipMap.DisownTile(Claim.RequestTile, Claim.OwnerID);
        }

        /// <summary>
        /// Sets the tile to the new type
        /// </summary>
        /// <param name="TilePosition"></param>
        /// <param name="TypeChange"></param>
        protected void SetTile(IntVector2 TilePosition, TileTypes TypeChange)
        {
            _tiles[TilePosition.x, TilePosition.y] = (byte)TypeChange;
        }

        /// <summary>
        /// Gets the current tile type of this tile index
        /// </summary>
        /// <param name="WorldPosition"></param>
        /// <returns></returns>
        public TileTypes GetTileType(IntVector3 WorldPosition)
        {
            // Offset the Position
            WorldPosition.x -= Settings.Offset.x;
            WorldPosition.z -= Settings.Offset.y;

            WorldPosition.x = (int)Math.Floor((float)WorldPosition.x / Settings.TileSize);
            WorldPosition.z = (int)Math.Floor((float)WorldPosition.z / Settings.TileSize);

            if (WorldPosition.x < 0 || WorldPosition.z < 0 || WorldPosition.x >= Settings.MapWidth || WorldPosition.z >= Settings.MapHeight)
            {
                return TileTypes.OutOfBounds;
            }

            return (TileTypes)Tiles[WorldPosition.x, WorldPosition.z];
        }

        /// <summary>
        /// Gets the current tile type of this tile index
        /// </summary>
        /// <param name="TilePosition"></param>
        /// <returns></returns>
        public TileTypes GetTileType(IntVector2 TilePosition)
        {
            if (TilePosition.x < 0 || TilePosition.y < 0 || TilePosition.x >= Settings.MapWidth || TilePosition.y >= Settings.MapHeight)
            {
                return TileTypes.OutOfBounds;
            }

            return (TileTypes)Tiles[TilePosition.x, TilePosition.y];
        }

        /// <summary>
        /// Gets the current tile type of this tile index
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public TileTypes GetTileType(int x, int y)
        {
            if (x < 0 || y < 0 || x >= Settings.MapWidth || y >= Settings.MapHeight)
            {
                return TileTypes.OutOfBounds;
            }

            return (TileTypes)Tiles[x, y];
        }

        /// <summary>
        /// Gets the current tile index at this world position
        /// </summary>
        /// <param name="WorldTileIndexPosition"></param>
        /// <returns></returns>
        public IntVector2 GetTileIndexPosition(IntVector3 WorldTileIndexPosition)
        {
            // Offset the Position
            IntVector2 TileIndex = new IntVector2();
            TileIndex.x = (int)((WorldTileIndexPosition.x - _settings.Offset.x) / (float)_settings.TileSize);
            TileIndex.y = (int)((WorldTileIndexPosition.z - _settings.Offset.z) / (float)_settings.TileSize);

            return TileIndex;
        }

        /// <summary>
        /// Adds a navigation Request to the Map
        /// </summary>
        /// <param name="Job"></param>
        public void AddNavigationJob(NavigationJob Job)
        {
            lock (_requestedJobs)
            {
                Job.CurrentStep = PathfindingCalculationStep.Pathfinding;
                _requestedJobs.Add(Job);
            }
        }

        /// <summary>
        /// Calculates the Requested Path on the Map
        /// </summary>
        /// <param name="Path"></param>
        protected void CompletePathfind(NavigationPath Path)
        {
            if (Path.Info.NavigationType == NavigationTypes.JumpPointSearch)
            {
                JumpPointSearch.Instance.CalculatePath(Path, Path.HostMap, out string Status, out bool Succeeded);
                Path.PreviousStatus = Status;
                Path.ValidPath = Succeeded;
            }
            else if (Path.Info.NavigationType == NavigationTypes.AStar)
            {
                AStar.Instance.CalculatePath(Path, Path.HostMap, out string Status, out bool Succeeded);
                Path.PreviousStatus = Status;
                Path.ValidPath = Succeeded;
            }
            else if (Path.Info.NavigationType == NavigationTypes.Free)
            {
                Path.Path.Clear();
                Path.Path.Add(Path.Info.PointA);
                Path.Path.Add(Path.Info.PointB);
                Path.PreviousStatus = "Free Pathed";
                Path.ValidPath = true;
            }
        }

        /// <summary>
        /// Comples the Navigation Job Request
        /// </summary>
        /// <param name="Job"></param>
        protected void FinishPathfinding(NavigationJob Job)
        {
            Job.PathfindingComplete();
        }
    }

    /// <summary>
    /// The Map settings used to shape the Map
    /// </summary>
    [Serializable]
    public class MapSettings
    {
        /// <summary>
        /// The Size of the tile in centimeters not meters
        /// </summary>
        public int TileSize = 100; // In cm

        /// <summary>
        /// The Number of tiles in the x direction
        /// </summary>
        public int MapWidth = 100;

        /// <summary>
        /// The Number of tiles in the z direction
        /// </summary>
        public int MapHeight = 100;

        /// <summary>
        /// The Physical offset of the map in centimeters
        /// </summary>
        public IntVector3 Offset = new IntVector3(); // in cm

        /// <summary>
        /// If corner cutting is enabled in the pathfinding on this Map
        /// </summary>
        public bool CornerCutting = false;

        /// <summary>
        /// The Data for each tile on this Map
        /// </summary>
        public List<TileData> TileData = new List<TileData>();

        /// The Data used to keep track of the map width and height
        /// </summary>
        public MapData MapInfo = new MapData();

        /// <summary>
        /// The Default traversable tiles for this Map
        /// </summary>
        public TileTypes[] DefaultTraversaleTiles = new TileTypes[2] { TileTypes.Standard, TileTypes.Free };

        public MapSettings()
        {
        }

        /// <summary>
        /// Adjusts the Map Size in a specific direction
        /// </summary>
        /// <param name="TilesChanged"></param>
        /// <param name="ExpansionSide"></param>
        /// <param name="NewOffset"></param>
        /// <param name="Translation"></param>
        public void AdjustMap(int TilesChanged, MapExpansionSide ExpansionSide, int NewOffset, int Translation = 0)
        {
            if (TilesChanged == 0)
            {
                return;
            }

            if (ExpansionSide == MapExpansionSide.Left)
            {
                if (TilesChanged <= 0)
                {
                    return;
                }

                if (TilesChanged >= 100000)
                {
                    return;
                }

                Translation = MapWidth - TilesChanged;
                MapWidth = TilesChanged;
                Offset.x = NewOffset;
            }
            else if (ExpansionSide == MapExpansionSide.Right)
            {
                if (TilesChanged <= 0)
                {
                    return;
                }

                if (TilesChanged >= 100000)
                {
                    return;
                }

                MapWidth = TilesChanged;
                Offset.x = NewOffset;
            }
            else if (ExpansionSide == MapExpansionSide.Top)
            {
                if (TilesChanged <= 0)
                {
                    return;
                }

                if (TilesChanged >= 100000)
                {
                    return;
                }

                MapHeight = TilesChanged;
                Offset.z = NewOffset;
            }
            else if (ExpansionSide == MapExpansionSide.Bottom)
            {
                if (TilesChanged <= 0)
                {
                    return;
                }

                if (TilesChanged >= 100000)
                {
                    return;
                }

                Translation = MapHeight - TilesChanged;
                MapHeight = TilesChanged;
                Offset.z = NewOffset;
            }

            UpdateMapSize();
            if (ExpansionSide == MapExpansionSide.Left)
            {
                MapInfo.TransalateValues(Translation, 0);
            }
            else if (ExpansionSide == MapExpansionSide.Right)
            {
                MapInfo.TransalateValues(Translation, 0);
            }
            else if (ExpansionSide == MapExpansionSide.Bottom)
            {
                MapInfo.TransalateValues(0, Translation);
            }
            else if (ExpansionSide == MapExpansionSide.Top)
            {
                MapInfo.TransalateValues(0, Translation);
            }
        }

        /// <summary>
        /// Updates the Map Size
        /// </summary>
        public void UpdateMapSize()
        {
            MapInfo.UpdateToMapSize(MapWidth, MapHeight);
        }
    }

    /// <summary>
    /// A Set of points that the navigation can use to transfer a Agent from one map to another
    /// </summary>
    [Serializable]
    public class MapBridge
    {
        /// <summary>
        /// Map on Side A of the Bridge
        /// </summary>
        public MapSet MapA; /*{ get { return _mapA; } set { _mapA = value; } }*/
        //protected MapSet _mapA;

        /// <summary>
        /// Map on Side B of the Bridge
        /// </summary>
        public MapSet MapB; /*{ get { return _mapB; } set { _mapB = value; } }*/
        //protected MapSet _mapB;

        /// <summary>
        /// Which ways the bridge can be used
        /// </summary>
        public BridgeDirections WalkableDirections;

        /// <summary>
        /// All the points from Map A to Map B
        /// </summary>
        public List<IntVector3> MidPoints;

        /// <summary>
        /// If this map is a valid and can be used
        /// </summary>
        public bool ValidBridge = false;

        public MapBridge(MapSet A, MapSet B, List<IntVector3> BridgePoints)
        {
            WalkableDirections = BridgeDirections.Both;
            MapA = A;
            MapB = B;
            MidPoints = BridgePoints;
        }

        /// <summary>
        /// Checks to see this bridge connects two maps
        /// </summary>
        /// <param name="startingMap"></param>
        /// <param name="endingMap"></param>
        /// <returns></returns>
        public bool DirectConnects(MapSet startingMap, MapSet endingMap)
        {
            if (startingMap.InstanceID == MapA.InstanceID)
            {
                if (endingMap.InstanceID == MapB.InstanceID)
                {
                    return true;
                }
            }
            else if (startingMap.InstanceID == MapB.InstanceID)
            {
                if (endingMap.InstanceID == MapA.InstanceID)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checks to see if you can walk on the bridge in this direction. Starting Map to Ending Map
        /// </summary>
        /// <param name="startingMap"></param>
        /// <param name="endingMap"></param>
        /// <returns></returns>
        public bool CanWalkInDirection(MapSet startingMap, MapSet endingMap)
        {
            // Make sure these maps are even connected
            if (!DirectConnects(startingMap, endingMap))
            {
                return false;
            }

            if (WalkableDirections == BridgeDirections.Both)
            {
                return true;
            }

            if (startingMap.InstanceID == MapA.InstanceID)
            {
                if (endingMap.InstanceID == MapB.InstanceID)
                {
                    return WalkableDirections == BridgeDirections.A_To_B;
                }
            }
            else if (startingMap.InstanceID == MapB.InstanceID)
            {
                if (endingMap.InstanceID == MapA.InstanceID)
                {
                    return WalkableDirections == BridgeDirections.B_To_A;
                }
            }

            return false;
        }

        /// <summary>
        /// Gets the starting point for a Map if that map connects to this Bridge
        /// </summary>
        /// <param name="startingMap"></param>
        /// <returns></returns>
        public IntVector3 GetPoint(MapSet startingMap)
        {
            if (startingMap.InstanceID == MapA.InstanceID)
            {
                return MidPoints[0];
            }
            else if (startingMap.InstanceID == MapB.InstanceID)
            {
                return MidPoints[MidPoints.Count - 1];
            }

            return new IntVector3();
        }

        /// <summary>
        /// Gets the ending point for a Map if that map connects to this Bridge
        /// </summary>
        /// <param name="startingMap"></param>
        /// <returns></returns>
        public IntVector3 GetOpposingPoint(MapSet startingMap)
        {
            if (startingMap.InstanceID == MapA.InstanceID)
            {
                return MidPoints[MidPoints.Count - 1];
            }
            else if (startingMap.InstanceID == MapB.InstanceID)
            {
                return MidPoints[0];
            }

            return new IntVector3();
        }

        /// <summary>
        /// Checks to see if the given maps is on one side of the Bridge
        /// </summary>
        /// <param name="Map"></param>
        /// <returns></returns>
        public bool Contains(MapSet Map)
        {
            if (Map.InstanceID == MapA.InstanceID)
            {
                return true;
            }
            else if (Map.InstanceID == MapB.InstanceID)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Gets the other Map if you know one side the bridge connects too
        /// </summary>
        /// <param name="Map"></param>
        /// <returns></returns>
        public MapSet Other(MapSet Map)
        {
            if (MapA.InstanceID == Map.InstanceID)
            {
                return MapB;
            }
            else if (MapB.InstanceID == Map.InstanceID)
            {
                return MapA;
            }

            return null;
        }

        public void RefreshWithNew(MapSet Map)
        {
            if (MapA.InstanceID == Map.InstanceID)
            {
                MapA = Map;
            }
            else if (MapB.InstanceID == Map.InstanceID)
            {
                MapB = Map;
            }
        }

        public void Verify()
        {
            ValidBridge = MapA != null && MapB != null;
        }
    }

    /// <summary>
    /// A Set of points that the navigation can use to transfer a Agent from one map to another
    /// </summary>
    [Serializable]
    public class MapSeam
    {
        /// <summary>
        /// Map on Side A of the Bridge
        /// </summary>
        public MapSet MapA;

        /// <summary>
        ///  The connection side for Map A
        /// </summary>
        public SeamConnectionSide MapA_ConectionSide;
        /// <summary>
        ///  The Map A Starting Tile Index
        /// </summary>
        public IntVector2 MapA_StartingTileIndex;
        /// <summary>
        ///  The Map A Ending Tile Index
        /// </summary>
        public IntVector2 MapA_EndingTileIndex;

        /// <summary>
        /// Map on Side B of the Bridge
        /// </summary>
        public MapSet MapB;

        /// <summary>
        ///  The connection side for Map A
        /// </summary>
        public SeamConnectionSide MapB_ConectionSide;
        /// <summary>
        ///  The Map B Starting Tile Index
        /// </summary>
        public IntVector2 MapB_StartingTileIndex;
        /// <summary>
        ///  The Map B Ending Tile Index
        /// </summary>
        public IntVector2 MapB_EndingTileIndex;

        /// <summary>
        /// If this map is a valid and can be used
        /// </summary>
        public bool ValidSeam = false;

        public MapSeam(MapSet A, MapSet B, SeamConnectionSide mapA_ConectionSide, SeamConnectionSide mapB_ConectionSide)
        {
            MapA = A;
            MapB = B;
            MapA_ConectionSide = mapA_ConectionSide;
            MapB_ConectionSide = mapB_ConectionSide;

            if (mapA_ConectionSide == SeamConnectionSide.South)
            {
                MapA_StartingTileIndex = IntVector2.Zero;
                MapA_EndingTileIndex = new IntVector2(A.SetSettings.MapSettings.MapWidth - 1, 0);
            }
            else if (mapA_ConectionSide == SeamConnectionSide.North)
            {
                MapA_StartingTileIndex = new IntVector2(0, A.SetSettings.MapSettings.MapHeight - 1);
                MapA_EndingTileIndex = new IntVector2(A.SetSettings.MapSettings.MapWidth - 1, A.SetSettings.MapSettings.MapHeight - 1);
            }
            else if (mapA_ConectionSide == SeamConnectionSide.West)
            {
                MapA_StartingTileIndex = IntVector2.Zero;
                MapA_EndingTileIndex = new IntVector2(0, A.SetSettings.MapSettings.MapHeight - 1);
            }
            else if (mapA_ConectionSide == SeamConnectionSide.East)
            {
                MapA_StartingTileIndex = new IntVector2(A.SetSettings.MapSettings.MapWidth - 1, 0);
                MapA_EndingTileIndex = new IntVector2(A.SetSettings.MapSettings.MapWidth - 1, A.SetSettings.MapSettings.MapHeight - 1);
            }

            if (mapB_ConectionSide == SeamConnectionSide.South)
            {
                MapB_StartingTileIndex = IntVector2.Zero;
                MapB_EndingTileIndex = new IntVector2(B.SetSettings.MapSettings.MapWidth - 1, 0);
            }
            else if (mapB_ConectionSide == SeamConnectionSide.North)
            {
                MapB_StartingTileIndex = new IntVector2(0, B.SetSettings.MapSettings.MapHeight - 1);
                MapB_EndingTileIndex = new IntVector2(B.SetSettings.MapSettings.MapWidth - 1, B.SetSettings.MapSettings.MapHeight - 1);
            }
            else if (mapB_ConectionSide == SeamConnectionSide.West)
            {
                MapB_StartingTileIndex = IntVector2.Zero;
                MapB_EndingTileIndex = new IntVector2(0, B.SetSettings.MapSettings.MapHeight - 1);
            }
            else if (mapB_ConectionSide == SeamConnectionSide.East)
            {
                MapB_StartingTileIndex = new IntVector2(B.SetSettings.MapSettings.MapWidth - 1, 0);
                MapB_EndingTileIndex = new IntVector2(B.SetSettings.MapSettings.MapWidth - 1, B.SetSettings.MapSettings.MapHeight - 1);
            }
        }

        /// <summary>
        /// Checks to see this bridge connects two maps
        /// </summary>
        /// <param name="startingMap"></param>
        /// <param name="endingMap"></param>
        /// <returns></returns>
        public bool DirectConnects(MapSet startingMap, MapSet endingMap)
        {
            if (startingMap.InstanceID == MapA.InstanceID)
            {
                if (endingMap.InstanceID == MapB.InstanceID)
                {
                    return true;
                }
            }
            else if (startingMap.InstanceID == MapB.InstanceID)
            {
                if (endingMap.InstanceID == MapA.InstanceID)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checks to see if the given maps is on one side of the Bridge
        /// </summary>
        /// <param name="Map"></param>
        /// <returns></returns>
        public bool Contains(MapSet Map)
        {
            if (Map.InstanceID == MapA.InstanceID)
            {
                return true;
            }
            else if (Map.InstanceID == MapB.InstanceID)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Gets the other Map if you know one side the bridge connects too
        /// </summary>
        /// <param name="Map"></param>
        /// <returns></returns>
        public MapSet Other(MapSet Map)
        {
            if (MapA.InstanceID == Map.InstanceID)
            {
                return MapB;
            }
            else if (MapB.InstanceID == Map.InstanceID)
            {
                return MapA;
            }

            return null;
        }

        public void RefreshWithNew(MapSet Map)
        {
            if (MapA.InstanceID == Map.InstanceID)
            {
                MapA = Map;
            }
            else if (MapB.InstanceID == Map.InstanceID)
            {
                MapB = Map;
            }
        }

        public void Verify()
        {
            ValidSeam = MapA != null && MapB != null;
        }

        /// <summary>
        /// Gets the closest valid point for the starting map to the seam
        /// </summary>
        /// <param name="startingMap"></param>
        /// <param name="start"></param>
        /// <returns></returns>
        public IntVector3 GetPoint(MapSet startingMap, IntVector3 start)
        {
            IntVector2 StartIndex = startingMap.GetClosestMapTileIndex(start);

            if (startingMap == MapA)
            {
                if (MapA_ConectionSide == SeamConnectionSide.North)
                {
                    StartIndex.y = startingMap.SetSettings.MapSettings.MapHeight - 1;
                }
                else if (MapA_ConectionSide == SeamConnectionSide.South)
                {
                    StartIndex.y = 0;
                }
                else if (MapA_ConectionSide == SeamConnectionSide.West)
                {
                    StartIndex.x = startingMap.SetSettings.MapSettings.MapWidth - 1;
                }
                else if (MapA_ConectionSide == SeamConnectionSide.East)
                {
                    StartIndex.x = 0;
                }
            }
            else
            {
                if (MapB_ConectionSide == SeamConnectionSide.North)
                {
                    StartIndex.y = startingMap.SetSettings.MapSettings.MapHeight - 1;
                }
                else if (MapB_ConectionSide == SeamConnectionSide.South)
                {
                    StartIndex.y = 0;
                }
                else if (MapB_ConectionSide == SeamConnectionSide.West)
                {
                    StartIndex.x = startingMap.SetSettings.MapSettings.MapWidth - 1;
                }
                else if (MapB_ConectionSide == SeamConnectionSide.East)
                {
                    StartIndex.x = 0;
                }
            }

            return startingMap.GetWorldTileIndex(StartIndex);
        }

        public IntVector3 GetOppositePoint(MapSet startingMap, IntVector3 start)
        {
            IntVector3 StartingPoint = GetPoint(startingMap, start);

            // Iterative Method - Would like to remove when calculative method is fixed
            MapSet EndingMap = Other(startingMap);
            if (EndingMap == MapA)
            {
                int ClosestScore = int.MaxValue;
                IntVector2 ClosestEndTileIndex = new IntVector2();
                for (int x = 0; x <= MapA_StartingTileIndex.x; x++ )
                {
                    for (int y = 0; y <= MapA_EndingTileIndex.y; y++)
                    {
                        int NewScore = IntVector3.Displacement(StartingPoint, EndingMap.GetWorldTileIndex(x, y));
                        if (NewScore < ClosestScore)
                        {
                            ClosestScore = NewScore;
                            ClosestEndTileIndex = new IntVector2(x, y);
                        }
                    }
                }

                return EndingMap.GetWorldTileIndex(ClosestEndTileIndex);
            }
            else if (EndingMap == MapB)
            {
                int ClosestScore = int.MaxValue;
                IntVector2 ClosestEndTileIndex = new IntVector2();
                for (int x = 0; x <= MapB_StartingTileIndex.x; x++)
                {
                    for (int y = 0; y <= MapB_EndingTileIndex.y; y++)
                    {
                        int NewScore = IntVector3.Displacement(StartingPoint, EndingMap.GetWorldTileIndex(x, y));
                        if (NewScore < ClosestScore)
                        {
                            ClosestScore = NewScore;
                            ClosestEndTileIndex = new IntVector2(x, y);
                        }
                    }
                }

                return EndingMap.GetWorldTileIndex(ClosestEndTileIndex);
            }

            // Caluclation Method - Needs Work somehwere
            IntVector2 StartIndex = startingMap.GetClosestMapTileIndex(StartingPoint);
            int startingMapLength;
            int endingMapLength;
            double startingMapRatio;
            if (startingMap == MapA)
            {
                if (MapA_ConectionSide == SeamConnectionSide.South || MapA_ConectionSide == SeamConnectionSide.North)
                {
                    startingMapLength = MapA.SetSettings.MapSettings.MapWidth;
                    startingMapRatio = (double)StartIndex.x / startingMapLength;
                }
                else
                {
                    startingMapLength = MapA.SetSettings.MapSettings.MapHeight;
                    startingMapRatio = (double)StartIndex.y / startingMapLength;
                }

                if (MapB_ConectionSide == SeamConnectionSide.South || MapB_ConectionSide == SeamConnectionSide.North)
                {
                    endingMapLength = MapB.SetSettings.MapSettings.MapWidth;
                }
                else
                {
                    endingMapLength = MapB.SetSettings.MapSettings.MapHeight;
                }
            }
            else
            {
                if (MapB_ConectionSide == SeamConnectionSide.South || MapB_ConectionSide == SeamConnectionSide.North)
                {
                    startingMapLength = MapB.SetSettings.MapSettings.MapWidth;
                    startingMapRatio = (double)StartIndex.x / startingMapLength;
                }
                else
                {
                    startingMapLength = MapB.SetSettings.MapSettings.MapHeight;
                    startingMapRatio = (double)StartIndex.y / startingMapLength;
                }

                if (MapA_ConectionSide == SeamConnectionSide.South || MapA_ConectionSide == SeamConnectionSide.North)
                {
                    endingMapLength = MapA.SetSettings.MapSettings.MapWidth;
                }
                else
                {
                    endingMapLength = MapA.SetSettings.MapSettings.MapHeight;
                }
            }

            double mapLengthRatio = (double)endingMapLength / startingMapLength;

            int mapEndFinalPoint = (int)System.Math.Round(mapLengthRatio * startingMapRatio * endingMapLength);
            IntVector2 EndingPoint;
            MapSet endingMap;
            if (startingMap == MapA)
            {
                endingMap = MapB;
                if (MapB_ConectionSide == SeamConnectionSide.South)
                {
                    EndingPoint = new IntVector2(mapEndFinalPoint, 0);
                }
                else if (MapB_ConectionSide == SeamConnectionSide.North)
                {
                    EndingPoint = new IntVector2(mapEndFinalPoint, MapB.SetSettings.MapSettings.MapHeight - 1);
                }
                else if (MapB_ConectionSide == SeamConnectionSide.West)
                {
                    EndingPoint = new IntVector2(MapB.SetSettings.MapSettings.MapWidth - 1, mapEndFinalPoint);
                }
                else
                {
                    EndingPoint = new IntVector2(0, mapEndFinalPoint);
                }
            }
            else
            {
                endingMap = MapA;
                if (MapA_ConectionSide == SeamConnectionSide.South)
                {
                    EndingPoint = new IntVector2(mapEndFinalPoint, 0);
                }
                else if (MapA_ConectionSide == SeamConnectionSide.North)
                {
                    EndingPoint = new IntVector2(mapEndFinalPoint, MapA.SetSettings.MapSettings.MapHeight - 1);
                }
                else if (MapA_ConectionSide == SeamConnectionSide.West)
                {
                    EndingPoint = new IntVector2(MapA.SetSettings.MapSettings.MapWidth - 1, mapEndFinalPoint);
                }
                else
                {
                    EndingPoint = new IntVector2(0, mapEndFinalPoint);
                }
            }

            return endingMap.GetWorldTileIndex(EndingPoint);
        }
    }

    /// <summary>
    /// Tile Size Data for the Map, used to save default tiles to the Map
    /// </summary>
    [System.Serializable]
    public class MapData
    {
        public byte[,] SavedTiles = new byte[100, 100];
        public int Width = 100;
        public int Height = 100;

        public MapData()
        {
        }

        public bool MakeSureSizeIsRight()
        {
            if (Width != SavedTiles.GetLength(0) || Height != SavedTiles.GetLength(1))
            {
                try
                {
                    byte[,] PreviousTiles = SavedTiles;
                    int PreviousWidth = SavedTiles.GetLength(0);
                    int PreviousHeight = SavedTiles.GetLength(1);
                    SavedTiles = new byte[Width, Height];
                    for (int x = 0; x < Width; x++)
                    {
                        for (int y = 0; y < Height; y++)
                        {
                            if (x < PreviousWidth && y < PreviousHeight)
                            {
                                SavedTiles[x, y] = PreviousTiles[x, y];
                            }
                            else
                            {
                                SavedTiles[x, y] = (byte)TileTypes.Standard;
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new Exception($"{e.Message} \n {e.StackTrace} \n ----------------------------------------- \n {Width} vs {SavedTiles.GetLength(0)} & {Height} vs {SavedTiles.GetLength(1)}");
                }
                return true;
            }

            return false;
        }

        /// <summary>
        /// Updates the size of the Data
        /// </summary>
        /// <param name="mapWidth"></param>
        /// <param name="mapHeight"></param>
        public void UpdateToMapSize(int mapWidth, int mapHeight)
        {
            if (mapWidth == Width && mapHeight == Height)
            {
                return;
            }

            try
            {
                byte[,] PreviousTiles = SavedTiles;
                SavedTiles = new byte[mapWidth, mapHeight];
                int PreviousWidth = Width;
                int PreviousHeight = Height;
                Width = mapWidth;
                Height = mapHeight;
                for (int x = 0; x < mapWidth; x++)
                {
                    for (int y = 0; y < mapHeight; y++)
                    {
                        if (x < PreviousWidth && y < PreviousHeight)
                        {
                            SavedTiles[x, y] = PreviousTiles[x, y];
                        }
                        else
                        {
                            SavedTiles[x, y] = (byte)TileTypes.Standard;
                        }
                    }
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// Moves the values of the tiles
        /// </summary>
        /// <param name="HorizontalTranslation"></param>
        /// <param name="VerticalTranslation"></param>
        public void TransalateValues(int HorizontalTranslation, int VerticalTranslation)
        {
            if (HorizontalTranslation == 0 && VerticalTranslation == 0)
            {
                return;
            }

            byte[,] NewTiles = new byte[Width, Height];
            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Height; y++)
                {
                    int TranslatedX = x + HorizontalTranslation;
                    int TranslatedY = y + VerticalTranslation;
                    if (TranslatedX < 0 || TranslatedY < 0 || TranslatedX >= Width || TranslatedY >= Height)
                    {
                        NewTiles[x, y] = (byte)TileTypes.Standard;
                    }
                    else
                    {
                        NewTiles[x, y] = SavedTiles[TranslatedX, TranslatedY];
                    }
                }
            }
            SavedTiles = NewTiles;
        }

        /// <summary>
        /// Clears the map of all tile types and turns them into standard tiles
        /// </summary>
        public void CleanMap()
        {
            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Height; y++)
                {
                    SavedTiles[x, y] = (byte)TileTypes.Standard;
                }
            }
        }
    }

    /// <summary>
    /// The Data that tells the speed multiplier for a given tile type
    /// </summary>
    [System.Serializable]
    public class TileData
    {
        public TileTypes Type;
        public byte Speed; 

        public TileData()
        {
            Type = TileTypes.Standard;
            Speed = 100; // 100 = 100% Speed
        }

        public TileData(TileTypes tileType, byte speedMult)
        {
            Type = tileType;
            Speed = speedMult;
        }
    }

    /// <summary>
    /// Used to keep track of a obstacles tiles and its state in the obstacle
    /// </summary>
    [Serializable]
    public struct ObstacleData
    {
        public IntVector3 TileIndexKey;
        public ObstacleValidationStates ValidationState;

        public ObstacleData(IntVector3 Index)
        {
            TileIndexKey = Index;
            ValidationState = ObstacleValidationStates.Added;
        }

        /// <summary>
        /// Means this tile is no longer valid and will need to be checked if it still belongs on this obstacle
        /// </summary>
        public void UnValidate()
        {
            ValidationState = ObstacleValidationStates.Invalidated;
        }

        /// <summary>
        /// Validates the tile, it belongs on this obstacle
        /// </summary>
        public void Validate()
        {
            ValidationState = ObstacleValidationStates.Validated;
        }
    }
}