using System.Collections.Generic;
using UnityEngine.Tilemaps;
using UnityEngine;
using System;
using System.Collections;
using System.Linq;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace GatherAndDefend.LevelEditor
{
    [Serializable]
    public class TilemapData : IEnumerable<TileData>
    {
        public const int INVISIBLE_LAYER = 6;
        [SerializeField]
        private string _key;
        [SerializeField]
        private List<TileData> _tiles;
        [SerializeField]
        private bool _isInvisible;
        [SerializeField]
        private bool _isCollidable;
        [SerializeField]
        private bool _isTrigger;
        [SerializeField]
        private int _renderOrder;
        [SerializeField]
        private string _renderLayer;
        [SerializeField]
        private Vector2 _position;
        [SerializeField]
        private Vector2 _scale;

        public string Key => _key;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="reference">the current tilemap</param>
        /// <returns></returns>
        public void LoadToTilemapEditor(Tilemap reference)
        {
            var tilesPerSecond = Application.isPlaying ? GlobalConfig.Instance.Current.baseTileSpawnSpeed : float.Epsilon;
            reference.transform.localPosition = _position;
            reference.transform.localScale = _scale;

            var rend = reference.GetComponent<TilemapRenderer>();
            rend.sortingOrder = _renderOrder;
            rend.sortingLayerName = _renderLayer;
            if (_isInvisible) rend.gameObject.layer = INVISIBLE_LAYER;
            if (_isCollidable)
            {
                var collision = rend.gameObject.AddComponent<TilemapCollider2D>();
                collision.isTrigger = _isTrigger;
            }

            //all tiles are loaded after their animation is over. use a task to asyncroneously load them and keep control over the flow
            var tasks = new List<Task>();
            foreach (TileData data in _tiles)
            {
                reference.SetTile(data.Position, data.Tile);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="reference">the current tilemap</param>
        /// <param name="placementAnimation">the tiles falling on the tilemap</param>
        /// <param name="allTilesSpawned">a reference value that is used in caller method to detect when all tiles have been spawned</param>
        /// <param name="tileSpawnAcceleration">a delegate which speeds up the spawning of the tiles over time</param>
        /// <returns></returns>
        public async Task LoadToTilemap(Tilemap reference, Func<bool> shouldKillTask, PlacementAnimationHandler placementAnimation = null, Action allTilesSpawned = default, Func<float> tileSpawnAcceleration = default)
        {
            // if the function we receive is null, we just make it constant 1
            if (tileSpawnAcceleration == default) tileSpawnAcceleration = () => 1;
            if (placementAnimation == null) placementAnimation = (tileData, placer, killer) =>
            {
                reference.SetTile(tileData.Position, tileData.Tile);
                return null;
            };


            var tilesPerSecond = Application.isPlaying ? GlobalConfig.Instance.Current.baseTileSpawnSpeed : float.Epsilon;
            reference.transform.localPosition = _position;
            reference.transform.localScale = _scale;

            var rend = reference.GetComponent<TilemapRenderer>();
            rend.sortingOrder = _renderOrder;
            rend.sortingLayerName = _renderLayer;
            if (_isInvisible) rend.gameObject.layer = INVISIBLE_LAYER;
            if (_isCollidable)
            {
                var collision = rend.gameObject.AddComponent<TilemapCollider2D>();
                collision.isTrigger = _isTrigger;
            }

            //all tiles are loaded after their animation is over. use a task to asyncroneously load them and keep control over the flow
            var tasks = new List<Task>();
            foreach (TileData data in _tiles)
            {
                tasks.Add(placementAnimation(data, () => reference.SetTile(data.Position, data.Tile), shouldKillTask));
                if (shouldKillTask())
                {
                    return;
                }
                await Task.Delay((int)(1000f / (tilesPerSecond * tileSpawnAcceleration())));
            }
            allTilesSpawned?.Invoke();
            if (shouldKillTask())
            {
                return;
            }
            await Task.WhenAll(tasks);
        }
        /// <summary>
        /// saves a tilemap into the level object
        /// </summary>
        /// <param name="reference"></param>
        /// <returns>the bounds of the tilemap</returns>
        public void SaveFromTilemap(Tilemap reference)
        {
            _key = reference.name;
            if (_isCollidable = reference.GetComponent<TilemapCollider2D>())
            {
                _isTrigger = reference.GetComponent<TilemapCollider2D>().isTrigger;
            }
            
            _isInvisible = reference.gameObject.layer == INVISIBLE_LAYER;
            _renderLayer = reference.GetComponent<TilemapRenderer>().sortingLayerName;
            _renderOrder = reference.GetComponent<TilemapRenderer>().sortingOrder;
            _position = reference.transform.localPosition;
            _scale = reference.transform.localScale;

            _tiles = new List<TileData>();

            BoundsInt bounds = reference.cellBounds;
            for (int i = bounds.xMin; i <= bounds.xMax; i++)
            {
                for (int j = bounds.yMin; j <= bounds.yMax; j++)
                {
                    Vector3Int position = new Vector3Int(i, j);
                    TileBase tile = reference.GetTile(position);
                    if (!tile) continue;
                    var tileData = new TileData(position, tile);
                    _tiles.Add(tileData);
                }
            }
        }

        /// <summary>
        /// returns a dictionary representation of the tilemap
        /// </summary>
        /// <returns></returns>
        public Dictionary<string, object> ToDictionary()
        {
            return new Dictionary<string, object>()
            {
                {nameof(_key), _key },
                {nameof(_isInvisible), _isInvisible },
                {nameof(_isCollidable), _isCollidable },
                {nameof(_isTrigger), _isTrigger },
                {nameof(_renderOrder), _renderOrder },
                {nameof(_renderLayer), _renderLayer },
                {nameof(_position), new float[]{_position.x, _position.y, 0 } },
                {nameof(_scale), new float[]{ _scale.x, _scale.y, 0 } },
                {nameof(_tiles), _tiles.FindAll(x => !(x.Tile is LevelTile))
                                       .Select(x => new Dictionary<string, object>() {
                                            {nameof(x.Position), new float[] { x.Position.x, x.Position.y, x.Position.z } }, 
                                           {nameof(x.Tile), x.Tile.name } }).ToArray() }
            };
        }

        /// <summary>
        /// builds a tilemap from a dictionary representation (from the save file)
        /// </summary>
        /// <param name="dict"></param>
        /// <returns></returns>
        public static Tilemap FromDictionary(Dictionary<string, object> dict)
        {
            //get all tilemap data
            var key = dict[nameof(_key)].ToString();
            var invisible = dict[nameof(_isInvisible)].ToBool();
            var collidable = dict[nameof(_isCollidable)].ToBool();
            var trigger = dict[nameof(_isTrigger)].ToBool();
            var renderOrder = dict[nameof(_renderOrder)].ToInt();
            var renderLayer = dict[nameof(_renderLayer)].ToString();
            var position = dict[nameof(_position)].ToVector3();
            var scale = dict[nameof(_scale)].ToVector3();
            var tiles = dict[nameof(_tiles)];

            //get grid
            var grid = GameObject.FindObjectOfType<Grid>();
            if (!grid)
            {
                var levelMgrScript = GameObject.FindObjectOfType<LevelManagerScript>();
                grid = new GameObject("Grid").AddComponent<Grid>();
                grid.transform.SetParent(levelMgrScript.transform.parent);
            }

            //get tilemap by name
            var tilemap = grid.GetComponentInChildren<Tilemap>(key);
            TilemapRenderer renderer;
            if (!tilemap)
            {
                tilemap = new GameObject(key).AddComponent<Tilemap>();
                tilemap.tileAnchor = Vector3.zero;
                tilemap.gameObject.AddComponent<TilemapRenderer>();
                tilemap.transform.SetParent(grid.transform);
            }
            tilemap.transform.localPosition = position;
            tilemap.transform.localScale = scale;
            renderer = tilemap.GetComponent<TilemapRenderer>();

            //populate tilemap according to specs
            renderer.sortingOrder = renderOrder;
            renderer.sortingLayerName = renderLayer;

            if (invisible) tilemap.gameObject.layer = INVISIBLE_LAYER;
            if (collidable)
            {
                tilemap.gameObject.AddComponent<TilemapCollider2D>().isTrigger = trigger;
            }

            //populate tilemaps with non-LevelTile tiles
            foreach (var tileObj in tiles as IEnumerable)
            {
                var tileDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(tileObj.ToString());
                var tileName = tileDict["Tile"].ToString();
                var tile = Database.Instance.ScriptableObjects[tileName];
                
                var tilePos = tileDict["Position"].ToVector3();

                tilemap.SetTile(Vector3Int.RoundToInt(tilePos), tile as TileBase);
            }

            return tilemap;
        }

        public IEnumerator<TileData> GetEnumerator()
        {
            return _tiles.GetEnumerator();
        }

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

        public TilemapData()
        {
            _tiles = new List<TileData>();
        }
    }
}