using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.XR;

namespace Escape_Room_Engine.Portal
{
    [RequireComponent(typeof(Camera))]
    public class PortalCamera : MonoBehaviour
    {
        private static readonly Camera.StereoscopicEye[] Eyes =
            { Camera.StereoscopicEye.Left, Camera.StereoscopicEye.Right };
        private static readonly Dictionary<Camera.StereoscopicEye, int> EyeTextureNames = new()
        {
            { Camera.StereoscopicEye.Left, Shader.PropertyToID("_LeftTex") },
            { Camera.StereoscopicEye.Right, Shader.PropertyToID("_RightTex") }
        };

        /// <summary>
        /// The minimum near clip plane distance to be used when calculating the oblique clip plane.
        /// </summary>
        public float minNearClipPlane = 0.0001f;
        /// <summary>
        /// The portal this camera renders through.
        /// </summary>
        [SerializeField] private Portal portal;
        /// <summary>
        /// The mesh where the rendered texture will be drawn on.
        /// </summary>
        [SerializeField] private MeshRenderer screen;

        private PlayerCamera _playerCamera;
        private Camera _camera;
        private readonly Dictionary<Camera.StereoscopicEye, RenderTexture> _textures = new();

        private void Awake()
        {
            // get player camera
            if (Camera.main != null) _playerCamera = Camera.main.GetComponent<PlayerCamera>();
            else throw new Exception("Main camera has no player camera script set up.");
            
            // get portal camera
            _camera = GetComponent<Camera>();
        }

        private void OnEnable()
        {
            RenderPipelineManager.beginCameraRendering += Render;
        }

        private void OnDisable()
        {
            RenderPipelineManager.beginCameraRendering -= Render;
        }
        
        private void Render(ScriptableRenderContext scriptableRenderContext, Camera _)
        {
            // check whether the portal plane is visible from the player camera
            var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(_playerCamera.camera);
            if (!GeometryUtility.TestPlanesAABB(frustumPlanes, portal.linkedPortal.portalCamera.screen.bounds))
                // don't render this portal if it is not visible
                return;
            
            var t = portal.transform;

            screen.enabled = false;

            foreach (var eye in Eyes)
            {
                // create portal texture if it doesn't exist yet
                if (!_textures.ContainsKey(eye))
                {
                    if (XRSettings.eyeTextureWidth > 0 && XRSettings.eyeTextureHeight > 0)
                    {
                        _textures.Add(eye,
                            new RenderTexture(XRSettings.eyeTextureWidth, XRSettings.eyeTextureHeight, 24));
                        portal.linkedPortal.portalCamera.screen.material.SetTexture(EyeTextureNames[eye], _textures[eye]);
                    }
                    else // no texture was created so nothing should be rendered
                        continue;
                }
                
                // position portal camera
                var m = t.localToWorldMatrix * Portal.HalfRotation * portal.linkedPortal.transform.worldToLocalMatrix *
                        _playerCamera.GetEyeTransform(eye).localToWorldMatrix;
                transform.SetPositionAndRotation(m.GetPosition(), m.rotation);
                _camera.projectionMatrix = _playerCamera.camera.GetStereoProjectionMatrix(eye);
        
                // set camera clip plane to portal (otherwise the wall behind the portal would be rendered)
                // calculating the clip plane: https://computergraphics.stackexchange.com/a/1506
                var n = -t.forward; // clip plane normal
                var portalPlane = new Plane(n, t.position); // clip plane in world space
                var clipPlane = _camera.worldToCameraMatrix.inverse.transpose *
                                new Vector4(n.x, n.y, n.z, portalPlane.distance); // vector format clip plane in camera space
                if (-portalPlane.GetDistanceToPoint(transform.position) >= minNearClipPlane)
                    // only adjust the near clip plane if it doesn't intersect with the camera
                    _camera.projectionMatrix = _camera.CalculateObliqueMatrix(clipPlane);
        
                // render portal view
                _camera.targetTexture = _textures[eye];
                UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, _camera);
            }

            screen.enabled = true;
        }
    }
}
