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

public struct TileChange
{
    public Tile NewTile;
    public Tile.TileType OldTile;
}

public class MapManager : MonoBehaviour
{
    /// <summary>
    /// Instance of the current map manager
    /// </summary>
    public static MapManager Instance;

    /// <summary>
    /// Size of each tile in world space
    /// </summary>
    public const int TileSize = 2;

    public static Action<TileChange> OnTileChanged;

    [SerializeField] private MapGraphics m_graphics;

    /// <summary>
    /// A hack to make a working spawn
    /// </summary>
    [SerializeField] private Vector3 m_spawnPoint;

    /// <summary>
    /// Dimentions of the map
    /// </summary>
    [SerializeField] private int m_width;
    [SerializeField] private int m_height;

    /// <summary>
    /// Logical representation of the map
    /// </summary>
    private Tile[,] m_map;

    private HashSet<Building> m_buildings;

    /// <summary>
    /// Convert world coordinates to tile indexes
    /// </summary>
    /// <param name="worldPos"> X, Y, Z coordinates in world space </param>
    /// <returns> X, Y index of the tile that contains the world position</returns>
    public static Vector2Int WorldPosToTile(Vector3 worldPos)
    {
        return new Vector2Int((int)(worldPos.x / TileSize), (int)(worldPos.z / TileSize));
    }

    public static Vector3 TileToWorld(Tile t)
    {
        return new Vector3(t.Position.x * TileSize + TileSize / 2f, 0, t.Position.y * TileSize + TileSize / 2f);
    }

    public static Vector3 ScreenPosToWorld(Vector3 screen)
    {
        Plane plane = new Plane(Vector3.up, 0);
        Ray ray = Camera.main.ScreenPointToRay(screen);
        if (plane.Raycast(ray, out float distance))
        {
            return ray.GetPoint(distance);
        }
        return default;
    }

    /// <summary>
    /// Gets the tile
    /// </summary>
    /// <param name="x">X Index</param>
    /// <param name="y">Y Index</param>
    /// <returns>Tile at specified index</returns>
    public Tile GetTile(int x, int y)
    {
        if(x < 0 || y < 0 || x >= m_width || y >= m_height)
        {
            return null;
        }
        return m_map[x, y];
    }

    public Tile GetTile(Vector2Int index)
    {
        return GetTile(index.x, index.y);
    }

    public static float Distance(Tile t1, Tile t2)
    {
        return Vector2Int.Distance(t1.Position, t2.Position);
    }
    public static float Distance(Tile t1, Vector2Int t2)
    {
        return Vector2Int.Distance(t1.Position, t2);
    }

    /// <summary>
    /// Checks if the indecies are inside the map
    /// </summary>
    public bool InsideMap(int x, int y)
    {
        return x >= 0 && x < m_width && y >= 0 && y < m_height;
    }

    /// <summary>
    /// Checks if the indecies are inside the map
    /// </summary>
    public bool InsideMap(Vector2Int index)
    {
        return InsideMap(index.x, index.y);
    }

    /// <summary>
    /// Destroy tile att specified index
    /// </summary>
    /// <param name="x">X Index</param>
    /// <param name="y">Y Index</param>
    public void DestroyTile(int x, int y)
    {
        // TODO:: Actually do some useful destruction

        // No reason to destroy ground
        if (m_map[x, y].Type == Tile.TileType.Ground)
        {
            return;
        }
        Tile.TileType original = m_map[x, y].Type;
        m_map[x, y].Type = Tile.TileType.Ground;
        m_map[x, y].Visible = true;
        UpdateVisibility(m_map[x, y]);
        m_graphics.UpdateTile(m_map[x, y]);
        foreach (Tile t in GetNeighbours(x, y))
        {
            if(t.Type != Tile.TileType.Ground)
            {
                m_graphics.UpdateTile(t);
            }
        }

        OnTileChanged?.Invoke(new TileChange { NewTile = m_map[x, y], OldTile = original});
    }

    public void DestroyTile(Tile tile)
    {
        DestroyTile(tile.Position.x, tile.Position.y);
    }

    public bool TileEmpty(int x, int y)
    {
        if (InsideMap(x, y) == false)
            return false;
        return m_map[x, y].Type == Tile.TileType.Ground;
    }

    public bool ConstructBuilding<T>(Tile tile) where T : Building, new()
    {
        var building = new T();
        int x = tile.Position.x;
        int y = tile.Position.y;
        for (int dx = 0; dx < building.Width; dx++)
        {
            for (int dy = 0; dy < building.Height; dy++)
            {
                if(TileEmpty(x + dx, y + dy) == false)
                {
                    return false;
                }
            }
        }
        building.Construct(tile);
        m_buildings.Add(building);

        return true;
    }

    private void Start()
    {
        Instance = this;
        CreateMap();
        m_graphics.Initialize(m_map);
        GenerateSpawn();
        GenerateExit();
        m_buildings = new HashSet<Building>();
        ConstructBuilding<Stockpile>(GetTile(WorldPosToTile(m_spawnPoint)));
    }

    /// <summary>
    /// Helper function to generate a basic map
    /// </summary>
    private void CreateMap()
    {
        m_map = new Tile[m_width, m_height];
        for (int x = 0; x < m_width; x++)
        {
            for (int y = 0; y < m_height; y++)
            {
                int tileType = Random.Range(0, 4);
                m_map[x, y] = new Tile { Type = tileType < 1 ? Tile.TileType.Ground : Tile.TileType.Wall, Position = new Vector2Int(x, y) };
                if(x == 0 || y == 0 || x == m_width - 1 || y == m_height -1) // Map border
                {
                    m_map[x, y].Type = Tile.TileType.Bedrock;
                }
                m_map[x, y].Visible = false;
            }
        }
    }

    private void GenerateSpawn()
    {
        var spawnTile = WorldPosToTile(m_spawnPoint);
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                var tile = spawnTile + new Vector2Int(dx, dy);
                if (InsideMap(tile))
                {
                    GetTile(tile).Type = Tile.TileType.Ground;
                    GetTile(tile).Visible = true;
                    UpdateVisibility(GetTile(tile));
                    m_graphics.UpdateTile(GetTile(tile));
                    foreach (Tile t in GetNeighbours(GetTile(tile)))
                    {
                        if (t.Type != Tile.TileType.Ground)
                        {
                            m_graphics.UpdateTile(t);
                        }
                    }
                }
            }
        }
    }

    private void GenerateExit()
    {
        Tile spawn = GetTile(WorldPosToTile(m_spawnPoint));
        while(true)
        {
            int x = Random.Range(0, m_width);
            int y = Random.Range(0, m_height);
            Tile t = GetTile(x, y);
            if(GetDistance(spawn, t) > 50)
            {
                Debug.LogFormat("Exit spawned at {0}", t);
                t.Type = Tile.TileType.Ladder;
                return;
            }
        }
    }

    private void UpdateVisibility(Tile t)
    {
        
        var neighbours = GetNeighbours(t);
        foreach(Tile n in neighbours)
        {
            if(n.Visible == false)
            {
                n.Visible = true;
                m_graphics.UpdateTile(n);
                if (n.Type == Tile.TileType.Ground)
                {
                    UpdateVisibility(n);
                }
            }
        }
    }

    private List<Tile> GetNeighbours(Tile t)
    {
        return GetNeighbours(t.Position.x, t.Position.y);
    }

    private List<Tile> GetNeighbours(int x, int y)
    {
        List<Tile> list = new List<Tile>();

        for (int xc = x - 1; xc <= x + 1; xc++)
        {
            for (int yc = y - 1; yc <= y + 1; yc++)
            {
                if(xc == x && yc == y)
                {
                    continue;
                }
                if(xc < 0 || xc >= m_map.GetLength(0))
                {
                    continue;
                }
                if (yc < 0 || yc >= m_map.GetLength(1))
                {
                    continue;
                }
                list.Add(m_map[xc, yc]);
            }
        }

        return list;
    }

    internal class Node : IComparable<Node>
    {
        public readonly Tile Tile;
        public int gCost;
        public int hCost;
        public Node Parent;

        public int fCost
        {
            get
            {
                return gCost + hCost;
            }
        }

        public Node(Tile t)
        {
            Tile = t;
        }

        public override bool Equals(object obj)
        {
            return obj is Node node &&
                   EqualityComparer<Tile>.Default.Equals(Tile, node.Tile);
        }

        public override int GetHashCode()
        {
            return -1811915951 + Tile.GetHashCode();
        }

        public int CompareTo(Node other)
        {
            return fCost.CompareTo(other.fCost);
        }
    }

    public Path FindPath(Vector2Int start, Vector2Int end)
    {
        return FindPath(m_map[start.x, start.y], m_map[end.x, end.y]);
    }

    public Path FindPath(Vector2Int start, Tile end)
    {
        return FindPath(m_map[start.x, start.y], end);
    }

    public Path FindPath(Tile start, Tile end)
    {
        // Early out
        if(start.Equals(end))
        {
            return new Path();
        }

        Node startNode = new Node(start) { gCost = 0, hCost = GetDistance(start, end)};
        Node goalNode = new Node(end);
        List<Node> openSet = new List<Node>(); // TODO:: Should be a min-heap
        HashSet<Node> closedSet = new HashSet<Node>();

        openSet.Add(startNode);
        while(openSet.Count > 0)
        {
            Node currentNode = openSet[0];

            openSet.Remove(currentNode);
            closedSet.Add(currentNode);

            if(currentNode.Equals(goalNode))
            {
                if (end.Walkable == false)
                    currentNode = currentNode.Parent;
                return RetracePath(startNode, currentNode);
            }

            foreach(Tile t in GetNeighbours(currentNode.Tile))
            {
                Node neighbour = new Node(t) { gCost = int.MaxValue, hCost = int.MaxValue };
                if (t != end && ( t.Walkable == false || closedSet.Contains(neighbour)))
                    continue;

                int movementCost = currentNode.gCost + GetDistance(currentNode.Tile, neighbour.Tile);

                // If the current path is shorter then one found previously or if the node has not been checked before
                // Add to the set of nodes to check
                if (movementCost < neighbour.gCost || !openSet.Contains(neighbour))
                {
                    neighbour.gCost = movementCost;
                    neighbour.hCost = GetDistance(neighbour.Tile, goalNode.Tile);
                    neighbour.Parent = currentNode;

                    if (!openSet.Contains(neighbour))
                        openSet.Add(neighbour);

                    openSet.Sort(); // TODO:: This is horrible for preformance, change to heap
                }
            }
        }
        return new Path();
    }

    /// <summary>
    /// Retraces the path once the target is reached
    /// </summary>
    /// <param name="startNode"> Start node of the search </param>
    /// <param name="endNode"> End node of the search </param>
    private static Path RetracePath(Node startNode, Node endNode)
    {
        LinkedList<Tile> path = new LinkedList<Tile>();
        Node currentNode = endNode;

        while (currentNode != startNode)
        {
            path.AddFirst(currentNode.Tile);
            currentNode = currentNode.Parent;
        }

        return new Path(path);
    }

    /// <summary>
    /// Calculates the distance value between two tiles
    /// 
    /// Note: this is not acutal distance but rather a relative one that is used for comparing paths
    /// </summary>
    /// <returns> A relative distance between the nodes</returns>
    public static int GetDistance(Tile t1, Tile t2)
    {
        int dx = Math.Abs(t1.Position.x - t2.Position.x);
        int dy = Math.Abs(t1.Position.y - t2.Position.y);

        int min = Math.Min(dx, dy);
        int max = Math.Max(dx, dy);
        
        return 14 * min + 10 * (max - min);
    }

    public class Path : IEnumerable<Tile>
    {
        private LinkedList<Tile> m_path;

        public bool Valid()
        {
            return m_path != null && m_path.Count > 0;
        }

        public Tile Next()
        {
            Tile t = m_path.First.Value;
            m_path.RemoveFirst();
            return t;
        }

        public IEnumerator<Tile> GetEnumerator()
        {
            foreach(Tile t in m_path)
            {
                yield return t;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public Path()
        {
            m_path = null;
        }

        public Path(LinkedList<Tile> path)
        {
            m_path = path;
        }
    }
}

/// <summary>
/// Tile data
/// </summary>
public class Tile
{
    /// <summary>
    /// Bit 1 denotes walkability, 1 = walkable, 0 = not walkable
    /// </summary>
    public enum TileType : byte
    {
        None = 0,
        Ground = (1 << 1) | 0x1,
        Wall = (2 << 1) | 0x0,
        Building = (3 << 1) | 0x1,
        Bedrock = (4 << 1) | 0x0,
        Ladder = (5 << 1) | 0x1,
    }

    public bool Visible;

    /*
     * Tile types
     * None,
     * Wall,
     * Water,
     * Ore
     */
    public TileType Type;
    public Vector2Int Position;
    public Building Building;

    public bool Walkable => ((byte)Type & 0x1) == 0x1;

    public override string ToString()
    {
        return Type.ToString() + Position.ToString();
    }

    public override bool Equals(object obj)
    {
        return obj is Tile tile &&
               Position.Equals(tile.Position);
    }

    public override int GetHashCode()
    {
        return -425505606 + Position.GetHashCode();
    }
}

// Tile Thoughs
/*
 * first 5 bits = Tile type, 32 possible tiles
 * bit 6 = walkable
 * bit 7 = los blocking
 * bit 8 = visible
 */