/**********************************************************************************
* 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.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";
        }

        public enum RenderMode
        {
            None, FullRender, Foreground
        }

        public const float MaxDepthInCutoffTexture = 65.525f;

        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;

        private RenderTexture fullRenderTarget, foregroundTarget;

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

        public RenderTexture LayersTexture { get; protected set; }
        private string layersTexId;
        private SharedTextureSender layersSender = new SharedTextureSender(true);

        private CommandBuffer grabAlphaCommand;
        public Material TransferAlphaMat { get; protected set; }
        public RenderTexture CleanForegroundTarget { get; protected set; }

        private Material transferResultsMat;

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

        public RenderMode CurrentRenderMode { get; protected set; }
        public uint RenderedFrameCount { 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();
            }

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

#if UNITY_EDITOR && !UNITY_5_5_OR_NEWER
            BuiltinRenderTextureType blitTarget = BuiltinRenderTextureType.CameraTarget;
#else
            BuiltinRenderTextureType blitTarget = BuiltinRenderTextureType.CurrentActive;
#endif
            applyCutoffCmd.Blit(Texture2D.whiteTexture, blitTarget, ApplyCutoffMat);    //actual depth texture will be applied not as _MainTex

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

            spawnedCamObj.name = isForeground ? "Foreground Camera" : "Full Render Camera";

            List<Camera> camList = isForeground ? foregroundCameraList : fullRenderCameraList;
            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;
            }

            if (isForeground)
            {
                SetCameraParametersFromMainCamera[] copyParamsScript = spawnedCamObj.GetComponentsInChildren<SetCameraParametersFromMainCamera>(true);
                for (int i = 0; i < copyParamsScript.Length; i++)
                {
                    copyParamsScript[i].clearSettings = false;
                }
                foreach (Camera newCam in camList)
                    newCam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, applyCutoffCmd);
            }

            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;
        }
        void SpawnLayerCameraFromScratch(bool isForeground)
        {
            GameObject newCamObj = new GameObject(isForeground ? "Foreground Camera" : "Full Render Camera")
            {
                //hideFlags = HideFlags.HideAndDontSave
            };

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

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

            SetCameraParametersFromMainCamera copyParamsScript = newCamObj.AddComponent<SetCameraParametersFromMainCamera>();

            if (isForeground)
            {
                copyParamsScript.clearSettings = false;
                CameraEvent insertDepthAtEv = newCam.actualRenderingPath == RenderingPath.Forward ? CameraEvent.BeforeForwardOpaque : CameraEvent.BeforeGBuffer;
                newCam.AddCommandBuffer(insertDepthAtEv, applyCutoffCmd);
            }

            List<Camera> camList = isForeground ? foregroundCameraList : fullRenderCameraList;
            camList.Add(newCam);

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

        protected void OnEnable()
        {
            ActiveCameras.Add(this);
            if (!string.IsNullOrEmpty(cameraContext.Identifier))
            {
                layersTexId = SharedTexIds.Cameras.ExperienceLayers.Get(cameraContext.Identifier);
                fgCutoffReceiver = SharedTextureReceiver.Create(SharedTexIds.Cameras.ForegroundCutoff.Get(cameraContext.Identifier));
            }
        }
        protected void OnDisable()
        {
            ActiveCameras.Remove(this);
            if (fgCutoffReceiver != null)
            {
                fgCutoffReceiver.Dispose();
                fgCutoffReceiver = null;
            }
            ReleaseOutput();
        }

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

            fullRenderTarget = new RenderTexture(cam.RenderResolutionWidth, cam.RenderResolutionHeight, 24, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear)
            {
                antiAliasing = SdkCameraUtilities.CalculateAntiAliasingValueForCamera(),
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };
            foregroundTarget = new RenderTexture(cam.RenderResolutionWidth, cam.RenderResolutionHeight, 24, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear)
            {
                antiAliasing = SdkCameraUtilities.CalculateAntiAliasingValueForCamera(),
                useMipMap = false,
#if UNITY_5_5_OR_NEWER
                autoGenerateMips = false,
#else
                generateMips = false,
#endif
            };

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

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

            if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha)
            {
                CleanForegroundTarget = new RenderTexture(cam.RenderResolutionWidth, cam.RenderResolutionHeight, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear)
                {
                    useMipMap = false,
#if UNITY_5_5_OR_NEWER
                    autoGenerateMips = false,
#else
                    generateMips = false,
#endif
                };

                grabAlphaCommand = new CommandBuffer() { name = "Get Correct Alpha" };
                grabAlphaCommand.Blit(BuiltinRenderTextureType.CurrentActive, CleanForegroundTarget/*, TransferAlphaMat*/);

                for (int i = 0; i < foregroundCameraList.Count; i++)
                {
                    foregroundCameraList[i].AddCommandBuffer(CameraEvent.AfterForwardAlpha, grabAlphaCommand);   //Instruction to copy out the state of the RenderTexture before Image Effects are applied
                }
            }
        }
        protected void ReleaseOutput()
        {
            if (CleanForegroundTarget != null)
            {
                for (int i = 0; i < foregroundCameraList.Count; i++)
                {
                    foregroundCameraList[i].RemoveCommandBuffer(CameraEvent.AfterForwardAlpha, grabAlphaCommand);
                }

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

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

            if (LayersTexture != null && !cam.VideoRecordingEnabled && !cam.VideoStreamingEnabled)
            {
                if (cam.RenderResolutionWidth != fullRenderTarget.width || cam.RenderResolutionHeight != fullRenderTarget.height)
                    ReleaseOutput();
            }
            if( LayersTexture == null )
                BuildOutput();

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

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

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

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

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

            Graphics.SetRenderTarget(null);

            RenderedFrameCount++;
            layersSender.UpdateFromTexture(layersTexId, LayersTexture);
        }

        void RenderBackground(VirtualCamera cam)
        {
            CurrentRenderMode = RenderMode.FullRender;
            for (int i = 0; i < fullRenderCameraList.Count; i++)
            {
                if (!fullRenderCameraList[i].gameObject.activeInHierarchy)
                    continue;

                if (!Mathf.Approximately(fullRenderCameraList[i].fieldOfView, (float)cam.FieldOfView))
                    fullRenderCameraList[i].fieldOfView = (float)cam.FieldOfView;

                fullRenderCameraList[i].Render();
            }
            CurrentRenderMode = RenderMode.None;
        }

        void RenderForeground(VirtualCamera cam)
        {
            CurrentRenderMode = RenderMode.Foreground;
            ApplyCutoffMat.SetTexture("_DepthTex", fgCutoffReceiver.Texture);
            for (int i = 0; i < foregroundCameraList.Count; i++)
            {
                if (!foregroundCameraList[i].gameObject.activeInHierarchy)
                    continue;

                if (!Mathf.Approximately(foregroundCameraList[i].fieldOfView, (float)cam.FieldOfView))
                    foregroundCameraList[i].fieldOfView = (float)cam.FieldOfView;

                if (i == 0)
                    foregroundCameraList[i].clearFlags = CameraClearFlags.Color;
                else
                    foregroundCameraList[i].clearFlags = (foregroundCameraList[i].clearFlags != CameraClearFlags.Nothing) ? CameraClearFlags.Depth : CameraClearFlags.Nothing;
                foregroundCameraList[i].backgroundColor = Color.clear;

                foregroundCameraList[i].Render();
                if (MixCastSdkData.ProjectSettings.grabUnfilteredAlpha)
                    Graphics.Blit(CleanForegroundTarget, foregroundTarget, TransferAlphaMat);
            }
            CurrentRenderMode = RenderMode.None;
        }

        void AtlasLayers(bool haveFullRender, bool haveForeground)
        {
            RenderTexture.active = LayersTexture;
            GL.PushMatrix();
            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);

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