// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //

#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.

using System.Collections.Generic;
using UnityEngine;

namespace Animancer
{
    /// <summary>
    /// Replaces the <see cref="SpriteRenderer.sprite"/> with a copy of it that uses a different <see cref="Texture"/>
    /// during every <see cref="LateUpdate"/>.
    /// </summary>
    /// 
    /// <remarks>
    /// This script is not specific to Animancer and will work with any animation system if you remove the
    /// [<see cref="AddComponentMenu"/>] and [<see cref="HelpURL"/>] attributes.
    /// </remarks>
    /// https://kybernetik.com.au/animancer/api/Animancer/SpriteRendererTextureSwap
    /// 
    [AddComponentMenu(Strings.MenuPrefix + "Sprite Renderer Texture Swap")]
    [HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(SpriteRendererTextureSwap))]
    [DefaultExecutionOrder(DefaultExecutionOrder)]
    public class SpriteRendererTextureSwap : MonoBehaviour
    {
        /************************************************************************************************************************/

        /// <summary>Execute very late (32000 is last).</summary>
        public const int DefaultExecutionOrder = 30000;

        /************************************************************************************************************************/

        [SerializeField]
        [Tooltip("The SpriteRenderer that will have its Sprite modified")]
        private SpriteRenderer _Renderer;

        /// <summary>The <see cref="SpriteRenderer"/> that will have its <see cref="Sprite"/> modified.</summary>
        public ref SpriteRenderer Renderer => ref _Renderer;

        /************************************************************************************************************************/

        [SerializeField]
        [Tooltip("The replacement for the original Sprite texture")]
        private Texture2D _Texture;

        /// <summary>The replacement for the original <see cref="Sprite.texture"/>.</summary>
        /// <remarks>
        /// If this texture has any <see cref="Sprite"/>s set up in its import settings, they will be completely
        /// ignored because this system creates new <see cref="Sprite"/>s at runtime. The texture doesn't even need to
        /// be set to <see cref="Sprite"/> mode.
        /// <para></para>
        /// Call <see cref="ClearCache"/> before setting this if you want to destroy any sprites created for the
        /// previous texture.
        /// </remarks>
        public Texture2D Texture
        {
            get => _Texture;
            set
            {
                _Texture = value;
                RefreshSpriteMap();
            }
        }

        /************************************************************************************************************************/

        private Dictionary<Sprite, Sprite> _SpriteMap;

        private void RefreshSpriteMap() => _SpriteMap = GetSpriteMap(_Texture);

        /************************************************************************************************************************/

        protected virtual void Awake() => RefreshSpriteMap();

        protected virtual void OnValidate() => RefreshSpriteMap();

        /************************************************************************************************************************/

        protected virtual void LateUpdate()
        {
            if (_Renderer == null)
                return;

            var sprite = _Renderer.sprite;
            if (TrySwapTexture(_SpriteMap, _Texture, ref sprite))
                _Renderer.sprite = sprite;
        }

        /************************************************************************************************************************/

        /// <summary>Destroys all sprites created for the current <see cref="Texture"/>.</summary>
        public void ClearCache()
        {
            DestroySprites(_SpriteMap);
        }

        /************************************************************************************************************************/

        private static readonly Dictionary<Texture2D, Dictionary<Sprite, Sprite>>
            TextureToSpriteMap = new Dictionary<Texture2D, Dictionary<Sprite, Sprite>>();

        /************************************************************************************************************************/

        /// <summary>Returns a cached dictionary mapping original sprites to duplicates using the specified `texture`.</summary>
        public static Dictionary<Sprite, Sprite> GetSpriteMap(Texture2D texture)
        {
            if (texture == null)
                return null;

            if (!TextureToSpriteMap.TryGetValue(texture, out var map))
                TextureToSpriteMap.Add(texture, map = new Dictionary<Sprite, Sprite>());

            return map;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// If the <see cref="Sprite.texture"/> is not already using the specified `texture`, this method replaces the
        /// `sprite` with a cached duplicate which uses that `texture` instead.
        /// </summary>
        public static bool TrySwapTexture(Dictionary<Sprite, Sprite> spriteMap, Texture2D texture, ref Sprite sprite)
        {
            if (spriteMap == null ||
                sprite == null ||
                texture == null ||
                sprite.texture == texture)
                return false;

            if (!spriteMap.TryGetValue(sprite, out var otherSprite))
            {
                var pivot = sprite.pivot;
                pivot.x /= sprite.rect.width;
                pivot.y /= sprite.rect.height;

                otherSprite = Sprite.Create(texture,
                    sprite.rect, pivot, sprite.pixelsPerUnit,
                    0, SpriteMeshType.FullRect, sprite.border, false);

#if UNITY_ASSERTIONS
                var name = sprite.name;
                var originalTextureName = sprite.texture.name;
                var index = name.IndexOf(originalTextureName);
                if (index >= 0)
                {
                    var newName =
                        texture.name +
                        name.Substring(index + originalTextureName.Length, name.Length - (index + originalTextureName.Length));

                    if (index > 0)
                        newName = name.Substring(0, index) + newName;

                    name = newName;
                }

                otherSprite.name = name;
#endif

                spriteMap.Add(sprite, otherSprite);
            }

            sprite = otherSprite;
            return true;
        }

        /************************************************************************************************************************/

        /// <summary>Destroys all the <see cref="Dictionary{TKey, TValue}.Values"/>.</summary>
        public static void DestroySprites(Dictionary<Sprite, Sprite> spriteMap)
        {
            if (spriteMap == null)
                return;

            foreach (var sprite in spriteMap.Values)
                Destroy(sprite);

            spriteMap.Clear();
        }

        /************************************************************************************************************************/

        /// <summary>Destroys all sprites created for the `texture`.</summary>
        public static void DestroySprites(Texture2D texture)
        {
            if (TextureToSpriteMap.TryGetValue(texture, out var spriteMap))
            {
                TextureToSpriteMap.Remove(texture);
                DestroySprites(spriteMap);
            }
        }

        /************************************************************************************************************************/
    }
}