//========= Copyright 2014, Valve Corporation, All rights reserved. ===========
//
// Purpose: Render model of associated tracked object
//
//=============================================================================

using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
using Valve.VR;

[ExecuteInEditMode]
public class SteamVR_RenderModel : MonoBehaviour
{
	public SteamVR_TrackedObject.EIndex index = SteamVR_TrackedObject.EIndex.None;
	public string modelOverride;

	// Shader to apply to model.
	public Shader shader;

	// Enable to print out when render models are loaded.
	public bool verbose = false;

	// If available, break down into separate components instead of loading as a single mesh.
	public bool createComponents = true;

	// Update transforms of components at runtime to reflect user action.
	public bool updateDynamically = true;

	// Name of the sub-object which represents the "local" coordinate space for each component.
	public const string k_localTransformName = "attach";

	// Cached name of this render model for updating component transforms at runtime.
	public string renderModelName { get; private set; }

	// If someone knows how to keep these from getting cleaned up every time
	// you exit play mode, let me know.  I've tried marking the RenderModel
	// class below as [System.Serializable] and switching to normal public
	// variables for mesh and material to get them to serialize properly,
	// as well as tried marking the mesh and material objects as
	// DontUnloadUnusedAsset, but Unity was still unloading them.
	// The hashtable is preserving its entries, but the mesh and material
	// variables are going null.

	public class RenderModel
	{
		public RenderModel(Mesh mesh, Material material)
		{
			this.mesh = mesh;
			this.material = material;
		}
		public Mesh mesh { get; private set; }
		public Material material { get; private set; }
	}

	public static Hashtable models = new Hashtable();
	public static Hashtable materials = new Hashtable();

	// Helper class to load render models interface on demand and clean up when done.
	public sealed class RenderModelInterfaceHolder : System.IDisposable
	{
		private bool needsShutdown, failedLoadInterface;
		private CVRRenderModels _instance;
		public CVRRenderModels instance
		{
			get
			{
				if (_instance == null && !failedLoadInterface)
				{
					if (!SteamVR.active && !SteamVR.usingNativeSupport)
					{
						var error = EVRInitError.None;
						OpenVR.Init(ref error, EVRApplicationType.VRApplication_Other);
						needsShutdown = true;
					}

					_instance = OpenVR.RenderModels;
					if (_instance == null)
					{
						Debug.LogError("Failed to load IVRRenderModels interface version " + OpenVR.IVRRenderModels_Version);
						failedLoadInterface = true;
                    }
				}
				return _instance;
			}
		}
		public void Dispose()
		{
			if (needsShutdown)
				OpenVR.Shutdown();
		}
	}

	private void OnHideRenderModels(params object[] args)
	{
		bool hidden = (bool)args[0];
		var meshRenderer = GetComponent<MeshRenderer>();
		if (meshRenderer != null)
			meshRenderer.enabled = !hidden;
		foreach (var child in transform.GetComponentsInChildren<MeshRenderer>())
			child.enabled = !hidden;
    }

	private void OnDeviceConnected(params object[] args)
	{
		var i = (int)args[0];
		if (i != (int)index)
			return;

		var connected = (bool)args[1];
		if (connected)
		{
			UpdateModel();
		}
		else
		{
			StripMesh(gameObject);
		}
	}

	public void UpdateModel()
	{
		var system = OpenVR.System;
		if (system == null)
			return;

		var error = ETrackedPropertyError.TrackedProp_Success;
		var capacity = system.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String, null, 0, ref error);
		if (capacity <= 1)
		{
			Debug.LogError("Failed to get render model name for tracked object " + index);
			return;
		}

		var buffer = new System.Text.StringBuilder((int)capacity);
		system.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String, buffer, capacity, ref error);

		var s = buffer.ToString();
		if (renderModelName != s)
		{
			renderModelName = s;
			StartCoroutine(SetModelAsync(s));
		}
	}

	IEnumerator SetModelAsync(string renderModelName)
	{
		if (string.IsNullOrEmpty(renderModelName))
			yield break;

		// Preload all render models before asking for the data to create meshes.
		using (var holder = new RenderModelInterfaceHolder())
		{
			var renderModels = holder.instance;
			if (renderModels == null)
				yield break;

			// Gather names of render models to preload.
			string[] renderModelNames;

			var count = renderModels.GetComponentCount(renderModelName);
			if (count > 0)
			{
				renderModelNames = new string[count];

				for (int i = 0; i < count; i++)
				{
					var capacity = renderModels.GetComponentName(renderModelName, (uint)i, null, 0);
					if (capacity == 0)
						continue;

					var componentName = new System.Text.StringBuilder((int)capacity);
					if (renderModels.GetComponentName(renderModelName, (uint)i, componentName, capacity) == 0)
						continue;

					capacity = renderModels.GetComponentRenderModelName(renderModelName, componentName.ToString(), null, 0);
					if (capacity == 0)
						continue;

					var name = new System.Text.StringBuilder((int)capacity);
					if (renderModels.GetComponentRenderModelName(renderModelName, componentName.ToString(), name, capacity) == 0)
						continue;

					var s = name.ToString();

					// Only need to preload if not already cached.
					var model = models[s] as RenderModel;
					if (model == null || model.mesh == null)
					{
						renderModelNames[i] = s;
					}
				}
			}
			else
			{
				// Only need to preload if not already cached.
				var model = models[renderModelName] as RenderModel;
				if (model == null || model.mesh == null)
				{
					renderModelNames = new string[] { renderModelName };
				}
				else
				{
					renderModelNames = new string[0];
				}
			}

			// Keep trying every 100ms until all components finish loading.
			while (true)
			{
				var loading = false;
				foreach (var name in renderModelNames)
				{
					if (string.IsNullOrEmpty(name))
						continue;

					var pRenderModel = System.IntPtr.Zero;

					var error = renderModels.LoadRenderModel_Async(name, ref pRenderModel);
                    if (error == EVRRenderModelError.Loading)
					{
						loading = true;
					}
					else if (error == EVRRenderModelError.None)
					{
						// Preload textures as well.
						var renderModel = (RenderModel_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t));

						// Check the cache first.
						var material = materials[renderModel.diffuseTextureId] as Material;
						if (material == null || material.mainTexture == null)
						{
							var pDiffuseTexture = System.IntPtr.Zero;

							error = renderModels.LoadTexture_Async(renderModel.diffuseTextureId, ref pDiffuseTexture);
							if (error == EVRRenderModelError.Loading)
							{
								loading = true;
							}
						}
					}
				}

				if (loading)
				{
					yield return new WaitForSeconds(0.1f);
				}
				else
				{
					break;
				}
			}
		}

		bool success = SetModel(renderModelName);
		SteamVR_Utils.Event.Send("render_model_loaded", this, success);
	}

	private bool SetModel(string renderModelName)
	{
		StripMesh(gameObject);

		using (var holder = new RenderModelInterfaceHolder())
		{
			if (createComponents)
			{
				if (LoadComponents(holder, renderModelName))
				{
					UpdateComponents();
					return true;
				}

				Debug.Log("[" + gameObject.name + "] Render model does not support components, falling back to single mesh.");
			}

			if (!string.IsNullOrEmpty(renderModelName))
			{
				var model = models[renderModelName] as RenderModel;
				if (model == null || model.mesh == null)
				{
					var renderModels = holder.instance;
					if (renderModels == null)
						return false;

					if (verbose)
						Debug.Log("Loading render model " + renderModelName);

					model = LoadRenderModel(renderModels, renderModelName, renderModelName);
					if (model == null)
						return false;

					models[renderModelName] = model;
				}

				gameObject.AddComponent<MeshFilter>().mesh = model.mesh;
				gameObject.AddComponent<MeshRenderer>().sharedMaterial = model.material;
				return true;
			}
		}

		return false;
	}

	RenderModel LoadRenderModel(CVRRenderModels renderModels, string renderModelName, string baseName)
	{
        var pRenderModel = System.IntPtr.Zero;

		EVRRenderModelError error;
		while ( true )
		{
			error = renderModels.LoadRenderModel_Async(renderModelName, ref pRenderModel);
			if (error != EVRRenderModelError.Loading)
				break;

			System.Threading.Thread.Sleep(1);
		}

		if (error != EVRRenderModelError.None)
		{
			Debug.LogError(string.Format("Failed to load render model {0} - {1}", renderModelName, error.ToString()));
			return null;
		}

        var renderModel = (RenderModel_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t));

		var vertices = new Vector3[renderModel.unVertexCount];
		var normals = new Vector3[renderModel.unVertexCount];
		var uv = new Vector2[renderModel.unVertexCount];

		var type = typeof(RenderModel_Vertex_t);
		for (int iVert = 0; iVert < renderModel.unVertexCount; iVert++)
		{
			var ptr = new System.IntPtr(renderModel.rVertexData.ToInt64() + iVert * Marshal.SizeOf(type));
			var vert = (RenderModel_Vertex_t)Marshal.PtrToStructure(ptr, type);

			vertices[iVert] = new Vector3(vert.vPosition.v0, vert.vPosition.v1, -vert.vPosition.v2);
			normals[iVert] = new Vector3(vert.vNormal.v0, vert.vNormal.v1, -vert.vNormal.v2);
			uv[iVert] = new Vector2(vert.rfTextureCoord0, vert.rfTextureCoord1);
		}

		int indexCount = (int)renderModel.unTriangleCount * 3;
		var indices = new short[indexCount];
		Marshal.Copy(renderModel.rIndexData, indices, 0, indices.Length);

		var triangles = new int[indexCount];
		for (int iTri = 0; iTri < renderModel.unTriangleCount; iTri++)
		{
			triangles[iTri * 3 + 0] = (int)indices[iTri * 3 + 2];
			triangles[iTri * 3 + 1] = (int)indices[iTri * 3 + 1];
			triangles[iTri * 3 + 2] = (int)indices[iTri * 3 + 0];
		}

		var mesh = new Mesh();
		mesh.vertices = vertices;
		mesh.normals = normals;
		mesh.uv = uv;
		mesh.triangles = triangles;

		mesh.Optimize();
		//mesh.hideFlags = HideFlags.DontUnloadUnusedAsset;

		// Check cache before loading texture.
		var material = materials[renderModel.diffuseTextureId] as Material;
		if (material == null || material.mainTexture == null)
		{
			var pDiffuseTexture = System.IntPtr.Zero;

			while (true)
			{
				error = renderModels.LoadTexture_Async(renderModel.diffuseTextureId, ref pDiffuseTexture);
				if (error != EVRRenderModelError.Loading)
					break;

				System.Threading.Thread.Sleep(1);
			}

			if (error == EVRRenderModelError.None)
			{
				var diffuseTexture = (RenderModel_TextureMap_t)Marshal.PtrToStructure(pDiffuseTexture, typeof(RenderModel_TextureMap_t));
				var texture = new Texture2D(diffuseTexture.unWidth, diffuseTexture.unHeight, TextureFormat.ARGB32, false);
				if (SteamVR.instance.graphicsAPI == EGraphicsAPIConvention.API_DirectX)
				{
					texture.Apply();

					while (true)
					{
						error = renderModels.LoadIntoTextureD3D11_Async(renderModel.diffuseTextureId, texture.GetNativeTexturePtr());
						if (error != EVRRenderModelError.Loading)
							break;

						System.Threading.Thread.Sleep(1);
					}
				}
				else
				{
					var textureMapData = new byte[diffuseTexture.unWidth * diffuseTexture.unHeight * 4]; // RGBA
					Marshal.Copy(diffuseTexture.rubTextureMapData, textureMapData, 0, textureMapData.Length);

					var colors = new Color32[diffuseTexture.unWidth * diffuseTexture.unHeight];
					int iColor = 0;
					for (int iHeight = 0; iHeight < diffuseTexture.unHeight; iHeight++)
					{
						for (int iWidth = 0; iWidth < diffuseTexture.unWidth; iWidth++)
						{
							var r = textureMapData[iColor++];
							var g = textureMapData[iColor++];
							var b = textureMapData[iColor++];
							var a = textureMapData[iColor++];
							colors[iHeight * diffuseTexture.unWidth + iWidth] = new Color32(r, g, b, a);
						}
					}

					texture.SetPixels32(colors);
					texture.Apply();
				}

				material = new Material(shader != null ? shader : Shader.Find("Standard"));
				material.mainTexture = texture;
				//material.hideFlags = HideFlags.DontUnloadUnusedAsset;

				materials[renderModel.diffuseTextureId] = material;

				renderModels.FreeTexture(pDiffuseTexture);
			}
			else
			{
				Debug.Log("Failed to load render model texture for render model " + renderModelName);
			}
		}

		// Delay freeing when we can since we'll often get multiple requests for the same model right
		// after another (e.g. two controllers or two basestations).
#if UNITY_EDITOR
		if (!Application.isPlaying)
			renderModels.FreeRenderModel(pRenderModel);
		else
#endif
			StartCoroutine(FreeRenderModel(pRenderModel));

		return new RenderModel(mesh, material);
	}

	IEnumerator FreeRenderModel(System.IntPtr pRenderModel)
	{
		yield return new WaitForSeconds(1.0f);

		using (var holder = new RenderModelInterfaceHolder())
		{
			var renderModels = holder.instance;
			renderModels.FreeRenderModel(pRenderModel);
		}
	}

	public Transform FindComponent(string componentName)
	{
		var t = transform;
		for (int i = 0; i < t.childCount; i++)
		{
			var child = t.GetChild(i);
			if (child.name == componentName)
				return child;
		}
		return null;
	}

	private void StripMesh(GameObject go)
	{
		var meshRenderer = go.GetComponent<MeshRenderer>();
		if (meshRenderer != null)
			DestroyImmediate(meshRenderer);

		var meshFilter = go.GetComponent<MeshFilter>();
		if (meshFilter != null)
			DestroyImmediate(meshFilter);
	}

	private bool LoadComponents(RenderModelInterfaceHolder holder, string renderModelName)
	{
		// Disable existing components (we will re-enable them if referenced by this new model).
		// Also strip mesh filter and renderer since these will get re-added if the new component needs them.
		var t = transform;
		for (int i = 0; i < t.childCount; i++)
		{
			var child = t.GetChild(i);
			child.gameObject.SetActive(false);
			StripMesh(child.gameObject);
		}

		// If no model specified, we're done; return success.
		if (string.IsNullOrEmpty(renderModelName))
			return true;

		var renderModels = holder.instance;
		if (renderModels == null)
			return false;

		var count = renderModels.GetComponentCount(renderModelName);
		if (count == 0)
			return false;

		for (int i = 0; i < count; i++)
		{
			var capacity = renderModels.GetComponentName(renderModelName, (uint)i, null, 0);
			if (capacity == 0)
				continue;

			var componentName = new System.Text.StringBuilder((int)capacity);
			if (renderModels.GetComponentName(renderModelName, (uint)i, componentName, capacity) == 0)
				continue;

			// Create (or reuse) a child object for this component (some components are dynamic and don't have meshes).
			t = FindComponent(componentName.ToString());
			if (t != null)
			{
				t.gameObject.SetActive(true);
			}
			else
			{
				t = new GameObject(componentName.ToString()).transform;
				t.parent = transform;
				t.gameObject.layer = gameObject.layer;

				// Also create a child 'attach' object for attaching things.
				var attach = new GameObject(k_localTransformName).transform;
				attach.parent = t;
				attach.localPosition = Vector3.zero;
				attach.localRotation = Quaternion.identity;
				attach.localScale = Vector3.one;
				attach.gameObject.layer = gameObject.layer;
			}

			// Reset transform.
			t.localPosition = Vector3.zero;
			t.localRotation = Quaternion.identity;
			t.localScale = Vector3.one;

			capacity = renderModels.GetComponentRenderModelName(renderModelName, componentName.ToString(), null, 0);
			if (capacity == 0)
				continue;

			var componentRenderModelName = new System.Text.StringBuilder((int)capacity);
			if (renderModels.GetComponentRenderModelName(renderModelName, componentName.ToString(), componentRenderModelName, capacity) == 0)
				continue;

			// Check the cache or load into memory.
			var model = models[componentRenderModelName] as RenderModel;
			if (model == null || model.mesh == null)
			{
				if (verbose)
					Debug.Log("Loading render model " + componentRenderModelName);

				model = LoadRenderModel(renderModels, componentRenderModelName.ToString(), renderModelName);
				if (model == null)
					continue;

				models[componentRenderModelName] = model;
			}

			t.gameObject.AddComponent<MeshFilter>().mesh = model.mesh;
			t.gameObject.AddComponent<MeshRenderer>().sharedMaterial = model.material;
		}

		return true;
	}

	void OnEnable()
	{
#if UNITY_EDITOR
		if (!Application.isPlaying)
			return;
#endif
		if (!string.IsNullOrEmpty(modelOverride))
		{
			Debug.Log("Model override is really only meant to be used in the scene view for lining things up; using it at runtime is discouraged.  Use tracked device index instead to ensure the correct model is displayed for all users.");
			enabled = false;
			return;
		}

		var system = OpenVR.System;
		if (system != null && system.IsTrackedDeviceConnected((uint)index))
		{
			UpdateModel();
		}

		SteamVR_Utils.Event.Listen("device_connected", OnDeviceConnected);
		SteamVR_Utils.Event.Listen("hide_render_models", OnHideRenderModels);
	}

	void OnDisable()
	{
#if UNITY_EDITOR
		if (!Application.isPlaying)
			return;
#endif
		SteamVR_Utils.Event.Remove("device_connected", OnDeviceConnected);
		SteamVR_Utils.Event.Remove("hide_render_models", OnHideRenderModels);
	}

#if UNITY_EDITOR
	Hashtable values;
#endif
	void Update()
	{
#if UNITY_EDITOR
		if (!Application.isPlaying)
		{
			// See if anything has changed since this gets called whenever anything gets touched.
			var fields = GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);

			bool modified = false;

			if (values == null)
			{
				modified = true;
			}
			else
			{
				foreach (var f in fields)
				{
					if (!values.Contains(f))
					{
						modified = true;
						break;
					}

					var v0 = values[f];
                    var v1 = f.GetValue(this);
					if (v1 != null)
					{
						if (!v1.Equals(v0))
						{
							modified = true;
							break;
						}
					}
					else if (v0 != null)
					{
						modified = true;
						break;
					}
				}
			}

			if (modified)
			{
				if (renderModelName != modelOverride)
				{
					renderModelName = modelOverride;
					SetModel(modelOverride);
				}

				values = new Hashtable();
				foreach (var f in fields)
					values[f] = f.GetValue(this);
			}

			return; // Do not update transforms (below) when not playing in Editor (to avoid keeping OpenVR running all the time).
		}
#endif
		// Update component transforms dynamically.
		if (updateDynamically)
			UpdateComponents();
	}

	public void UpdateComponents()
	{
		var t = transform;
		if (t.childCount == 0)
			return;

		using (var holder = new RenderModelInterfaceHolder())
		{
			var controllerState = (index != SteamVR_TrackedObject.EIndex.None) ?
				SteamVR_Controller.Input((int)index).GetState() : new VRControllerState_t();

			for (int i = 0; i < t.childCount; i++)
			{
				var child = t.GetChild(i);

				var renderModels = holder.instance;
				if (renderModels == null)
					break;

				var componentState = new RenderModel_ComponentState_t();
				var componentModeState = new RenderModel_ControllerMode_State_t();
                if (!renderModels.GetComponentState(renderModelName, child.name, ref controllerState, ref componentModeState, ref componentState))
					continue;

				var componentTransform = new SteamVR_Utils.RigidTransform(componentState.mTrackingToComponentRenderModel);
				child.localPosition = componentTransform.pos;
				child.localRotation = componentTransform.rot;

				var attach = child.FindChild(k_localTransformName);
				if (attach != null)
				{
					var attachTransform = new SteamVR_Utils.RigidTransform(componentState.mTrackingToComponentLocal);
					attach.position = t.TransformPoint(attachTransform.pos);
					attach.rotation = t.rotation * attachTransform.rot;
				}

				bool visible = (componentState.uProperties & (uint)EVRComponentProperty.IsVisible) != 0;
				if (visible != child.gameObject.activeSelf)
				{
					child.gameObject.SetActive(visible);
				}
			}
		}
	}

	public void SetDeviceIndex(int index)
	{
		this.index = (SteamVR_TrackedObject.EIndex)index;
		modelOverride = "";

		if (enabled)
		{
			UpdateModel();
		}
	}
}

