using System;
using Intrepid.Core.Camera.Events;
using Intrepid.GameManagers;
using Intrepid.Utilities;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Intrepid.Core.Camera
{
    [ExecuteAlways]
    [DisallowMultipleComponent]
    [RequireComponent(typeof(BoxCollider))]
    public sealed class CameraBoundary : MonoBehaviour
    {
        [GroupInjections, SerializeField] private BoxCollider boxCollider;
        [GroupConfigurations, SerializeField] private CameraZoomLevel cameraZoomLevel = CameraZoomLevel.None;
        [GroupConfigurations, SerializeField, Min(0f)] private float cameraPadding = 0.5f;
        [GroupConfigurations, SerializeField, Min(0.1f)] private float triggerHeight = 5f;

        public Bounds WorldBounds => boxCollider.bounds;
        
        private void Reset()
        {
            boxCollider = GetComponent<BoxCollider>();
            ConfigureAsTrigger();
        }

        private void OnValidate()
        {
            if (boxCollider.IsNull())
                boxCollider = GetComponent<BoxCollider>();

            ConfigureAsTrigger();
        }

        private void ConfigureAsTrigger()
        {
            if (boxCollider.IsNull())
                return;

            boxCollider.isTrigger = true;
        }

        private void OnTriggerEnter(Collider other)
        {
            Proxy.Instance.EventBus.Raise(new CameraBoundaryEnterEvent
            {
                Boundary = this,
                CameraZoomLevel = cameraZoomLevel,
            });
        }

        private void OnTriggerExit(Collider other)
        {
            Proxy.Instance.EventBus.Raise(new CameraBoundaryExitEvent
            {
                Boundary = this
            });
        }

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void SetToCameraMinimumSize()
        {
            if (!TryGetCameraFootprintLocalBounds(out var localBounds))
                return;

            var size = boxCollider.size;

            size.x = localBounds.size.x + cameraPadding * 2f;
            size.y = triggerHeight;
            size.z = localBounds.size.z + cameraPadding * 2f;

            boxCollider.size = size;
        }

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void ClampToCameraMinimumSize()
        {
            if (!TryGetCameraFootprintLocalBounds(out var localBounds))
                return;

            var size = boxCollider.size;

            size.x = Mathf.Max(size.x, localBounds.size.x + cameraPadding * 2f);
            size.y = triggerHeight;
            size.z = Mathf.Max(size.z, localBounds.size.z + cameraPadding * 2f);

            boxCollider.size = size;
        }

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void FitToCurrentCameraView()
        {
            if (!TryGetCameraFootprintLocalBounds(out var localBounds))
                return;

            boxCollider.center = new Vector3(
                localBounds.center.x,
                0f,
                localBounds.center.z
            );

            boxCollider.size = new Vector3(
                localBounds.size.x + cameraPadding * 2f,
                triggerHeight,
                localBounds.size.z + cameraPadding * 2f
            );
        }

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void ApplyTriggerHeight()
        {
            var size = boxCollider.size;
            size.y = triggerHeight;
            boxCollider.size = size;
        }

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void ResetColliderCenter()
        {
            boxCollider.center = Vector3.zero;
        }

        private bool TryGetCameraFootprintLocalBounds(out Bounds localBounds)
        {
            localBounds = default;

            if (!TryGetReferenceCamera(out var cam))
                return false;

            if (!cam.orthographic)
            {
                Debug.LogWarning($"{nameof(CameraBoundary)} only supports orthographic cameras for boundary utilities.", this);
                return false;
            }

            var planeY = boxCollider.transform.TransformPoint(boxCollider.center).y;
            var groundPlane = new Plane(Vector3.up, new Vector3(0f, planeY, 0f));

            var viewportCorners = new[]
            {
                new Vector3(0f, 0f, 0f),
                new Vector3(0f, 1f, 0f),
                new Vector3(1f, 1f, 0f),
                new Vector3(1f, 0f, 0f),
            };

            var hasPoint = false;

            foreach (var viewportCorner in viewportCorners)
            {
                var ray = cam.ViewportPointToRay(viewportCorner);

                if (!groundPlane.Raycast(ray, out var distance))
                    continue;

                var worldPoint = ray.GetPoint(distance);
                var localPoint = boxCollider.transform.InverseTransformPoint(worldPoint);

                if (!hasPoint)
                {
                    localBounds = new Bounds(localPoint, Vector3.zero);
                    hasPoint = true;
                }
                else
                {
                    localBounds.Encapsulate(localPoint);
                }
            }

            return hasPoint;
        }

        private bool TryGetReferenceCamera(out UnityEngine.Camera cam)
        {
            cam = CameraSystem.Instance?.MainCamera;

            if (cam.IsNotNull())
                return true;

            cam = UnityEngine.Camera.main;

            if (cam.IsNotNull())
                return true;

#if UNITY_EDITOR
            if (SceneView.lastActiveSceneView.IsNotNull())
            {
                cam = SceneView.lastActiveSceneView.camera;
                return cam.IsNotNull();
            }
#endif

            Debug.LogWarning($"{nameof(CameraBoundary)} could not find a reference camera.", this);
            return false;
        }

#if UNITY_EDITOR
        private void OnDrawGizmosSelected()
        {
            if (boxCollider.IsNull()) return;

            DrawBoxFill();
            DrawHatchedBoxXZ();
            DrawBoxWire();
        }

        private void DrawBoxFill()
        {
            var b = boxCollider.bounds;

            Gizmos.color = new Color(1f, 0f, 1f, 0.10f);
            Gizmos.DrawCube(b.center, b.size);
        }

        private void DrawBoxWire()
        {
            var b = boxCollider.bounds;

            Gizmos.color = Color.magenta;
            Gizmos.DrawWireCube(b.center, b.size);
        }

        private void DrawHatchedBoxXZ()
        {
            const float hatchSpacing = .5f;

            Handles.color = new Color(1f, 0f, 1f, 0.65f);

            var size = boxCollider.size;
            var center = boxCollider.center;

            var minX = center.x - size.x * 0.5f;
            var maxX = center.x + size.x * 0.5f;
            var minZ = center.z - size.z * 0.5f;
            var maxZ = center.z + size.z * 0.5f;
            var y = center.y;

            var previousMatrix = Handles.matrix;
            Handles.matrix = boxCollider.transform.localToWorldMatrix;

            var minC = minX - maxZ;
            var maxC = maxX - minZ;

            for (var c = minC; c <= maxC; c += hatchSpacing)
            {
                if (TryGetDiagonalSegment(
                        c,
                        minX,
                        maxX,
                        minZ,
                        maxZ,
                        y,
                        out var a,
                        out var b))
                {
                    Handles.DrawLine(a, b);
                }
            }

            Handles.matrix = previousMatrix;
        }

        private static bool TryGetDiagonalSegment(
            float c,
            float minX,
            float maxX,
            float minZ,
            float maxZ,
            float y,
            out Vector3 a,
            out Vector3 b)
        {
            var points = new Vector3[4];
            var count = 0;

            var xAtMinZ = c + minZ;
            if (xAtMinZ >= minX && xAtMinZ <= maxX)
                points[count++] = new Vector3(xAtMinZ, y, minZ);

            var xAtMaxZ = c + maxZ;
            if (xAtMaxZ >= minX && xAtMaxZ <= maxX)
                points[count++] = new Vector3(xAtMaxZ, y, maxZ);

            var zAtMinX = minX - c;
            if (zAtMinX >= minZ && zAtMinX <= maxZ)
                points[count++] = new Vector3(minX, y, zAtMinX);

            var zAtMaxX = maxX - c;
            if (zAtMaxX >= minZ && zAtMaxX <= maxZ)
                points[count++] = new Vector3(maxX, y, zAtMaxX);

            if (count < 2)
            {
                a = default;
                b = default;
                return false;
            }

            a = points[0];
            b = points[1];
            return true;
        }
#endif
    }
}