using System.Collections.Generic;
using UnityEngine;

namespace UnityEditor.U2D.Aseprite.Common
{
    internal class ModelPreviewer : System.IDisposable
    {
        const float k_TimeControlRectHeight = 20;

        readonly PreviewRenderUtility m_RenderUtility;

        bool m_Disposed = false;
        Rect m_PreviewRect;
        Bounds m_RenderableBounds;
        Vector2Int m_ActorSize;

        TimeControl m_TimeControl;
        int m_Fps;
        Animator m_Animator;
        AnimationClip[] m_Clips;
        AnimationClip m_SelectedClip;
        List<float> m_FrameTimings;
        int m_ClipIndex = 0;
        SpriteRenderer[] m_Renderers;
        
        Texture m_Texture;
        GameObject m_PreviewObject;

        GUIContent[] m_ClipNames;
        int[] m_ClipIndices;

        public ModelPreviewer(GameObject assetPrefab, AnimationClip[] clips) 
        {
            m_RenderUtility = new PreviewRenderUtility();
            m_RenderUtility.camera.fieldOfView = 30f;
            
            m_PreviewObject = m_RenderUtility.InstantiatePrefabInScene(assetPrefab);
            m_RenderUtility.AddManagedGameObject(m_PreviewObject);
            
            m_Renderers = m_PreviewObject.GetComponentsInChildren<SpriteRenderer>();
            m_RenderableBounds = GetRenderableBounds(m_Renderers);

            if (clips != null && clips.Length > 0)
            {
                SetupAnimation(clips);
                SelectClipFromIndex(m_ClipIndex);
            }
        }

        void SetupAnimation(AnimationClip[] clips)
        {
            m_TimeControl = new TimeControl();
            m_Animator = m_PreviewObject.GetComponent<Animator>();
            m_Clips = clips;

            var clipInfos = m_Animator.GetCurrentAnimatorClipInfo(0);
            
            var defaultClipName = string.Empty;
            if (clipInfos.Length > 0)
                defaultClipName = clipInfos[0].clip.name;
            
            m_ClipNames = new GUIContent[m_Clips.Length];
            m_ClipIndices = new int[m_Clips.Length];
            for (var i = 0; i < m_ClipNames.Length; ++i)
            {
                m_ClipNames[i] = new GUIContent(m_Clips[i].name);
                m_ClipIndices[i] = i;

                // Set starting clip to default clip.
                if (m_Clips[i].name == defaultClipName)
                    m_ClipIndex = i;
            }
        }

        void SelectClipFromIndex(int index)
        {
            m_SelectedClip = m_Clips[index];
            m_Fps = Mathf.RoundToInt(m_SelectedClip.frameRate);
            m_TimeControl.playbackSpeed = 1f / m_SelectedClip.length;
            m_TimeControl.currentTime = 0f;

            var timeSet = new HashSet<float>();
            var curveBindings = AnimationUtility.GetObjectReferenceCurveBindings(m_SelectedClip);

            for (var i = 0; i < curveBindings.Length; ++i)
            {
                var keyFrames = AnimationUtility.GetObjectReferenceCurve(m_SelectedClip, curveBindings[i]);
                for (var m = 0; m < keyFrames.Length; ++m)
                    timeSet.Add(keyFrames[m].time);
            }

            m_FrameTimings = new List<float>(timeSet.Count);
            foreach(var time in timeSet)
                m_FrameTimings.Add(time);
            m_FrameTimings.Sort();
            // Remove the final frame time, as we add it on generation
            m_FrameTimings.RemoveAt(m_FrameTimings.Count - 1);
        }

        public void DrawPreview(Rect r, GUIStyle background)
        {
            if (!ShaderUtil.hardwareSupportsRectRenderTexture)
                return;
            var isRepainting = (Event.current.type == EventType.Repaint);

            if (isRepainting)
            {
                if (m_Texture != null)
                    Object.DestroyImmediate(m_Texture);
                m_Texture = null;
                m_PreviewRect = r;

                m_PreviewObject.transform.position = Vector3.zero;
                m_RenderUtility.BeginPreview(r, background);
                DoRenderPreview();
                m_Texture = m_RenderUtility.EndPreview();

                m_TimeControl?.Update();
            }

            if (m_SelectedClip != null)
                UpdateAnimation(isRepainting);
            UpdateActorSize();
            
            GUI.DrawTexture(r, m_Texture, ScaleMode.StretchToFill, false);
            
            if (m_SelectedClip != null)
                DrawTimeControlGUI(m_PreviewRect);
            else
                DrawInfoText(m_PreviewRect);
        }

        void UpdateAnimation(bool isRepainting)
        {
            if (!isRepainting || m_PreviewObject == null)
                return;
         
            m_TimeControl.loop = true;
            
            m_Animator.Play(m_SelectedClip.name, 0, m_TimeControl.normalizedTime);
            m_Animator.Update(m_TimeControl.deltaTime);
        }

        void UpdateActorSize()
        {
            var ppu = m_Renderers[0].sprite.pixelsPerUnit;
            var bounds = GetRenderableBounds(m_Renderers);

            m_ActorSize = new Vector2Int()
            {
                x = Mathf.RoundToInt(bounds.size.x * ppu),
                y = Mathf.RoundToInt(bounds.size.y * ppu)
            };
        }
        
        void DrawTimeControlGUI(Rect rect)
        {
            const float kSliderWidth = 150f;
            const float kSpacing = 4f;
            var timeControlRect = rect;

            // background
            GUI.Box(rect, GUIContent.none, EditorStyles.toolbar);

            timeControlRect.height = k_TimeControlRectHeight;
            timeControlRect.xMax -= kSliderWidth;

            var sliderControlRect = rect;
            sliderControlRect.height = k_TimeControlRectHeight;
            sliderControlRect.yMin += 1;
            sliderControlRect.yMax -= 1;
            sliderControlRect.xMin = sliderControlRect.xMax - kSliderWidth + kSpacing;

            m_TimeControl.DoTimeControl(timeControlRect);
            
            EditorGUI.BeginChangeCheck();
            m_ClipIndex = EditorGUI.IntPopup(sliderControlRect, m_ClipIndex, m_ClipNames, m_ClipIndices);
            if (EditorGUI.EndChangeCheck())
            {
                SelectClipFromIndex(m_ClipIndex);
            }
            
            DrawInfoText(rect);
        }

        void DrawInfoText(Rect rect)
        {
            rect.y = rect.yMax - 24;
            rect.height = 20;

            var text = "";
            if (m_TimeControl != null)
            {
                var currentTime = m_TimeControl.normalizedTime * m_SelectedClip.length;
                var currentFrame = GetFrameFromTime(currentTime);
                text += $"Frame {currentFrame} | ";
            }

            text += $"{m_ActorSize.x}x{m_ActorSize.y}";
            
            EditorGUI.DropShadowLabel(rect, text);
        }

        int GetFrameFromTime(float currentTime)
        {
            var frame = 0;
            for (var i = 0; i < m_FrameTimings.Count; ++i)
            {
                if (currentTime < m_FrameTimings[i])
                    break;
                frame++;
            }
            
            // Remove one to get the frame number start from 0
            return frame - 1;
        }

        void DoRenderPreview()
        {
            var num1 = Mathf.Max(m_RenderableBounds.extents.magnitude, 0.0001f);
            var num2 = num1 * 3.8f;
            var vector3 = m_RenderableBounds.center - Quaternion.identity * (Vector3.forward * num2);
            m_RenderUtility.camera.transform.position = vector3;
            m_RenderUtility.camera.nearClipPlane = num2 - num1 * 1.1f;
            m_RenderUtility.camera.farClipPlane = num2 + num1 * 5.1f;
            m_RenderUtility.lights[0].intensity = 0.7f;
            m_RenderUtility.lights[1].intensity = 0.7f;
            m_RenderUtility.ambientColor = new Color(0.1f, 0.1f, 0.1f, 0.0f);

            m_RenderUtility.Render(true);
        }

        static Bounds GetRenderableBounds(SpriteRenderer[] renderers)
        {
            if (renderers.Length == 1)
            {
                var renderBound = renderers[0].bounds;
                var localPos = renderers[0].transform.localPosition;
                renderBound.center -= localPos;
                return renderBound;
            }
            
            var bounds = new Bounds();
            foreach (var rendererComponents in renderers)
            {
                var renderBound = rendererComponents.bounds;
                if (bounds.extents == Vector3.zero)
                    bounds = renderBound;
                else if(rendererComponents.enabled)
                    bounds.Encapsulate(renderBound);
            }
            return bounds;
        }
        
        public void Dispose()
        {
            if (m_Disposed)
                return;
            
            m_RenderUtility.Cleanup();
            Object.DestroyImmediate(m_PreviewObject);
            m_PreviewObject = null;
            m_Disposed = true;
        }
    }
}