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

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

    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 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 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]

    public void UpdateLevel()
    {
        _levelObjects.ForEach(levelObject => levelObject.LevelUpdate());

        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();
        });
        toRemove.Clear();
    }

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

    /// <summary>
    /// permet de loader un scriptable object de niveau
    /// </summary>
    /// <param name="level">le niveau à loader</param>
    /// <param name="shouldClear">est ce qu'on veut effacer ce qui est déjà là?</param>
    public void LoadLevel(Level level, bool shouldClear = false)
    {
        if (shouldClear)
        {
            ClearLevel();
        }

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

        //generate all tilemaps
        foreach (TilemapData tilemapData in _currentLevel)
        {
            var tilemap = new GameObject(tilemapData.Key).AddComponent<Tilemap>();
            tilemap.tileAnchor = Vector3.zero;
            tilemap.gameObject.AddComponent<TilemapRenderer>();
            tilemapData.LoadToTilemap(tilemap);
            tilemap.transform.SetParent(grid.transform);
        }
        Debug.Log("level loaded successfully");
    }

    /// <summary>
    /// permet de loader un scriptable object de niveau
    /// </summary>
    /// <param name="levelName">le nom du niveau à loader</param>
    /// <param name="shouldClear">est ce qu'on veut effacer ce qui est déjà là?</param>
    public void LoadLevel(string levelName, bool shouldClear = false)
    {
        if (shouldClear)
        {
            ClearLevel();
        }

        //fetch level from database
        _currentLevel = Database.Instance.ScriptableObjects[levelName] as Level;

        LoadLevel(_currentLevel, shouldClear);
    }


    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);
        }
        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);

        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);
    }
    #endregion
}