/****************************************************************************
* Copyright 2019 Nreal Techonology Limited. All rights reserved.
*                                                                                                                                                          
* This file is part of NRSDK.                                                                                                          
*                                                                                                                                                           
* https://www.nreal.ai/        
* 
*****************************************************************************/

namespace NRKernal
{
    using System.Text;
    using UnityEngine;
    using UnityEngine.Profiling;

#if WINDOWS_UWP
using Windows.System;
#endif

    /// <summary>
    /// ABOUT: The VisualProfiler provides a drop in, single file, solution for viewing your Nreal
    /// Unity application's frame rate and memory usage. Missed frames are displayed over time to
    /// visually find problem areas. Memory is reported as current, peak and max usage in a bar graph.
    /// 
    /// USAGE: To use this profiler simply add this script as a component of any GameObject in your
    /// Unity scene. The profiler is initially active and visible (toggle-able via the IsVisible
    /// property), but can be toggled via the enabled/disable voice commands keywords.
    /// 
    /// NOTE: For improved rendering performance you can optionally include the "Nreal/Instanced-
    /// Colored" shader in your project along with the VisualProfiler. </summary>
    public class NRVisualProfiler : MonoBehaviour
    {
        /// <summary> The maximum length of the string. </summary>
        private static readonly int maxStringLength = 32;
        /// <summary> The maximum target frame rate. </summary>
        private static readonly int maxTargetFrameRate = 120;
        /// <summary> The maximum frame timings. </summary>
        private static readonly int maxFrameTimings = 128;
        /// <summary> The frame range. </summary>
        private static readonly int frameRange = 30;
        /// <summary> The default window rotation. </summary>
        private static readonly Vector2 defaultWindowRotation = new Vector2(10.0f, 20.0f);
        /// <summary> The default window scale. </summary>
        private static readonly Vector3 defaultWindowScale = new Vector3(0.2f, 0.04f, 1.0f);
        /// <summary> The used memory string. </summary>
        private static readonly string usedMemoryString = "Used: ";
        /// <summary> The peak memory string. </summary>
        private static readonly string peakMemoryString = "Peak: ";
        /// <summary> The limit memory string. </summary>
        private static readonly string limitMemoryString = "Limit: ";

        /// <summary> Gets or sets the window parent. </summary>
        /// <value> The window parent. </value>
        public Transform WindowParent { get; set; } = null;

        /// <summary> True if is visible, false if not. </summary>
        [Header("Profiler Settings")]
        [SerializeField, Tooltip("Is the profiler currently visible.")]
        private bool isVisible = true;

        /// <summary> Gets or sets a value indicating whether this object is visible. </summary>
        /// <value> True if this object is visible, false if not. </value>
        public bool IsVisible
        {
            get { return isVisible; }
            set { isVisible = value; }
        }

        /// <summary> The frame sample rate. </summary>
        [SerializeField, Tooltip("The amount of time, in seconds, to collect frames for frame rate calculation.")]
        private float frameSampleRate = 0.1f;

        /// <summary> Gets or sets the frame sample rate. </summary>
        /// <value> The frame sample rate. </value>
        public float FrameSampleRate
        {
            get { return frameSampleRate; }
            set { frameSampleRate = value; }
        }

        /// <summary> The window anchor. </summary>
        [Header("Window Settings")]
        [SerializeField, Tooltip("What part of the view port to anchor the window to.")]
        private TextAnchor windowAnchor = TextAnchor.LowerCenter;

        /// <summary> Gets or sets the window anchor. </summary>
        /// <value> The window anchor. </value>
        public TextAnchor WindowAnchor
        {
            get { return windowAnchor; }
            set { windowAnchor = value; }
        }

        /// <summary> The window offset. </summary>
        [SerializeField, Tooltip("The offset from the view port center applied based on the window anchor selection.")]
        private Vector2 windowOffset = new Vector2(0.1f, 0.1f);

        /// <summary> Gets or sets the window offset. </summary>
        /// <value> The window offset. </value>
        public Vector2 WindowOffset
        {
            get { return windowOffset; }
            set { windowOffset = value; }
        }

        /// <summary> The window scale. </summary>
        [SerializeField, Range(0.5f, 5.0f), Tooltip("Use to scale the window size up or down, can simulate a zooming effect.")]
        private float windowScale = 1.0f;

        /// <summary> Gets or sets the window scale. </summary>
        /// <value> The window scale. </value>
        public float WindowScale
        {
            get { return windowScale; }
            set { windowScale = Mathf.Clamp(value, 0.5f, 5.0f); }
        }

        /// <summary> The window follow speed. </summary>
        [SerializeField, Range(0.0f, 100.0f), Tooltip("How quickly to interpolate the window towards its target position and rotation.")]
        private float windowFollowSpeed = 5.0f;

        /// <summary> Gets or sets the window follow speed. </summary>
        /// <value> The window follow speed. </value>
        public float WindowFollowSpeed
        {
            get { return windowFollowSpeed; }
            set { windowFollowSpeed = Mathf.Abs(value); }
        }

        /// <summary> The toggle keyworlds. </summary>
        [Header("UI Settings")]
        [SerializeField, Tooltip("Voice commands to toggle the profiler on and off.")]
        private string[] toggleKeyworlds = new string[] { "Profiler", "Toggle Profiler", "Show Profiler", "Hide Profiler" };
        /// <summary> The displayed decimal digits. </summary>
        [SerializeField, Range(0, 3), Tooltip("How many decimal places to display on numeric strings.")]
        private int displayedDecimalDigits = 1;
        /// <summary> The base color. </summary>
        [SerializeField, Tooltip("The color of the window backplate.")]
        private Color baseColor = new Color(80 / 256.0f, 80 / 256.0f, 80 / 256.0f, 1.0f);
        /// <summary> The target frame rate color. </summary>
        [SerializeField, Tooltip("The color to display on frames which meet or exceed the target frame rate.")]
        private Color targetFrameRateColor = new Color(127 / 256.0f, 186 / 256.0f, 0 / 256.0f, 1.0f);
        /// <summary> The missed frame rate color. </summary>
        [SerializeField, Tooltip("The color to display on frames which fall below the target frame rate.")]
        private Color missedFrameRateColor = new Color(242 / 256.0f, 80 / 256.0f, 34 / 256.0f, 1.0f);
        /// <summary> The memory used color. </summary>
        [SerializeField, Tooltip("The color to display for current memory usage values.")]
        private Color memoryUsedColor = new Color(0 / 256.0f, 164 / 256.0f, 239 / 256.0f, 1.0f);
        /// <summary> The memory peak color. </summary>
        [SerializeField, Tooltip("The color to display for peak (aka max) memory usage values.")]
        private Color memoryPeakColor = new Color(255 / 256.0f, 185 / 256.0f, 0 / 256.0f, 1.0f);
        /// <summary> The memory limit color. </summary>
        [SerializeField, Tooltip("The color to display for the platforms memory usage limit.")]
        private Color memoryLimitColor = new Color(150 / 256.0f, 150 / 256.0f, 150 / 256.0f, 1.0f);

        /// <summary> The window. </summary>
        private GameObject window;
        /// <summary> The CPU frame rate text. </summary>
        private TextMesh cpuFrameRateText;

#if USING_XR_SDK && !UNITY_EDITOR
        /// <summary> The Dropped frame count in last one second. </summary>
        private TextMesh droppedFrameCount;
        private static readonly string droppedFrameCountString = "DroppedFrameCount: {0}";
#endif
        /// <summary> The GPU frame rate text. </summary>
        private TextMesh gpuFrameRateText;
        /// <summary> The used memory text. </summary>
        private TextMesh usedMemoryText;
        /// <summary> The peak memory text. </summary>
        private TextMesh peakMemoryText;
        /// <summary> The limit memory text. </summary>
        private TextMesh limitMemoryText;
        /// <summary> The used anchor. </summary>
        private Transform usedAnchor;
        /// <summary> The peak anchor. </summary>
        private Transform peakAnchor;
        /// <summary> The window horizontal rotation. </summary>
        private Quaternion windowHorizontalRotation;
        /// <summary> The window horizontal rotation inverse. </summary>
        private Quaternion windowHorizontalRotationInverse;
        /// <summary> The window vertical rotation. </summary>
        private Quaternion windowVerticalRotation;
        /// <summary> The window vertical rotation inverse. </summary>
        private Quaternion windowVerticalRotationInverse;

        /// <summary> The frame information matrices. </summary>
        private Matrix4x4[] frameInfoMatrices;
        /// <summary> List of colors of the frame informations. </summary>
        private Vector4[] frameInfoColors;
        /// <summary> The frame information property block. </summary>
        private MaterialPropertyBlock frameInfoPropertyBlock;
        /// <summary> Identifier for the color. </summary>
        private int colorID;
        /// <summary> Identifier for the parent matrix. </summary>
        private int parentMatrixID;
        /// <summary> Number of frames. </summary>
        private int frameCount;
        /// <summary> The stopwatch. </summary>
        private System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
        /// <summary> The frame timings. </summary>
        private FrameTiming[] frameTimings = new FrameTiming[maxFrameTimings];
        /// <summary> The CPU frame rate strings. </summary>
        private string[] cpuFrameRateStrings;
        /// <summary> The GPU frame rate strings. </summary>
        private string[] gpuFrameRateStrings;
        /// <summary> Buffer for string data. </summary>
        private char[] stringBuffer = new char[maxStringLength];

        /// <summary> The memory usage. </summary>
        private ulong memoryUsage;
        /// <summary> The peak memory usage. </summary>
        private ulong peakMemoryUsage;
        /// <summary> The limit memory usage. </summary>
        private ulong limitMemoryUsage;

        /// <summary> Rendering resources. </summary>
        [SerializeField, HideInInspector]
        private Material defaultMaterial;
        /// <summary> The default instanced material. </summary>
        [SerializeField, HideInInspector]
        private Material defaultInstancedMaterial;
        /// <summary> The background material. </summary>
        private Material backgroundMaterial;
        /// <summary> The foreground material. </summary>
        private Material foregroundMaterial;
        /// <summary> The text material. </summary>
        private Material textMaterial;
        /// <summary> The quad mesh. </summary>
        private Mesh quadMesh;

        private Transform m_CenterCamera;
        private Transform CenterCamera
        {
            get
            {
                if (m_CenterCamera == null)
                {
                    if (NRSessionManager.Instance.CenterCameraAnchor != null)
                    {
                        m_CenterCamera = NRSessionManager.Instance.CenterCameraAnchor;
                    }
                    else
                    {
                        m_CenterCamera = Camera.main.transform;
                    }
                }
                return m_CenterCamera;
            }
        }

        /// <summary> The instance. </summary>
        private static NRVisualProfiler m_Instance = null;
        /// <summary> Gets the instance. </summary>
        /// <value> The instance. </value>
        public static NRVisualProfiler Instance
        {
            get
            {
                if (m_Instance == null)
                {
                    GameObject go = new GameObject("VisualProfiler");
                    DontDestroyOnLoad(go);
                    m_Instance = go.AddComponent<NRVisualProfiler>();
                }
                return m_Instance;
            }
        }

        /// <summary> Awakes this object. </summary>
        void Awake()
        {
            if (m_Instance != null && m_Instance != this)
            {
                DestroyImmediate(this.gameObject);
                return;
            }

            m_Instance = this;
        }

        /// <summary> Switches. </summary>
        /// <param name="flag"> True to flag.</param>
        public void Switch(bool flag)
        {
            this.gameObject.SetActive(flag);
        }

        /// <summary> Resets this object. </summary>
        private void Reset()
        {
            if (defaultMaterial == null)
            {
                defaultMaterial = new Material(Shader.Find("Nreal/Instanced-Colored"));
                defaultMaterial.SetFloat("_ZWrite", 0.0f);
                defaultMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Disabled);
                defaultMaterial.renderQueue = 5000;
            }

            if (defaultInstancedMaterial == null)
            {
                Shader defaultInstancedShader = Shader.Find("Nreal/Instanced-Colored");

                if (defaultInstancedShader != null)
                {
                    defaultInstancedMaterial = new Material(defaultInstancedShader);
                    defaultInstancedMaterial.enableInstancing = true;
                    defaultInstancedMaterial.SetFloat("_ZWrite", 0.0f);
                    defaultInstancedMaterial.SetFloat("_ZTest", (float)UnityEngine.Rendering.CompareFunction.Disabled);
                    defaultInstancedMaterial.renderQueue = 5000;
                }
                else
                {
                    Debug.LogWarning("A shader supporting instancing could not be found for the VisualProfiler, falling back to traditional rendering. This may impact performance.");
                }
            }

            if (Application.isPlaying)
            {
                backgroundMaterial = new Material(defaultMaterial);
                foregroundMaterial = new Material(defaultMaterial);
                defaultMaterial.renderQueue = foregroundMaterial.renderQueue - 1;
                backgroundMaterial.renderQueue = defaultMaterial.renderQueue - 1;

                MeshRenderer meshRenderer = new GameObject().AddComponent<TextMesh>().GetComponent<MeshRenderer>();
                textMaterial = new Material(meshRenderer.sharedMaterial);
                textMaterial.renderQueue = defaultMaterial.renderQueue;
                Destroy(meshRenderer.gameObject);

                MeshFilter quadMeshFilter = GameObject.CreatePrimitive(PrimitiveType.Quad).GetComponent<MeshFilter>();

                if (defaultInstancedMaterial != null)
                {
                    // Create a quad mesh with artificially large bounds to disable culling for instanced rendering.
                    quadMesh = quadMeshFilter.mesh;
                    quadMesh.bounds = new Bounds(Vector3.zero, Vector3.one * float.MaxValue);
                }
                else
                {
                    quadMesh = quadMeshFilter.sharedMesh;
                }

                Destroy(quadMeshFilter.gameObject);
            }

            stopwatch.Reset();
            stopwatch.Start();
        }

        /// <summary> Starts this object. </summary>
        private void Start()
        {
            Reset();
            BuildWindow();
            BuildFrameRateStrings();
        }

        /// <summary> Executes the 'destroy' action. </summary>
        private void OnDestroy()
        {
            Destroy(window);
        }

        /// <summary> Late update. </summary>
        private void LateUpdate()
        {
            if (window == null)
            {
                return;
            }

            // Update window transformation.
            if (window.activeSelf && CenterCamera != null)
            {
                float t = Time.deltaTime * windowFollowSpeed;
                window.transform.position = Vector3.Lerp(window.transform.position, CalculateWindowPosition(CenterCamera.transform), t);
                window.transform.rotation = Quaternion.Slerp(window.transform.rotation, CalculateWindowRotation(CenterCamera.transform), t);
                window.transform.localScale = defaultWindowScale * windowScale;
            }

            // Capture frame timings every frame and read from it depending on the frameSampleRate.
            FrameTimingManager.CaptureFrameTimings();

            ++frameCount;
            float elapsedSeconds = stopwatch.ElapsedMilliseconds * 0.001f;

            if (elapsedSeconds >= frameSampleRate)
            {
                int cpuFrameRate = (int)(1.0f / (elapsedSeconds / frameCount));
                int gpuFrameRate = 0;

                // Many platforms do not yet support the FrameTimingManager. When timing data is returned from the FrameTimingManager we will use
                // its timing data, else we will depend on the stopwatch.
                uint frameTimingsCount = FrameTimingManager.GetLatestTimings((uint)Mathf.Min(frameCount, maxFrameTimings), frameTimings);

                if (frameTimingsCount != 0)
                {
                    float cpuFrameTime, gpuFrameTime;
                    AverageFrameTiming(frameTimings, frameTimingsCount, out cpuFrameTime, out gpuFrameTime);
                    cpuFrameRate = (int)(1.0f / (cpuFrameTime / frameCount));
                    gpuFrameRate = (int)(1.0f / (gpuFrameTime / frameCount));
                }

                // Update frame rate text.
                cpuFrameRateText.text = cpuFrameRateStrings[Mathf.Clamp(cpuFrameRate, 0, maxTargetFrameRate)];
#if USING_XR_SDK && !UNITY_EDITOR
                int dropped_framecount = 0;
                NRSessionManager.Instance.XRDisplaySubsystem?.TryGetDroppedFrameCount(out dropped_framecount);
                droppedFrameCount.text = string.Format(droppedFrameCountString, dropped_framecount);
#endif

                if (gpuFrameRate != 0)
                {
                    gpuFrameRateText.gameObject.SetActive(true);
                    gpuFrameRateText.text = gpuFrameRateStrings[Mathf.Clamp(gpuFrameRate, 0, maxTargetFrameRate)];
                }

                // Update frame colors.
                for (int i = frameRange - 1; i > 0; --i)
                {
                    frameInfoColors[i] = frameInfoColors[i - 1];
                }

                // Ideally we would query a device specific API (like the HolographicFramePresentationReport) to detect missed frames.
                // But, many of these APIs are inaccessible in Unity. Currently missed frames are assumed when the average cpuFrameRate 
                // is under the target frame rate.
                frameInfoColors[0] = (cpuFrameRate < ((int)(AppFrameRate) - 1)) ? missedFrameRateColor : targetFrameRateColor;
                frameInfoPropertyBlock.SetVectorArray(colorID, frameInfoColors);

                // Reset timers.
                frameCount = 0;
                stopwatch.Reset();
                stopwatch.Start();
            }

            // Draw frame info.
            if (window.activeSelf)
            {
                Matrix4x4 parentLocalToWorldMatrix = window.transform.localToWorldMatrix;

                //if (defaultInstancedMaterial != null)
                //{
                //    frameInfoPropertyBlock.SetMatrix(parentMatrixID, parentLocalToWorldMatrix);
                //    Graphics.DrawMeshInstanced(quadMesh, 0, defaultInstancedMaterial, frameInfoMatrices, frameInfoMatrices.Length, frameInfoPropertyBlock, UnityEngine.Rendering.ShadowCastingMode.Off, false);
                //}
                //else
                //{
                // If a instanced material is not available, fall back to non-instanced rendering.
                for (int i = 0; i < frameInfoMatrices.Length; ++i)
                {
                    frameInfoPropertyBlock.SetColor(colorID, frameInfoColors[i]);
                    Graphics.DrawMesh(quadMesh, parentLocalToWorldMatrix * frameInfoMatrices[i], defaultMaterial, 0, null, 0, frameInfoPropertyBlock, false, false, false);
                }
                //}
            }

            // Update memory statistics.
            ulong limit = AppMemoryUsageLimit;

            if (limit != limitMemoryUsage)
            {
                if (window.activeSelf && WillDisplayedMemoryUsageDiffer(limitMemoryUsage, limit, displayedDecimalDigits))
                {
                    MemoryUsageToString(stringBuffer, displayedDecimalDigits, limitMemoryText, limitMemoryString, limit);
                }

                limitMemoryUsage = limit;
            }

            ulong usage = AppMemoryUsage;

            if (usage != memoryUsage)
            {
                usedAnchor.localScale = new Vector3((float)usage / limitMemoryUsage, usedAnchor.localScale.y, usedAnchor.localScale.z);

                if (window.activeSelf && WillDisplayedMemoryUsageDiffer(memoryUsage, usage, displayedDecimalDigits))
                {
                    MemoryUsageToString(stringBuffer, displayedDecimalDigits, usedMemoryText, usedMemoryString, usage);
                }

                memoryUsage = usage;
            }

            if (memoryUsage > peakMemoryUsage)
            {
                peakAnchor.localScale = new Vector3((float)memoryUsage / limitMemoryUsage, peakAnchor.localScale.y, peakAnchor.localScale.z);

                if (window.activeSelf && WillDisplayedMemoryUsageDiffer(peakMemoryUsage, memoryUsage, displayedDecimalDigits))
                {
                    MemoryUsageToString(stringBuffer, displayedDecimalDigits, peakMemoryText, peakMemoryString, memoryUsage);
                }

                peakMemoryUsage = memoryUsage;
            }

            window.SetActive(isVisible);
        }

        /// <summary> Calculates the window position. </summary>
        /// <param name="cameraTransform"> The camera transform.</param>
        /// <returns> The calculated window position. </returns>
        private Vector3 CalculateWindowPosition(Transform cameraTransform)
        {
            float windowDistance = Mathf.Max(16.0f / Camera.main.fieldOfView, Camera.main.nearClipPlane + 0.25f);
            Vector3 position = cameraTransform.position + (cameraTransform.forward * windowDistance);
            Vector3 horizontalOffset = cameraTransform.right * windowOffset.x;
            Vector3 verticalOffset = cameraTransform.up * windowOffset.y;

            switch (windowAnchor)
            {
                case TextAnchor.UpperLeft: position += verticalOffset - horizontalOffset; break;
                case TextAnchor.UpperCenter: position += verticalOffset; break;
                case TextAnchor.UpperRight: position += verticalOffset + horizontalOffset; break;
                case TextAnchor.MiddleLeft: position -= horizontalOffset; break;
                case TextAnchor.MiddleRight: position += horizontalOffset; break;
                case TextAnchor.LowerLeft: position -= verticalOffset + horizontalOffset; break;
                case TextAnchor.LowerCenter: position -= verticalOffset; break;
                case TextAnchor.LowerRight: position -= verticalOffset - horizontalOffset; break;
            }

            return position;
        }

        /// <summary> Calculates the window rotation. </summary>
        /// <param name="cameraTransform"> The camera transform.</param>
        /// <returns> The calculated window rotation. </returns>
        private Quaternion CalculateWindowRotation(Transform cameraTransform)
        {
            Quaternion rotation = cameraTransform.rotation;

            switch (windowAnchor)
            {
                case TextAnchor.UpperLeft: rotation *= windowHorizontalRotationInverse * windowVerticalRotationInverse; break;
                case TextAnchor.UpperCenter: rotation *= windowHorizontalRotationInverse; break;
                case TextAnchor.UpperRight: rotation *= windowHorizontalRotationInverse * windowVerticalRotation; break;
                case TextAnchor.MiddleLeft: rotation *= windowVerticalRotationInverse; break;
                case TextAnchor.MiddleRight: rotation *= windowVerticalRotation; break;
                case TextAnchor.LowerLeft: rotation *= windowHorizontalRotation * windowVerticalRotationInverse; break;
                case TextAnchor.LowerCenter: rotation *= windowHorizontalRotation; break;
                case TextAnchor.LowerRight: rotation *= windowHorizontalRotation * windowVerticalRotation; break;
            }

            return rotation;
        }

        /// <summary> Builds the window. </summary>
        private void BuildWindow()
        {
            // Initialize property block state.
            colorID = Shader.PropertyToID("_Color");
            parentMatrixID = Shader.PropertyToID("_ParentLocalToWorldMatrix");
            WindowParent = transform;
            // Build the window root.
            {
                window = CreateQuad("VisualProfiler", null);
                window.transform.parent = WindowParent;
                InitializeRenderer(window, backgroundMaterial, colorID, baseColor);
                window.transform.localScale = defaultWindowScale;
                windowHorizontalRotation = Quaternion.AngleAxis(defaultWindowRotation.y, Vector3.right);
                windowHorizontalRotationInverse = Quaternion.Inverse(windowHorizontalRotation);
                windowVerticalRotation = Quaternion.AngleAxis(defaultWindowRotation.x, Vector3.up);
                windowVerticalRotationInverse = Quaternion.Inverse(windowVerticalRotation);
            }

            // Add frame rate text and frame indicators.
            {
                cpuFrameRateText = CreateText("CPUFrameRateText", new Vector3(-0.495f, 0.5f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, Color.white, string.Empty);
#if USING_XR_SDK && !UNITY_EDITOR
                droppedFrameCount = CreateText("DroppedFrameCount", new Vector3(0, 0.5f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, Color.white, string.Empty);
#endif
                gpuFrameRateText = CreateText("GPUFrameRateText", new Vector3(0.495f, 0.5f, 0.0f), window.transform, TextAnchor.UpperRight, textMaterial, Color.white, string.Empty);
                gpuFrameRateText.gameObject.SetActive(false);

                frameInfoMatrices = new Matrix4x4[frameRange];
                frameInfoColors = new Vector4[frameRange];
                Vector3 scale = new Vector3(1.0f / frameRange, 0.2f, 1.0f);
                Vector3 position = new Vector3(0.5f - (scale.x * 0.5f), 0.15f, 0.0f);

                for (int i = 0; i < frameRange; ++i)
                {
                    frameInfoMatrices[i] = Matrix4x4.TRS(position, Quaternion.identity, new Vector3(scale.x * 0.8f, scale.y, scale.z));
                    position.x -= scale.x;
                    frameInfoColors[i] = targetFrameRateColor;
                }

                frameInfoPropertyBlock = new MaterialPropertyBlock();
                frameInfoPropertyBlock.SetVectorArray(colorID, frameInfoColors);
            }

            // Add memory usage text and bars.
            {
                usedMemoryText = CreateText("UsedMemoryText", new Vector3(-0.495f, 0.0f, 0.0f), window.transform, TextAnchor.UpperLeft, textMaterial, memoryUsedColor, usedMemoryString);
                peakMemoryText = CreateText("PeakMemoryText", new Vector3(0.0f, 0.0f, 0.0f), window.transform, TextAnchor.UpperCenter, textMaterial, memoryPeakColor, peakMemoryString);
                limitMemoryText = CreateText("LimitMemoryText", new Vector3(0.495f, 0.0f, 0.0f), window.transform, TextAnchor.UpperRight, textMaterial, Color.white, limitMemoryString);

                GameObject limitBar = CreateQuad("LimitBar", window.transform);
                InitializeRenderer(limitBar, defaultMaterial, colorID, memoryLimitColor);
                limitBar.transform.localScale = new Vector3(0.99f, 0.2f, 1.0f);
                limitBar.transform.localPosition = new Vector3(0.0f, -0.37f, 0.0f);

                {
                    usedAnchor = CreateAnchor("UsedAnchor", limitBar.transform);
                    GameObject bar = CreateQuad("UsedBar", usedAnchor);
                    Material material = new Material(foregroundMaterial);
                    material.renderQueue = material.renderQueue + 1;
                    InitializeRenderer(bar, material, colorID, memoryUsedColor);
                    bar.transform.localScale = Vector3.one;
                    bar.transform.localPosition = new Vector3(0.5f, 0.0f, 0.0f);
                }
                {
                    peakAnchor = CreateAnchor("PeakAnchor", limitBar.transform);
                    GameObject bar = CreateQuad("PeakBar", peakAnchor);
                    InitializeRenderer(bar, foregroundMaterial, colorID, memoryPeakColor);
                    bar.transform.localScale = Vector3.one;
                    bar.transform.localPosition = new Vector3(0.5f, 0.0f, 0.0f);
                }
            }

            window.SetActive(isVisible);
        }

        /// <summary> Builds frame rate strings. </summary>
        private void BuildFrameRateStrings()
        {
            cpuFrameRateStrings = new string[maxTargetFrameRate + 1];
            gpuFrameRateStrings = new string[maxTargetFrameRate + 1];
            string displayedDecimalFormat = string.Format("{{0:F{0}}}", displayedDecimalDigits);

            StringBuilder stringBuilder = new StringBuilder(32);
            StringBuilder milisecondStringBuilder = new StringBuilder(16);

            for (int i = 0; i < cpuFrameRateStrings.Length; ++i)
            {
                float miliseconds = (i == 0) ? 0.0f : (1.0f / i) * 1000.0f;
                milisecondStringBuilder.AppendFormat(displayedDecimalFormat, miliseconds);
                stringBuilder.AppendFormat("CPU: {0} fps ({1} ms)", i.ToString(), milisecondStringBuilder.ToString());
                cpuFrameRateStrings[i] = stringBuilder.ToString();
                stringBuilder.Length = 0;
                stringBuilder.AppendFormat("GPU: {0} fps ({1} ms)", i.ToString(), milisecondStringBuilder.ToString());
                gpuFrameRateStrings[i] = stringBuilder.ToString();
                milisecondStringBuilder.Length = 0;
                stringBuilder.Length = 0;
            }
        }

        /// <summary> Creates an anchor. </summary>
        /// <param name="name">   The name.</param>
        /// <param name="parent"> The parent.</param>
        /// <returns> The new anchor. </returns>
        private static Transform CreateAnchor(string name, Transform parent)
        {
            Transform anchor = new GameObject(name).transform;
            anchor.parent = parent;
            anchor.localScale = Vector3.one;
            anchor.localPosition = new Vector3(-0.5f, 0.0f, 0.0f);

            return anchor;
        }

        /// <summary> Creates a quad. </summary>
        /// <param name="name">   The name.</param>
        /// <param name="parent"> The parent.</param>
        /// <returns> The new quad. </returns>
        private static GameObject CreateQuad(string name, Transform parent)
        {
            GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
            Destroy(quad.GetComponent<Collider>());
            quad.name = name;
            quad.transform.parent = parent;

            return quad;
        }

        /// <summary> Creates a text. </summary>
        /// <param name="name">     The name.</param>
        /// <param name="position"> The position.</param>
        /// <param name="parent">   The parent.</param>
        /// <param name="anchor">   The anchor.</param>
        /// <param name="material"> The material.</param>
        /// <param name="color">    The color.</param>
        /// <param name="text">     The text.</param>
        /// <returns> The new text. </returns>
        private static TextMesh CreateText(string name, Vector3 position, Transform parent, TextAnchor anchor, Material material, Color color, string text)
        {
            GameObject obj = new GameObject(name);
            obj.transform.localScale = Vector3.one * 0.0016f;
            obj.transform.parent = parent;
            obj.transform.localPosition = position;
            TextMesh textMesh = obj.AddComponent<TextMesh>();
            textMesh.fontSize = 48;
            textMesh.anchor = anchor;
            textMesh.color = color;
            textMesh.text = text;
            textMesh.richText = false;

            Renderer renderer = obj.GetComponent<Renderer>();
            renderer.sharedMaterial = material;

            OptimizeRenderer(renderer);

            return textMesh;
        }

        /// <summary> Initializes the renderer. </summary>
        /// <param name="obj">      The object.</param>
        /// <param name="material"> The material.</param>
        /// <param name="colorID">  Identifier for the color.</param>
        /// <param name="color">    The color.</param>
        /// <returns> A Renderer. </returns>
        private static Renderer InitializeRenderer(GameObject obj, Material material, int colorID, Color color)
        {
            Renderer renderer = obj.GetComponent<Renderer>();
            renderer.sharedMaterial = material;

            MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock();
            renderer.GetPropertyBlock(propertyBlock);
            propertyBlock.SetColor(colorID, color);
            renderer.SetPropertyBlock(propertyBlock);

            OptimizeRenderer(renderer);

            return renderer;
        }

        /// <summary> Optimize renderer. </summary>
        /// <param name="renderer"> The renderer.</param>
        private static void OptimizeRenderer(Renderer renderer)
        {
            renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
            renderer.receiveShadows = false;
            renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
            renderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
            renderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
            renderer.allowOcclusionWhenDynamic = false;
        }

        /// <summary> Memory usage to string. </summary>
        /// <param name="stringBuffer">           Buffer for string data.</param>
        /// <param name="displayedDecimalDigits"> The displayed decimal digits.</param>
        /// <param name="textMesh">               The text mesh.</param>
        /// <param name="prefixString">           The prefix string.</param>
        /// <param name="memoryUsage">            The memory usage.</param>
        private static void MemoryUsageToString(char[] stringBuffer, int displayedDecimalDigits, TextMesh textMesh, string prefixString, ulong memoryUsage)
        {
            // Using a custom number to string method to avoid the overhead, and allocations, of built in string.Format/StringBuilder methods.
            // We can also make some assumptions since the domain of the input number (memoryUsage) is known.
            float memoryUsageMB = ConvertBytesToMegabytes(memoryUsage);
            int memoryUsageIntegerDigits = (int)memoryUsageMB;
            int memoryUsageFractionalDigits = (int)((memoryUsageMB - memoryUsageIntegerDigits) * Mathf.Pow(10.0f, displayedDecimalDigits));
            int bufferIndex = 0;

            for (int i = 0; i < prefixString.Length; ++i)
            {
                stringBuffer[bufferIndex++] = prefixString[i];
            }

            bufferIndex = MemoryItoA(memoryUsageIntegerDigits, stringBuffer, bufferIndex);
            stringBuffer[bufferIndex++] = '.';

            if (memoryUsageFractionalDigits != 0)
            {
                bufferIndex = MemoryItoA(memoryUsageFractionalDigits, stringBuffer, bufferIndex);
            }
            else
            {
                for (int i = 0; i < displayedDecimalDigits; ++i)
                {
                    stringBuffer[bufferIndex++] = '0';
                }
            }

            stringBuffer[bufferIndex++] = 'M';
            stringBuffer[bufferIndex++] = 'B';
            textMesh.text = new string(stringBuffer, 0, bufferIndex);
        }

        /// <summary> Memory ito a. </summary>
        /// <param name="value">        The value.</param>
        /// <param name="stringBuffer"> Buffer for string data.</param>
        /// <param name="bufferIndex">  Zero-based index of the buffer.</param>
        /// <returns> An int. </returns>
        private static int MemoryItoA(int value, char[] stringBuffer, int bufferIndex)
        {
            int startIndex = bufferIndex;

            for (; value != 0; value /= 10)
            {
                stringBuffer[bufferIndex++] = (char)((char)(value % 10) + '0');
            }

            char temp;
            for (int endIndex = bufferIndex - 1; startIndex < endIndex; ++startIndex, --endIndex)
            {
                temp = stringBuffer[startIndex];
                stringBuffer[startIndex] = stringBuffer[endIndex];
                stringBuffer[endIndex] = temp;
            }

            return bufferIndex;
        }

        /// <summary> Gets the application frame rate. </summary>
        /// <value> The application frame rate. </value>
        private static float AppFrameRate
        {
            get
            {
                // If the current XR SDK does not report refresh rate information, assume 60Hz.
                //float refreshRate = UnityEngine.XR.XRDevice.refreshRate;
                //return ((int)refreshRate == 0) ? 60.0f : refreshRate;
                return 45f;
            }
        }

        /// <summary> Average frame timing. </summary>
        /// <param name="frameTimings">      The frame timings.</param>
        /// <param name="frameTimingsCount"> Number of frame timings.</param>
        /// <param name="cpuFrameTime">      [out] The CPU frame time.</param>
        /// <param name="gpuFrameTime">      [out] The GPU frame time.</param>
        private static void AverageFrameTiming(FrameTiming[] frameTimings, uint frameTimingsCount, out float cpuFrameTime, out float gpuFrameTime)
        {
            double cpuTime = 0.0f;
            double gpuTime = 0.0f;

            for (int i = 0; i < frameTimingsCount; ++i)
            {
                cpuTime += frameTimings[i].cpuFrameTime;
                gpuTime += frameTimings[i].gpuFrameTime;
            }

            cpuTime /= frameTimingsCount;
            gpuTime /= frameTimingsCount;

            cpuFrameTime = (float)(cpuTime * 0.001);
            gpuFrameTime = (float)(gpuTime * 0.001);
        }

        /// <summary> Gets the application memory usage. </summary>
        /// <value> The application memory usage. </value>
        private static ulong AppMemoryUsage
        {
            get
            {
#if WINDOWS_UWP
                return MemoryManager.AppMemoryUsage;
#else
                return (ulong)Profiler.GetTotalAllocatedMemoryLong();
#endif
            }
        }

        /// <summary> Gets the application memory usage limit. </summary>
        /// <value> The application memory usage limit. </value>
        private static ulong AppMemoryUsageLimit
        {
            get
            {
#if WINDOWS_UWP
                return MemoryManager.AppMemoryUsageLimit;
#else
                return ConvertMegabytesToBytes(SystemInfo.systemMemorySize);
#endif
            }
        }

        /// <summary> Will displayed memory usage differ. </summary>
        /// <param name="oldUsage">               The old usage.</param>
        /// <param name="newUsage">               The new usage.</param>
        /// <param name="displayedDecimalDigits"> The displayed decimal digits.</param>
        /// <returns> True if it succeeds, false if it fails. </returns>
        private static bool WillDisplayedMemoryUsageDiffer(ulong oldUsage, ulong newUsage, int displayedDecimalDigits)
        {
            float oldUsageMBs = ConvertBytesToMegabytes(oldUsage);
            float newUsageMBs = ConvertBytesToMegabytes(newUsage);
            float decimalPower = Mathf.Pow(10.0f, displayedDecimalDigits);

            return (int)(oldUsageMBs * decimalPower) != (int)(newUsageMBs * decimalPower);
        }

        /// <summary> Convert megabytes to bytes. </summary>
        /// <param name="megabytes"> The megabytes.</param>
        /// <returns> The megabytes converted to bytes. </returns>
        private static ulong ConvertMegabytesToBytes(int megabytes)
        {
            return ((ulong)megabytes * 1024UL) * 1024UL;
        }

        /// <summary> Convert bytes to megabytes. </summary>
        /// <param name="bytes"> The bytes.</param>
        /// <returns> The bytes converted to megabytes. </returns>
        private static float ConvertBytesToMegabytes(ulong bytes)
        {
            return (bytes / 1024.0f) / 1024.0f;
        }
    }
}