/*
 * 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 http://live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */


using Live2D.Cubism.Core;
using Live2D.Cubism.Framework;
using System;
using UnityEngine;

using Object = UnityEngine.Object;


namespace Live2D.Cubism.Rendering
{
    /// <summary>
    /// Controls rendering of a <see cref="CubismModel"/>.
    /// </summary>
    [ExecuteInEditMode, CubismDontMoveOnReimport]
    public sealed class CubismRenderController : MonoBehaviour
    {
        /// <summary>
        /// Model opacity.
        /// </summary>
        /// <remarks>
        /// This is turned into a field to be available to <see cref="AnimationClip"/>s...
        /// </remarks>
        [SerializeField, HideInInspector]
        public float Opacity = 1f;

        /// <summary>
        /// <see cref="LastOpacity"/> backing field.
        /// </summary>
        [SerializeField, HideInInspector]
        private float _lastOpacity;

        /// <summary>
        /// Last model opacity.
        /// </summary>
        private float LastOpacity
        {
            get { return _lastOpacity; }
            set { _lastOpacity = value; }
        }


        /// <summary>
        /// Sorting layer name.
        /// </summary>
        public string SortingLayer
        {
            get
            {
                return UnityEngine.SortingLayer.IDToName(SortingLayerId);
            }
            set
            {
                SortingLayerId = UnityEngine.SortingLayer.NameToID(value);
            }
        }

        /// <summary>
        /// <see cref="SortingLayerId"/> backing field.
        /// </summary>
        [SerializeField, HideInInspector]
        private int _sortingLayerId;

        /// <summary>
        /// Sorting layer Id.
        /// </summary>
        public int SortingLayerId
        {
            get
            {
                return _sortingLayerId;
            }
            set
            {
                if (value == _sortingLayerId)
                {
                    return;
                }


                _sortingLayerId = value;


                // Apply sorting layer.
                var renderers = Renderers;


                for (var i = 0; i < renderers.Length; ++i)
                {
                    renderers[i].OnControllerSortingLayerDidChange(_sortingLayerId);
                }
            }
        }


        /// <summary>
        /// <see cref="SortingMode"/> backing field.
        /// </summary>
        [SerializeField, HideInInspector]
        private CubismSortingMode _sortingMode;

        /// <summary>
        /// <see cref="CubismDrawable"/> sorting.
        /// </summary>
        public CubismSortingMode SortingMode
        {
            get
            {
                return _sortingMode;
            }
            set
            {
                // Return early if same value given.
                if (value == _sortingMode)
                {
                    return;
                }


                _sortingMode = value;


                // Flip sorting.
                var renderers = Renderers;


                for (var i = 0; i < renderers.Length; ++i)
                {
                    renderers[i].OnControllerSortingModeDidChange(_sortingMode);
                }
            }
        }


        /// <summary>
        /// Order in sorting layer.
        /// </summary>
        [SerializeField, HideInInspector]
        private int _sortingOrder;

        /// <summary>
        /// Order in sorting layer.
        /// </summary>
        public int SortingOrder
        {
            get
            {
                return _sortingOrder;
            }
            set
            {
                // Return early in case same value given.
                if (value == _sortingOrder)
                {
                    return;
                }


                _sortingOrder = value;


                // Apply new sorting order.
                var renderers = Renderers;


                for (var i = 0; i < renderers.Length; ++i)
                {
                    renderers[i].OnControllerSortingOrderDidChange(SortingOrder);
                }
            }
        }


        /// <summary>
        /// [Optional] Camera to face.
        /// </summary>
        [SerializeField]
        public Camera CameraToFace;



        /// <summary>
        /// <see cref="DrawOrderHandler"/> backing field.
        /// </summary>
        [SerializeField, HideInInspector]
        private Object _drawOrderHandler;

        /// <summary>
        /// Draw order handler proxy object.
        /// </summary>
        public Object DrawOrderHandler
        {
            get { return _drawOrderHandler; }
            set { _drawOrderHandler = value.ToNullUnlessImplementsInterface<ICubismDrawOrderHandler>(); }
        }


        /// <summary>
        /// <see cref="DrawOrderHandlerInterface"/> backing field.
        /// </summary>
        [NonSerialized]
        private ICubismDrawOrderHandler _drawOrderHandlerInterface;

        /// <summary>
        /// Listener for draw order changes.
        /// </summary>
        private ICubismDrawOrderHandler DrawOrderHandlerInterface
        {
            get
            {
                if (_drawOrderHandlerInterface == null)
                {
                    _drawOrderHandlerInterface = DrawOrderHandler.GetInterface<ICubismDrawOrderHandler>();
                }


                return _drawOrderHandlerInterface;
            }
        }


        /// <summary>
        /// <see cref="OpacityHandler"/> backing field.
        /// </summary>
        [SerializeField, HideInInspector]
        private Object _opacityHandler;

        /// <summary>
        /// Opacity handler proxy object.
        /// </summary>
        public Object OpacityHandler
        {
            get { return _opacityHandler; }
            set { _opacityHandler = value.ToNullUnlessImplementsInterface<ICubismOpacityHandler>(); }
        }


        /// <summary>
        /// <see cref="OpacityHandler"/> backing field.
        /// </summary>
        private ICubismOpacityHandler _opacityHandlerInterface;

        /// <summary>
        /// Listener for opacity changes.
        /// </summary>
        private ICubismOpacityHandler OpacityHandlerInterface
        {
            get
            {
                if (_opacityHandlerInterface == null)
                {
                    _opacityHandlerInterface = OpacityHandler.GetInterface<ICubismOpacityHandler>();
                }


                return _opacityHandlerInterface;
            }
        }


        /// <summary>
        /// The value to offset the <see cref="CubismDrawable"/>s by.
        /// </summary>
        /// <remarks>
        /// You only need to adjust this value when using perspetive cameras.
        /// </remarks>
        [SerializeField, HideInInspector]
        public float _depthOffset = 0.00001f;

        /// <summary>
        /// Depth offset used when sorting by depth.
        /// </summary>
        public float DepthOffset
        {
            get { return _depthOffset; }
            set
            {
                // Return if same value given.
                if (Mathf.Abs(value - _depthOffset) < Mathf.Epsilon)
                {
                    return;
                }


                // Store value.
                _depthOffset = value;


                // Apply it.
                var renderers = Renderers;


                for (var i = 0; i < renderers.Length; ++i)
                {
                    renderers[i].OnControllerDepthOffsetDidChange(_depthOffset);
                }
            }
        }


        /// <summary>
        /// Model the controller belongs to.
        /// </summary>
        private CubismModel Model
        {
            get { return this.FindCubismModel(); }
        }


        /// <summary>
        /// <see cref="DrawablesRootTransform"/> backing field.
        /// </summary>
        private Transform _drawablesRootTransform;

        /// <summary>
        /// Root transform of all <see cref="CubismDrawable"/>s of the model.
        /// </summary>
        private Transform DrawablesRootTransform
        {
            get
            {
                if (_drawablesRootTransform == null)
                {
                    _drawablesRootTransform = Model.Drawables[0].transform.parent;
                }


                return _drawablesRootTransform;
            }
        }


        /// <summary>
        /// <see cref="Renderers"/>s backing field.
        /// </summary>
        [NonSerialized]
        private CubismRenderer[] _renderers;

        /// <summary>
        /// <see cref="CubismRenderer"/>s.
        /// </summary>
        public CubismRenderer[] Renderers
        {
            get
            {
                if (_renderers== null)
                {
                    _renderers = Model.Drawables.GetComponentsMany<CubismRenderer>();
                }


                return _renderers;
            }
        }


        /// <summary>
        /// Makes sure all <see cref="CubismDrawable"/>s have <see cref="CubismRenderer"/>s attached to them.
        /// </summary>
        private void TryAddRenderers()
        {
            var drawables = this
                .FindCubismModel()
                .Drawables;


            // Return early if already initialized.
            if (drawables[0].GetComponent<CubismRenderer>() != null)
            {
                return;
            }


            // Create renders and apply it to backing field...
            _renderers = drawables.AddComponentEach<CubismRenderer>();


            // Initialize sorting layer.
            _sortingLayerId = _renderers[0]
                .MeshRenderer
                .sortingLayerID;
        }


        /// <summary>
        /// Updates opacity if necessary.
        /// </summary>
        private void UpdateOpacity()
        {
            // Return if same value given.
            if (Mathf.Abs(Opacity - LastOpacity) < Mathf.Epsilon)
            {
                return;
            }


            // Store value.
            Opacity = Mathf.Clamp(Opacity, 0f, 1f);
            LastOpacity = Opacity;


            // Apply opacity.
            var applyOpacityToRenderers = (OpacityHandlerInterface == null || Opacity > (1f - Mathf.Epsilon));


            if (applyOpacityToRenderers)
            {
                var renderers = Renderers;


                for (var i = 0; i < renderers.Length; ++i)
                {
                    renderers[i].OnModelOpacityDidChange(Opacity);
                }
            }


            // Call handler.
            if (OpacityHandlerInterface != null)
            {
                OpacityHandlerInterface.OnOpacityDidChange(this, Opacity);
            }
        }

        #region Unity Event Handling

        /// <summary>
        /// Called by Unity. Applies billboarding.
        /// </summary>
        private void LateUpdate()
        {
            // Update opacity if necessary.
            UpdateOpacity();


            // Return early in case no camera is to be faced.
            if (CameraToFace == null)
            {
                return;
            }


            // Face camera.
            DrawablesRootTransform.rotation = (Quaternion.LookRotation(CameraToFace.transform.forward, Vector3.up));
        }


        /// <summary>
        /// Called by Unity. Enables listening to render data updates.
        /// </summary>
        private void OnEnable()
        {
            // Fail silently.
            if (Model == null)
            {
                return;
            }


            // Make sure renderers are available.
            TryAddRenderers();


            // Register listener.
            Model.OnDynamicDrawableData += OnDynamicDrawableData;
        }

        /// <summary>
        /// Called by Unity. Disables listening to render data updates.
        /// </summary>
        private void OnDisable()
        {
            // Fail silently.
            if (Model == null)
            {
                return;
            }


            // Deregister listener.
            Model.OnDynamicDrawableData -= OnDynamicDrawableData;
        }

        #endregion

        #region Cubism Event Handling

        /// <summary>
        /// Called whenever new render data is available. 
        /// </summary>
        /// <param name="sender">Model with new render data.</param>
        /// <param name="data">New render data.</param>
        private void OnDynamicDrawableData(CubismModel sender, CubismDynamicDrawableData[] data)
        {
            // Get drawables.
            var drawables = sender.Drawables;
            var renderers = Renderers;


            // Handle render data changes.
            for (var i = 0; i < data.Length; ++i)
            {
                // Skip completely non-dirty data.
                if (!data[i].IsAnyDirty)
                {
                    continue;
                }


                // Update visibility.
                if (data[i].IsVisibilityDirty)
                {
                    renderers[i].OnDrawableVisiblityDidChange(data[i].IsVisible);
                }


                // Update render order.
                if (data[i].IsRenderOrderDirty)
                {
                    renderers[i].OnDrawableRenderOrderDidChange(data[i].RenderOrder);
                }


                // Update opacity.
                if (data[i].IsOpacityDirty)
                {
                    renderers[i].OnDrawableOpacityDidChange(data[i].Opacity);
                }


                // Update vertex positions.
                if (data[i].AreVertexPositionsDirty)
                {
                    renderers[i].OnDrawableVertexPositionsDidChange(data[i].VertexPositions);
                }
            }


            // Pass draw order changes to handler (if available).
            var drawOrderHandler = DrawOrderHandlerInterface;


            if (drawOrderHandler != null)
            {
                for (var i = 0; i < data.Length; ++i)
                {
                    if (data[i].IsDrawOrderDirty)
                    {
                        drawOrderHandler.OnDrawOrderDidChange(this, drawables[i], data[i].DrawOrder);
                    }
                }
            }
        }

        #endregion
    }
}
