using System;
using System.Collections.Generic;
using Intrepid.Core.Camera.Events;
using Intrepid.GameManagers;
using Intrepid.SimpleEvents;
using Intrepid.Utilities;
using UnityEngine;

namespace Intrepid.Core.Camera
{
    public class CameraBoundaryManager : EventListener
    {
        [GroupConfigurations, SerializeField] private float enterBlendTime = 1f;
        [GroupConfigurations, SerializeField] private float boundsBlendTime = 0.75f;
        [GroupConfigurations, SerializeField] private float exitBlendTime = 0.5f;

        private readonly List<CameraBoundary> activeBoundaries = new();

        private Bounds currentBounds;
        private Bounds targetBounds;
        private Bounds blendStartBounds;
        private Bounds fadingOutBounds;

        private bool hasCurrentBounds;
        private bool hasTargetBounds;
        private bool hasFadingOutBounds;

        private float targetWeight;
        private float boundsBlend = 1f;

        public float BoundaryWeight { get; private set; }

        public float SmoothBoundaryWeight => SmoothStep(BoundaryWeight);

        public bool HasActiveBoundary => activeBoundaries.Count > 0;
        
        protected override void InitializeVariables()
        {
        }

        protected override void OnBootstrap()
        {
            
        }

        protected override void Subscriptions()
        {
            Subscribe<CameraBoundaryEnterEvent>(e => Enter(e.Boundary, e.CameraZoomLevel));
            Subscribe<CameraBoundaryExitEvent>(e => Exit(e.Boundary));
        }
        
        private void Enter(
            CameraBoundary boundary,
            // for testing...
            CameraZoomLevel zoomLevel)
        {
            if (boundary.IsNull())
            {
                return;
            }

            if (activeBoundaries.Contains(boundary))
            {
                return;
            }

            activeBoundaries.Add(boundary);
            // for testing...
            Proxy.Instance.EventBus.Raise(new CameraZoomEvent
            {
                ZoomLevel = zoomLevel,
            });

            hasFadingOutBounds = false;
            targetWeight = 1f;

            RefreshTargetBounds();
        }

        private void Exit(CameraBoundary boundary)
        {
            if (boundary.IsNull())
            {
                return;
            }

            if (!activeBoundaries.Contains(boundary))
            {
                return;
            }

            var hadCameraBounds = TryGetCameraBounds(out var previousCameraBounds);

            activeBoundaries.Remove(boundary);

            if (activeBoundaries.Count == 0)
            {
                if (hadCameraBounds)
                {
                    fadingOutBounds = previousCameraBounds;
                    hasFadingOutBounds = true;
                }

                hasTargetBounds = false;
                hasCurrentBounds = false;

                targetWeight = 0f;
                return;
            }

            hasFadingOutBounds = false;
            targetWeight = 1f;

            RefreshTargetBounds();
        }

        private void Update()
        {
            UpdateWeight();
            UpdateBoundsBlend();
        }

        private void UpdateWeight()
        {
            var blendTime = targetWeight > BoundaryWeight
                ? enterBlendTime
                : exitBlendTime;

            if (blendTime <= 0f)
            {
                BoundaryWeight = targetWeight;

                if (BoundaryWeight <= 0f)
                {
                    hasFadingOutBounds = false;
                }

                return;
            }

            var speed = 1f / blendTime;

            BoundaryWeight = Mathf.MoveTowards(
                BoundaryWeight,
                targetWeight,
                speed * Time.deltaTime
            );

            if (BoundaryWeight <= 0f && targetWeight <= 0f)
            {
                BoundaryWeight = 0f;
                hasFadingOutBounds = false;
            }
        }

        private void UpdateBoundsBlend()
        {
            if (!hasTargetBounds)
            {
                return;
            }

            if (boundsBlend >= 1f)
            {
                currentBounds = targetBounds;
                hasCurrentBounds = true;
                return;
            }

            if (boundsBlendTime <= 0f)
            {
                boundsBlend = 1f;
                currentBounds = targetBounds;
                hasCurrentBounds = true;
                return;
            }

            boundsBlend += Time.deltaTime / boundsBlendTime;
            boundsBlend = Mathf.Clamp01(boundsBlend);

            var t = SmoothStep(boundsBlend);

            currentBounds = LerpBounds(blendStartBounds, targetBounds, t);
            hasCurrentBounds = true;
        }

        public bool TryGetCameraBounds(out Bounds bounds)
        {
            if (hasCurrentBounds)
            {
                bounds = currentBounds;
                return true;
            }

            if (hasFadingOutBounds)
            {
                bounds = fadingOutBounds;
                return true;
            }

            bounds = default;
            return false;
        }

        private void RefreshTargetBounds()
        {
            if (!TryCalculateEffectiveBounds(out var newTargetBounds))
            {
                hasTargetBounds = false;
                return;
            }

            if (!hasCurrentBounds)
            {
                currentBounds = newTargetBounds;
                targetBounds = newTargetBounds;
                blendStartBounds = newTargetBounds;

                hasCurrentBounds = true;
                hasTargetBounds = true;

                boundsBlend = 1f;
                return;
            }

            blendStartBounds = currentBounds;
            targetBounds = newTargetBounds;

            hasTargetBounds = true;
            boundsBlend = 0f;
        }

        private bool TryCalculateEffectiveBounds(out Bounds bounds)
        {
            if (activeBoundaries.Count == 0)
            {
                bounds = default;
                return false;
            }

            bounds = activeBoundaries[0].WorldBounds;

            for (var i = 1; i < activeBoundaries.Count; i++)
            {
                bounds = Intersect(bounds, activeBoundaries[i].WorldBounds);
            }

            return true;
        }

        private static Bounds Intersect(Bounds a, Bounds b)
        {
            var min = Vector3.Max(a.min, b.min);
            var max = Vector3.Min(a.max, b.max);

            if (min.x > max.x)
            {
                var x = (min.x + max.x) * 0.5f;
                min.x = x;
                max.x = x;
            }

            if (min.y > max.y)
            {
                var y = (min.y + max.y) * 0.5f;
                min.y = y;
                max.y = y;
            }

            if (min.z > max.z)
            {
                var z = (min.z + max.z) * 0.5f;
                min.z = z;
                max.z = z;
            }

            var result = new Bounds();
            result.SetMinMax(min, max);
            return result;
        }

        private static Bounds LerpBounds(Bounds from, Bounds to, float t)
        {
            var min = Vector3.Lerp(from.min, to.min, t);
            var max = Vector3.Lerp(from.max, to.max, t);

            var result = new Bounds();
            result.SetMinMax(min, max);
            return result;
        }

        private static float SmoothStep(float t)
        {
            t = Mathf.Clamp01(t);
            return t * t * (3f - 2f * t);
        }
    }
}