using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Scripting.APIUpdating;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace UnityEngine.Tilemaps
{
    /// <summary>
    /// Rule Override Tiles are Tiles which can override a subset of Rules for a given Rule Tile to provide specialised behaviour while keeping most of the Rules originally set in the Rule Tile.
    /// </summary>
    [MovedFrom(true, "UnityEngine")]
    [Serializable]
    [HelpURL("https://docs.unity3d.com/Packages/com.unity.2d.tilemap.extras@latest/index.html?subfolder=/manual/RuleOverrideTile.html")]
    public class RuleOverrideTile : TileBase
    {

        /// <summary>
        /// A data structure storing the Sprite overriding the original RuleTile Sprite
        /// </summary>
        [Serializable]
        public class TileSpritePair
        {
            /// <summary>
            /// Original Sprite from the original RuleTile.
            /// </summary>
            public Sprite m_OriginalSprite;
            /// <summary>
            /// Overriding Sprite for the Original Sprite.
            /// </summary>
            public Sprite m_OverrideSprite;
        }

        /// <summary>
        /// A data structure storing the GameObject overriding the original RuleTile GameObject
        /// </summary>
        [Serializable]
        public class TileGameObjectPair
        {
            /// <summary>
            /// Original GameObject from the original RuleTile.
            /// </summary>
            public GameObject m_OriginalGameObject;
            /// <summary>
            /// Overriding GameObject for the Original Sprite.
            /// </summary>
            public GameObject m_OverrideGameObject;
        }

        /// <summary>
        /// Gets the overriding Sprite of a given Sprite. 
        /// </summary>
        /// <param name="originalSprite">The original Sprite that is overridden</param>
        public Sprite this[Sprite originalSprite]
        {
            get
            {
                foreach (TileSpritePair spritePair in m_Sprites)
                {
                    if (spritePair.m_OriginalSprite == originalSprite)
                    {
                        return spritePair.m_OverrideSprite;
                    }
                }
                return null;
            }
            set
            {
                if (value == null)
                {
                    m_Sprites = m_Sprites.Where(spritePair => spritePair.m_OriginalSprite != originalSprite).ToList();
                }
                else
                {
                    foreach (TileSpritePair spritePair in m_Sprites)
                    {
                        if (spritePair.m_OriginalSprite == originalSprite)
                        {
                            spritePair.m_OverrideSprite = value;
                            return;
                        }
                    }
                    m_Sprites.Add(new TileSpritePair()
                    {
                        m_OriginalSprite = originalSprite,
                        m_OverrideSprite = value,
                    });
                }
            }
        }

        /// <summary>
        /// Gets the overriding GameObject of a given GameObject. 
        /// </summary>
        /// <param name="originalGameObject">The original GameObject that is overridden</param>
        public GameObject this[GameObject originalGameObject]
        {
            get
            {
                foreach (TileGameObjectPair gameObjectPair in m_GameObjects)
                {
                    if (gameObjectPair.m_OriginalGameObject == originalGameObject)
                    {
                        return gameObjectPair.m_OverrideGameObject;
                    }
                }
                return null;
            }
            set
            {
                if (value == null)
                {
                    m_GameObjects = m_GameObjects.Where(gameObjectPair => gameObjectPair.m_OriginalGameObject != originalGameObject).ToList();
                }
                else
                {
                    foreach (TileGameObjectPair gameObjectPair in m_GameObjects)
                    {
                        if (gameObjectPair.m_OriginalGameObject == originalGameObject)
                        {
                            gameObjectPair.m_OverrideGameObject = value;
                            return;
                        }
                    }
                    m_GameObjects.Add(new TileGameObjectPair()
                    {
                        m_OriginalGameObject = originalGameObject,
                        m_OverrideGameObject = value,
                    });
                }
            }
        }

        /// <summary>
        /// The RuleTile to override
        /// </summary>
        public RuleTile m_Tile;
        /// <summary>
        /// A list of Sprite Overrides
        /// </summary>
        public List<TileSpritePair> m_Sprites = new List<TileSpritePair>();
        /// <summary>
        /// A list of GameObject Overrides
        /// </summary>
        public List<TileGameObjectPair> m_GameObjects = new List<TileGameObjectPair>();

        /// <summary>
        /// Returns the Rule Tile for retrieving TileData
        /// </summary>
        [HideInInspector] public RuleTile m_InstanceTile;

        private void CreateInstanceTile()
        {
            var t = m_Tile.GetType();
            RuleTile instanceTile = CreateInstance(t) as RuleTile;
            instanceTile.hideFlags = HideFlags.NotEditable;
            instanceTile.name = m_Tile.name + " (Override)";
            m_InstanceTile = instanceTile;

#if UNITY_EDITOR
            if(AssetDatabase.Contains(this))
                AssetDatabase.AddObjectToAsset(instanceTile, this);
            EditorUtility.SetDirty(this);
#endif            
        }
        
        /// <summary>
        /// Applies overrides to this
        /// </summary>
        /// <param name="overrides">A list of overrides to apply</param>
        /// <exception cref="ArgumentNullException">The input overrides list is not valid</exception>
        public void ApplyOverrides(IList<KeyValuePair<Sprite, Sprite>> overrides)
        {
            if (overrides == null)
                throw new ArgumentNullException("overrides");

            for (int i = 0; i < overrides.Count; i++)
                this[overrides[i].Key] = overrides[i].Value;
        }

        /// <summary>
        /// Applies overrides to this
        /// </summary>
        /// <param name="overrides">A list of overrides to apply</param>
        /// <exception cref="ArgumentNullException">The input overrides list is not valid</exception>
        public void ApplyOverrides(IList<KeyValuePair<GameObject, GameObject>> overrides)
        {
            if (overrides == null)
                throw new ArgumentNullException("overrides");

            for (int i = 0; i < overrides.Count; i++)
                this[overrides[i].Key] = overrides[i].Value;
        }

        /// <summary>
        /// Gets overrides for this
        /// </summary>
        /// <param name="overrides">A list of overrides to fill</param>
        /// <param name="validCount">Returns the number of valid overrides for Sprites</param>
        /// <exception cref="ArgumentNullException">The input overrides list is not valid</exception>
        public void GetOverrides(List<KeyValuePair<Sprite, Sprite>> overrides, ref int validCount)
        {
            if (overrides == null)
                throw new ArgumentNullException("overrides");

            overrides.Clear();

            List<Sprite> originalSprites = new List<Sprite>();

            if (m_Tile)
            {
                if (m_Tile.m_DefaultSprite)
                    originalSprites.Add(m_Tile.m_DefaultSprite);

                foreach (RuleTile.TilingRule rule in m_Tile.m_TilingRules)
                    foreach (Sprite sprite in rule.m_Sprites)
                        if (sprite && !originalSprites.Contains(sprite))
                            originalSprites.Add(sprite);
            }

            validCount = originalSprites.Count;

            foreach (var pair in m_Sprites)
                if (!originalSprites.Contains(pair.m_OriginalSprite))
                    originalSprites.Add(pair.m_OriginalSprite);

            foreach (Sprite sprite in originalSprites)
                overrides.Add(new KeyValuePair<Sprite, Sprite>(sprite, this[sprite]));
        }

        /// <summary>
        /// Gets overrides for this
        /// </summary>
        /// <param name="overrides">A list of overrides to fill</param>
        /// <param name="validCount">Returns the number of valid overrides for GameObjects</param>
        /// <exception cref="ArgumentNullException">The input overrides list is not valid</exception>
        public void GetOverrides(List<KeyValuePair<GameObject, GameObject>> overrides, ref int validCount)
        {
            if (overrides == null)
                throw new ArgumentNullException("overrides");

            overrides.Clear();

            List<GameObject> originalGameObjects = new List<GameObject>();

            if (m_Tile)
            {
                if (m_Tile.m_DefaultGameObject)
                    originalGameObjects.Add(m_Tile.m_DefaultGameObject);

                foreach (RuleTile.TilingRule rule in m_Tile.m_TilingRules)
                    if (rule.m_GameObject && !originalGameObjects.Contains(rule.m_GameObject))
                        originalGameObjects.Add(rule.m_GameObject);
            }

            validCount = originalGameObjects.Count;

            foreach (var pair in m_GameObjects)
                if (!originalGameObjects.Contains(pair.m_OriginalGameObject))
                    originalGameObjects.Add(pair.m_OriginalGameObject);

            foreach (GameObject gameObject in originalGameObjects)
                overrides.Add(new KeyValuePair<GameObject, GameObject>(gameObject, this[gameObject]));
        }

        /// <summary>
        /// Updates the Rules with the Overrides set for this RuleOverrideTile
        /// </summary>
        public virtual void Override()
        {
            if (!m_Tile)
                return;

            if (!m_InstanceTile)
                CreateInstanceTile();
            
            PrepareOverride();

            var tile = m_InstanceTile;

            tile.m_DefaultSprite = this[tile.m_DefaultSprite] ?? tile.m_DefaultSprite;
            tile.m_DefaultGameObject = this[tile.m_DefaultGameObject] ?? tile.m_DefaultGameObject;

            foreach (var rule in tile.m_TilingRules)
            {
                for (int i = 0; i < rule.m_Sprites.Length; i++)
                {
                    Sprite sprite = rule.m_Sprites[i];
                    rule.m_Sprites[i] = this[sprite] ?? sprite;
                }

                rule.m_GameObject = this[rule.m_GameObject] ?? rule.m_GameObject;
            }
        }

        /// <summary>
        /// Prepares the Overrides set for this RuleOverrideTile
        /// </summary>
        public void PrepareOverride()
        {
            // Create clone of instanceTile to keep data from collections being overridden by JsonUtility
            var tempTile = Instantiate(m_InstanceTile);
            
            var customData = m_InstanceTile.GetCustomFields(true)
                .ToDictionary(field => field, field => field.GetValue(tempTile));

            JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(m_Tile), m_InstanceTile);

            foreach (var kvp in customData)
                kvp.Key.SetValue(m_InstanceTile, kvp.Value);
        }

        /// <summary>
        /// Retrieves any tile animation data from the scripted tile.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        /// <param name="tileAnimationData">Data to run an animation on the tile.</param>
        /// <returns>Whether the call was successful.</returns>
        public override bool GetTileAnimationData(Vector3Int position, ITilemap tilemap, ref TileAnimationData tileAnimationData)
        {
            if (!m_InstanceTile)
                return false;
            return m_InstanceTile.GetTileAnimationData(position, tilemap, ref tileAnimationData);
        }

        /// <summary>
        /// Retrieves any tile rendering data from the scripted tile.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        /// <param name="tileData">Data to render the tile.</param>
        public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
        {
            if (!m_InstanceTile)
                return;
            m_InstanceTile.GetTileData(position, tilemap, ref tileData);
        }

        /// <summary>
        /// This method is called when the tile is refreshed.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        public override void RefreshTile(Vector3Int position, ITilemap tilemap)
        {
            if (!m_InstanceTile)
                return;
            m_InstanceTile.RefreshTile(position, tilemap);
        }

        /// <summary>
        /// StartUp is called on the first frame of the running Scene.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        /// <param name="go">The GameObject instantiated for the Tile.</param>
        /// <returns>Whether StartUp was successful</returns>
        public override bool StartUp(Vector3Int position, ITilemap tilemap, GameObject go)
        {
            if (!m_InstanceTile)
                return true;
            return m_InstanceTile.StartUp(position, tilemap, go);
        }

        /// <summary>
        /// Callback when the tile is enabled
        /// </summary>
        public void OnEnable()
        {
            if (m_Tile == null)
                return;

            if (m_InstanceTile == null)
                Override();
        }
    }
}