/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */
using Live2D.Cubism.Core;
using Live2D.Cubism.Rendering.Masking;
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace Live2D.Cubism.Rendering
{
    /// 
    /// Wrapper for drawing s.
    /// 
    [ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
    public sealed class CubismRenderer : MonoBehaviour
    {
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private int _localSortingOrder;
        /// 
        /// Local sorting order.
        /// 
        public int LocalSortingOrder
        {
            get
            {
                return _localSortingOrder;
            }
            set
            {
                // Return early if same value given.
                if (value == _localSortingOrder)
                {
                    return;
                }
                // Store value.
                _localSortingOrder = value;
                // Apply it.
                ApplySorting();
            }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private Color _color = Color.white;
        /// 
        /// Color.
        /// 
        public Color Color
        {
            get { return _color; }
            set
            {
                // Return early if same value given.
                if (value == _color)
                {
                    return;
                }
                // Store value.
                _color = value;
                // Apply color.
                ApplyVertexColors();
            }
        }
        /// 
        /// .
        /// 
        public Material Material
        {
            get
            {
                #if UNITY_EDITOR
                if (!Application.isPlaying)
                {
                    return MeshRenderer.sharedMaterial;
                }
                #endif
                return MeshRenderer.material;
            }
            set
            {
                #if UNITY_EDITOR
                if (!Application.isPlaying)
                {
                    MeshRenderer.sharedMaterial = value;
                    return;
                }
                #endif
                MeshRenderer.material = value;
            }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private Texture2D _mainTexture;
        /// 
        /// 's main texture.
        /// 
        public Texture2D MainTexture
        {
            get { return _mainTexture; }
            set
            {
                // Return early if same value given and main texture is valid.
                if (value == _mainTexture && _mainTexture != null)
                {
                    return;
                }
                // Store value.
                _mainTexture = (value != null)
                    ? value
                    : Texture2D.whiteTexture;
                // Apply it.
                ApplyMainTexture();
            }
        }
        /// 
        /// Meshes.
        /// 
        /// 
        /// Double buffering dynamic meshes increases performance on mobile, so we double-buffer them here.
        /// 
        private Mesh[] Meshes { get; set; }
        /// 
        /// Index of front buffer mesh.
        /// 
        private int FrontMesh { get; set; }
        /// 
        /// Index of back buffer mesh..
        /// 
        private int BackMesh { get; set; }
        /// 
        /// .
        /// 
        public Mesh Mesh
        {
            get { return Meshes[FrontMesh]; }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private MeshFilter _meshFilter;
        /// 
        /// .
        /// 
        public MeshFilter MeshFilter
        {
            get
            {
                return _meshFilter;
            }
        }
        /// 
        ///  backing field.
        /// 
        [NonSerialized]
        private MeshRenderer _meshRenderer;
        /// 
        /// .
        /// 
        public MeshRenderer MeshRenderer
        {
            get
            {
                return _meshRenderer;
            }
        }
        #region Interface For CubismRenderController
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private CubismSortingMode _sortingMode;
        /// 
        /// Sorting mode.
        /// 
        private CubismSortingMode SortingMode
        {
            get { return _sortingMode; }
            set { _sortingMode = value; }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private int _sortingOrder;
        /// 
        /// Sorting mode.
        /// 
        private int SortingOrder
        {
            get { return _sortingOrder; }
            set { _sortingOrder = value; }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private int _renderOrder;
        /// 
        /// Sorting mode.
        /// 
        private int RenderOrder
        {
            get { return _renderOrder; }
            set { _renderOrder = value; }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private float _depthOffset = 0.00001f;
        /// 
        /// Offset to apply in case of depth sorting.
        /// 
        private float DepthOffset
        {
            get { return _depthOffset; }
            set { _depthOffset = value; }
        }
        /// 
        ///  backing field.
        /// 
        [SerializeField, HideInInspector]
        private float _opacity;
        /// 
        /// Opacity.
        /// 
        private float Opacity
        {
            get { return _opacity; }
            set { _opacity = value; }
        }
        /// 
        /// Buffer for vertex colors.
        /// 
        private Color[] VertexColors { get; set; }
        /// 
        /// Allows tracking of what vertex data was updated last swap.
        /// 
        private SwapInfo LastSwap { get; set; }
        /// 
        /// Allows tracking of what vertex data will be swapped.
        /// 
        private SwapInfo ThisSwap { get; set; }
        /// 
        /// Swaps mesh buffers.
        /// 
        /// 
        /// Make sure to manually call this method in case you changed the .
        /// 
        public void SwapMeshes()
        {
            // Perform internal swap.
            BackMesh = FrontMesh;
            FrontMesh = (FrontMesh == 0) ? 1 : 0;
            var mesh = Meshes[FrontMesh];
            // Update colors.
            Meshes[BackMesh].colors = VertexColors;
            // Update swap info.
            LastSwap = ThisSwap;
            ResetSwapInfoFlags();
            // Apply swap.
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                MeshFilter.mesh = mesh;
                return;
            }
#endif
            MeshFilter.mesh = mesh;
        }
        /// 
        /// Updates visibility.
        /// 
        public void UpdateVisibility()
        {
            if (LastSwap.DidBecomeVisible)
            {
                MeshRenderer.enabled = true;
            }
            else if (LastSwap.DidBecomeInvisible)
            {
                MeshRenderer.enabled = false;
            }
            ResetVisibilityFlags();
        }
        /// 
        /// Updates render order.
        /// 
        public void UpdateRenderOrder()
        {
            if (LastSwap.NewRenderOrder)
            {
                ApplySorting();
            }
            ResetRenderOrderFlag();
        }
        /// 
        /// Updates sorting layer.
        /// 
        /// New sorting layer.
        internal void OnControllerSortingLayerDidChange(int newSortingLayer)
        {
            MeshRenderer.sortingLayerID = newSortingLayer;
        }
        /// 
        /// Updates sorting mode.
        /// 
        /// New sorting mode.
        internal void OnControllerSortingModeDidChange(CubismSortingMode newSortingMode)
        {
            SortingMode = newSortingMode;
            ApplySorting();
        }
        /// 
        /// Updates sorting order.
        /// 
        /// New sorting order.
        internal void OnControllerSortingOrderDidChange(int newSortingOrder)
        {
            SortingOrder = newSortingOrder;
            ApplySorting();
        }
        /// 
        /// Updates depth offset.
        /// 
        /// 
        internal void OnControllerDepthOffsetDidChange(float newDepthOffset)
        {
            DepthOffset = newDepthOffset;
            ApplySorting();
        }
        /// 
        /// Sets the opacity.
        /// 
        /// New opacity.
        internal void OnDrawableOpacityDidChange(float newOpacity)
        {
            Opacity = newOpacity;
            ApplyVertexColors();
        }
        /// 
        /// Updates render order.
        /// 
        /// New render order.
        internal void OnDrawableRenderOrderDidChange(int newRenderOrder)
        {
            RenderOrder = newRenderOrder;
            SetNewRenderOrder();
        }
        /// 
        /// Sets the .
        /// 
        /// Vertex positions to set.
        internal void OnDrawableVertexPositionsDidChange(Vector3[] newVertexPositions)
        {
            var mesh = Mesh;
            // Apply positions and update bounds.
            mesh.vertices = newVertexPositions;
            mesh.RecalculateBounds();
            // Set swap flag.
            SetNewVertexPositions();
        }
        /// 
        /// Sets visibility.
        /// 
        /// New visibility.
        internal void OnDrawableVisiblityDidChange(bool newVisibility)
        {
            // Set swap flag if visible.
            if (newVisibility)
            {
                BecomeVisible();
            }
            else
            {
                BecomeInvisible();
            }
        }
        /// 
        /// Sets mask properties.
        /// 
        /// Value to set.
        internal void OnMaskPropertiesDidChange(CubismMaskProperties newMaskProperties)
        {
            MeshRenderer.GetPropertyBlock(SharedPropertyBlock);
            // Write properties.
            SharedPropertyBlock.SetTexture(CubismShaderVariables.MaskTexture, newMaskProperties.Texture);
            SharedPropertyBlock.SetVector(CubismShaderVariables.MaskTile, newMaskProperties.Tile);
            SharedPropertyBlock.SetVector(CubismShaderVariables.MaskTransform, newMaskProperties.Transform);
            MeshRenderer.SetPropertyBlock(SharedPropertyBlock);
        }
        /// 
        /// Sets model opacity.
        /// 
        /// Opacity to set.
        internal void OnModelOpacityDidChange(float newModelOpacity)
        {
            _meshRenderer.GetPropertyBlock(SharedPropertyBlock);
            // Write property.
            SharedPropertyBlock.SetFloat(CubismShaderVariables.ModelOpacity, newModelOpacity);
            MeshRenderer.SetPropertyBlock(SharedPropertyBlock);
        }
        #endregion
        /// 
        ///  backing field.
        /// 
        private static MaterialPropertyBlock _sharedPropertyBlock;
        /// 
        ///  that can be shared on the main script thread.
        /// 
        private static MaterialPropertyBlock SharedPropertyBlock
        {
            get
            {
                // Lazily initialize.
                if (_sharedPropertyBlock == null)
                {
                    _sharedPropertyBlock = new MaterialPropertyBlock();
                }
                return _sharedPropertyBlock;
            }
        }
        /// 
        /// Applies main texture for rendering.
        /// 
        private void ApplyMainTexture()
        {
            MeshRenderer.GetPropertyBlock(SharedPropertyBlock);
            // Write property.
            SharedPropertyBlock.SetTexture(CubismShaderVariables.MainTexture, MainTexture);
            MeshRenderer.SetPropertyBlock(SharedPropertyBlock);
        }
        /// 
        /// Applies sorting.
        /// 
        private void ApplySorting()
        {
            // Sort by order.
            if (SortingMode.SortByOrder())
            {
                MeshRenderer.sortingOrder = SortingOrder + ((SortingMode == CubismSortingMode.BackToFrontOrder)
                    ? (RenderOrder + LocalSortingOrder)
                    : -(RenderOrder + LocalSortingOrder));
                transform.localPosition = Vector3.zero;
                return;
            }
            // Sort by depth.
            var offset = (SortingMode == CubismSortingMode.BackToFrontZ)
                    ? -DepthOffset
                    : DepthOffset;
            MeshRenderer.sortingOrder = SortingOrder + LocalSortingOrder;
            transform.localPosition = new Vector3(0f, 0f, RenderOrder * offset);
        }
        /// 
        /// Uploads mesh vertex colors.
        /// 
        public void ApplyVertexColors()
        {
            var vertexColors = VertexColors;
            var color = Color;
            color.a *= Opacity;
            for (var i = 0; i < vertexColors.Length; ++i)
            {
                vertexColors[i] = color;
            }
            // Set swap flag.
            SetNewVertexColors();
        }
        /// 
        /// Initializes the mesh renderer.
        /// 
        private void TryInitializeMeshRenderer()
        {
            if (_meshRenderer == null)
            {
                _meshRenderer = GetComponent();
                // Lazily add component.
                if (_meshRenderer == null)
                {
                    _meshRenderer = gameObject.AddComponent();
                    _meshRenderer.hideFlags = HideFlags.HideInInspector;
                    _meshRenderer.receiveShadows = false;
                    _meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
                    _meshRenderer.lightProbeUsage = LightProbeUsage.BlendProbes;
                }
            }
        }
        /// 
        /// Initializes the mesh filter.
        /// 
        private void TryInitializeMeshFilter()
        {
            if (_meshFilter == null)
            {
                _meshFilter = GetComponent();
                // Lazily add component.
                if (_meshFilter == null)
                {
                    _meshFilter = gameObject.AddComponent();
                    _meshFilter.hideFlags = HideFlags.HideInInspector;
                }
            }
        }
        /// 
        /// Initializes the mesh if necessary.
        /// 
        private void TryInitializeMesh()
        {
            // Only create mesh if necessary.
            // HACK 'Mesh.vertex > 0' makes sure mesh is recreated in case of runtime instantiation.
            if (Meshes != null && Mesh.vertexCount > 0)
            {
                return;
            }
            // Create mesh for attached drawable.
            var drawable = GetComponent();
            if (Meshes == null)
            {
                Meshes = new Mesh[2];
            }
            for (var i = 0; i < 2; ++i)
            {
                var mesh = new Mesh
                {
                    name = drawable.name,
                    vertices = drawable.VertexPositions,
                    uv = drawable.VertexUvs,
                    triangles = drawable.Indices
                };
                mesh.MarkDynamic();
                mesh.RecalculateBounds();
                // Store mesh.
                Meshes[i] = mesh;
            }
        }
        /// 
        /// Initializes vertex colors.
        /// 
        private void TryInitializeVertexColor()
        {
            var mesh = Mesh;
            VertexColors = new Color[mesh.vertexCount];
            for (var i = 0; i < VertexColors.Length; ++i)
            {
                VertexColors[i] = Color;
                VertexColors[i].a *= Opacity;
            }
        }
        /// 
        /// Initializes the main texture if possible.
        /// 
        private void TryInitializeMainTexture()
        {
            if (MainTexture == null)
            {
                MainTexture = null;
            }
            ApplyMainTexture();
        }
        /// 
        /// Initializes components if possible.
        /// 
        public void TryInitialize()
        {
            TryInitializeMeshRenderer();
            TryInitializeMeshFilter();
            TryInitializeMesh();
            TryInitializeVertexColor();
            TryInitializeMainTexture();
            ApplySorting();
        }
        #region Swap Info
        /// 
        /// Sets .
        /// 
        private void SetNewVertexPositions()
        {
            var swapInfo = ThisSwap;
            swapInfo.NewVertexPositions = true;
            ThisSwap = swapInfo;
        }
        /// 
        /// Sets .
        /// 
        private void SetNewVertexColors()
        {
            var swapInfo = ThisSwap;
            swapInfo.NewVertexColors = true;
            ThisSwap = swapInfo;
        }
        /// 
        /// Sets  on visible.
        /// 
        private void BecomeVisible()
        {
            var swapInfo = ThisSwap;
            swapInfo.DidBecomeVisible = true;
            ThisSwap = swapInfo;
        }
        /// 
        /// Sets  on invisible.
        /// 
        private void BecomeInvisible()
        {
            var swapInfo = ThisSwap;
            swapInfo.DidBecomeInvisible = true;
            ThisSwap = swapInfo;
        }
        /// 
        /// Sets .
        /// 
        private void SetNewRenderOrder()
        {
            var swapInfo = ThisSwap;
            swapInfo.NewRenderOrder = true;
            ThisSwap = swapInfo;
        }
        /// 
        /// Resets flags.
        /// 
        private void ResetSwapInfoFlags()
        {
            var swapInfo = ThisSwap;
            swapInfo.NewVertexColors = false;
            swapInfo.NewVertexPositions = false;
            swapInfo.DidBecomeVisible = false;
            swapInfo.DidBecomeInvisible = false;
            ThisSwap = swapInfo;
        }
        /// 
        /// Reset visibility flags.
        /// 
        private void ResetVisibilityFlags()
        {
            var swapInfo = LastSwap;
            swapInfo.DidBecomeVisible = false;
            swapInfo.DidBecomeInvisible = false;
            LastSwap = swapInfo;
        }
        /// 
        /// Reset render order flag.
        /// 
        private void ResetRenderOrderFlag()
        {
            var swapInfo = LastSwap;
            swapInfo.NewRenderOrder = false;
            LastSwap = swapInfo;
        }
        /// 
        /// Allows tracking of  data changed on a swap.
        /// 
        private struct SwapInfo
        {
            /// 
            /// Vertex positions were changed.
            /// 
            public bool NewVertexPositions { get; set; }
            /// 
            /// Vertex colors were changed.
            /// 
            public bool NewVertexColors { get; set; }
            /// 
            /// Visibility were changed to visible.
            /// 
            public bool DidBecomeVisible { get; set; }
            /// 
            /// Visibility were changed to invisible.
            /// 
            public bool DidBecomeInvisible { get; set; }
            /// 
            /// Render order were changed.
            /// 
            public bool NewRenderOrder { get; set; }
        }
        #endregion
        #region Unity Events Handling
        /// 
        /// Finalizes instance.
        /// 
        private void OnDestroy()
        {
            if (Meshes == null)
            {
                return;
            }
            for (var i = 0; i < Meshes.Length; i++)
            {
                DestroyImmediate(Meshes[i]);
            }
        }
        #endregion
    }
}