// Copyright (c) Wojciech Figat. All rights reserved.

#if USE_LARGE_WORLDS
using Real = System.Double;
#else
using Real = System.Single;
#endif

// -----------------------------------------------------------------------------
// Original code from SharpDX project. https://github.com/sharpdx/SharpDX/
// Greetings to Alexandre Mutel. Original code published with the following license:
// -----------------------------------------------------------------------------
// Copyright (c) 2010-2014 SharpDX - Alexandre Mutel
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace FlaxEngine
{
    /// <summary>
    /// Defines a frustum which can be used in frustum culling, zoom to Extents (zoom to fit) operations, (matrix, frustum, camera) interchange, and many kind of intersection testing.
    /// </summary>
    [Serializable]
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct BoundingFrustum : IEquatable<BoundingFrustum>
    {
        private Matrix pMatrix;
        private Plane pNear;
        private Plane pFar;
        private Plane pLeft;
        private Plane pRight;
        private Plane pTop;
        private Plane pBottom;

        /// <summary>
        /// Gets or sets the Matrix that describes this bounding frustum.
        /// </summary>
        public Matrix Matrix
        {
            get => pMatrix;
            set
            {
                pMatrix = value;
                GetPlanesFromMatrix(ref pMatrix, out pNear, out pFar, out pLeft, out pRight, out pTop, out pBottom);
            }
        }

        /// <summary>
        /// Gets the near plane of the BoundingFrustum.
        /// </summary>
        public Plane Near => pNear;

        /// <summary>
        /// Gets the far plane of the BoundingFrustum.
        /// </summary>
        public Plane Far => pFar;

        /// <summary>
        /// Gets the left plane of the BoundingFrustum.
        /// </summary>
        public Plane Left => pLeft;

        /// <summary>
        /// Gets the right plane of the BoundingFrustum.
        /// </summary>
        public Plane Right => pRight;

        /// <summary>
        /// Gets the top plane of the BoundingFrustum.
        /// </summary>
        public Plane Top => pTop;

        /// <summary>
        /// Gets the bottom plane of the BoundingFrustum.
        /// </summary>
        public Plane Bottom => pBottom;

        /// <summary>
        /// Creates a new instance of BoundingFrustum.
        /// </summary>
        /// <param name="matrix">Combined matrix that usually takes view × projection matrix.</param>
        public BoundingFrustum(Matrix matrix)
        {
            pMatrix = matrix;
            GetPlanesFromMatrix(ref pMatrix, out pNear, out pFar, out pLeft, out pRight, out pTop, out pBottom);
        }

        /// <summary>
        /// Creates a new instance of BoundingFrustum.
        /// </summary>
        /// <param name="matrix">Combined matrix that usually takes view × projection matrix.</param>
        public BoundingFrustum(ref Matrix matrix)
        {
            pMatrix = matrix;
            GetPlanesFromMatrix(ref pMatrix, out pNear, out pFar, out pLeft, out pRight, out pTop, out pBottom);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
        public override int GetHashCode()
        {
            return pMatrix.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="BoundingFrustum" /> is equal to this instance.
        /// </summary>
        /// <param name="other">The <see cref="BoundingFrustum" /> to compare with this instance.</param>
        /// <returns><c>true</c> if the specified <see cref="BoundingFrustum" /> is equal to this instance; otherwise, <c>false</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool Equals(ref BoundingFrustum other)
        {
            return pMatrix == other.pMatrix;
        }

        /// <summary>
        /// Determines whether the specified <see cref="BoundingFrustum" /> is equal to this instance.
        /// </summary>
        /// <param name="other">The <see cref="BoundingFrustum" /> to compare with this instance.</param>
        /// <returns><c>true</c> if the specified <see cref="BoundingFrustum" /> is equal to this instance; otherwise, <c>false</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool Equals(BoundingFrustum other)
        {
            return Equals(ref other);
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
        /// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
        public override bool Equals(object obj)
        {
            return obj is BoundingFrustum other && Equals(ref other);
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool operator ==(BoundingFrustum left, BoundingFrustum right)
        {
            return left.Equals(ref right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool operator !=(BoundingFrustum left, BoundingFrustum right)
        {
            return !left.Equals(ref right);
        }

        /// <summary>
        /// Returns one of the 6 planes related to this frustum.
        /// </summary>
        /// <param name="index">Plane index where 0 for Left, 1 for Right, 2 for Top, 3 for Bottom, 4 for Near, 5 for Far</param>
        /// <returns>The frustum plane.</returns>
        public Plane GetPlane(int index)
        {
            switch (index)
            {
            case 0: return pLeft;
            case 1: return pRight;
            case 2: return pTop;
            case 3: return pBottom;
            case 4: return pNear;
            case 5: return pFar;
            default: return new Plane();
            }
        }

        private static void GetPlanesFromMatrix(ref Matrix matrix, out Plane near, out Plane far, out Plane left, out Plane right, out Plane top, out Plane bottom)
        {
            //http://www.chadvernon.com/blog/resources/directx9/frustum-culling/

            // Left plane
            left.Normal.X = matrix.M14 + matrix.M11;
            left.Normal.Y = matrix.M24 + matrix.M21;
            left.Normal.Z = matrix.M34 + matrix.M31;
            left.D = matrix.M44 + matrix.M41;
            left.Normalize();

            // Right plane
            right.Normal.X = matrix.M14 - matrix.M11;
            right.Normal.Y = matrix.M24 - matrix.M21;
            right.Normal.Z = matrix.M34 - matrix.M31;
            right.D = matrix.M44 - matrix.M41;
            right.Normalize();

            // Top plane
            top.Normal.X = matrix.M14 - matrix.M12;
            top.Normal.Y = matrix.M24 - matrix.M22;
            top.Normal.Z = matrix.M34 - matrix.M32;
            top.D = matrix.M44 - matrix.M42;
            top.Normalize();

            // Bottom plane
            bottom.Normal.X = matrix.M14 + matrix.M12;
            bottom.Normal.Y = matrix.M24 + matrix.M22;
            bottom.Normal.Z = matrix.M34 + matrix.M32;
            bottom.D = matrix.M44 + matrix.M42;
            bottom.Normalize();

            // Near plane
            near.Normal.X = matrix.M13;
            near.Normal.Y = matrix.M23;
            near.Normal.Z = matrix.M33;
            near.D = matrix.M43;
            near.Normalize();

            // Far plane
            far.Normal.X = matrix.M14 - matrix.M13;
            far.Normal.Y = matrix.M24 - matrix.M23;
            far.Normal.Z = matrix.M34 - matrix.M33;
            far.D = matrix.M44 - matrix.M43;
            far.Normalize();
        }

        private static Vector3 Get3PlanesInterPoint(ref Plane p1, ref Plane p2, ref Plane p3)
        {
            Vector3.Cross(ref p2.Normal, ref p3.Normal, out var n2Xn3);
            Vector3.Cross(ref p3.Normal, ref p1.Normal, out var n3Xn1);
            Vector3.Cross(ref p1.Normal, ref p2.Normal, out var n1Xn2);
            var div1 = Vector3.Dot(ref p1.Normal, ref n2Xn3);
            var div2 = Vector3.Dot(ref p2.Normal, ref n3Xn1);
            var div3 = Vector3.Dot(ref p3.Normal, ref n1Xn2);
            if (Mathf.IsZero(div1 * div2 * div3))
                return Vector3.Zero;
            return n2Xn3 * (-p1.D / div1) - n3Xn1 * (p2.D / div2) - n1Xn2 * (p3.D / div3);
        }

        /// <summary>
        /// Creates a new frustum based on a perspective camera parameters.
        /// </summary>
        /// <param name="cameraPos">The camera position.</param>
        /// <param name="lookDir">The look direction.</param>
        /// <param name="upDir">Up direction.</param>
        /// <param name="fov">The fov in radians.</param>
        /// <param name="zNear">The Z near plane.</param>
        /// <param name="zFar">The Z far plane.</param>
        /// <param name="aspectRatio">The aspect ratio.</param>
        /// <returns>The bounding frustum calculated from the perspective camera</returns>
        public static BoundingFrustum FromCamera(Vector3 cameraPos, Vector3 lookDir, Vector3 upDir, float fov, float zNear, float zFar, float aspectRatio)
        {
            //http://knol.google.com/k/view-frustum

            lookDir = Vector3.Normalize(lookDir);
            upDir = Vector3.Normalize(upDir);

            Vector3 nearCenter = cameraPos + lookDir * zNear;
            Vector3 farCenter = cameraPos + lookDir * zFar;
            var nearHalfHeight = (float)(zNear * Math.Tan(fov / 2f));
            var farHalfHeight = (float)(zFar * Math.Tan(fov / 2f));
            float nearHalfWidth = nearHalfHeight * aspectRatio;
            float farHalfWidth = farHalfHeight * aspectRatio;

            Vector3 rightDir = Vector3.Normalize(Vector3.Cross(upDir, lookDir));
            Vector3 near1 = nearCenter - nearHalfHeight * upDir + nearHalfWidth * rightDir;
            Vector3 near2 = nearCenter + nearHalfHeight * upDir + nearHalfWidth * rightDir;
            Vector3 near3 = nearCenter + nearHalfHeight * upDir - nearHalfWidth * rightDir;
            Vector3 near4 = nearCenter - nearHalfHeight * upDir - nearHalfWidth * rightDir;
            Vector3 far1 = farCenter - farHalfHeight * upDir + farHalfWidth * rightDir;
            Vector3 far2 = farCenter + farHalfHeight * upDir + farHalfWidth * rightDir;
            Vector3 far3 = farCenter + farHalfHeight * upDir - farHalfWidth * rightDir;
            Vector3 far4 = farCenter - farHalfHeight * upDir - farHalfWidth * rightDir;

            var result = new BoundingFrustum
            {
                pNear = new Plane(near1, near2, near3),
                pFar = new Plane(far3, far2, far1),
                pLeft = new Plane(near4, near3, far3),
                pRight = new Plane(far1, far2, near2),
                pTop = new Plane(near2, far2, far3),
                pBottom = new Plane(far4, far1, near1)
            };

            result.pNear.Normalize();
            result.pFar.Normalize();
            result.pLeft.Normalize();
            result.pRight.Normalize();
            result.pTop.Normalize();
            result.pBottom.Normalize();

            result.pMatrix = Matrix.LookAt(cameraPos, cameraPos + lookDir * 10, upDir) * Matrix.PerspectiveFov(fov, aspectRatio, zNear, zFar);

            return result;
        }

        /// <summary>
        /// Returns the 8 corners of the frustum:
        /// <para>[0] is Near1 (Near right down corner)</para>
        /// <para>[1] is Near2 (Near right top corner)</para>
        /// <para>[2] is Near3 (Near left top corner)</para>
        /// <para>[3] is Near4 (Near left down corner)</para>
        /// <para>[4] is Far1 (Far right down corner)</para>
        /// <para>[5] is Far2 (Far right top corner)</para>
        /// <para>[6] is Far3 (Far left top corner)</para>
        /// <para>[7] is Far4 (Far left down corner)</para>
        /// </summary>
        /// <returns>The 8 corners of the frustum</returns>
        public Vector3[] GetCorners()
        {
            var corners = new Vector3[8];
            GetCorners(corners);
            return corners;
        }

        /// <summary>
        /// Populates the array with the 8 corners of the frustum:
        /// <para>[0] is Near1 (Near right down corner)</para>
        /// <para>[1] is Near2 (Near right top corner)</para>
        /// <para>[2] is Near3 (Near left top corner)</para>
        /// <para>[3] is Near4 (Near left down corner)</para>
        /// <para>[4] is Far1 (Far right down corner)</para>
        /// <para>[5] is Far2 (Far right top corner)</para>
        /// <para>[6] is Far3 (Far left top corner)</para>
        /// <para>[7] is Far4 (Far left down corner)</para>
        /// </summary>
        public void GetCorners(Vector3[] corners)
        {
            corners[0] = Get3PlanesInterPoint(ref pNear, ref pBottom, ref pRight); //Near1
            corners[1] = Get3PlanesInterPoint(ref pNear, ref pTop, ref pRight); //Near2
            corners[2] = Get3PlanesInterPoint(ref pNear, ref pTop, ref pLeft); //Near3
            corners[3] = Get3PlanesInterPoint(ref pNear, ref pBottom, ref pLeft); //Near3
            corners[4] = Get3PlanesInterPoint(ref pFar, ref pBottom, ref pRight); //Far1
            corners[5] = Get3PlanesInterPoint(ref pFar, ref pTop, ref pRight); //Far2
            corners[6] = Get3PlanesInterPoint(ref pFar, ref pTop, ref pLeft); //Far3
            corners[7] = Get3PlanesInterPoint(ref pFar, ref pBottom, ref pLeft); //Far3
        }

        /// <summary>
        /// Checks whether a point lays inside, intersects or lays outside the frustum.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(ref Vector3 point)
        {
            var result = PlaneIntersectionType.Front;
            var planeResult = PlaneIntersectionType.Front;
            for (var i = 0; i < 6; i++)
            {
                switch (i)
                {
                case 0:
                    planeResult = pNear.Intersects(ref point);
                    break;
                case 1:
                    planeResult = pFar.Intersects(ref point);
                    break;
                case 2:
                    planeResult = pLeft.Intersects(ref point);
                    break;
                case 3:
                    planeResult = pRight.Intersects(ref point);
                    break;
                case 4:
                    planeResult = pTop.Intersects(ref point);
                    break;
                case 5:
                    planeResult = pBottom.Intersects(ref point);
                    break;
                }
                switch (planeResult)
                {
                case PlaneIntersectionType.Back: return ContainmentType.Disjoint;
                case PlaneIntersectionType.Intersecting:
                    result = PlaneIntersectionType.Intersecting;
                    break;
                }
            }
            switch (result)
            {
            case PlaneIntersectionType.Intersecting: return ContainmentType.Intersects;
            default: return ContainmentType.Contains;
            }
        }

        /// <summary>
        /// Checks whether a point lay inside, intersects or lay outside the frustum.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(Vector3 point)
        {
            return Contains(ref point);
        }

        private void GetBoxToPlanePVertexNVertex(ref BoundingBox box, ref Vector3 planeNormal, out Vector3 p, out Vector3 n)
        {
            p = box.Minimum;
            if (planeNormal.X >= 0)
                p.X = box.Maximum.X;
            if (planeNormal.Y >= 0)
                p.Y = box.Maximum.Y;
            if (planeNormal.Z >= 0)
                p.Z = box.Maximum.Z;

            n = box.Maximum;
            if (planeNormal.X >= 0)
                n.X = box.Minimum.X;
            if (planeNormal.Y >= 0)
                n.Y = box.Minimum.Y;
            if (planeNormal.Z >= 0)
                n.Z = box.Minimum.Z;
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding box.
        /// </summary>
        /// <param name="box">The box.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(ref BoundingBox box)
        {
            var result = ContainmentType.Contains;
            for (var i = 0; i < 6; i++)
            {
                var plane = GetPlane(i);
                GetBoxToPlanePVertexNVertex(ref box, ref plane.Normal, out var p, out var n);
                if (CollisionsHelper.PlaneIntersectsPoint(ref plane, ref p) == PlaneIntersectionType.Back)
                    return ContainmentType.Disjoint;

                if (CollisionsHelper.PlaneIntersectsPoint(ref plane, ref n) == PlaneIntersectionType.Back)
                    result = ContainmentType.Intersects;
            }
            return result;
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding box.
        /// </summary>
        /// <param name="box">The box.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(BoundingBox box)
        {
            return Contains(ref box);
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding box.
        /// </summary>
        /// <param name="box">The box.</param>
        /// <param name="result">Type of the containment.</param>
        public void Contains(ref BoundingBox box, out ContainmentType result)
        {
            result = Contains(ref box);
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding sphere.
        /// </summary>
        /// <param name="sphere">The sphere.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(ref BoundingSphere sphere)
        {
            var result = PlaneIntersectionType.Front;
            var planeResult = PlaneIntersectionType.Front;
            for (var i = 0; i < 6; i++)
            {
                switch (i)
                {
                case 0:
                    planeResult = pNear.Intersects(ref sphere);
                    break;
                case 1:
                    planeResult = pFar.Intersects(ref sphere);
                    break;
                case 2:
                    planeResult = pLeft.Intersects(ref sphere);
                    break;
                case 3:
                    planeResult = pRight.Intersects(ref sphere);
                    break;
                case 4:
                    planeResult = pTop.Intersects(ref sphere);
                    break;
                case 5:
                    planeResult = pBottom.Intersects(ref sphere);
                    break;
                }
                switch (planeResult)
                {
                case PlaneIntersectionType.Back: return ContainmentType.Disjoint;
                case PlaneIntersectionType.Intersecting:
                    result = PlaneIntersectionType.Intersecting;
                    break;
                }
            }
            switch (result)
            {
            case PlaneIntersectionType.Intersecting: return ContainmentType.Intersects;
            default: return ContainmentType.Contains;
            }
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding sphere.
        /// </summary>
        /// <param name="sphere">The sphere.</param>
        /// <returns>Type of the containment</returns>
        public ContainmentType Contains(BoundingSphere sphere)
        {
            return Contains(ref sphere);
        }

        /// <summary>
        /// Determines the intersection relationship between the frustum and a bounding sphere.
        /// </summary>
        /// <param name="sphere">The sphere.</param>
        /// <param name="result">Type of the containment.</param>
        public void Contains(ref BoundingSphere sphere, out ContainmentType result)
        {
            result = Contains(ref sphere);
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects a BoundingSphere.
        /// </summary>
        /// <param name="sphere">The sphere.</param>
        /// <returns>Type of the containment</returns>
        public bool Intersects(ref BoundingSphere sphere)
        {
            return Contains(ref sphere) != ContainmentType.Disjoint;
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects a BoundingSphere.
        /// </summary>
        /// <param name="sphere">The sphere.</param>
        /// <param name="result">Set to <c>true</c> if the current BoundingFrustum intersects a BoundingSphere.</param>
        public void Intersects(ref BoundingSphere sphere, out bool result)
        {
            result = Contains(ref sphere) != ContainmentType.Disjoint;
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects a BoundingBox.
        /// </summary>
        /// <param name="box">The box.</param>
        /// <returns><c>true</c> if the current BoundingFrustum intersects a BoundingSphere.</returns>
        public bool Intersects(ref BoundingBox box)
        {
            return Contains(ref box) != ContainmentType.Disjoint;
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects a BoundingBox.
        /// </summary>
        /// <param name="box">The box.</param>
        /// <param name="result"><c>true</c> if the current BoundingFrustum intersects a BoundingSphere.</param>
        public void Intersects(ref BoundingBox box, out bool result)
        {
            result = Contains(ref box) != ContainmentType.Disjoint;
        }

        private PlaneIntersectionType PlaneIntersectsPoints(ref Plane plane, Vector3[] points)
        {
            PlaneIntersectionType result = CollisionsHelper.PlaneIntersectsPoint(ref plane, ref points[0]);
            for (var i = 1; i < points.Length; i++)
                if (CollisionsHelper.PlaneIntersectsPoint(ref plane, ref points[i]) != result)
                    return PlaneIntersectionType.Intersecting;
            return result;
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects the specified Plane.
        /// </summary>
        /// <param name="plane">The plane.</param>
        /// <returns>Plane intersection type.</returns>
        public PlaneIntersectionType Intersects(ref Plane plane)
        {
            return PlaneIntersectsPoints(ref plane, GetCorners());
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects the specified Plane.
        /// </summary>
        /// <param name="plane">The plane.</param>
        /// <param name="result">Plane intersection type.</param>
        public void Intersects(ref Plane plane, out PlaneIntersectionType result)
        {
            result = PlaneIntersectsPoints(ref plane, GetCorners());
        }

        /// <summary>
        /// Get the width of the frustum at specified depth.
        /// </summary>
        /// <param name="depth">the depth at which to calculate frustum width.</param>
        /// <returns>With of the frustum at the specified depth</returns>
        public float GetWidthAtDepth(float depth)
        {
            var hAngle = (float)(Math.PI / 2.0 - Math.Acos(Vector3.Dot(pNear.Normal, pLeft.Normal)));
            return (float)(Math.Tan(hAngle) * depth * 2);
        }

        /// <summary>
        /// Get the height of the frustum at specified depth.
        /// </summary>
        /// <param name="depth">the depth at which to calculate frustum height.</param>
        /// <returns>Height of the frustum at the specified depth</returns>
        public float GetHeightAtDepth(float depth)
        {
            var vAngle = (float)(Math.PI / 2.0 - Math.Acos(Vector3.Dot(pNear.Normal, pTop.Normal)));
            return (float)(Math.Tan(vAngle) * depth * 2);
        }

        private BoundingFrustum GetInsideOutClone()
        {
            BoundingFrustum frustum = this;
            frustum.pNear.Normal = -frustum.pNear.Normal;
            frustum.pFar.Normal = -frustum.pFar.Normal;
            frustum.pLeft.Normal = -frustum.pLeft.Normal;
            frustum.pRight.Normal = -frustum.pRight.Normal;
            frustum.pTop.Normal = -frustum.pTop.Normal;
            frustum.pBottom.Normal = -frustum.pBottom.Normal;
            return frustum;
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects the specified Ray.
        /// </summary>
        /// <param name="ray">The ray.</param>
        /// <returns><c>true</c> if the current BoundingFrustum intersects the specified Ray.</returns>
        public bool Intersects(ref Ray ray)
        {
            return Intersects(ref ray, out _, out _);
        }

        /// <summary>
        /// Checks whether the current BoundingFrustum intersects the specified Ray.
        /// </summary>
        /// <param name="ray">The Ray to check for intersection with.</param>
        /// <param name="inDistance">The distance at which the ray enters the frustum if there is an intersection and the ray starts outside the frustum.</param>
        /// <param name="outDistance">The distance at which the ray exits the frustum if there is an intersection.</param>
        /// <returns><c>true</c> if the current BoundingFrustum intersects the specified Ray.</returns>
        public bool Intersects(ref Ray ray, out Real? inDistance, out Real? outDistance)
        {
            if (Contains(ray.Position) != ContainmentType.Disjoint)
            {
                Real nearestPlaneDistance = Real.MaxValue;
                for (var i = 0; i < 6; i++)
                {
                    Plane plane = GetPlane(i);
                    if (CollisionsHelper.RayIntersectsPlane(ref ray, ref plane, out Real distance) && (distance < nearestPlaneDistance))
                        nearestPlaneDistance = distance;
                }

                inDistance = nearestPlaneDistance;
                outDistance = null;
                return true;
            }
            //We will find the two points at which the ray enters and exists the frustum
            //These two points make a line which center inside the frustum if the ray intersects it
            //Or outside the frustum if the ray intersects frustum planes outside it.
            Real minDist = Real.MaxValue;
            Real maxDist = Real.MinValue;
            for (var i = 0; i < 6; i++)
            {
                Plane plane = GetPlane(i);
                if (CollisionsHelper.RayIntersectsPlane(ref ray, ref plane, out Real distance))
                {
                    minDist = Mathf.Min(minDist, distance);
                    maxDist = Mathf.Max(maxDist, distance);
                }
            }

            Vector3 minPoint = ray.Position + ray.Direction * minDist;
            Vector3 maxPoint = ray.Position + ray.Direction * maxDist;
            Vector3 center = (minPoint + maxPoint) / 2f;
            if (Contains(ref center) != ContainmentType.Disjoint)
            {
                inDistance = minDist;
                outDistance = maxDist;
                return true;
            }
            inDistance = null;
            outDistance = null;
            return false;
        }

        /// <summary>
        /// Get the distance which when added to camera position along the look-at direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view.
        /// if the returned value is positive, the camera will move toward the look-at direction (ZoomIn).
        /// if the returned value is negative, the camera will move in the reverse direction of the look-at direction (ZoomOut).
        /// </summary>
        /// <param name="points">The points.</param>
        /// <returns>The zoom to fit distance</returns>
        public Real GetZoomToExtentsShiftDistance(Vector3[] points)
        {
            var vAngle = (float)(Math.PI / 2.0 - Math.Acos(Vector3.Dot(pNear.Normal, pTop.Normal)));
            var vSin = (float)Math.Sin(vAngle);
            var hAngle = (float)(Math.PI / 2.0 - Math.Acos(Vector3.Dot(pNear.Normal, pLeft.Normal)));
            var hSin = (float)Math.Sin(hAngle);
            float horizontalToVerticalMapping = vSin / hSin;

            BoundingFrustum ioFrustrum = GetInsideOutClone();

            var maxPointDist = Real.MinValue;
            for (var i = 0; i < points.Length; i++)
            {
                var pointDist = CollisionsHelper.DistancePlanePoint(ref ioFrustrum.pTop, ref points[i]);
                pointDist = Mathf.Max(pointDist, CollisionsHelper.DistancePlanePoint(ref ioFrustrum.pBottom, ref points[i]));
                pointDist = Mathf.Max(pointDist, CollisionsHelper.DistancePlanePoint(ref ioFrustrum.pLeft, ref points[i]) * horizontalToVerticalMapping);
                pointDist = Mathf.Max(pointDist, CollisionsHelper.DistancePlanePoint(ref ioFrustrum.pRight, ref points[i]) * horizontalToVerticalMapping);
                maxPointDist = Mathf.Max(maxPointDist, pointDist);
            }
            return -maxPointDist / vSin;
        }

        /// <summary>
        /// Get the distance which when added to camera position along the look-at direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view.
        /// if the returned value is positive, the camera will move toward the look-at direction (ZoomIn).
        /// if the returned value is negative, the camera will move in the reverse direction of the look-at direction (ZoomOut).
        /// </summary>
        /// <param name="boundingBox">The bounding box.</param>
        /// <returns>The zoom to fit distance</returns>
        public Real GetZoomToExtentsShiftDistance(ref BoundingBox boundingBox)
        {
            return GetZoomToExtentsShiftDistance(boundingBox.GetCorners());
        }

        /// <summary>
        /// Get the vector shift which when added to camera position will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <returns>The zoom to fit vector</returns>
        public Vector3 GetZoomToExtentsShiftVector(Vector3[] points)
        {
            return GetZoomToExtentsShiftDistance(points) * pNear.Normal;
        }

        /// <summary>
        /// Get the vector shift which when added to camera position will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view.</summary>
        /// <param name="boundingBox">The bounding box.</param>
        /// <returns>The zoom to fit vector</returns>
        public Vector3 GetZoomToExtentsShiftVector(ref BoundingBox boundingBox)
        {
            return GetZoomToExtentsShiftDistance(boundingBox.GetCorners()) * pNear.Normal;
        }

        /// <summary>
        /// Indicate whether the current BoundingFrustum is Orthographic.
        /// </summary>
        public bool IsOrthographic => (pLeft.Normal == -pRight.Normal) && (pTop.Normal == -pBottom.Normal);
    }
}
