/**********************************************************************************
* Blueprint Reality Inc. CONFIDENTIAL
* 2020 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.Textures;
using BlueprintReality.MixCast.Data;
using BlueprintReality.MixCast.Shared;
using BlueprintReality.SharedTextures;
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, VirtualCamera> 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; }

        private SharedTextureReceiver fgCutoffReceiver;
        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 bool FullRenderSetAsTransparent { get; protected set; }

        public RenderMode CurrentRenderMode { get; protected set; }

        public FrameTimer Timer { 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" };

            SpawnSceneLayerCameras();

            Timer = new FrameTimer();
        }
        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()
        {
            ActiveCameras.Add(this);
            if (!string.IsNullOrEmpty(cameraContext.Identifier))
            {
                fgCutoffReceiver = SharedTextureReceiver.Create(SharedTexIds.Cameras.ForegroundCutoff.Get(cameraContext.Identifier));
                fgCutoffReceiver.OnTextureChanged += HandleForegroundCutoffTextureChanged;

                HandleForegroundCutoffTextureChanged();

                FramePipe = new ExpFrameSender(this);
            }
        }
        protected void OnDisable()
        {
            ActiveCameras.Remove(this);
            if (fgCutoffReceiver != null)
            {
                fgCutoffReceiver.OnTextureChanged -= HandleForegroundCutoffTextureChanged;
                fgCutoffReceiver.Dispose();
                fgCutoffReceiver = null;
            }
            if (FramePipe != null)
            {
                FramePipe.Dispose();
                FramePipe = null;
            }
            ReleaseOutput();
        }

        protected void BuildOutput()
        {
            VirtualCamera cam = MixCastSdkData.GetCameraWithId(cameraContext.Identifier);

            fullRenderTarget = CreateLayerRenderTarget(cam.RenderResolutionWidth, cam.RenderResolutionHeight);
            foregroundTarget = CreateLayerRenderTarget(cam.RenderResolutionWidth, cam.RenderResolutionHeight);

            float aspect = (float)cam.RenderResolutionWidth / cam.RenderResolutionHeight;

            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;
            }

            LayersTexture = new RenderTexture(cam.RenderResolutionWidth, cam.RenderResolutionHeight * 2, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear)
            {
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };
            LayersTexture.Create();

            if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha)
            {
                CleanAlphaTarget = CreateLayerAlphaTarget(cam.RenderResolutionWidth, cam.RenderResolutionHeight);

                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()
        {
            VirtualCamera cam = MixCastSdkData.GetCameraWithId(cameraContext.Identifier);
            if (cam == null)
                return;

            if (positionTransform != null)
                positionTransform.localPosition = cam.CurrentPosition.unity;
            if (rotationTransform != null)
                rotationTransform.localRotation = cam.CurrentRotation.unity;
        }

        public void RenderIfNeeded()
        {
            VirtualCamera camInfo = MixCastSdkData.GetCameraWithId(cameraContext.Identifier);
            if (camInfo == null)
                return;

            if (camInfo.RenderFramerate == 0)
                return;

            bool justStartedTimer = false;
            if (!Timer.IsStarted || Timer.FrameRate != camInfo.RenderFramerate ) {
                Timer.Start(camInfo.RenderFramerate);
                justStartedTimer = true;
            }

            ulong oldSyncTime = Timer.LastIdealTime;
            ulong oldFrameIndex = Timer.LastFrameIndex;
            Timer.Update();
            ulong elapsedFrames = Timer.LastFrameIndex - oldFrameIndex;
            if (!justStartedTimer && elapsedFrames == 0)
                return;

            ulong oldMidnightIndex = FrameTimer.GetMidnightRelativeFrameIndex(oldSyncTime, camInfo.RenderFramerate);
            ulong newMidnightIndex = FrameTimer.GetMidnightRelativeFrameIndex(Timer.LastIdealTime, camInfo.RenderFramerate);
            if (!justStartedTimer && oldMidnightIndex == newMidnightIndex)
                UnityEngine.Debug.LogError(string.Format("ExpCameraBehaviour: Duplicate 'from-midnight' index detected from both {0} and {1}!", oldSyncTime, Timer.LastIdealTime));

            if (elapsedFrames > 1 && notifyWhenFrameDropped)
            {
                ulong oldSyncFrameIndexSinceMidnight = FrameTimer.GetMidnightRelativeFrameIndex(oldSyncTime, camInfo.RenderFramerate);
                ulong repeatFramesCount = elapsedFrames - 1;
                if (repeatFramesCount > 1)
                    UnityEngine.Debug.LogWarning(string.Format("Experience dropped frames {0} - {1} for camera {2}",
                        MixCastTimestamp.GetSMPTE(oldSyncFrameIndexSinceMidnight + 1, camInfo.RenderFramerate),
                        MixCastTimestamp.GetSMPTE(oldSyncFrameIndexSinceMidnight + repeatFramesCount, camInfo.RenderFramerate),
                        camInfo.Identifier));
                else
                    UnityEngine.Debug.LogWarning(string.Format("Experience dropped frame {0} for camera {1}",
                        MixCastTimestamp.GetSMPTE(oldSyncFrameIndexSinceMidnight + 1, camInfo.RenderFramerate),
                        camInfo.Identifier));
            }

            Render(camInfo);
        }

        void Render(VirtualCamera camInfo)
        {
            if (!fgCutoffReceiver.RequestSucceeded)
                fgCutoffReceiver.RefreshTextureInfo();

            if (LayersTexture != null)
            {
                if (camInfo.RenderResolutionWidth != fullRenderTarget.width || camInfo.RenderResolutionHeight != fullRenderTarget.height)
                    ReleaseOutput();
            }
            if (LayersTexture == null)
                BuildOutput();

            if (positionTransform != null)
                positionTransform.localPosition = camInfo.CurrentPosition.unity;
            if (rotationTransform != null)
                rotationTransform.localRotation = camInfo.CurrentRotation.unity;

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

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

            if (camInfo.UsesFullRender)
                RenderBackground(camInfo);
            if (camInfo.UsesForeground && fgCutoffReceiver.Texture != null)
                RenderForeground(camInfo);

            AtlasLayers(camInfo.UsesFullRender, camInfo.UsesForeground && fgCutoffReceiver.Texture != null);

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

            Graphics.SetRenderTarget(null);

            RenderedFrameCount++;
        }

        void RenderBackground(VirtualCamera camInfo)
        {
            CurrentRenderMode = RenderMode.FullRender;

            RenderCameraStack(camInfo, fullRenderCameraList, (float)camInfo.FieldOfView, camInfo.IsBackgroundTranslucent);

            CurrentRenderMode = RenderMode.None;
        }

        void RenderForeground(VirtualCamera camInfo)
        {
            CurrentRenderMode = RenderMode.Foreground;

            bool needsAlpha = camInfo.UsesFullRender || camInfo.IsBackgroundTranslucent;
            if (!needsAlpha)
                ApplyCutoffMat.EnableKeyword("CLIP_FAR");
            else
                ApplyCutoffMat.DisableKeyword("CLIP_FAR");

            RenderCameraStack(camInfo, foregroundCameraList, (float)camInfo.FieldOfView, needsAlpha);

            CurrentRenderMode = RenderMode.None;
        }

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

            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 = camInfo.IsBackgroundTranslucent ? MixCastSdkData.ProjectSettings.transparentOnlyBgLayers : MixCastSdkData.ProjectSettings.opaqueOnlyBgLayers;
                LayerMask excludeMask = camInfo.IsBackgroundTranslucent ? 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);

            Graphics.DrawTexture(new Rect(0, 0, 1, 1), haveFullRender ? (Texture)fullRenderTarget : clearTex, transferResultsMat);
            Graphics.DrawTexture(new Rect(0, 1, 1, 1), haveForeground ? (Texture)foregroundTarget : clearTex, transferResultsMat);

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


        void HandleForegroundCutoffTextureChanged()
        {
            ApplyCutoffMat.SetTexture("_DepthTex", fgCutoffReceiver.Texture);
            applyCutoffCmd.Clear();
            applyCutoffCmd.Blit(Texture2D.whiteTexture, BuiltinRenderTextureType.CameraTarget, ApplyCutoffMat);    //actual depth texture will be applied not as _MainTex

            foregroundCameraList.ForEach(fgCam =>
            {
                for( int i = 0; i < ApplyCutoffOnEvents.Length; i++ )
                {
                    fgCam.RemoveCommandBuffer(ApplyCutoffOnEvents[i], applyCutoffCmd);
                    fgCam.AddCommandBuffer(ApplyCutoffOnEvents[i], applyCutoffCmd);
                }
            });
        }
    }
}
#endif
