/**********************************************************************************
* Blueprint Reality Inc. CONFIDENTIAL
* 2019 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;

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 const float MaxDepthInCutoffTexture = 65.525f;

        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;
        private Material applyCutoffMat;
        private CommandBuffer applyCutoffCmd;

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

        private CommandBuffer grabAlphaCommand;
        private CommandBuffer replaceAlphaCommand;
        private Material copyAlphaMat;
        private RenderTexture cleanForegroundTarget;

        private Material transferResultsMat;

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

        public uint RenderedFrameCount { get; protected set; }

#if MIXCAST_LWRP
        List<InsertCmdBufferBehaviour_AfterTransparent> foregroundAlphaGrabCmdInserters = new List<InsertCmdBufferBehaviour_AfterTransparent>();
        List<InsertCmdBufferBehaviour_AfterEverything> foregroundAlphaSetCmdInserters = new List<InsertCmdBufferBehaviour_AfterEverything>();
#endif

        protected void Awake()
        {
            copyAlphaMat = 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.CurrentActive, 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);
                replaceAlphaCommand = new CommandBuffer() { name = "Set Correct Alpha" };
#if UNITY_5_5_OR_NEWER
                replaceAlphaCommand.Blit(cleanForegroundTarget as RenderTexture, BuiltinRenderTextureType.CurrentActive, copyAlphaMat);
#else
                replaceAlphaCommand.Blit(cleanForegroundTarget as Texture, BuiltinRenderTextureType.CurrentActive, copyAlphaMat);
#endif

                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
                    foregroundCameraList[i].AddCommandBuffer(CameraEvent.AfterImageEffects, replaceAlphaCommand);   //Instruction to paste in the state of the RenderTexture from before Image Effects are applied

#if MIXCAST_LWRP
                    if (GraphicsSettings.renderPipelineAsset is UnityEngine.Experimental.Rendering.LightweightPipeline.LightweightRenderPipelineAsset)
                    {
                        InsertCmdBufferBehaviour_AfterTransparent foregroundAlphaGrabCmdInserter = foregroundCameraList[i].gameObject.AddComponent<InsertCmdBufferBehaviour_AfterTransparent>();
                        foregroundAlphaGrabCmdInserter.Create("AfterTransparentMixCastPass", (c) =>
                        {
                            c.ExecuteCommandBuffer(grabAlphaCommand);
                        });
                        foregroundAlphaGrabCmdInserters.Add(foregroundAlphaGrabCmdInserter);

                        InsertCmdBufferBehaviour_AfterEverything foregroundAlphaSetCmdInserter = foregroundCameraList[i].gameObject.AddComponent<InsertCmdBufferBehaviour_AfterEverything>();
                        foregroundAlphaSetCmdInserter.Create("AfterEverythingMixCastPass", (c) =>
                        {
                            c.ExecuteCommandBuffer(replaceAlphaCommand);
                        });
                        foregroundAlphaSetCmdInserters.Add(foregroundAlphaSetCmdInserter);
                    }
#endif
                }
            }
        }
        protected void ReleaseOutput()
        {
            if (cleanForegroundTarget != null)
            {
                for (int i = 0; i < foregroundCameraList.Count; i++)
                {
                    foregroundCameraList[i].RemoveCommandBuffer(CameraEvent.AfterForwardAlpha, grabAlphaCommand);
                    foregroundCameraList[i].RemoveCommandBuffer(CameraEvent.AfterImageEffects, replaceAlphaCommand);
                }
#if MIXCAST_LWRP
                for( int i = 0; i < foregroundAlphaGrabCmdInserters.Count; i++ ) 
                {
                    Destroy(foregroundAlphaGrabCmdInserters[i]);    
                }
                for( int i = 0; i < foregroundAlphaSetCmdInserters.Count; i++ ) 
                {
                    Destroy(foregroundAlphaSetCmdInserters[i]);    
                }
#endif
                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)
        {
            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();
            }
        }

        bool RenderForeground(VirtualCamera cam)
        {
            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();
            }

            return true;
        }

        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 : Texture2D.blackTexture, transferResultsMat);
            Graphics.DrawTexture(new Rect(0, 1, 1, 1), haveForeground ? (Texture)foregroundTarget : Texture2D.blackTexture, transferResultsMat);

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