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

        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 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;
        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 string layersTexId;
        private SharedTextureSender layersSender = new SharedTextureSender(true);
        

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

            grabAlphaCommand = new CommandBuffer() { name = "Get Correct Alpha" };
            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" };
            applyCutoffCmd.Blit(Texture2D.whiteTexture, BuiltinRenderTextureType.CameraTarget, ApplyCutoffMat);    //actual depth texture will be applied not as _MainTex

            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)
                {
                    newCam.AddCommandBuffer(GetEventForCutoff(newCam.actualRenderingPath), 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)
            {
                newCam.AddCommandBuffer(GetEventForCutoff(newCam.actualRenderingPath), applyCutoffCmd);
            }

            return new List<Camera>() { newCam };
        }
        static CameraEvent GetEventForCutoff(RenderingPath path)
        {
            return path == RenderingPath.Forward ?
                CameraEvent.BeforeForwardOpaque :
                CameraEvent.BeforeGBuffer;
        }

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

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

            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 camInfo)
        {
            CurrentRenderMode = RenderMode.FullRender;

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

            CurrentRenderMode = RenderMode.None;
        }

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

            ApplyCutoffMat.SetTexture("_DepthTex", fgCutoffReceiver.Texture);

            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();
        }
    }
}
#endif
