//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// Purpose: Helper for smoothing over transitions between levels.
//
//=============================================================================

using UnityEngine;
using System.Collections;
using Valve.VR;
using System.IO;

namespace Valve.VR
{
    public class SteamVR_LoadLevel : MonoBehaviour
    {
        private static SteamVR_LoadLevel _active = null;
        public static bool loading { get { return _active != null; } }
        public static float progress
        {
            get { return (_active != null && _active.async != null) ? _active.async.progress : 0.0f; }
        }
        public static Texture progressTexture
        {
            get { return (_active != null) ? _active.renderTexture : null; }
        }

        // Name of level to load.
        public string levelName;

        // Name of internal process to launch (instead of levelName).
        public string internalProcessPath;

        // The command-line args for the internal process to launch.
        public string internalProcessArgs;

        // If true, call LoadLevelAdditiveAsync instead of LoadLevelAsync.
        public bool loadAdditive;

        // Async load causes crashes in some apps.
        public bool loadAsync = true;

        // Optional logo texture.
        public Texture loadingScreen;

        // Optional progress bar textures.
        public Texture progressBarEmpty, progressBarFull;

        // Sizes of overlays.
        public float loadingScreenWidthInMeters = 6.0f;
        public float progressBarWidthInMeters = 3.0f;

        // If specified, the loading screen will be positioned in the player's view this far away.
        public float loadingScreenDistance = 0.0f;

        // Optional overrides for where to display loading screen and progress bar overlays.
        // Otherwise defaults to using this object's transform.
        public Transform loadingScreenTransform, progressBarTransform;

        // Optional skybox override textures.
        public Texture front, back, left, right, top, bottom;

        // Colors to use when dropping to the compositor between levels if no skybox is set.
        public Color backgroundColor = Color.black;

        // If false, the background color above gets applied as the foreground color in the compositor.
        // This does not have any effect when using a skybox instead.
        public bool showGrid = false;

        // Time to fade from current scene to the compositor and back.
        public float fadeOutTime = 0.5f;
        public float fadeInTime = 0.5f;

        // Additional time to wait after finished loading before we start fading the new scene back in.
        // This is to cover up any initial hitching that takes place right at the start of levels.
        // Most scenes should hopefully not require this.
        public float postLoadSettleTime = 0.0f;

        // Time to fade loading screen in and out (also used for progress bar).
        public float loadingScreenFadeInTime = 1.0f;
        public float loadingScreenFadeOutTime = 0.25f;

        float fadeRate = 1.0f;
        float alpha = 0.0f;

        AsyncOperation async; // used to track level load progress
        RenderTexture renderTexture; // used to render progress bar

        ulong loadingScreenOverlayHandle = OpenVR.k_ulOverlayHandleInvalid;
        ulong progressBarOverlayHandle = OpenVR.k_ulOverlayHandleInvalid;

        public bool autoTriggerOnEnable = false;

        void OnEnable()
        {
            if (autoTriggerOnEnable)
                Trigger();
        }

        public void Trigger()
        {
            if (!loading && !string.IsNullOrEmpty(levelName))
                StartCoroutine(LoadLevel());
        }

        // Helper function to quickly and simply load a level from script.
        public static void Begin(string levelName,
            bool showGrid = false, float fadeOutTime = 0.5f,
            float r = 0.0f, float g = 0.0f, float b = 0.0f, float a = 1.0f)
        {
            var loader = new GameObject("loader").AddComponent<SteamVR_LoadLevel>();
            loader.levelName = levelName;
            loader.showGrid = showGrid;
            loader.fadeOutTime = fadeOutTime;
            loader.backgroundColor = new Color(r, g, b, a);
            loader.Trigger();
        }

        // Updates progress bar.
        void OnGUI()
        {
            if (_active != this)
                return;

            // Optionally create an overlay for our progress bar to use, separate from the loading screen.
            if (progressBarEmpty != null && progressBarFull != null)
            {
                if (progressBarOverlayHandle == OpenVR.k_ulOverlayHandleInvalid)
                    progressBarOverlayHandle = GetOverlayHandle("progressBar", progressBarTransform != null ? progressBarTransform : transform, progressBarWidthInMeters);

                if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                {
                    var progress = (async != null) ? async.progress : 0.0f;

                    // Use the full bar size for everything.
                    var w = progressBarFull.width;
                    var h = progressBarFull.height;

                    // Create a separate render texture so we can composite the full image on top of the empty one.
                    if (renderTexture == null)
                    {
                        renderTexture = new RenderTexture(w, h, 0);
                        renderTexture.Create();
                    }

                    var prevActive = RenderTexture.active;
                    RenderTexture.active = renderTexture;

                    if (Event.current.type == EventType.Repaint)
                        GL.Clear(false, true, Color.clear);

                    GUILayout.BeginArea(new Rect(0, 0, w, h));

                    GUI.DrawTexture(new Rect(0, 0, w, h), progressBarEmpty);

                    // Reveal the full bar texture based on progress.
                    GUI.DrawTextureWithTexCoords(new Rect(0, 0, progress * w, h), progressBarFull, new Rect(0.0f, 0.0f, progress, 1.0f));

                    GUILayout.EndArea();

                    RenderTexture.active = prevActive;

                    // Texture needs to be set every frame after it is updated since SteamVR makes a copy internally to a shared texture.
                    var overlay = OpenVR.Overlay;
                    if (overlay != null)
                    {
                        var texture = new Texture_t();
                        texture.handle = renderTexture.GetNativeTexturePtr();
                        texture.eType = SteamVR.instance.textureType;
                        texture.eColorSpace = EColorSpace.Auto;
                        overlay.SetOverlayTexture(progressBarOverlayHandle, ref texture);
                    }
                }
            }

#if false
		// Draw loading screen and progress bar to 2d companion window as well.
		if (loadingScreen != null)
		{
			var screenAspect = (float)Screen.width / Screen.height;
			var textureAspect = (float)loadingScreen.width / loadingScreen.height;

			float w, h;
			if (screenAspect < textureAspect)
			{
				// Clamp horizontally
				w = Screen.width * 0.9f;
				h = w / textureAspect;
			}
			else
			{
				// Clamp vertically
				h = Screen.height * 0.9f;
				w = h * textureAspect;
			}

			GUILayout.BeginArea(new Rect(0, 0, Screen.width, Screen.height));

			var x = Screen.width / 2 - w / 2;
			var y = Screen.height / 2 - h / 2;
			GUI.DrawTexture(new Rect(x, y, w, h), loadingScreen);

			GUILayout.EndArea();
		}

		if (renderTexture != null)
		{
			var x = Screen.width / 2 - renderTexture.width / 2;
			var y = Screen.height * 0.9f - renderTexture.height;
			GUI.DrawTexture(new Rect(x, y, renderTexture.width, renderTexture.height), renderTexture);
		}
#endif
        }

        // Fade our overlays in/out over time.
        void Update()
        {
            if (_active != this)
                return;

            alpha = Mathf.Clamp01(alpha + fadeRate * Time.deltaTime);

            var overlay = OpenVR.Overlay;
            if (overlay != null)
            {
                if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                    overlay.SetOverlayAlpha(loadingScreenOverlayHandle, alpha);

                if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                    overlay.SetOverlayAlpha(progressBarOverlayHandle, alpha);
            }
        }

        // Corourtine to handle all the steps across loading boundaries.
        IEnumerator LoadLevel()
        {
            // Optionally rotate loading screen transform around the camera into view.
            // We assume here that the loading screen is already facing toward the origin,
            // and that the progress bar transform (if any) is a child and will follow along.
            if (loadingScreen != null && loadingScreenDistance > 0.0f)
            {
                Transform hmd = this.transform;
                if (Camera.main != null)
                    hmd = Camera.main.transform;

                Quaternion rot = Quaternion.Euler(0.0f, hmd.eulerAngles.y, 0.0f);
                Vector3 pos = hmd.position + (rot * new Vector3(0.0f, 0.0f, loadingScreenDistance));

                var t = loadingScreenTransform != null ? loadingScreenTransform : transform;
                t.position = pos;
                t.rotation = rot;
            }

            _active = this;

            SteamVR_Events.Loading.Send(true);

            // Calculate rate for fading in loading screen and progress bar.
            if (loadingScreenFadeInTime > 0.0f)
            {
                fadeRate = 1.0f / loadingScreenFadeInTime;
            }
            else
            {
                alpha = 1.0f;
            }

            var overlay = OpenVR.Overlay;

            // Optionally create our loading screen overlay.
            if (loadingScreen != null && overlay != null)
            {
                loadingScreenOverlayHandle = GetOverlayHandle("loadingScreen", loadingScreenTransform != null ? loadingScreenTransform : transform, loadingScreenWidthInMeters);
                if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                {
                    var texture = new Texture_t();
                    texture.handle = loadingScreen.GetNativeTexturePtr();
                    texture.eType = SteamVR.instance.textureType;
                    texture.eColorSpace = EColorSpace.Auto;
                    overlay.SetOverlayTexture(loadingScreenOverlayHandle, ref texture);
                }
            }

            bool fadedForeground = false;

            // Fade out to compositor
            SteamVR_Events.LoadingFadeOut.Send(fadeOutTime);

            // Optionally set a skybox to use as a backdrop in the compositor.
            var compositor = OpenVR.Compositor;
            if (compositor != null)
            {
                if (front != null)
                {
                    SteamVR_Skybox.SetOverride(front, back, left, right, top, bottom);

                    // Explicitly fade to the compositor since loading will cause us to stop rendering.
                    compositor.FadeGrid(fadeOutTime, true);
                    yield return new WaitForSeconds(fadeOutTime);
                }
                else if (backgroundColor != Color.clear)
                {
                    // Otherwise, use the specified background color.
                    if (showGrid)
                    {
                        // Set compositor background color immediately, and start fading to it.
                        compositor.FadeToColor(0.0f, backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, true);
                        compositor.FadeGrid(fadeOutTime, true);
                        yield return new WaitForSeconds(fadeOutTime);
                    }
                    else
                    {
                        // Fade the foreground color in (which will blend on top of the scene), and then cut to the compositor.
                        compositor.FadeToColor(fadeOutTime, backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, false);
                        yield return new WaitForSeconds(fadeOutTime + 0.1f);
                        compositor.FadeGrid(0.0f, true);
                        fadedForeground = true;
                    }
                }
            }

            // Now that we're fully faded out, we can stop submitting frames to the compositor.
            SteamVR_Render.pauseRendering = true;

            // Continue waiting for the overlays to fully fade in before continuing.
            while (alpha < 1.0f)
                yield return null;

            // Keep us from getting destroyed when loading the new level, otherwise this coroutine will get stopped prematurely.
            transform.parent = null;
            DontDestroyOnLoad(gameObject);

            if (!string.IsNullOrEmpty(internalProcessPath))
            {
                Debug.Log("<b>[SteamVR]</b> Launching external application...");
                var applications = OpenVR.Applications;
                if (applications == null)
                {
                    Debug.Log("<b>[SteamVR]</b> Failed to get OpenVR.Applications interface!");
                }
                else
                {
                    var workingDirectory = Directory.GetCurrentDirectory();
                    var fullPath = Path.Combine(workingDirectory, internalProcessPath);
                    Debug.Log("<b>[SteamVR]</b> LaunchingInternalProcess");
                    Debug.Log("<b>[SteamVR]</b> ExternalAppPath = " + internalProcessPath);
                    Debug.Log("<b>[SteamVR]</b> FullPath = " + fullPath);
                    Debug.Log("<b>[SteamVR]</b> ExternalAppArgs = " + internalProcessArgs);
                    Debug.Log("<b>[SteamVR]</b> WorkingDirectory = " + workingDirectory);
                    var error = applications.LaunchInternalProcess(fullPath, internalProcessArgs, workingDirectory);
                    Debug.Log("<b>[SteamVR]</b> LaunchInternalProcessError: " + error);
#if UNITY_EDITOR
                    UnityEditor.EditorApplication.isPlaying = false;
#elif !UNITY_METRO
				System.Diagnostics.Process.GetCurrentProcess().Kill();
#endif
                }
            }
            else
            {
                var mode = loadAdditive ? UnityEngine.SceneManagement.LoadSceneMode.Additive : UnityEngine.SceneManagement.LoadSceneMode.Single;
                if (loadAsync)
                {
                    Application.backgroundLoadingPriority = ThreadPriority.Low;
                    async = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(levelName, mode);

                    // Performing this in a while loop instead seems to help smooth things out.
                    //yield return async;
                    while (!async.isDone)
                    {
                        yield return null;
                    }
                }
                else
                {
                    UnityEngine.SceneManagement.SceneManager.LoadScene(levelName, mode);
                }
            }

            yield return null;

            System.GC.Collect();

            yield return null;

            Shader.WarmupAllShaders();

            // Optionally wait a short period of time after loading everything back in, but before we start rendering again
            // in order to give everything a change to settle down to avoid any hitching at the start of the new level.
            yield return new WaitForSeconds(postLoadSettleTime);

            SteamVR_Render.pauseRendering = false;

            // Fade out loading screen.
            if (loadingScreenFadeOutTime > 0.0f)
            {
                fadeRate = -1.0f / loadingScreenFadeOutTime;
            }
            else
            {
                alpha = 0.0f;
            }

            // Fade out to compositor
            SteamVR_Events.LoadingFadeIn.Send(fadeInTime);

            // Refresh compositor reference since loading scenes might have invalidated it.
            compositor = OpenVR.Compositor;
            if (compositor != null)
            {
                // Fade out foreground color if necessary.
                if (fadedForeground)
                {
                    compositor.FadeGrid(0.0f, false);
                    compositor.FadeToColor(fadeInTime, 0.0f, 0.0f, 0.0f, 0.0f, false);
                    yield return new WaitForSeconds(fadeInTime);
                }
                else
                {
                    // Fade scene back in, and reset skybox once no longer visible.
                    compositor.FadeGrid(fadeInTime, false);
                    yield return new WaitForSeconds(fadeInTime);

                    if (front != null)
                    {
                        SteamVR_Skybox.ClearOverride();
                    }
                }
            }

            // Finally, stick around long enough for our overlays to fully fade out.
            while (alpha > 0.0f)
                yield return null;

            if (overlay != null)
            {
                if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                    overlay.HideOverlay(progressBarOverlayHandle);
                if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
                    overlay.HideOverlay(loadingScreenOverlayHandle);
            }

            Destroy(gameObject);

            _active = null;

            SteamVR_Events.Loading.Send(false);
        }

        // Helper to create (or reuse if possible) each of our different overlay types.
        ulong GetOverlayHandle(string overlayName, Transform transform, float widthInMeters = 1.0f)
        {
            ulong handle = OpenVR.k_ulOverlayHandleInvalid;

            var overlay = OpenVR.Overlay;
            if (overlay == null)
                return handle;

            var key = SteamVR_Overlay.key + "." + overlayName;

            var error = overlay.FindOverlay(key, ref handle);
            if (error != EVROverlayError.None)
                error = overlay.CreateOverlay(key, overlayName, ref handle);
            if (error == EVROverlayError.None)
            {
                overlay.ShowOverlay(handle);
                overlay.SetOverlayAlpha(handle, alpha);
                overlay.SetOverlayWidthInMeters(handle, widthInMeters);

                // D3D textures are upside-down in Unity to match OpenGL.
                if (SteamVR.instance.textureType == ETextureType.DirectX)
                {
                    var textureBounds = new VRTextureBounds_t();
                    textureBounds.uMin = 0;
                    textureBounds.vMin = 1;
                    textureBounds.uMax = 1;
                    textureBounds.vMax = 0;
                    overlay.SetOverlayTextureBounds(handle, ref textureBounds);
                }

                // Convert from world space to tracking space using the top-most camera.
                var vrcam = (loadingScreenDistance == 0.0f) ? SteamVR_Render.Top() : null;
                if (vrcam != null && vrcam.origin != null)
                {
                    var offset = new SteamVR_Utils.RigidTransform(vrcam.origin, transform);
                    offset.pos.x /= vrcam.origin.localScale.x;
                    offset.pos.y /= vrcam.origin.localScale.y;
                    offset.pos.z /= vrcam.origin.localScale.z;

                    var t = offset.ToHmdMatrix34();
                    overlay.SetOverlayTransformAbsolute(handle, SteamVR.settings.trackingSpace, ref t);
                }
                else
                {
                    var t = new SteamVR_Utils.RigidTransform(transform).ToHmdMatrix34();
                    overlay.SetOverlayTransformAbsolute(handle, SteamVR.settings.trackingSpace, ref t);
                }
            }

            return handle;
        }
    }
}