//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// Purpose: Access to SteamVR system (hmd) and compositor (distort) interfaces.
//
//=============================================================================

using UnityEngine;
using Valve.VR;

public class SteamVR : System.IDisposable
{
	// Use this to check if SteamVR is currently active without attempting
	// to activate it in the process.
	public static bool active { get { return _instance != null; } }

	// Set this to false to keep from auto-initializing when calling SteamVR.instance.
	private static bool _enabled = true;
	public static bool enabled
	{
		get
		{
			if (!UnityEngine.XR.XRSettings.enabled)
				enabled = false;
			return _enabled;
		}
		set
		{
			_enabled = value;
			if (!_enabled)
				SafeDispose();
		}
	}

	private static SteamVR _instance;
	public static SteamVR instance
	{
		get
		{
#if UNITY_EDITOR
			if (!Application.isPlaying)
				return null;
#endif
			if (!enabled)
				return null;

			if (_instance == null)
			{
				_instance = CreateInstance();

				// If init failed, then auto-disable so scripts don't continue trying to re-initialize things.
				if (_instance == null)
					_enabled = false;
			}

			return _instance;
		}
	}

	public static bool usingNativeSupport
	{
		get { return UnityEngine.XR.XRDevice.GetNativePtr() != System.IntPtr.Zero; }
	}

	static SteamVR CreateInstance()
	{
		try
		{
			var error = EVRInitError.None;
			if (!SteamVR.usingNativeSupport)
			{
				Debug.Log("OpenVR initialization failed.  Ensure 'Virtual Reality Supported' is checked in Player Settings, and OpenVR is added to the list of Virtual Reality SDKs.");
				return null;
			}

			// Verify common interfaces are valid.

			OpenVR.GetGenericInterface(OpenVR.IVRCompositor_Version, ref error);
			if (error != EVRInitError.None)
			{
				ReportError(error);
				return null;
			}

			OpenVR.GetGenericInterface(OpenVR.IVROverlay_Version, ref error);
			if (error != EVRInitError.None)
			{
				ReportError(error);
				return null;
			}
		}
		catch (System.Exception e)
		{
			Debug.LogError(e);
			return null;
		}

		return new SteamVR();
	}

	static void ReportError(EVRInitError error)
	{
		switch (error)
		{
			case EVRInitError.None:
				break;
			case EVRInitError.VendorSpecific_UnableToConnectToOculusRuntime:
				Debug.Log("SteamVR Initialization Failed!  Make sure device is on, Oculus runtime is installed, and OVRService_*.exe is running.");
				break;
			case EVRInitError.Init_VRClientDLLNotFound:
				Debug.Log("SteamVR drivers not found!  They can be installed via Steam under Library > Tools.  Visit http://steampowered.com to install Steam.");
				break;
			case EVRInitError.Driver_RuntimeOutOfDate:
				Debug.Log("SteamVR Initialization Failed!  Make sure device's runtime is up to date.");
				break;
			default:
				Debug.Log(OpenVR.GetStringForHmdError(error));
				break;
		}
	}

	// native interfaces
	public CVRSystem hmd { get; private set; }
	public CVRCompositor compositor { get; private set; }
	public CVROverlay overlay { get; private set; }

	// tracking status
	static public bool initializing { get; private set; }
	static public bool calibrating { get; private set; }
	static public bool outOfRange { get; private set; }

	static public bool[] connected = new bool[OpenVR.k_unMaxTrackedDeviceCount];

	// render values
	public float sceneWidth { get; private set; }
	public float sceneHeight { get; private set; }
	public float aspect { get; private set; }
	public float fieldOfView { get; private set; }
	public Vector2 tanHalfFov { get; private set; }
	public VRTextureBounds_t[] textureBounds { get; private set; }
	public SteamVR_Utils.RigidTransform[] eyes { get; private set; }
	public ETextureType textureType;

	// hmd properties
	public string hmd_TrackingSystemName { get { return GetStringProperty(ETrackedDeviceProperty.Prop_TrackingSystemName_String); } }
	public string hmd_ModelNumber { get { return GetStringProperty(ETrackedDeviceProperty.Prop_ModelNumber_String); } }
	public string hmd_SerialNumber { get { return GetStringProperty(ETrackedDeviceProperty.Prop_SerialNumber_String); } }

	public float hmd_SecondsFromVsyncToPhotons { get { return GetFloatProperty(ETrackedDeviceProperty.Prop_SecondsFromVsyncToPhotons_Float); } }
	public float hmd_DisplayFrequency { get { return GetFloatProperty(ETrackedDeviceProperty.Prop_DisplayFrequency_Float); } }

	public string GetTrackedDeviceString(uint deviceId)
	{
		var error = ETrackedPropertyError.TrackedProp_Success;
		var capacity = hmd.GetStringTrackedDeviceProperty(deviceId, ETrackedDeviceProperty.Prop_AttachedDeviceId_String, null, 0, ref error);
		if (capacity > 1)
		{
			var result = new System.Text.StringBuilder((int)capacity);
			hmd.GetStringTrackedDeviceProperty(deviceId, ETrackedDeviceProperty.Prop_AttachedDeviceId_String, result, capacity, ref error);
			return result.ToString();
		}
		return null;
	}

	public string GetStringProperty(ETrackedDeviceProperty prop, uint deviceId = OpenVR.k_unTrackedDeviceIndex_Hmd)
	{
		var error = ETrackedPropertyError.TrackedProp_Success;
		var capactiy = hmd.GetStringTrackedDeviceProperty(deviceId, prop, null, 0, ref error);
		if (capactiy > 1)
		{
			var result = new System.Text.StringBuilder((int)capactiy);
			hmd.GetStringTrackedDeviceProperty(deviceId, prop, result, capactiy, ref error);
			return result.ToString();
		}
		return (error != ETrackedPropertyError.TrackedProp_Success) ? error.ToString() : "<unknown>";
	}

	public float GetFloatProperty(ETrackedDeviceProperty prop, uint deviceId = OpenVR.k_unTrackedDeviceIndex_Hmd)
	{
		var error = ETrackedPropertyError.TrackedProp_Success;
		return hmd.GetFloatTrackedDeviceProperty(deviceId, prop, ref error);
	}

	#region Event callbacks

	private void OnInitializing(bool initializing)
	{
		SteamVR.initializing = initializing;
	}

	private void OnCalibrating(bool calibrating)
	{
		SteamVR.calibrating = calibrating;
	}

	private void OnOutOfRange(bool outOfRange)
	{
		SteamVR.outOfRange = outOfRange;
	}

	private void OnDeviceConnected(int i, bool connected)
	{
		SteamVR.connected[i] = connected;
	}

	private void OnNewPoses(TrackedDevicePose_t[] poses)
	{
		// Update eye offsets to account for IPD changes.
		eyes[0] = new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Left));
		eyes[1] = new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Right));

		for (int i = 0; i < poses.Length; i++)
		{
			var connected = poses[i].bDeviceIsConnected;
			if (connected != SteamVR.connected[i])
			{
				SteamVR_Events.DeviceConnected.Send(i, connected);
			}
		}

		if (poses.Length > OpenVR.k_unTrackedDeviceIndex_Hmd)
		{
			var result = poses[OpenVR.k_unTrackedDeviceIndex_Hmd].eTrackingResult;

			var initializing = result == ETrackingResult.Uninitialized;
			if (initializing != SteamVR.initializing)
			{
				SteamVR_Events.Initializing.Send(initializing);
			}

			var calibrating =
				result == ETrackingResult.Calibrating_InProgress ||
				result == ETrackingResult.Calibrating_OutOfRange;
			if (calibrating != SteamVR.calibrating)
			{
				SteamVR_Events.Calibrating.Send(calibrating);
			}

			var outOfRange =
				result == ETrackingResult.Running_OutOfRange ||
				result == ETrackingResult.Calibrating_OutOfRange;
			if (outOfRange != SteamVR.outOfRange)
			{
				SteamVR_Events.OutOfRange.Send(outOfRange);
			}
		}
	}

	#endregion

	private SteamVR()
	{
		hmd = OpenVR.System;
		Debug.Log("Connected to " + hmd_TrackingSystemName + ":" + hmd_SerialNumber);

		compositor = OpenVR.Compositor;
		overlay = OpenVR.Overlay;

		// Setup render values
		uint w = 0, h = 0;
		hmd.GetRecommendedRenderTargetSize(ref w, ref h);
		sceneWidth = (float)w;
		sceneHeight = (float)h;

		float l_left = 0.0f, l_right = 0.0f, l_top = 0.0f, l_bottom = 0.0f;
		hmd.GetProjectionRaw(EVREye.Eye_Left, ref l_left, ref l_right, ref l_top, ref l_bottom);

		float r_left = 0.0f, r_right = 0.0f, r_top = 0.0f, r_bottom = 0.0f;
		hmd.GetProjectionRaw(EVREye.Eye_Right, ref r_left, ref r_right, ref r_top, ref r_bottom);

		tanHalfFov = new Vector2(
			Mathf.Max(-l_left, l_right, -r_left, r_right),
			Mathf.Max(-l_top, l_bottom, -r_top, r_bottom));

		textureBounds = new VRTextureBounds_t[2];

		textureBounds[0].uMin = 0.5f + 0.5f * l_left / tanHalfFov.x;
		textureBounds[0].uMax = 0.5f + 0.5f * l_right / tanHalfFov.x;
		textureBounds[0].vMin = 0.5f - 0.5f * l_bottom / tanHalfFov.y;
		textureBounds[0].vMax = 0.5f - 0.5f * l_top / tanHalfFov.y;

		textureBounds[1].uMin = 0.5f + 0.5f * r_left / tanHalfFov.x;
		textureBounds[1].uMax = 0.5f + 0.5f * r_right / tanHalfFov.x;
		textureBounds[1].vMin = 0.5f - 0.5f * r_bottom / tanHalfFov.y;
		textureBounds[1].vMax = 0.5f - 0.5f * r_top / tanHalfFov.y;

		// Grow the recommended size to account for the overlapping fov
		sceneWidth = sceneWidth / Mathf.Max(textureBounds[0].uMax - textureBounds[0].uMin, textureBounds[1].uMax - textureBounds[1].uMin);
		sceneHeight = sceneHeight / Mathf.Max(textureBounds[0].vMax - textureBounds[0].vMin, textureBounds[1].vMax - textureBounds[1].vMin);

		aspect = tanHalfFov.x / tanHalfFov.y;
		fieldOfView = 2.0f * Mathf.Atan(tanHalfFov.y) * Mathf.Rad2Deg;

		eyes = new SteamVR_Utils.RigidTransform[] {
			new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Left)),
			new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Right)) };

		switch (SystemInfo.graphicsDeviceType)
		{
#if (UNITY_5_4)
			case UnityEngine.Rendering.GraphicsDeviceType.OpenGL2:
#endif
			case UnityEngine.Rendering.GraphicsDeviceType.OpenGLCore:
			case UnityEngine.Rendering.GraphicsDeviceType.OpenGLES2:
			case UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3:
				textureType = ETextureType.OpenGL;
				break;
#if !(UNITY_5_4)
			case UnityEngine.Rendering.GraphicsDeviceType.Vulkan:
				textureType = ETextureType.Vulkan;
				break;
#endif
			default:
				textureType = ETextureType.DirectX;
				break;
		}

		SteamVR_Events.Initializing.Listen(OnInitializing);
		SteamVR_Events.Calibrating.Listen(OnCalibrating);
		SteamVR_Events.OutOfRange.Listen(OnOutOfRange);
		SteamVR_Events.DeviceConnected.Listen(OnDeviceConnected);
		SteamVR_Events.NewPoses.Listen(OnNewPoses);
	}

	~SteamVR()
	{
		Dispose(false);
	}

	public void Dispose()
	{
		Dispose(true);
		System.GC.SuppressFinalize(this);
	}

	private void Dispose(bool disposing)
	{
		SteamVR_Events.Initializing.Remove(OnInitializing);
		SteamVR_Events.Calibrating.Remove(OnCalibrating);
		SteamVR_Events.OutOfRange.Remove(OnOutOfRange);
		SteamVR_Events.DeviceConnected.Remove(OnDeviceConnected);
		SteamVR_Events.NewPoses.Remove(OnNewPoses);

		_instance = null;
	}

	// Use this interface to avoid accidentally creating the instance in the process of attempting to dispose of it.
	public static void SafeDispose()
	{
		if (_instance != null)
			_instance.Dispose();
	}
}

