using System;
using System.Collections.Generic;
using EscapeRoomEngine.Engine.Runtime.Modules.State;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using EscapeRoomEngine.VR.Runtime;
using NaughtyAttributes;
using UnityEngine;

namespace EscapeRoomEngine.Portal.Runtime
{
    /// <summary>
    /// The portal is a specific type of <see cref="DoorState"/> for seamless transitions between spaces.
    /// </summary>
    public class Portal : DoorState
    {
        private static readonly int PortalNumberProperty = Shader.PropertyToID("_PortalNumber");
        public static readonly Matrix4x4 HalfRotation = Matrix4x4.Rotate(Quaternion.Euler(0, 180, 0));

        private static int _portalCounter = 1;
        
        /// <summary>
        /// The portal that is connected with this one.
        /// </summary>
        public Portal linkedPortal;
        /// <summary>
        /// The minimum near clip plane distance to be used when calculating the oblique clip plane.
        /// </summary>
        public float minNearClipPlane = 0.0001f;
        /// <summary>
        /// The meshes where the portal will draw its stencil and depth.
        /// </summary>
        [BoxGroup("Internal")] [SerializeField]
        private MeshRenderer screen;
        /// <summary>
        /// The transform marking the edge of the portal plane.
        /// </summary>
        [BoxGroup("Internal")] public Transform portalTransform;

        /// <summary>
        /// Whether this portal is connected is determined by whether the reference to its linked portal is set.
        /// </summary>
        internal bool Connected => linkedPortal != null;
        [ShowNativeProperty] internal int PortalNumber { get; private set; }

        internal readonly List<PortalDriver> closePortalDrivers = new();

        protected virtual void Awake()
        {
            PortalNumber = _portalCounter++;
            screen.material.SetInt(PortalNumberProperty, PortalNumber);
            
            DoorEvent += (_, type) =>
            {
                // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
                switch (type)
                {
                    case DoorEventType.Connected:
                        linkedPortal = FromDoorState(Module.ConnectedDoorState);
                        screen.gameObject.SetActive(true);
                        break;
                    case DoorEventType.Locked:
                        screen.gameObject.SetActive(false);
                        linkedPortal = null;
                        break;
                }
            };
        }

        private void FixedUpdate()
        {
            if (Connected)
            {
                for (var i = 0; i < closePortalDrivers.Count; i++)
                {
                    var portalDriver = closePortalDrivers[i];
                    if (portalDriver.entrySide < 0 && CalculateSide(portalDriver.transform) >= 0) // must have entered from the front and exited the back
                    {
                        StopTrackingDriver(portalDriver);
                        linkedPortal.StartTrackingDriver(portalDriver, -1);
                        portalDriver.Teleport(this, linkedPortal);
                        if (portalDriver.player)
                        {
                            linkedPortal.ExitFrom();
                        }
                        i--; // decrease the loop counter because the list is one element smaller now
                    }
                }
            }
        }

        public Camera SetUpCamera(Camera playerCamera, Camera.MonoOrStereoscopicEye monoOrEye)
        {
            // place camera
            Matrix4x4 cameraTransform;
            if (monoOrEye == Camera.MonoOrStereoscopicEye.Mono || playerCamera != Player.Instance.camera)
            {
                cameraTransform = portalTransform.localToWorldMatrix * HalfRotation *
                                  linkedPortal.portalTransform.worldToLocalMatrix *
                                  playerCamera.transform.localToWorldMatrix;
            }
            else
            {
                var eye = monoOrEye == Camera.MonoOrStereoscopicEye.Left
                    ? Camera.StereoscopicEye.Left
                    : Camera.StereoscopicEye.Right;
                cameraTransform = portalTransform.localToWorldMatrix * HalfRotation *
                                  linkedPortal.portalTransform.worldToLocalMatrix *
                                  Player.Instance.GetEye(eye).localToWorldMatrix;
                playerCamera.projectionMatrix = playerCamera.GetStereoProjectionMatrix(eye);
            }
            playerCamera.transform.SetPositionAndRotation(cameraTransform.GetPosition(), cameraTransform.rotation);
        
            // 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
            // clip plane normal
            var n = -portalTransform.forward;
            // clip plane in world space
            var portalPlane = new Plane(n, portalTransform.position);
            if (-portalPlane.GetDistanceToPoint(playerCamera.transform.position) >= minNearClipPlane)
            {
                // vector format clip plane in camera space
                var clipPlane = playerCamera.worldToCameraMatrix.inverse.transpose *
                                new Vector4(n.x, n.y, n.z, portalPlane.distance);
                // only adjust the near clip plane if it doesn't intersect with the camera
                playerCamera.projectionMatrix = playerCamera.CalculateObliqueMatrix(clipPlane);
            }

            return playerCamera;
        }

        /// <summary>
        /// Begin tracking a portal driver that came close to this portal and might need to be teleported.
        /// </summary>
        internal void StartTrackingDriver(PortalDriver portalDriver, int entrySide)
        {
            closePortalDrivers.Add(portalDriver);
            portalDriver.EnableClone(linkedPortal);
            portalDriver.entrySide = entrySide;
        }

        /// <summary>
        /// End tracking a portal driver that distanced itself from this portal or was teleported to the linked portal.
        /// </summary>
        internal void StopTrackingDriver(PortalDriver portalDriver)
        {
            closePortalDrivers.Remove(portalDriver);
            portalDriver.DisableClone(linkedPortal);
        }

        /// <summary>
        /// Calculate which side of the portal plane a transform is.
        /// </summary>
        internal int CalculateSide(Transform portalDriverTransform)
        {
            return Math.Sign(Vector3.Dot(portalTransform.forward, portalDriverTransform.position - portalTransform.position));
        }

        private static Portal FromDoorState(DoorState state)
        {
            if (state is Portal portal)
            {
                return portal;
            }
            
            throw new WrongTypeException(typeof(Portal), state.GetType(), typeof(DoorState));
        }
    }
}
