using System.Collections.Generic;
using EscapeRoomEngine.VR.Runtime;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace EscapeRoomEngine.Portal.Runtime
{
    public class RenderPortalCameras : ScriptableRendererFeature
    {
        private enum PassType
        {
            Opaque,
            Transparent
        }

        private class PortalCameraRenderPass : ScriptableRenderPass
        {
            private static readonly List<ShaderTagId> ShaderTagIds = new()
            {
                new ShaderTagId("SRPDefaultUnlit"), new ShaderTagId("UniversalForward"), new ShaderTagId("UniversalForwardOnly")
            };

            private RenderStateBlock _renderStateBlock;
            private CommandBuffer _commandBuffer;

            private readonly LayerMask _layers;
            private readonly PassType _passType;

            public PortalCameraRenderPass(LayerMask layers, PassType passType)
            {
                _layers = layers;
                _passType = passType;

                renderPassEvent = passType == PassType.Opaque ? RenderPassEvent.AfterRenderingOpaques : RenderPassEvent.AfterRenderingSkybox;
                _renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);

                // set stencil
                var stencilState = StencilState.defaultValue;
                stencilState.enabled = true;
                stencilState.SetCompareFunction(CompareFunction.Equal);
                stencilState.SetPassOperation(StencilOp.Keep);
                stencilState.SetFailOperation(StencilOp.Keep);
                stencilState.SetZFailOperation(StencilOp.Keep);
                _renderStateBlock.mask |= RenderStateMask.Stencil;
                _renderStateBlock.stencilState = stencilState;
            }

            public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
            {
                // we need to store a reference to the command buffer before executing, because the reference in RenderingData is internal
                _commandBuffer = cmd;
            }

            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                if (Player.Instance)
                {
                    var drawingSettings = CreateDrawingSettings(ShaderTagIds, ref renderingData, _passType == PassType.Opaque ? renderingData.cameraData.defaultOpaqueSortFlags : SortingCriteria.CommonTransparent);
                    var filteringSettings = new FilteringSettings(_passType == PassType.Opaque ? RenderQueueRange.opaque : RenderQueueRange.transparent, _layers);
                    
                    foreach (var portal in FindObjectsByType<Portal>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
                    {
                        var linkedPortal = portal.linkedPortal;
                        if (linkedPortal)
                        {
                            // set which portal to render
                            _renderStateBlock.stencilReference = portal.PortalNumber;
                            
                            // prepare the camera
                            ref var cameraData = ref renderingData.cameraData;
                            var camera = cameraData.camera;
                            var cameraTransform = camera.transform;
                            var originalPosition = cameraTransform.position;
                            var originalRotation = cameraTransform.rotation;
                            var originalProjection = camera.projectionMatrix;
                            camera = linkedPortal.SetUpCamera(camera,
                                camera.stereoEnabled
                                    ? cameraData.xr.multipassId == 0
                                        ? Camera.MonoOrStereoscopicEye.Left
                                        : Camera.MonoOrStereoscopicEye.Right
                                    : Camera.MonoOrStereoscopicEye.Mono);
                            RenderingUtils.SetViewAndProjectionMatrices(_commandBuffer,
                                camera.worldToCameraMatrix,
                                GL.GetGPUProjectionMatrix(camera.projectionMatrix, true), false);

                            // execute command buffer
                            context.ExecuteCommandBuffer(_commandBuffer);
                            _commandBuffer.Clear();
                            
                            // frustum culling
                            camera.TryGetCullingParameters(true, out var cullingParameters);
                            var cullingResults = context.Cull(ref cullingParameters);

                            // render the portal
                            context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings, ref _renderStateBlock);

                            // reset
                            RenderingUtils.SetViewAndProjectionMatrices(_commandBuffer, cameraData.GetViewMatrix(), cameraData.GetGPUProjectionMatrix(), false);
                            camera.transform.SetPositionAndRotation(originalPosition, originalRotation);
                            camera.projectionMatrix = originalProjection;
                        }
                    }
                }
            }
        }

        private PortalCameraRenderPass _opaquePass, _transparentPass;

        public LayerMask layers;

        public override void Create()
        {
            _opaquePass = new PortalCameraRenderPass(layers, PassType.Opaque);
            _transparentPass = new PortalCameraRenderPass(layers, PassType.Transparent);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            renderer.EnqueuePass(_opaquePass);
            renderer.EnqueuePass(_transparentPass);
        }
    }
}