/**********************************************************************************
* Blueprint Reality Inc. CONFIDENTIAL
* 2023 Blueprint Reality Inc.
* All Rights Reserved.
*
* NOTICE:  All information contained herein is, and remains, the property of
* Blueprint Reality Inc. and its suppliers, if any.  The intellectual and
* technical concepts contained herein are proprietary to Blueprint Reality Inc.
* and its suppliers and may be covered by Patents, pending patents, and are
* protected by trade secret or copyright law.
*
* Dissemination of this information or reproduction of this material is strictly
* forbidden unless prior written permission is obtained from Blueprint Reality Inc.
***********************************************************************************/

#if UNITY_STANDALONE_WIN
using UnityEngine;
using System.Collections;
using BlueprintReality.MixCast.Shared;
using BlueprintReality.MixCast.Data;
using BlueprintReality.SharedTextures;
using System.Collections.Generic;

namespace BlueprintReality.MixCast.VideoInputs
{
    public class ExpVideoInputBehaviour : MonoBehaviour
    {
        const float NearDist = 0.01f;

        private static readonly Matrix4x4 ViewportToUvs = Matrix4x4.Scale(Vector3.one * 0.5f) * Matrix4x4.TRS(Vector3.one, Quaternion.identity, Vector3.one);
        private static readonly Vector3 FlipZScale = new Vector3(1, 1, -1);

        public IdentifierContext videoInputId;

        public Transform positionTransform;
        public Transform rotationTransform;

        public Shader projectionShader;
        public MeshRenderer projectionRenderer;

#if UNITY_2017_3_OR_NEWER
        [Range(0, 4)]
        public int decimation = 0;

        private int lastMeshWidth;
        private int lastMeshHeight;
#endif

        private SharedTextureReceiver colorTexReceiver;
        private SharedTextureReceiver depthTexReceiver;

        private Material mat;
        private Mesh mesh;

        private void OnEnable()
        {
            mat = new Material(projectionShader);
			mat.SetFloat("_MaxDist", ExpCameraBehaviour.MaxDepthInCutoffTexture);

			projectionRenderer.material = mat;

			if (!string.IsNullOrEmpty(videoInputId.Identifier))
			{
				colorTexReceiver = SharedTextureReceiver.Create(SharedTexIds.VideoInputs.LatestColor.Get(videoInputId.Identifier));
				depthTexReceiver = SharedTextureReceiver.Create(SharedTexIds.VideoInputs.LatestDepth.Get(videoInputId.Identifier));

				ExpCameraBehaviour.FrameStarted += HandleFrameStarted;
				ExpCameraBehaviour.FrameEnded += HandleFrameEnded;
			}

#if !UNITY_2017_3_OR_NEWER
            RegenerateMesh(479, 134);   //older versions of Unity have a 65k cap on vertices, this amount is just under that
#endif
		}
        private void OnDisable()
        {
            if (!string.IsNullOrEmpty(videoInputId.Identifier))
            {
                ExpCameraBehaviour.FrameStarted -= HandleFrameStarted;
                ExpCameraBehaviour.FrameEnded -= HandleFrameEnded;

                if (colorTexReceiver != null)
                {
                    colorTexReceiver.Dispose();
                    colorTexReceiver = null;
                }
                if (depthTexReceiver != null)
                {
                    depthTexReceiver.Dispose();
                    depthTexReceiver = null;
                }
            }
        }

        private void LateUpdate()
        {
            if (string.IsNullOrEmpty(videoInputId.Identifier))
                return;

            Shared.VideoInput videoInput = MixCastSdkData.GetVideoInputWithId(videoInputId.Identifier);
            if (videoInput == null)
                return;

            if (!colorTexReceiver.RequestSucceeded)
                colorTexReceiver.RefreshTextureInfo();
            else if (colorTexReceiver.Texture != null)
                colorTexReceiver.Texture.filterMode = FilterMode.Point;

			if (!depthTexReceiver.RequestSucceeded)
				depthTexReceiver.RefreshTextureInfo();
            else if(depthTexReceiver.Texture != null)
				depthTexReceiver.Texture.filterMode = FilterMode.Point;

			UpdateRendering(
                transform.parent,
                videoInput.CurrentPosition.unity, videoInput.CurrentRotation.unity, 
                (float)videoInput.FieldOfView, videoInput.OpticalCenter.unity,
                colorTexReceiver.Texture, depthTexReceiver.Texture);
        }
        public void UpdateRendering(Transform originParent, Vector3 originPos, Quaternion originRot, float fov, Vector2 opticalCenter, Texture colorTex, Texture depthTex)
        { 
            positionTransform.position = originParent.TransformPoint(originPos);
            rotationTransform.rotation = originParent.rotation * originRot;

#if UNITY_2017_3_OR_NEWER
            if (depthTex != null)
            {
                int newCellsX = Mathf.Max(2, depthTex.width / (decimation + 1)) - 1;
                int newCellsY = Mathf.Max(2, depthTex.height / (decimation + 1)) - 1;

                if (newCellsX != lastMeshWidth || newCellsY != lastMeshHeight)
                    RegenerateMesh(newCellsX, newCellsY);
            }
#endif

            bool drawable = colorTex != null && depthTex != null;
            projectionRenderer.gameObject.SetActive(drawable);

            if (drawable)
            {
                mat.SetTexture("_MainTex", colorTex);
                mat.SetTexture("_DepthTex", depthTex);

                float aspect = (float)colorTex.width / colorTex.height;
                Matrix4x4 localToUvs = ViewportToUvs *
                    ExpCameraBehaviour.CreateProjectionMatrix(fov, aspect, opticalCenter, NearDist, ExpCameraBehaviour.MaxDepthInCutoffTexture) *
                    Matrix4x4.Scale(FlipZScale).inverse;
                Matrix4x4 uvsToLocal = localToUvs.inverse;

                Vector3 projSpace = localToUvs.MultiplyPoint(new Vector3(0, 0, 1));
                Vector3 topRight = uvsToLocal.MultiplyPoint(new Vector3(1, 1, projSpace.z));
                Vector3 bottomLeft = uvsToLocal.MultiplyPoint(new Vector3(0, 0, projSpace.z));

                Vector3 mins = Vector3.Min(Vector3.zero, bottomLeft * ExpCameraBehaviour.MaxDepthInCutoffTexture);
                Vector3 maxs = Vector3.Max(Vector3.zero, topRight * ExpCameraBehaviour.MaxDepthInCutoffTexture);
                mesh.bounds = new Bounds(Vector3.Lerp(mins, maxs, 0.5f), maxs - mins);

                mat.SetVector("_UvAndDepthToLocal", new Vector4(bottomLeft.x, bottomLeft.y, topRight.x, topRight.y));
            }
        }

        public void ClearRendering()
        {
			mat.SetTexture("_MainTex", null);
			mat.SetTexture("_DepthTex", null);
			projectionRenderer.gameObject.SetActive(false);
		}

        void HandleFrameStarted(ExpCameraBehaviour cam)
        {
            Shared.VideoInput videoInput = MixCastSdkData.GetVideoInputWithId(videoInputId.Identifier);
            if (videoInput == null)
                return;

            bool displayToCamera = false;
            for (int i = 0; i < videoInput.ProjectToCameras.Count && !displayToCamera; i++)
                displayToCamera |= videoInput.ProjectToCameras[i] == cam.cameraContext.Identifier;

            projectionRenderer.enabled = displayToCamera;
        }
        void HandleFrameEnded(ExpCameraBehaviour cam)
        {
            Shared.VideoInput videoInput = MixCastSdkData.GetVideoInputWithId(videoInputId.Identifier);
            if (videoInput == null)
                return;

            projectionRenderer.enabled = videoInput.ProjectToUser;
        }


        void RegenerateMesh(int cellsX, int cellsY)
        {
            if (mesh == null)
            {
                mesh = new Mesh();
#if UNITY_2017_3_OR_NEWER
                mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
#endif
            }
            else
                mesh.Clear();

            List<Vector3> positions = new List<Vector3>();
            List<Vector2> uvs = new List<Vector2>();
            List<int> indices = new List<int>();

            for (int y = 0; y <= cellsY; y++)
            {
                float v = (float)y / cellsY;
                for (int x = 0; x <= cellsX; x++)
                {
                    float u = (float)x / cellsX;

                    positions.Add(2 * new Vector3(u - 0.5f, v - 0.5f, 0));
                    uvs.Add(new Vector2(u, v));
                }
            }
            for (int y = 0; y < cellsY; y++)
            {
                for (int x = 0; x < cellsX; x++)
                {
                    int bottomLeft = y * (cellsX + 1) + x;
                    int bottomRight = bottomLeft + 1;
                    int topLeft = (y + 1) * (cellsX + 1) + x;
                    int topRight = topLeft + 1;

                    //Alternate how quads are broken into triangles per row
                    if (y % 2 == 0)
                    {
                        indices.Add(bottomLeft);
                        indices.Add(topLeft);
                        indices.Add(topRight);

                        indices.Add(bottomLeft);
                        indices.Add(topRight);
                        indices.Add(bottomRight);
                    }
                    else
                    {
                        indices.Add(bottomRight);
                        indices.Add(bottomLeft);
                        indices.Add(topLeft);

                        indices.Add(bottomRight);
                        indices.Add(topLeft);
                        indices.Add(topRight);
                    }
                }
            }
            mesh.SetVertices(positions);
            mesh.SetUVs(0, uvs);
            mesh.SetTriangles(indices, 0);
            mesh.bounds = new Bounds(Vector3.forward * 5, Vector3.one * 10); //should have a better bounds check?

            projectionRenderer.GetComponent<MeshFilter>().sharedMesh = mesh;
#if UNITY_2017_3_OR_NEWER
            lastMeshWidth = cellsX;
            lastMeshHeight = cellsY;
#endif
        }
    }
}
#endif
