/**********************************************************************************
* Blueprint Reality Inc. CONFIDENTIAL
* 2021 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 BlueprintReality.Interprocess;
using BlueprintReality.Interprocess.Textures;
using BlueprintReality.MixCast.Data;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
#if MIXCAST_LWRP 
using UnityEngine.Rendering.LWRP;
#elif MIXCAST_URP
using UnityEngine.Rendering.Universal;
#endif

namespace BlueprintReality.MixCast
{
    public class ExpCameraBehaviour : MonoBehaviour
    {
        private static class ShaderNames
        {
            public const string Blit = "Hidden/BPR/Blit";
            public const string BlitAlpha = "Hidden/BPR/AlphaTransfer";

            public const string ApplyDepthCutoff = "Hidden/BPR/ApplyDepthCutoff";
        }

        private class CamComponentInfo
        {
            public CameraClearFlags clearFlags;
            public Color clearColor;

            public bool hasGrabAlphaCommand = false;
        }
        public enum RenderMode
        {
            None, FullRender, Foreground
        }

        public const float MaxDepthInCutoffTexture = 65.525f;

        private const string ForegroundCamName = "Foreground Camera";
        private const string FullRenderCamName = "Full Render Camera";

        private static readonly CameraEvent[] ApplyCutoffOnEvents =
        {
             CameraEvent.BeforeGBuffer,
             CameraEvent.AfterForwardOpaque,
        };


        private static Texture2D clearTex;

        public static List<ExpCameraBehaviour> ActiveCameras { get; protected set; }
        public static ExpCameraBehaviour CurrentlyRendering { get; protected set; } //Assigned to the MixCastCamera that is being processed between FrameStarted and FrameEnded

        public static event Action<ExpCameraBehaviour> FrameStarted;
        public static event Action<ExpCameraBehaviour> FrameEnded;

        static ExpCameraBehaviour()
        {
            ActiveCameras = new List<ExpCameraBehaviour>();
        }

        public IdentifierContext cameraContext;

        public Transform positionTransform;
        public Transform rotationTransform;

        public bool notifyWhenFrameDropped = false;

        private RenderTexture fullRenderTarget, foregroundTarget;
        public Material TransferAlphaMat { get; protected set; }
        private CommandBuffer grabAlphaCommand;
        public RenderTexture CleanAlphaTarget { get; protected set; }

        public Material ApplyCutoffMat { get; protected set; }
        private CommandBuffer applyCutoffCmd;

        public RenderTexture LayersTexture { get; protected set; }
        private Material transferResultsMat;


        private List<Camera> fullRenderCameraList = new List<Camera>();
        private List<Camera> foregroundCameraList = new List<Camera>();

        private Dictionary<Camera, CamComponentInfo> subcomponentInfo = new Dictionary<Camera, CamComponentInfo>();

        public RenderMode CurrentRenderMode { get; protected set; }

        private SharedQueueConsumer<ExpFrame> requestedFrames;
        private Dictionary<long, WatchedUnitySharedTexture> externalTextureTable = new Dictionary<long, WatchedUnitySharedTexture>();

        private ExpFrame lastReceivedFrame;
        private bool receivedLastFrameTooEarly;

        public ExpFrame LatestFrameInfo { get; protected set; }
        public uint RenderedFrameCount { get; protected set; }
        public ExpFrameSender FramePipe { get; protected set; }

        protected void Awake()
        {
            if (clearTex == null)
            {
                clearTex = new Texture2D(2, 2);
                clearTex.SetPixels(new Color[] { Color.clear, Color.clear, Color.clear, Color.clear });
                clearTex.Apply();
            }

            grabAlphaCommand = new CommandBuffer() { name = "Get Correct Alpha" };
            TransferAlphaMat = new Material(Shader.Find(ShaderNames.BlitAlpha));
            transferResultsMat = new Material(Shader.Find(ShaderNames.Blit));
            if (QualitySettings.desiredColorSpace == ColorSpace.Linear)
                transferResultsMat.EnableKeyword("CONVERT_TO_SRGB");

            ApplyCutoffMat = new Material(Shader.Find(ShaderNames.ApplyDepthCutoff));
            ApplyCutoffMat.SetFloat("_MaxDist", MaxDepthInCutoffTexture);
            applyCutoffCmd = new CommandBuffer() { name = "Apply Cutoff" };

            lastReceivedFrame = new ExpFrame();
            LatestFrameInfo = new ExpFrame();

            SpawnSceneLayerCameras();
        }
        void SpawnSceneLayerCameras()
        {
            if (MixCastSdkData.ProjectSettings.layerCamPrefab != null)
            {
                fullRenderCameraList = SpawnLayerCameraFromPrefab(false);
                foregroundCameraList = SpawnLayerCameraFromPrefab(true);
            }
            else
            {
                fullRenderCameraList = SpawnLayerCameraFromScratch(false);
                foregroundCameraList = SpawnLayerCameraFromScratch(true);
            }
        }
        List<Camera> SpawnLayerCameraFromPrefab(bool isForeground)
        {
            GameObject spawnedCamObj = Instantiate(MixCastSdkData.ProjectSettings.layerCamPrefab);

            spawnedCamObj.name = isForeground ? ForegroundCamName : FullRenderCamName;

            spawnedCamObj.transform.SetParent(rotationTransform != null ? rotationTransform : (positionTransform != null ? positionTransform : transform));
            spawnedCamObj.transform.localPosition = Vector3.zero;
            spawnedCamObj.transform.localRotation = Quaternion.identity;
            spawnedCamObj.transform.localScale = Vector3.one;


            List<Camera> camList = new List<Camera>();
            camList.AddRange(spawnedCamObj.GetComponentsInChildren<Camera>(true));
            camList.Sort((x, y) => x.depth.CompareTo(y.depth));

            for (int i = 0; i < camList.Count; i++)
            {
                Camera newCam = camList[i];
                newCam.stereoTargetEye = StereoTargetEyeMask.None;
                newCam.enabled = false;

                subcomponentInfo.Add(newCam, new CamComponentInfo()
                {
                    clearFlags = newCam.clearFlags,
                    clearColor = newCam.backgroundColor,
                });

                if (isForeground)
                {
                    for (int j = 0; j < ApplyCutoffOnEvents.Length; j++)
                        newCam.AddCommandBuffer(ApplyCutoffOnEvents[j], applyCutoffCmd);
                }
            }

            return camList;
        }
        List<Camera> SpawnLayerCameraFromScratch(bool isForeground)
        {
            GameObject newCamObj = new GameObject(isForeground ? ForegroundCamName : FullRenderCamName)
            {
                //hideFlags = HideFlags.HideAndDontSave
            };

            newCamObj.transform.SetParent(rotationTransform != null ? rotationTransform : (positionTransform != null ? positionTransform : transform));
            newCamObj.transform.localPosition = Vector3.zero;
            newCamObj.transform.localRotation = Quaternion.identity;
            newCamObj.transform.localScale = Vector3.one;


            Camera newCam = newCamObj.AddComponent<Camera>();

            newCam.depth = isForeground ? 2 : 1;
            newCam.stereoTargetEye = StereoTargetEyeMask.None;
            newCam.enabled = false;

            subcomponentInfo.Add(newCam, new CamComponentInfo()
            {
                clearFlags = newCam.clearFlags,
                clearColor = newCam.backgroundColor,
            });

            if (isForeground)
            {
                for (int j = 0; j < ApplyCutoffOnEvents.Length; j++)
                    newCam.AddCommandBuffer(ApplyCutoffOnEvents[j], applyCutoffCmd);
            }

            return new List<Camera>() { newCam };
        }

        protected void OnEnable()
        {
            if (string.IsNullOrEmpty(cameraContext.Identifier))
                return;

            requestedFrames = new SharedQueueConsumer<ExpFrame>(string.Format("RequestedExpFrames({0})", cameraContext.Identifier));
            FramePipe = new ExpFrameSender(this);

            ActiveCameras.Add(this);
        }
        protected void OnDisable()
        {
            if (!ActiveCameras.Remove(this))
                return;

            foreach (var kvp in externalTextureTable)
                kvp.Value.Dispose();
            externalTextureTable.Clear();

            requestedFrames.Dispose();
            FramePipe.Dispose();

            ReleaseOutput();
        }

        protected void BuildOutput(int targetWidth, int targetHeight)
        {
            fullRenderTarget = CreateLayerRenderTarget(targetWidth, targetHeight);
            foregroundTarget = CreateLayerRenderTarget(targetWidth, targetHeight);

            float aspect = (float)targetWidth / targetHeight;

            for (int i = 0; i < fullRenderCameraList.Count; i++)
            {
                fullRenderCameraList[i].targetTexture = fullRenderTarget;
                fullRenderCameraList[i].aspect = aspect;
            }
            for (int i = 0; i < foregroundCameraList.Count; i++)
            {
                foregroundCameraList[i].targetTexture = foregroundTarget;
                foregroundCameraList[i].aspect = aspect;
            }

            RenderTextureFormat sharedTexFmt = SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12 ?
                RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGBFloat;   //workaround for DX12 sharing issue
            LayersTexture = new RenderTexture(targetWidth, targetHeight * 2, 0, sharedTexFmt, RenderTextureReadWrite.Linear)
            {
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };
            LayersTexture.Create();

            if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha)
            {
                CleanAlphaTarget = CreateLayerAlphaTarget(targetWidth, targetHeight);

                grabAlphaCommand.Clear();
                grabAlphaCommand.Blit(BuiltinRenderTextureType.CurrentActive, CleanAlphaTarget/*, TransferAlphaMat*/);
            }
        }
        RenderTexture CreateLayerRenderTarget(int width, int height)
        {
            return new RenderTexture(width, height, 24, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear)
            {
                antiAliasing = CalculateAntiAliasingValueForCamera(),
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };
        }
        private static int CalculateAntiAliasingValueForCamera()
        {
            if (MixCastSdkData.ProjectSettings.overrideQualitySettingsAA)
                return 1 << MixCastSdkData.ProjectSettings.overrideAntialiasingVal;    //{unity-antialiasing-units} === 2^{saved-units}
            else
                return Mathf.Max(QualitySettings.antiAliasing, 1);  //Disabled can equal 0 rather than 1
        }
        RenderTexture CreateLayerAlphaTarget(int width, int height)
        {
            return new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear)
            {
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };
        }

        protected void ReleaseOutput()
        {
            for (int i = 0; i < fullRenderCameraList.Count; i++)
                SetCameraAlphaCommandAttached(fullRenderCameraList[i], false);
            for (int i = 0; i < foregroundCameraList.Count; i++)
                SetCameraAlphaCommandAttached(foregroundCameraList[i], false);

            if (CleanAlphaTarget != null)
            {
                CleanAlphaTarget.Release();
                CleanAlphaTarget = null;
            }
            if (fullRenderTarget != null)
            {
                fullRenderTarget.Release();
                fullRenderTarget = null;
            }
            if (foregroundTarget != null)
            {
                foregroundTarget.Release();
                foregroundTarget = null;
            }

            if (LayersTexture != null)
            {
                LayersTexture.Release();
                LayersTexture = null;
            }

            for (int i = 0; i < fullRenderCameraList.Count; i++)
            {
                fullRenderCameraList[i].targetTexture = null;
                fullRenderCameraList[i].ResetAspect();
            }
            for (int i = 0; i < foregroundCameraList.Count; i++)
            {
                foregroundCameraList[i].targetTexture = null;
                foregroundCameraList[i].ResetAspect();
            }
        }

        protected void LateUpdate()
        {
            if (RenderedFrameCount == 0)
                return;

            if (positionTransform != null)
                positionTransform.localPosition = LatestFrameInfo.camPos;
            if (rotationTransform != null)
                rotationTransform.localRotation = LatestFrameInfo.camRot;
        }

        public void RenderIfNeeded()
        {
            double ticksPerFrame = ExpCameraScheduler.AverageMsPerFrame * 10000;

            ulong curTime = MixCastTimestamp.Get();
            ulong syncWindowStart = curTime - (ulong)(ticksPerFrame * 0.75f);
            ulong syncWindowEnd = curTime + (ulong)(ticksPerFrame * 0.5f);

            bool haveFrameToRender = false;

            if (receivedLastFrameTooEarly)
            {
                if (lastReceivedFrame.syncTime <= syncWindowEnd)
                {
                    receivedLastFrameTooEarly = false;
                    LatestFrameInfo.CopyFrom(lastReceivedFrame);

                    if (LatestFrameInfo.syncTime >= syncWindowStart)
                        haveFrameToRender = true;
                    else
                    {
                        if (notifyWhenFrameDropped)
                            Debug.LogWarning(string.Format("Too late to render frame {0}, missed by {1:F2}ms",
                                LatestFrameInfo.frameIndex,
                                1000 * (float)(syncWindowStart - LatestFrameInfo.syncTime) / MixCastTimestamp.TicksPerSecond));
                        requestedFrames.MarkEmptied();
                    }
                }
            }

            if (!receivedLastFrameTooEarly)
            {
                while (!haveFrameToRender && requestedFrames.WaitUntilNextFilled(0))
                {
                    requestedFrames.Read(ref lastReceivedFrame);

                    if (lastReceivedFrame.syncTime > syncWindowEnd)
                    {
                        receivedLastFrameTooEarly = true;
                        break;
                    }

                    LatestFrameInfo.CopyFrom(lastReceivedFrame); //keep the last frame's data regardless of rendering

                    if (LatestFrameInfo.syncTime < syncWindowStart)
                    {
                        if (notifyWhenFrameDropped)
                            Debug.LogWarning(string.Format("Too late to render frame {0}, missed by {1:F2}ms",
                                LatestFrameInfo.frameIndex,
                                1000 * (float)(syncWindowStart - LatestFrameInfo.syncTime) / MixCastTimestamp.TicksPerSecond));
                        requestedFrames.MarkEmptied();
                        continue;
                    }

                    haveFrameToRender = true;
                }
            }

            if (!haveFrameToRender)
                return;

            Render();
            requestedFrames.InsertEmptiedFence();
        }

        void Render()
        {
            if (LayersTexture != null)
            {
                if (LatestFrameInfo.camWidth != fullRenderTarget.width || LatestFrameInfo.camHeight != fullRenderTarget.height)
                    ReleaseOutput();
            }
            if (LayersTexture == null)
                BuildOutput((int)LatestFrameInfo.camWidth, (int)LatestFrameInfo.camHeight);

            if (positionTransform != null)
                positionTransform.localPosition = LatestFrameInfo.camPos;
            if (rotationTransform != null)
                rotationTransform.localRotation = LatestFrameInfo.camRot;

            CurrentlyRendering = this;
            if (FrameStarted != null)
                FrameStarted(this);

            if (LatestFrameInfo.renderFull)
                RenderBackground();
            if (LatestFrameInfo.renderForeground)
                RenderForeground();

            AtlasLayers(LatestFrameInfo.renderFull, LatestFrameInfo.renderForeground);

            if (FrameEnded != null)
                FrameEnded(this);
            CurrentlyRendering = null;

            Graphics.SetRenderTarget(null);

            RenderedFrameCount++;
        }

        void RenderBackground()
        {
            CurrentRenderMode = RenderMode.FullRender;

            RenderCameraStack(fullRenderCameraList, LatestFrameInfo.camFoV, LatestFrameInfo.HasCamFlag(ExpCamFlagBit.Translucent));

            CurrentRenderMode = RenderMode.None;
        }

        void RenderForeground()
        {
            CurrentRenderMode = RenderMode.Foreground;

            bool needsAlpha = LatestFrameInfo.renderFull || LatestFrameInfo.HasCamFlag(ExpCamFlagBit.Translucent);
            if (!needsAlpha)
                ApplyCutoffMat.EnableKeyword("CLIP_FAR");
            else
                ApplyCutoffMat.DisableKeyword("CLIP_FAR");

            ApplyCutoffMat.SetFloat("_PlayerScale", transform.TransformVector(Vector3.forward).magnitude);

            WatchedUnitySharedTexture occlusionTex;
            if (externalTextureTable.TryGetValue(LatestFrameInfo.occlusionTex.ToInt64(), out occlusionTex))
			{
                if(occlusionTex.Texture.width != LatestFrameInfo.camWidth || occlusionTex.Texture.height != LatestFrameInfo.camHeight)
				{
                    occlusionTex.Dispose();
                    occlusionTex = null;
                    externalTextureTable.Remove(LatestFrameInfo.occlusionTex.ToInt64());
				}
			}
            if (occlusionTex == null)
            {
                occlusionTex = new WatchedUnitySharedTexture(MixCastSdkBehaviour.Instance.ClientProc.Handle, LatestFrameInfo.occlusionTex);
                externalTextureTable.Add(LatestFrameInfo.occlusionTex.ToInt64(), occlusionTex);
            }

            occlusionTex.AcquireSync();
            ApplyCutoffMat.SetTexture("_DepthTex", occlusionTex.Texture);
            applyCutoffCmd.Blit(Texture2D.whiteTexture, BuiltinRenderTextureType.CameraTarget, ApplyCutoffMat);    //actual depth texture will be applied not as _MainTex

            for( int i = 0; i < foregroundCameraList.Count; i++ )
                for (int j = 0; j < ApplyCutoffOnEvents.Length; j++)
                    foregroundCameraList[i].AddCommandBuffer(ApplyCutoffOnEvents[j], applyCutoffCmd);

            RenderCameraStack(foregroundCameraList, LatestFrameInfo.camFoV, needsAlpha);

            for (int i = 0; i < foregroundCameraList.Count; i++)
                for (int j = 0; j < ApplyCutoffOnEvents.Length; j++)
                    foregroundCameraList[i].RemoveCommandBuffer(ApplyCutoffOnEvents[j], applyCutoffCmd);

            applyCutoffCmd.Clear();

            occlusionTex.ReleaseSync();
            CurrentRenderMode = RenderMode.None;
        }

        void RenderCameraStack(List<Camera> cameras, float fieldOfView, bool needsAlpha)
        {
            RenderTexture.active = cameras[0].targetTexture;
            GL.Clear(true, true, Color.clear);
            RenderTexture.active = null;

            bool bgIsTranslucent = LatestFrameInfo.HasCamFlag(ExpCamFlagBit.Translucent);

            bool firstCam = true;
            for (int i = 0; i < cameras.Count; i++)
            {
                if (!cameras[i].gameObject.activeInHierarchy)
                    continue;

                if (!Mathf.Approximately(cameras[i].fieldOfView, fieldOfView))
                    cameras[i].fieldOfView = fieldOfView;

                LayerMask includeMask = bgIsTranslucent ? MixCastSdkData.ProjectSettings.transparentOnlyBgLayers : MixCastSdkData.ProjectSettings.opaqueOnlyBgLayers;
                LayerMask excludeMask = bgIsTranslucent ? MixCastSdkData.ProjectSettings.opaqueOnlyBgLayers : MixCastSdkData.ProjectSettings.transparentOnlyBgLayers;
                cameras[i].cullingMask = (cameras[i].cullingMask | includeMask) & ~excludeMask;

                Color oldClearColor = cameras[i].backgroundColor;
                CameraClearFlags oldClearFlags = cameras[i].clearFlags;
                if (needsAlpha && firstCam)
                {
                    cameras[i].backgroundColor = Color.clear;
                    cameras[i].clearFlags = CameraClearFlags.Color;
                }

                if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha)
                    SetCameraAlphaCommandAttached(cameras[i], needsAlpha);

                cameras[i].Render();

                if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha && needsAlpha)
                    Graphics.Blit(CleanAlphaTarget, cameras[i].targetTexture, TransferAlphaMat);

                if (needsAlpha && firstCam)
                {
                    cameras[i].backgroundColor = oldClearColor;
                    cameras[i].clearFlags = oldClearFlags;
                }

                firstCam = false;
            }
        }
        void SetCameraAlphaCommandAttached(Camera cam, bool attach)
        {
            CamComponentInfo camInfo = subcomponentInfo[cam];
            if (camInfo.hasGrabAlphaCommand == attach)
                return;

            if (attach)
                cam.AddCommandBuffer(CameraEvent.AfterForwardAlpha, grabAlphaCommand);
            else
                cam.RemoveCommandBuffer(CameraEvent.AfterForwardAlpha, grabAlphaCommand);
            camInfo.hasGrabAlphaCommand = attach;
        }

        void AtlasLayers(bool haveFullRender, bool haveForeground)
        {
            GL.PushMatrix();

            RenderTexture.active = LayersTexture;
            GL.LoadPixelMatrix(0, 1, 2, 0);

            if (haveFullRender)
                Graphics.DrawTexture(new Rect(0, 0, 1, 1), fullRenderTarget, transferResultsMat);
            if (haveForeground)
                Graphics.DrawTexture(new Rect(0, 1, 1, 1), foregroundTarget, transferResultsMat);

            RenderTexture.active = null;
            GL.PopMatrix();
        }
    }
}
#endif
