using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class Pathfinder : MonoBehaviour
{
    [System.Serializable]
    public class TilemapSet
    {
        public string Name;
        public GameObject Root;
        public Tilemap BaseTilemap;
        public Tilemap[] BlockTilemaps;
    }

    [SerializeField] private TilemapSet[] _tilemapSets;
    [SerializeField] private int _activeSetIndex;

    private Tilemap _baseTilemap;
    private Tilemap[] _blockTilemaps;
    private HashSet<Vector2Int> _blocked = new();
    private BoundsInt _bounds;

    public TilemapSet[] TilemapSets => _tilemapSets;
    public int ActiveSetIndex => _activeSetIndex;
    public Bounds MapWorldBounds
    {
        get
        {
            var min = TileToWorld(new Vector2Int(_bounds.xMin, _bounds.yMin));
            var max = TileToWorld(new Vector2Int(_bounds.xMax, _bounds.yMax));
            var bounds = new Bounds();
            bounds.SetMinMax(min, max);
            return bounds;
        }
    }

    void Awake()
    {
        ApplyActiveSet();
    }

    public void Rebuild()
    {
        BuildBlockedSet();
    }

    public void SetActiveMap(int index)
    {
        if (_tilemapSets == null || index < 0 || index >= _tilemapSets.Length) return;
        _activeSetIndex = index;
        ApplyActiveSet();
    }

    void ApplyActiveSet()
    {
        if (_tilemapSets == null || _tilemapSets.Length == 0) return;
        if (_activeSetIndex < 0 || _activeSetIndex >= _tilemapSets.Length)
            _activeSetIndex = 0;

        var set = _tilemapSets[_activeSetIndex];
        _baseTilemap = set.BaseTilemap;
        _blockTilemaps = set.BlockTilemaps;

        // Show only active set root, hide others
        for (int i = 0; i < _tilemapSets.Length; i++)
        {
            if (_tilemapSets[i].Root != null)
                _tilemapSets[i].Root.SetActive(i == _activeSetIndex);
        }

        BuildBlockedSet();
        Debug.Log($"Pathfinder: map '{set.Name}' blocked {_blocked.Count} tiles");
    }

    void BuildBlockedSet()
    {
        _blocked.Clear();
        _baseTilemap.CompressBounds();
        _bounds = _baseTilemap.cellBounds;

        foreach (var tilemap in _blockTilemaps)
        {
            if (tilemap == null) continue;
            tilemap.CompressBounds();
            var b = tilemap.cellBounds;
            for (int x = b.xMin; x < b.xMax; x++)
                for (int y = b.yMin; y < b.yMax; y++)
                {
                    var pos3 = new Vector3Int(x, y, 0);
                    if (!tilemap.HasTile(pos3)) continue;

                    var sprite = tilemap.GetSprite(pos3);
                    if (sprite == null)
                    {
                        _blocked.Add(new Vector2Int(x, y));
                        continue;
                    }

                    // Actual tile count from sprite size
                    float ppu = sprite.pixelsPerUnit;
                    int tw = Mathf.CeilToInt(sprite.rect.width / ppu);
                    int th = Mathf.CeilToInt(sprite.rect.height / ppu);

                    // Pivot-based offset
                    int ox = Mathf.FloorToInt(sprite.pivot.x / ppu);
                    int oy = Mathf.FloorToInt(sprite.pivot.y / ppu);

                    for (int dx = 0; dx < tw; dx++)
                        for (int dy = 0; dy < th; dy++)
                            _blocked.Add(new Vector2Int(x + dx - ox, y + dy - oy));
                }
        }
    }

    public bool IsWalkable(Vector2Int pos)
    {
        if (!_baseTilemap.HasTile(new Vector3Int(pos.x, pos.y, 0))) return false;
        return !_blocked.Contains(pos);
    }

    public List<Vector2Int> FindPath(Vector2Int start, Vector2Int end)
    {
        if (!IsWalkable(end)) return null;

        var open = new List<(int f, Vector2Int pos)>();
        var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
        var gScore = new Dictionary<Vector2Int, int>();

        gScore[start] = 0;
        open.Add((Heuristic(start, end), start));

        Vector2Int[] dirs = {
            Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right,
            new(1, 1), new(1, -1), new(-1, 1), new(-1, -1)
        };

        while (open.Count > 0)
        {
            // Find lowest f-score
            int bestIdx = 0;
            for (int i = 1; i < open.Count; i++)
                if (open[i].f < open[bestIdx].f)
                    bestIdx = i;

            var current = open[bestIdx];
            open.RemoveAt(bestIdx);
            var pos = current.pos;

            if (pos == end)
                return ReconstructPath(cameFrom, end);

            foreach (var dir in dirs)
            {
                var next = pos + dir;
                if (!IsWalkable(next)) continue;

                // Diagonal: check adjacent tiles to prevent corner-cutting
                bool isDiagonal = dir.x != 0 && dir.y != 0;
                if (isDiagonal)
                {
                    if (!IsWalkable(new Vector2Int(pos.x + dir.x, pos.y)) ||
                        !IsWalkable(new Vector2Int(pos.x, pos.y + dir.y)))
                        continue;
                }

                int tentativeG = gScore[pos] + 1;
                if (!gScore.ContainsKey(next) || tentativeG < gScore[next])
                {
                    gScore[next] = tentativeG;
                    cameFrom[next] = pos;
                    open.Add((tentativeG + Heuristic(next, end), next));
                }
            }
        }

        return null;
    }

    public Vector2Int WorldToTile(Vector3 worldPos)
    {
        var cell = _baseTilemap.WorldToCell(worldPos);
        return new Vector2Int(cell.x, cell.y);
    }

    public Vector3 TileToWorld(Vector2Int tilePos)
    {
        return _baseTilemap.GetCellCenterWorld(new Vector3Int(tilePos.x, tilePos.y, 0));
    }

    public Vector2Int MapCenter => new Vector2Int(
        (_bounds.xMin + _bounds.xMax) / 2,
        (_bounds.yMin + _bounds.yMax) / 2);

    public Vector2Int GetRandomWalkableTile(int range = 0, Vector2Int from = default, int minDistance = 0)
    {
        var center = MapCenter;
        int xMin = range > 0 ? center.x - range : _bounds.xMin;
        int xMax = range > 0 ? center.x + range : _bounds.xMax;
        int yMin = range > 0 ? center.y - range : _bounds.yMin;
        int yMax = range > 0 ? center.y + range : _bounds.yMax;

        for (int i = 0; i < 100; i++)
        {
            var pos = new Vector2Int(
                Random.Range(xMin, xMax),
                Random.Range(yMin, yMax));
            if (IsWalkable(pos) && Heuristic(pos, from) >= minDistance)
                return pos;
        }
        return FindNearestWalkable(center);
    }

    public List<Vector2Int> GetPath(Vector2Int start, Vector2Int end)
    {
        if (!IsWalkable(start))
            return new List<Vector2Int> { start, FindNearestWalkable(start) };

        var path = FindPath(start, end);
        if (path != null && path.Count > 1)
            return path;

        return new List<Vector2Int> { start, FindNearestWalkable(start) };
    }

    public Vector2Int FindNearestWalkable(Vector2Int from)
    {
        for (int r = 1; r < 20; r++)
        {
            for (int x = -r; x <= r; x++)
            {
                for (int y = -r; y <= r; y++)
                {
                    if (Mathf.Abs(x) != r && Mathf.Abs(y) != r) continue;
                    var pos = from + new Vector2Int(x, y);
                    if (IsWalkable(pos)) return pos;
                }
            }
        }
        return GetRandomWalkableTile();
    }

    int Heuristic(Vector2Int a, Vector2Int b)
    {
        return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
    }

    List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
    {
        var path = new List<Vector2Int> { current };
        while (cameFrom.ContainsKey(current))
        {
            current = cameFrom[current];
            path.Add(current);
        }
        path.Reverse();
        return path;
    }
}
