using UnityEngine;

namespace pdinklag
{
    public static class CameraExtensions
    {
        /// <summary>
        /// Sets up the camera's rotation and FOV such that the given points are just contained on the screen.
        /// </summary>
        /// <param name="camera">the camera</param>
        /// <param name="points">the points to contain</param>
        /// <param name="minFOV">the minimum allowed FOV</param>
        /// <param name="maxFOV">the maximum allowed FOV</param>
        /// <param name="iterations">the number of iterations, more yielding higher accuracy</param>
        public static void Contain(this Camera camera, InlineArray<Vector3> points, float minFOV = 10.0f, float maxFOV = 80.0f, int iterations = 10)
        {
            var num = points.Length;
            if (num == 0) return;

            // look at world centroid first and make sure all of the points fit by applying the maximum FOV
            {
                camera.transform.rotation = Quaternion.LookRotation(points.Center() - camera.transform.position, Vector3.up);
                camera.fieldOfView = maxFOV;
            }

            // find the focus point (the center of the viewport bounds)
            {
                var focus = camera.ViewportToWorldPoint(points.Center(camera.WorldToViewportPoint));
                camera.transform.rotation = Quaternion.LookRotation(focus - camera.transform.position, Vector3.up);
            }

            // binary search for the FOV to fit all objects
            {
                var fovLeft = minFOV;
                var fovRight = maxFOV;

                var fovMid = fovLeft + 0.5f * (fovRight - fovLeft);
                camera.fieldOfView = fovMid;

                for (var k = 0; k < iterations; k++)
                {
                    var tooSmall = false;
                    for (var i = 0; i < num; i++)
                    {
                        var v = camera.WorldToViewportPoint(points[i]);
                        if (v.x < 0 || v.x > 1 || v.y < 0 || v.y > 1)
                        {
                            tooSmall = true;
                            break;
                        }
                    }

                    if (tooSmall)
                    {
                        fovLeft = fovMid;
                    }
                    else
                    {
                        fovRight = fovMid;
                    }

                    fovMid = fovLeft + 0.5f * (fovRight - fovLeft);
                    camera.fieldOfView = fovMid;
                }
            }
        }

        /// <summary>
        /// Sets up the camera's rotation and FOV such that the given bounds are just contained on the screen.
        /// </summary>
        /// <param name="bounds">the bounds</param>
        /// <param name="points">the points to contain</param>
        /// <param name="minFOV">the minimum allowed FOV</param>
        /// <param name="maxFOV">the maximum allowed FOV</param>
        /// <param name="iterations">the number of iterations, more yielding higher accuracy</param>
        public static void Contain(this Camera camera, Bounds bounds, float minFOV = 10.0f, float maxFOV = 80.0f, int iterations = 10)
            => Contain(camera, bounds.Corners(), minFOV, maxFOV, iterations);
    }
}
