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

public class Pathfinding : MonoBehaviour {
    private const int moveStraightCost = 10;
    private const int moveDiagonalCost = 14;

    private const float raycastOffsetDistance = 5f;

    [SerializeField] private Transform gridDebugObjectPrefab;
    [SerializeField] private LayerMask obstaclesLayerMask;
    public static Pathfinding Instance { get; private set; }
    private GridSystem<PathNode> GridSystem { get; set; }

    private int Width { get; set; }
    private int Height { get; set; }
    public float CellSize { get; private set; }

    private void Awake() {
        if (Instance is not null) {
            Debug.LogError($"There is more than one Pathfinding! {transform} - {Instance}");
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    public void Setup(int width, int height, float cellSize) {
        Width = width;
        Height = height;
        CellSize = cellSize;
        GridSystem = new(width, height, cellSize, (_, gridPosition) => new(gridPosition));
        // GridSystem.CreateDebugObjects(gridDebugObjectPrefab);

        for (int x = 0; x < Width; x++) {
            for (int z = 0; z < Height; z++) {
                GridPosition gridPosition = new(x, z);
                Vector3 worldPosition = LevelGrid.Instance.GetWorldPosition(gridPosition);
                if (Physics.Raycast(worldPosition + Vector3.down * raycastOffsetDistance, Vector3.up, raycastOffsetDistance * 2, obstaclesLayerMask)) {
                    GetNode(x, z).IsWalkable = false;
                }
            }
        }
    }

    public bool IsWalkableGridPosition(GridPosition gridPosition) => GridSystem.GetGridObject(gridPosition).IsWalkable;

    public void SetWalkableGridPosition(GridPosition gridPosition, bool isWalkable) => GridSystem.GetGridObject(gridPosition).IsWalkable = isWalkable;

    public List<GridPosition> FindPath(GridPosition startGridPosition, GridPosition endGridPosition, out int pathLength) {
        List<PathNode> openList = new();
        List<PathNode> closedList = new();

        PathNode startNode = GridSystem.GetGridObject(startGridPosition);
        PathNode endNode = GridSystem.GetGridObject(endGridPosition);
        openList.Add(startNode);

        for (int x = 0; x < GridSystem.Width; x++) {
            for (int z = 0; z < GridSystem.Height; z++) {
                PathNode pathNode = GridSystem.GetGridObject(new(x, z));
                pathNode.GCost = int.MaxValue;
                pathNode.HCost = 0;
                pathNode.CameFromPathNode = null;
            }
        }

        startNode.GCost = 0;
        startNode.HCost = CalculateDistance(startGridPosition, endGridPosition);

        while (openList.Count > 0) {
            PathNode currentNode = GetLowestFCostPathNode(openList);

            if (currentNode == endNode) {
                // Reached final node
                pathLength = endNode.FCost;
                return CalculatePath(endNode);
            }

            openList.Remove(currentNode);
            closedList.Add(currentNode); //currentNode is tested

            foreach (PathNode neighbourNode in GetNeighbourList(currentNode).Where(neighbourNode => !closedList.Contains(neighbourNode))) {
                if (!neighbourNode.IsWalkable) {
                    closedList.Add(neighbourNode); // Not walkable
                    continue;
                }

                int tentativeGCost = currentNode.GCost + CalculateDistance(currentNode.GridPosition, neighbourNode.GridPosition);
                if (tentativeGCost >= neighbourNode.GCost) continue;
                neighbourNode.CameFromPathNode = currentNode;
                neighbourNode.GCost = tentativeGCost;
                neighbourNode.HCost = CalculateDistance(neighbourNode.GridPosition, endGridPosition);
                if (!openList.Contains(neighbourNode)) openList.Add(neighbourNode);
            }
        }

        pathLength = 0; //no path found
        return null;
    }

    private static int CalculateDistance(GridPosition gridPositionA, GridPosition gridPositionB) {
        GridPosition gridPositionDistance = gridPositionA - gridPositionB;
        int xDistance = Mathf.Abs(gridPositionDistance.X);
        int zDistance = Mathf.Abs(gridPositionDistance.Z);
        int remaining = Mathf.Abs(xDistance - zDistance);
        return moveDiagonalCost * Mathf.Min(xDistance, zDistance) + moveStraightCost * remaining;
    }

    private static PathNode GetLowestFCostPathNode(IReadOnlyList<PathNode> pathNodeList) {
        PathNode lowestFCostPathNode = pathNodeList[0];
        foreach (PathNode currentPathNode in pathNodeList.Where(t => t.FCost < lowestFCostPathNode.FCost)) lowestFCostPathNode = currentPathNode;
        return lowestFCostPathNode;
    }

    private IEnumerable<PathNode> GetNeighbourList(PathNode currentNode) {
        List<PathNode> neighbourList = new();
        GridPosition gridPosition = currentNode.GridPosition;

        if (gridPosition.X - 1 >= 0) {
            neighbourList.Add(GetNode(gridPosition.X - 1, gridPosition.Z + 0)); //Left
            if (gridPosition.Z - 1 >= 0) neighbourList.Add(GetNode(gridPosition.X - 1, gridPosition.Z - 1)); //LeftDown
            if (gridPosition.Z < GridSystem.Height) neighbourList.Add(GetNode(gridPosition.X - 1, gridPosition.Z + 1)); //LeftUp
        }

        if (gridPosition.X + 1 < GridSystem.Width) {
            neighbourList.Add(GetNode(gridPosition.X + 1, gridPosition.Z + 0)); //Right
            if (gridPosition.Z - 1 >= 0) neighbourList.Add(GetNode(gridPosition.X + 1, gridPosition.Z - 1)); //RightDown
            if (gridPosition.Z + 1 < GridSystem.Height) neighbourList.Add(GetNode(gridPosition.X + 1, gridPosition.Z + 1)); //RightUp
        }

        if (gridPosition.Z - 1 >= 0) neighbourList.Add(GetNode(gridPosition.X - 0, gridPosition.Z - 1)); //Down
        if (gridPosition.Z + 1 < GridSystem.Height) neighbourList.Add(GetNode(gridPosition.X - 0, gridPosition.Z + 1)); //Up

        return neighbourList;
    }

    private PathNode GetNode(int x, int z) => GridSystem.GetGridObject(new(x, z));

    private static List<GridPosition> CalculatePath(PathNode endNode) {
        List<PathNode> pathNodeList = new() { endNode };
        PathNode currentNode = endNode;
        while (currentNode.CameFromPathNode != null) {
            pathNodeList.Add(currentNode.CameFromPathNode);
            currentNode = currentNode.CameFromPathNode;
        }

        pathNodeList.Reverse();
        return pathNodeList.Select(pathNode => pathNode.GridPosition).ToList();
    }

    public bool HasPath(GridPosition startGridPosition, GridPosition endGridPosition) => FindPath(startGridPosition, endGridPosition, out _) != null;

    public int GetPathLength(GridPosition startGridPosition, GridPosition endGridPosition) {
        FindPath(startGridPosition, endGridPosition, out int pathLength);
        return pathLength;
    }
}