using GatherAndDefend.LevelEditor;
using System.Collections.Generic;
using System.Text;
using UnityEngine.Tilemaps;
using UnityEngine;
using System.Linq;
using Newtonsoft.Json;
using System.IO;
using System.Collections;
using System.Threading.Tasks;
using GatherAndDefend.Events;

/// <summary>
/// data class for containing everything level related
/// </summary>
public class LevelManager : Singleton<LevelManager>
{
    string SavePath => Application.dataPath + "/save.txt";

    public delegate void OnLevelLoaded(Level level);
    public delegate void LevelAction(ILevelObject levelObject);
    public delegate bool LevelPredicate<T>(T levelObject) where T : ILevelObject;
    
    private readonly List<ILevelObject> _toAdd;
    private readonly List<ILevelObject> _toRemove;
    private readonly List<ILevelObject> _levelObjects;
    private WaveObserver _waveObserver;
    private bool _isLoading = true;
    private bool _shouldKillTask = false;

    private Tilemap _dynamicTilemap;
    public Tilemap DynamicTilemap
    {
        get
        {
            if (!_dynamicTilemap)
            {
                var grid = Object.FindObjectOfType<Grid>();
                _dynamicTilemap = new GameObject("Dynamic").AddComponent<Tilemap>();
                _dynamicTilemap.gameObject.AddComponent<TilemapRenderer>().sortingOrder = 3;
                _dynamicTilemap.transform.SetParent(grid.transform);
                _dynamicTilemap.tileAnchor = Vector3.zero;
            }
            return _dynamicTilemap;
        }
    }
    private Level _currentLevel;
    public Level CurrentLevel => _currentLevel;

    public Transform LevelTransform
    {
        get;
        set;
    }

    public bool Loading { get; private set; } = false;

    public LevelManager()
    {
        _toAdd = new List<ILevelObject>();
        _toRemove = new List<ILevelObject>();
        _levelObjects = new List<ILevelObject>();
    }

    #region [~CRUD~]
    public void Add(ILevelObject levelObject)
    {
        _toAdd.Add(levelObject);
    }
    public void Remove(ILevelObject levelObject)
    {
        _toRemove.Add(levelObject);
    }
    public void Clear()
    {
        _toAdd.Clear();
        _toRemove.Clear();
        _levelObjects.Clear();
    }
    public T Get<T>(LevelPredicate<T> predicate = null) where T : ILevelObject
    {
        if (predicate == null) predicate = (generic) => true;
        return (T)_levelObjects.Find(levelObject => levelObject is T generic && predicate(generic));
    }
    public List<T> GetAll<T>(LevelPredicate<T> predicate = null) where T : ILevelObject
    {
        if (predicate == null) predicate = (generic) => true;
        List<T> ret = new();
        foreach (var levelObject in _levelObjects) if (levelObject is T generic && predicate(generic)) ret.Add(generic);
        return ret;
    }
    public int Count<T>(LevelPredicate<T> predicate = null) where T : ILevelObject
    {
        return GetAll(predicate).Count;
    }
    public bool Has<T>(LevelPredicate<T> predicate = null) where T : ILevelObject
    {
        if (predicate == null) predicate = (generic) => true;
        return _levelObjects.Exists(levelObject => levelObject is T generic && predicate(generic));
    }
    #endregion

    #region [Level management]
    
    void AddAndRemoveObjects()
    {
        //add and remove
        var toAdd = new List<ILevelObject>(_toAdd);
        toAdd.ForEach(addedObject =>
        {
            _toAdd.Remove(addedObject);
            _levelObjects.Add(addedObject);
            addedObject.LevelStart();
        });

        var toRemove = new List<ILevelObject>(_toRemove);
        toRemove.ForEach(removedObject =>
        {
            _toRemove.Remove(removedObject);
            _levelObjects.Remove(removedObject);
            removedObject.LevelDestroy();
        });
    }

    public void UpdateLevel()
    {
        if (_isLoading) return;
        AddAndRemoveObjects();

        _levelObjects.ForEach(levelObject =>
        {
            levelObject.LevelUpdate();
        });

        AddAndRemoveObjects();
    }

    public void ClearLevel()
    {
        foreach (var obj in _levelObjects)
        {
            obj.RemoveFromLevel();
        }
        Clear();
    }

    public void KillLoading()
    {
        _shouldKillTask = true;
    }
    /// <summary>
    /// loads a Level scriptable object
    /// </summary>
    /// <param name="level">the level to load</param>
    /// <param name="shouldClear">do we want to clear what's already there before loading?</param>
    /// <param name="placementAnimation">the tiles falling from the top of the screen</param>
    public async Task LoadLevel(Level level, PlacementAnimationHandler placementAnimation = null)
    {
        _isLoading = true;
        _shouldKillTask = false;
        float accelerationOfAcceleration = GlobalConfig.Instance.Current.tileSpawnAcceleration;
        float tileCurrentAcceleration = 1;
        float tileSpawnAccelerationFunc() => (tileCurrentAcceleration += accelerationOfAcceleration * Time.deltaTime);

        ClearLevel();

        Debug.Log("Load level: " + _currentLevel.name + " using " + _currentLevel.WaveConfig.name);

        _currentLevel = level;
        _waveObserver = WaveObserver.Instance;
        _waveObserver.Init(_currentLevel.WaveConfig);
        Grid grid = Object.FindObjectOfType<Grid>();
        //create new grid if there is none
        if (!grid)
        {
            var levelMgrScript = Object.FindObjectOfType<LevelManagerScript>();
            grid = new GameObject("Grid").AddComponent<Grid>();
            grid.transform.SetParent(levelMgrScript.transform);
        }
        //remove all tilemaps if there is a grid
        else
        {
            foreach (Transform child in grid.transform)
            {
                Object.Destroy(child.gameObject);
            }
        }

        var tasks = new List<Task>();
        //generate all tilemaps
        foreach (TilemapData tilemapData in _currentLevel)
        {
            bool allTilesSpawned = false;
            System.Action OnAllTilesSpawned = () => allTilesSpawned = true;
            var tilemap = new GameObject(tilemapData.Key).AddComponent<Tilemap>();
            tilemap.tileAnchor = Vector3.zero;
            tilemap.gameObject.AddComponent<TilemapRenderer>();
            tilemap.transform.SetParent(grid.transform);

            tasks.Add(tilemapData.LoadToTilemap(tilemap, () => _shouldKillTask, placementAnimation, OnAllTilesSpawned, tileSpawnAccelerationFunc));
          
            if (_shouldKillTask)
            {
                return;
            }
            await Extensions.WaitWhile(() => !allTilesSpawned);
        }

        if (_shouldKillTask)
        {
            return;
        }
        await Task.WhenAll(tasks);
       
        EventAggregator.Instance.GetEvent<LevelLoadedEvent>().Invoke(level);
        _isLoading = false;
        Debug.Log("level loaded successfully");
    }

    /// <summary>
    /// load a Level scriptable object using its name
    /// </summary>
    /// <param name="levelName">the name of the loaded Level</param>
    /// <param name="shouldClear">should we clear what's already there?</param>
    /// <param name="placementAnimation">the tiles falling animation</param>
    public async Task LoadLevel(string levelName, PlacementAnimationHandler placementAnimation = null)
    {
        //fetch level from database
        _currentLevel = Database.Instance.ScriptableObjects[levelName] as Level;
        await LoadLevel(_currentLevel, placementAnimation).ConfigureAwait(false);
    }

    public void SaveFile()
    {
        var list = _levelObjects.Select(obj => obj.ToDictionary()).ToList();

        foreach (var tilemapData in _currentLevel)
        {
            var levelConfig = tilemapData.ToDictionary();
            levelConfig[nameof(ILevelObject.ObjectType)] = nameof(ILevelObject.ObjectType.Tilemap);
            list.Add(levelConfig);
        }

        list.Add(OtherValuesToDict());

        string saved = JsonConvert.SerializeObject(list);
        
        File.WriteAllText(SavePath, saved, Encoding.UTF8);
        Debug.Log("game saved successfully");
    }
    public void LoadFile()
    {
        string saved = File.ReadAllText(SavePath, Encoding.UTF8);
        var dicts = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(saved);

        ClearLevel();

        var tilemapDicts = dicts.FindAll(x => x[nameof(ILevelObject.ObjectType)].ToString() == nameof(ILevelObject.ObjectType.Tilemap));
        foreach (var tilemapDict in tilemapDicts) CreateTilemap(tilemapDict);

        var prefabDicts = dicts.FindAll(x => x[nameof(ILevelObject.ObjectType)].ToString() == nameof(ILevelObject.ObjectType.Prefab));
        foreach (var prefabDict in prefabDicts) CreatePrefab(prefabDict);

        var tileDicts = dicts.FindAll(x => x[nameof(ILevelObject.ObjectType)].ToString() == nameof(ILevelObject.ObjectType.Tile));
        foreach (var tileDict in tileDicts) CreateTile(tileDict);

        var otherDict = dicts.Find(x => x[nameof(ILevelObject.ObjectType)].ToString() == nameof(ILevelObject.ObjectType.Other));
        DictToOtherValues(otherDict);

        EventAggregator.Instance.GetEvent<LevelLoadedEvent>().Invoke(_currentLevel);
        Debug.Log("game loaded successfully");
    }

    private void CreatePrefab(Dictionary<string, object> dict)
    {
        var name = dict["Name"].ToString();
        var prefab = Database.Instance.Prefabs[name];
        var instance = prefab.Create(Vector3.zero, parent: LevelTransform);
        var comp = instance.GetComponent<LevelObject>();
        comp.LoadDictionary(dict);
    }
    private void CreateTile(Dictionary<string, object> dict)
    {
        var name = dict["Name"].ToString();

        var tile = Object.Instantiate(Database.Instance.ScriptableObjects[name]) as LevelTile;
        tile.Instantiated = true;
        tile.LoadDictionary(dict);
        tile.AddToLevel();
    }
    private void CreateTilemap(Dictionary<string, object> dict)
    {
        TilemapData.FromDictionary(dict);
    }
    private Dictionary<string, object> OtherValuesToDict()
    {
        return new Dictionary<string, object>()
        {
            [nameof(ILevelObject.ObjectType)] = nameof(ILevelObject.ObjectType.Other),
            [nameof(_currentLevel)] = _currentLevel.name
        };
    }
    private void DictToOtherValues(Dictionary<string, object> dict)
    {
        // fetch current level
        var levelName = dict[nameof(_currentLevel)].ToString();
        var level = Database.Instance.ScriptableObjects[levelName] as Level;
        _currentLevel = level;


    }
    #endregion
}