using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.XR;

public class Portal : MonoBehaviour
{
    public bool allowRender;
	private bool _canRender;
    private bool _shouldRender = true;
    
    public Portal targetPortal;
    public Transform normalVisible;
    public Transform normalInvisible;
    
    public Camera portalLCamera;
    public Camera portalRCamera;

    public Renderer viewThroughRenderer;

    private RenderTexture _viewThroughRenderTextureL;
    private RenderTexture _viewThroughRenderTextureR;

    private Material _viewThroughMaterial;

    private Camera _mainCamera;
    private Vector4 _vectorPlane;
    
    public bool _shouldTeleport;

    private bool ShouldRender(Plane[] cameraPlanes) =>
        viewThroughRenderer.isVisible &&
        GeometryUtility.TestPlanesAABB(cameraPlanes,
            viewThroughRenderer.bounds);

    private void Start()
    {
        // Generate bounding plane
        var plane = new Plane(normalVisible.forward, transform.position + normalVisible.forward * -0.01f);
        _vectorPlane = new Vector4(plane.normal.x, plane.normal.y, plane.normal.z, plane.distance);
        
        if (allowRender)
        {
            float scaleX = transform.localScale.x;
            float scaleY = transform.localScale.y;

            // Create render texture
            _viewThroughRenderTextureL =
                new RenderTexture((1440), (1600), 24);
            _viewThroughRenderTextureL.Create();

            _viewThroughRenderTextureR =
                new RenderTexture((1440), (1600), 24);
            _viewThroughRenderTextureR.Create();
            

            // Assign render texture to portal camera
            portalLCamera.targetTexture = _viewThroughRenderTextureL;
            portalRCamera.targetTexture = _viewThroughRenderTextureR;

            // Assign render texture to portal material (cloned)
            _viewThroughMaterial = viewThroughRenderer.material;
            _viewThroughMaterial.SetTexture("_TexL", _viewThroughRenderTextureL);
            _viewThroughMaterial.SetTexture("_TexR", _viewThroughRenderTextureR);

            // Cache the main camera
            _mainCamera = Camera.main;

            Application.onBeforeRender += OnBeforeRender;
            _shouldTeleport = true;
            _canRender = true;
        }
        else
        {
            portalLCamera.gameObject.SetActive(false);
            portalRCamera.gameObject.SetActive(false);
            viewThroughRenderer.enabled = false;
        }
    }
    
    private void OnBeforeRender()
    {
        if (!_canRender) return;
        var cameraPlanes = GeometryUtility.CalculateFrustumPlanes(_mainCamera);
        if (!ShouldRender(cameraPlanes))
        {
            if (!_shouldRender) return;
            Debug.Log("Disabling render for " + transform.name);
            viewThroughRenderer.material = new Material(Shader.Find("Unlit/Color"));
            portalLCamera.enabled = false;
            portalRCamera.enabled = false;
            _shouldRender = false;
            return;
        }

        if (!_shouldRender)
        {
            Debug.Log("Enabling render for " + transform.name);
            viewThroughRenderer.material = _viewThroughMaterial;
            portalLCamera.enabled = true;
            portalRCamera.enabled = true;
            _shouldRender = true;
        }

        UpdateCamera(portalLCamera, XRNode.LeftEye);
        UpdateCamera(portalRCamera, XRNode.RightEye);
    }

    private void UpdateCamera(Camera portalCamera, XRNode eye)
    {
        // Calculate portal camera position and rotation
        var virtualPosition = TransformPositionBetweenPortals(this, targetPortal, GetEyeWorldPosition(eye));
        var virtualRotation = TransformRotationBetweenPortals(this, targetPortal, GetEyeRotation(eye));

        // Position camera
        portalCamera.transform.SetPositionAndRotation(virtualPosition, virtualRotation);

        // Calculate projection matrix
        var clipThroughSpace = Matrix4x4.Transpose(Matrix4x4.Inverse(portalCamera.worldToCameraMatrix)) *
                               targetPortal._vectorPlane;

        // Set portal camera projection matrix to clip walls between target portal and portal camera
        // Inherits main camera near/far clip plane and FOV settings
            portalCamera.projectionMatrix =
                CalculateObliqueMatrix(
                    _mainCamera.GetStereoProjectionMatrix(eye == XRNode.LeftEye
                        ? Camera.StereoscopicEye.Left
                        : Camera.StereoscopicEye.Right), clipThroughSpace);
    }
    private void OnDestroy()
    {
        if (!_canRender) return;
        Application.onBeforeRender -= OnBeforeRender;
        // Release render texture from GPU
        _viewThroughRenderTextureL.Release();
        _viewThroughRenderTextureR.Release();


        // Destroy cloned material and render texture
        Destroy(_viewThroughMaterial);
        Destroy(_viewThroughRenderTextureL);
        Destroy(_viewThroughRenderTextureR);
    }


    private static Vector3 TransformPositionBetweenPortals(Portal sender, Portal target, Vector3 position)
    {
        return
            target.normalInvisible.TransformPoint(
                sender.normalVisible.InverseTransformPoint(position));
    }

    private static Quaternion TransformRotationBetweenPortals(Portal sender, Portal target, Quaternion rotation)
    {
        return
            target.normalInvisible.rotation *
            Quaternion.Inverse(sender.normalVisible.rotation) *
            rotation;
    }

    private Vector3 GetEyeWorldPosition(XRNode eye)
    {
        if (!XRSettings.enabled) return _mainCamera.transform.position;

        InputDevice device = InputDevices.GetDeviceAtXRNode(eye);

        if (!device.isValid) return default;

        float cameraSeparation = _mainCamera.stereoSeparation;

        return _mainCamera.transform.position + _mainCamera.transform.right *
            (eye == XRNode.LeftEye ? -cameraSeparation : cameraSeparation) / 2;
    }

    private Quaternion GetEyeRotation(XRNode _)
    {
        return _mainCamera.transform.rotation;
    }

    static Matrix4x4 CalculateObliqueMatrix(Matrix4x4 projection, Vector4 clipPlane)
    {
        Matrix4x4 obliqueMatrix = projection;
        Vector4 q = projection.inverse * new Vector4(
            Math.Sign(clipPlane.x),
            Math.Sign(clipPlane.y),
            1.0f,
            1.0f
        );
        Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q)));
        obliqueMatrix[2] = c.x - projection[3];
        obliqueMatrix[6] = c.y - projection[7];
        obliqueMatrix[10] = c.z - projection[11];
        obliqueMatrix[14] = c.w - projection[15];
        return obliqueMatrix;
    }

    private void OnTriggerEnter(Collider other)
    {
        if (!_canRender || !_shouldTeleport) return;
        if (!other.CompareTag("Player")) return;
        Debug.Log(transform.name + " player entered and should teleport");
        targetPortal._shouldTeleport = false;
        // Move player to target portal collider relative position
        other.transform.position = TransformPositionBetweenPortals(this, targetPortal, other.transform.position);
        other.transform.rotation = TransformRotationBetweenPortals(this, targetPortal, other.transform.rotation);
    }

    private void OnTriggerExit(Collider other)
    {
        if (!_canRender || !_shouldTeleport || IsInvoking(nameof(AllowTeleport))) return;
        if (!other.CompareTag("Player")) return;
        Debug.Log(transform.name + " player exited");
        Invoke(nameof(AllowTeleport), 1f);
    }

    private void AllowTeleport()
    {
        _shouldTeleport = true;
    }
}