using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;

namespace FairyGUI
{
    /// <summary>
    /// 
    /// </summary>
    public enum DestroyMethod
    {
        Destroy,
        Unload,
        None,
        ReleaseTemp,
        Custom
    }

    /// <summary>
    /// 
    /// </summary>
    public class NTexture
    {
        /// <summary>
        /// This event will trigger when a texture is destroying if its destroyMethod is Custom
        /// </summary>
        public static event Action<Texture> CustomDestroyMethod;

        /// <summary>
        /// 
        /// </summary>
        public Rect uvRect;

        /// <summary>
        /// 
        /// </summary>
        public bool rotated;

        /// <summary>
        /// 
        /// </summary>
        public int refCount;

        /// <summary>
        /// 
        /// </summary>
        public float lastActive;

        /// <summary>
        /// 
        /// </summary>
        public DestroyMethod destroyMethod;

        /// <summary>
        /// This event will trigger when texture reloaded and size changed.
        /// </summary>
        public event Action<NTexture> onSizeChanged;

        /// <summary>
        /// This event will trigger when ref count is zero.
        /// </summary>
        public event Action<NTexture> onRelease;

        Texture _nativeTexture;
        Texture _alphaTexture;

        Rect _region;
        Vector2 _offset;
        Vector2 _originalSize;

        NTexture _root;
        Dictionary<string, MaterialManager> _materialManagers;

        internal static Texture2D CreateEmptyTexture()
        {
            Texture2D emptyTexture = new Texture2D(1, 1, TextureFormat.RGB24, false);
            emptyTexture.name = "White Texture";
            emptyTexture.hideFlags = DisplayObject.hideFlags;
            emptyTexture.SetPixel(0, 0, Color.white);
            emptyTexture.Apply();
            return emptyTexture;
        }

        static NTexture _empty;

        /// <summary>
        /// 
        /// </summary>
        public static NTexture Empty
        {
            get
            {
                if (_empty == null)
                    _empty = new NTexture(CreateEmptyTexture());

                return _empty;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public static void DisposeEmpty()
        {
            if (_empty != null)
            {
                NTexture tmp = _empty;
                _empty = null;
                tmp.Dispose();
            }
        }

#if UNITY_2019_3_OR_NEWER
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        static void InitializeOnLoad()
        {
            DisposeEmpty();
            CustomDestroyMethod = null;
        }
#endif

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        public NTexture(Texture texture) : this(texture, null, 1, 1)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="xScale"></param>
        /// <param name="yScale"></param>
        public NTexture(Texture texture, Texture alphaTexture, float xScale, float yScale)
        {
            _root = this;
            _nativeTexture = texture;
            _alphaTexture = alphaTexture;
            uvRect = new Rect(0, 0, xScale, yScale);
            if (yScale < 0)
            {
                uvRect.y = -yScale;
                uvRect.yMax = 0;
            }
            if (xScale < 0)
            {
                uvRect.x = -xScale;
                uvRect.xMax = 0;
            }
            if (_nativeTexture != null)
                _originalSize = new Vector2(_nativeTexture.width, _nativeTexture.height);
            _region = new Rect(0, 0, _originalSize.x, _originalSize.y);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="region"></param>
        public NTexture(Texture texture, Rect region)
        {
            _root = this;
            _nativeTexture = texture;
            _region = region;
            _originalSize = new Vector2(_region.width, _region.height);
            if (_nativeTexture != null)
                uvRect = new Rect(region.x / _nativeTexture.width, 1 - region.yMax / _nativeTexture.height,
                    region.width / _nativeTexture.width, region.height / _nativeTexture.height);
            else
                uvRect.Set(0, 0, 1, 1);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="root"></param>
        /// <param name="region"></param>
        /// <param name="rotated"></param>
        public NTexture(NTexture root, Rect region, bool rotated)
        {
            _root = root;
            this.rotated = rotated;
            region.x += root._region.x;
            region.y += root._region.y;
            uvRect = new Rect(region.x * root.uvRect.width / root.width, 1 - region.yMax * root.uvRect.height / root.height,
                region.width * root.uvRect.width / root.width, region.height * root.uvRect.height / root.height);
            if (rotated)
            {
                float tmp = region.width;
                region.width = region.height;
                region.height = tmp;

                tmp = uvRect.width;
                uvRect.width = uvRect.height;
                uvRect.height = tmp;
            }
            _region = region;
            _originalSize = _region.size;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="root"></param>
        /// <param name="region"></param>
        /// <param name="rotated"></param>
        /// <param name="originalSize"></param>
        /// <param name="offset"></param>
        public NTexture(NTexture root, Rect region, bool rotated, Vector2 originalSize, Vector2 offset)
            : this(root, region, rotated)
        {
            _originalSize = originalSize;
            _offset = offset;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sprite"></param>
        public NTexture(Sprite sprite)
        {
            Rect rect = sprite.textureRect;
            rect.y = sprite.texture.height - rect.yMax;

            _root = this;
            _nativeTexture = sprite.texture;
            _region = rect;
            _originalSize = new Vector2(_region.width, _region.height);
            uvRect = new Rect(_region.x / _nativeTexture.width, 1 - _region.yMax / _nativeTexture.height,
                _region.width / _nativeTexture.width, _region.height / _nativeTexture.height);
        }

        /// <summary>
        /// 
        /// </summary>
        public int width
        {
            get { return (int)_region.width; }
        }

        /// <summary>
        /// 
        /// </summary>
        public int height
        {
            get { return (int)_region.height; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Vector2 offset
        {
            get { return _offset; }
            set { _offset = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Vector2 originalSize
        {
            get { return _originalSize; }
            set { _originalSize = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="drawRect"></param>
        /// <returns></returns>
        public Rect GetDrawRect(Rect drawRect)
        {
            return GetDrawRect(drawRect, FlipType.None);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="drawRect"></param>
        /// <param name="flip"></param>
        /// <returns></returns>
        public Rect GetDrawRect(Rect drawRect, FlipType flip)
        {
            if (_originalSize.x == _region.width && _originalSize.y == _region.height)
                return drawRect;

            float sx = drawRect.width / _originalSize.x;
            float sy = drawRect.height / _originalSize.y;
            Rect rect = new Rect(_offset.x * sx, _offset.y * sy, _region.width * sx, _region.height * sy);

            if (flip != FlipType.None)
            {
                if (flip == FlipType.Horizontal || flip == FlipType.Both)
                {
                    rect.x = drawRect.width - rect.xMax;
                }
                if (flip == FlipType.Vertical || flip == FlipType.Both)
                {
                    rect.y = drawRect.height - rect.yMax;
                }
            }

            return rect;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uv"></param>
        public void GetUV(Vector2[] uv)
        {
            uv[0] = uvRect.position;
            uv[1] = new Vector2(uvRect.xMin, uvRect.yMax);
            uv[2] = new Vector2(uvRect.xMax, uvRect.yMax);
            uv[3] = new Vector2(uvRect.xMax, uvRect.yMin);
            if (rotated)
            {
                float xMin = uvRect.xMin;
                float yMin = uvRect.yMin;
                float yMax = uvRect.yMax;

                float tmp;
                for (int i = 0; i < 4; i++)
                {
                    Vector2 m = uv[i];
                    tmp = m.y;
                    m.y = yMin + m.x - xMin;
                    m.x = xMin + yMax - tmp;
                    uv[i] = m;
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public NTexture root
        {
            get { return _root; }
        }

        /// <summary>
        /// 
        /// </summary>
        public bool disposed
        {
            get { return _root == null; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Texture nativeTexture
        {
            get { return _root != null ? _root._nativeTexture : null; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Texture alphaTexture
        {
            get { return _root != null ? _root._alphaTexture : null; }
        }

        /// <summary>
        /// 
        /// </summary>
        public MaterialManager GetMaterialManager(string shaderName)
        {
            if (_root != this)
            {
                if (_root == null)
                    return null;
                else
                    return _root.GetMaterialManager(shaderName);
            }

            if (_materialManagers == null)
                _materialManagers = new Dictionary<string, MaterialManager>();

            MaterialManager mm;
            if (!_materialManagers.TryGetValue(shaderName, out mm))
            {
                mm = new MaterialManager(this, ShaderConfig.GetShader(shaderName));
                _materialManagers.Add(shaderName, mm);
            }

            return mm;
        }

        /// <summary>
        /// 
        /// </summary>
        public void Unload()
        {
            Unload(false);
        }

        /// <summary>
        /// 
        /// </summary>
        public void Unload(bool destroyMaterials)
        {
            if (this == _empty)
                return;

            if (_root != this)
                throw new Exception("Unload is not allow to call on none root NTexture.");

            if (_nativeTexture != null)
            {
                DestroyTexture();

                if (destroyMaterials)
                    DestroyMaterials();
                else
                    RefreshMaterials();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="nativeTexture"></param>
        /// <param name="alphaTexture"></param>
        public void Reload(Texture nativeTexture, Texture alphaTexture)
        {
            if (_root != this)
                throw new System.Exception("Reload is not allow to call on none root NTexture.");

            if (_nativeTexture != null && _nativeTexture != nativeTexture)
                DestroyTexture();

            _nativeTexture = nativeTexture;
            _alphaTexture = alphaTexture;

            Vector2 lastSize = _originalSize;
            if (_nativeTexture != null)
                _originalSize = new Vector2(_nativeTexture.width, _nativeTexture.height);
            else
                _originalSize = Vector2.zero;
            _region = new Rect(0, 0, _originalSize.x, _originalSize.y);

            RefreshMaterials();

            if (onSizeChanged != null && lastSize != _originalSize)
                onSizeChanged(this);
        }

        void DestroyTexture()
        {
            switch (destroyMethod)
            {
                case DestroyMethod.Destroy:
                    Object.DestroyImmediate(_nativeTexture, true);
                    if (_alphaTexture != null)
                        Object.DestroyImmediate(_alphaTexture, true);
                    break;
                case DestroyMethod.Unload:
                    Resources.UnloadAsset(_nativeTexture);
                    if (_alphaTexture != null)
                        Resources.UnloadAsset(_alphaTexture);
                    break;
                case DestroyMethod.ReleaseTemp:
                    RenderTexture.ReleaseTemporary((RenderTexture)_nativeTexture);
                    if (_alphaTexture is RenderTexture)
                        RenderTexture.ReleaseTemporary((RenderTexture)_alphaTexture);
                    break;
                case DestroyMethod.Custom:
                    if (CustomDestroyMethod == null)
                        Debug.LogWarning("NTexture.CustomDestroyMethod must be set to handle DestroyMethod.Custom");
                    else
                    {
                        CustomDestroyMethod(_nativeTexture);
                        if (_alphaTexture != null)
                            CustomDestroyMethod(_alphaTexture);
                    }
                    break;
            }

            _nativeTexture = null;
            _alphaTexture = null;
        }

        void RefreshMaterials()
        {
            if (_materialManagers != null && _materialManagers.Count > 0)
            {
                Dictionary<string, MaterialManager>.Enumerator iter = _materialManagers.GetEnumerator();
                while (iter.MoveNext())
                    iter.Current.Value.RefreshMaterials();
                iter.Dispose();
            }
        }

        void DestroyMaterials()
        {
            if (_materialManagers != null && _materialManagers.Count > 0)
            {
                Dictionary<string, MaterialManager>.Enumerator iter = _materialManagers.GetEnumerator();
                while (iter.MoveNext())
                    iter.Current.Value.DestroyMaterials();
                iter.Dispose();
            }
        }

        public void AddRef()
        {
            if (_root == null) //disposed
                return;

            if (_root != this && refCount == 0)
                _root.AddRef();

            refCount++;
        }

        public void ReleaseRef()
        {
            if (_root == null) //disposed
                return;

            refCount--;

            if (refCount == 0)
            {
                if (_root != this)
                    _root.ReleaseRef();

                if (onRelease != null)
                    onRelease(this);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public void Dispose()
        {
            if (this == _empty)
                return;

            if (_root == this)
                Unload(true);
            _root = null;
            onSizeChanged = null;
            onRelease = null;
        }
    }
}
