﻿//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// Purpose: Used to render an external camera of vr player (split front/back).
//
//=============================================================================

using UnityEngine;
using UnityEngine.Rendering;
using Valve.VR;

public class SteamVR_ExternalCamera : MonoBehaviour
{
	[System.Serializable]
	public struct Config
	{
		public float x, y, z;
		public float rx, ry, rz;
		public float fov;
		public float near, far;
		public float sceneResolutionScale;
		public float frameSkip;
		public float nearOffset, farOffset;
		public float hmdOffset;
		public float r, g, b, a; // chroma key override
		public bool disableStandardAssets;
	}

	public Config config;
	public string configPath;

	public void ReadConfig()
	{
		try
		{
			var mCam = new HmdMatrix34_t();
			var readCamMatrix = false;

			object c = config; // box
			var lines = System.IO.File.ReadAllLines(configPath);
			foreach (var line in lines)
			{
				var split = line.Split('=');
				if (split.Length == 2)
				{
					var key = split[0];
					if (key == "m")
					{
						var values = split[1].Split(',');
						if (values.Length == 12)
						{
							mCam.m0 = float.Parse(values[0]);
							mCam.m1 = float.Parse(values[1]);
							mCam.m2 = float.Parse(values[2]);
							mCam.m3 = float.Parse(values[3]);
							mCam.m4 = float.Parse(values[4]);
							mCam.m5 = float.Parse(values[5]);
							mCam.m6 = float.Parse(values[6]);
							mCam.m7 = float.Parse(values[7]);
							mCam.m8 = float.Parse(values[8]);
							mCam.m9 = float.Parse(values[9]);
							mCam.m10 = float.Parse(values[10]);
							mCam.m11 = float.Parse(values[11]);
							readCamMatrix = true;
						}
					}
#if !UNITY_METRO
					else if (key == "disableStandardAssets")
					{
						var field = c.GetType().GetField(key);
						if (field != null)
							field.SetValue(c, bool.Parse(split[1]));
					}
					else
					{
						var field = c.GetType().GetField(key);
						if (field != null)
							field.SetValue(c, float.Parse(split[1]));
					}
#endif
				}
			}
			config = (Config)c; //unbox

			// Convert calibrated camera matrix settings.
			if (readCamMatrix)
			{
				var t = new SteamVR_Utils.RigidTransform(mCam);
				config.x = t.pos.x;
				config.y = t.pos.y;
				config.z = t.pos.z;
				var angles = t.rot.eulerAngles;
				config.rx = angles.x;
				config.ry = angles.y;
				config.rz = angles.z;
			}
		}
		catch { }

		// Clear target so AttachToCamera gets called to pick up any changes.
		target = null;
#if !UNITY_METRO
		// Listen for changes.
		if (watcher == null)
		{
			var fi = new System.IO.FileInfo(configPath);
			watcher = new System.IO.FileSystemWatcher(fi.DirectoryName, fi.Name);
			watcher.NotifyFilter = System.IO.NotifyFilters.LastWrite;
			watcher.Changed += new System.IO.FileSystemEventHandler(OnChanged);
			watcher.EnableRaisingEvents = true;
		}
	}

	void OnChanged(object source, System.IO.FileSystemEventArgs e)
	{
		ReadConfig();
	}

	System.IO.FileSystemWatcher watcher;
#else
	}
#endif
	Camera cam;
	Transform target;
	GameObject clipQuad;
	Material clipMaterial;

	public void AttachToCamera(SteamVR_Camera vrcam)
	{
		if (target == vrcam.head)
			return;

		target = vrcam.head;

		var root = transform.parent;
		var origin = vrcam.head.parent;
		root.parent = origin;
		root.localPosition = Vector3.zero;
		root.localRotation = Quaternion.identity;
		root.localScale = Vector3.one;

		// Make a copy of the eye camera to pick up any camera fx.
		vrcam.enabled = false;
		var go = Instantiate(vrcam.gameObject);
		vrcam.enabled = true;
		go.name = "camera";

		DestroyImmediate(go.GetComponent<SteamVR_Camera>());
		DestroyImmediate(go.GetComponent<SteamVR_Fade>());

		cam = go.GetComponent<Camera>();
		cam.stereoTargetEye = StereoTargetEyeMask.None;
		cam.fieldOfView = config.fov;
		cam.useOcclusionCulling = false;
		cam.enabled = false; // manually rendered

		colorMat = new Material(Shader.Find("Custom/SteamVR_ColorOut"));
		alphaMat = new Material(Shader.Find("Custom/SteamVR_AlphaOut"));
		clipMaterial = new Material(Shader.Find("Custom/SteamVR_ClearAll"));

		var offset = go.transform;
		offset.parent = transform;
		offset.localPosition = new Vector3(config.x, config.y, config.z);
		offset.localRotation = Quaternion.Euler(config.rx, config.ry, config.rz);
		offset.localScale = Vector3.one;

		// Strip children of cloned object (AudioListener in particular).
		while (offset.childCount > 0)
			DestroyImmediate(offset.GetChild(0).gameObject);

		// Setup clipping quad (using camera clip causes problems with shadows).
		clipQuad = GameObject.CreatePrimitive(PrimitiveType.Quad);
		clipQuad.name = "ClipQuad";
		DestroyImmediate(clipQuad.GetComponent<MeshCollider>());

		var clipRenderer = clipQuad.GetComponent<MeshRenderer>();
		clipRenderer.material = clipMaterial;
		clipRenderer.shadowCastingMode = ShadowCastingMode.Off;
		clipRenderer.receiveShadows = false;
		clipRenderer.lightProbeUsage = LightProbeUsage.Off;
		clipRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;

		var clipTransform = clipQuad.transform;
		clipTransform.parent = offset;
		clipTransform.localScale = new Vector3(1000.0f, 1000.0f, 1.0f);
		clipTransform.localRotation = Quaternion.identity;

		clipQuad.SetActive(false);
	}

	public float GetTargetDistance()
	{
		if (target == null)
			return config.near + 0.01f;

		var offset = cam.transform;
		var forward = new Vector3(offset.forward.x, 0.0f, offset.forward.z).normalized;
		var targetPos = target.position + new Vector3(target.forward.x, 0.0f, target.forward.z).normalized * config.hmdOffset;

		var distance = -(new Plane(forward, targetPos)).GetDistanceToPoint(offset.position);
		return Mathf.Clamp(distance, config.near + 0.01f, config.far - 0.01f);
	}

	Material colorMat, alphaMat;

	public void RenderNear()
	{
		var w = Screen.width / 2;
		var h = Screen.height / 2;

		if (cam.targetTexture == null || cam.targetTexture.width != w || cam.targetTexture.height != h)
		{
			var tex = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
			tex.antiAliasing = QualitySettings.antiAliasing == 0 ? 1 : QualitySettings.antiAliasing;
			cam.targetTexture = tex;
		}

		cam.nearClipPlane = config.near;
		cam.farClipPlane = config.far;

		var clearFlags = cam.clearFlags;
		var backgroundColor = cam.backgroundColor;

		cam.clearFlags = CameraClearFlags.Color;
		cam.backgroundColor = Color.clear;

		clipMaterial.color = new Color(config.r, config.g, config.b, config.a);

		float dist = Mathf.Clamp(GetTargetDistance() + config.nearOffset, config.near, config.far);
		var clipParent = clipQuad.transform.parent;
		clipQuad.transform.position = clipParent.position + clipParent.forward * dist;

		MonoBehaviour[] behaviours = null;
		bool[] wasEnabled = null;
		if (config.disableStandardAssets)
		{
			behaviours = cam.gameObject.GetComponents<MonoBehaviour>();
			wasEnabled = new bool[behaviours.Length];
			for (int i = 0; i < behaviours.Length; i++)
			{
				var behaviour = behaviours[i];
				if (behaviour.enabled && behaviour.GetType().ToString().StartsWith("UnityStandardAssets."))
				{
					behaviour.enabled = false;
					wasEnabled[i] = true;
				}
			}
		}

		clipQuad.SetActive(true);

		cam.Render();

		Graphics.DrawTexture(new Rect(0, 0, w, h), cam.targetTexture, colorMat);

		// Re-render scene with post-processing fx disabled (if necessary) since they override alpha.
		var pp = cam.gameObject.GetComponent("PostProcessingBehaviour") as MonoBehaviour;
		if ((pp != null) && pp.enabled)
		{
			pp.enabled = false;
			cam.Render();
			pp.enabled = true;
		}

		Graphics.DrawTexture(new Rect(w, 0, w, h), cam.targetTexture, alphaMat);

		// Restore settings.
		clipQuad.SetActive(false);

		if (behaviours != null)
		{
			for (int i = 0; i < behaviours.Length; i++)
			{
				if (wasEnabled[i])
				{
					behaviours[i].enabled = true;
				}
			}
		}

		cam.clearFlags = clearFlags;
		cam.backgroundColor = backgroundColor;
	}

	public void RenderFar()
	{
		cam.nearClipPlane = config.near;
		cam.farClipPlane = config.far;
		cam.Render();

		var w = Screen.width / 2;
		var h = Screen.height / 2;
		Graphics.DrawTexture(new Rect(0, h, w, h), cam.targetTexture, colorMat);
	}

	void OnGUI()
	{
		// Necessary for Graphics.DrawTexture to work even though we don't do anything here.
	}

	Camera[] cameras;
	Rect[] cameraRects;
	float sceneResolutionScale;

	void OnEnable()
	{
		// Move game view cameras to lower-right quadrant.
		cameras = FindObjectsOfType<Camera>() as Camera[];
		if (cameras != null)
		{
			var numCameras = cameras.Length;
			cameraRects = new Rect[numCameras];
			for (int i = 0; i < numCameras; i++)
			{
				var cam = cameras[i];
				cameraRects[i] = cam.rect;

				if (cam == this.cam)
					continue;

				if (cam.targetTexture != null)
					continue;

				if (cam.GetComponent<SteamVR_Camera>() != null)
					continue;

				cam.rect = new Rect(0.5f, 0.0f, 0.5f, 0.5f);
			}
		}

		if (config.sceneResolutionScale > 0.0f)
		{
			sceneResolutionScale = SteamVR_Camera.sceneResolutionScale;
			SteamVR_Camera.sceneResolutionScale = config.sceneResolutionScale;
		}
	}

	void OnDisable()
	{
		// Restore game view cameras.
		if (cameras != null)
		{
			var numCameras = cameras.Length;
			for (int i = 0; i < numCameras; i++)
			{
				var cam = cameras[i];
				if (cam != null)
					cam.rect = cameraRects[i];
			}
			cameras = null;
			cameraRects = null;
		}

		if (config.sceneResolutionScale > 0.0f)
		{
			SteamVR_Camera.sceneResolutionScale = sceneResolutionScale;
		}
	}
}

