using PixelPlay.OffScreenIndicator;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace PixelPlay.OffScreenIndicator
{
    /// <summary>
    /// Attach the script to the off screen indicator panel.
    /// </summary>
    [DefaultExecutionOrder(-1)]
    public class OffScreenIndicator : MonoBehaviour
    {
        [Range(0.5f, 0.9f)]
        [Tooltip("Distance offset of the indicators from the centre of the screen")]
        [SerializeField] private float screenBoundOffset = 0.9f;
        [SerializeField] private bool updateScreenBoundsOnScreenChange = false;

        private Camera mainCamera;
        private Vector3 screenCentre;
        private Vector3 screenBounds;
        private ScreenOrientation currentOrientation;
        private float currentAspectRatio;

        private List<Target> targets = new List<Target>();

        public static Action<Target, bool> TargetStateChanged;

        void Awake()
        {
            mainCamera = Camera.main;
            screenCentre = new Vector3(Screen.width, Screen.height, 0) / 2;
            screenBounds = screenCentre * screenBoundOffset;
            currentOrientation = Screen.orientation;
            currentAspectRatio = (float)Screen.width / Screen.height;
            TargetStateChanged += HandleTargetStateChanged;
        }

        void LateUpdate()
        {
            CheckScreenOrientation();
            DrawIndicators();
        }

        /// <summary>
        /// Draw the indicators on the screen and set thier position and rotation and other properties.
        /// </summary>
        void DrawIndicators()
        {
            foreach (Target target in targets)
            {
                Vector3 screenPosition = OffScreenIndicatorCore.GetScreenPosition(mainCamera, target.transform.position);
                bool isTargetVisible = OffScreenIndicatorCore.IsTargetVisible(screenPosition);
                float distanceFromCamera = target.NeedDistanceText ? target.GetDistanceFromCamera(mainCamera.transform.position) : float.MinValue;// Gets the target distance from the camera.
                Indicator indicator = null;

                if (target.NeedBoxIndicator && isTargetVisible)
                {
                    screenPosition.z = 0;
                    indicator = GetIndicator(ref target.indicator, IndicatorType.BOX); // Gets the box indicator from the pool.
                }
                else if (target.NeedArrowIndicator && !isTargetVisible)
                {
                    float angle = float.MinValue;
                    OffScreenIndicatorCore.GetArrowIndicatorPositionAndAngle(ref screenPosition, ref angle, screenCentre, screenBounds);
                    indicator = GetIndicator(ref target.indicator, IndicatorType.ARROW); // Gets the arrow indicator from the pool.
                    indicator.transform.rotation = Quaternion.Euler(0, 0, angle * Mathf.Rad2Deg); // Sets the rotation for the arrow indicator.
                }
                else
                {
                    target.indicator?.Activate(false);
                    target.indicator = null;
                }
                if (indicator)
                {
                    indicator.SetImageColor(target.TargetColor);// Sets the image color of the indicator.
                    indicator.SetDistanceText(distanceFromCamera); //Set the distance text for the indicator.
                    indicator.transform.position = screenPosition; //Sets the position of the indicator on the screen.
                    indicator.SetTextRotation(Quaternion.identity); // Sets the rotation of the distance text of the indicator.
                }
            }
        }

        /// <summary>
        /// Check if the screen orientation has changed and update the screen bounds accordingly.
        /// </summary>
        private void CheckScreenOrientation()
        {
            if (updateScreenBoundsOnScreenChange && (currentOrientation != Screen.orientation || HasAspectRatioChanged()))
            {
                screenCentre = new Vector3(Screen.width, Screen.height, 0) / 2;
                screenBounds = screenCentre * screenBoundOffset;
                currentAspectRatio = (float)Screen.width / Screen.height;
                currentOrientation = Screen.orientation;
            }
        }

        /// <summary>
        /// Check if the aspect ratio has changed.
        /// Using Mathf.Approximately to compare the aspect ratio as floating point values can have precision issues.
        /// </summary>
        /// <returns></returns>
        private bool HasAspectRatioChanged()
        {
            float cAp = (float)Screen.width / Screen.height;
            return !Mathf.Approximately(cAp, currentAspectRatio);
        }

        /// <summary>
        /// 1. Add the target to targets list if <paramref name="active"/> is true.
        /// 2. If <paramref name="active"/> is false deactivate the targets indicator, 
        ///     set its reference null and remove it from the targets list.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="active"></param>
        private void HandleTargetStateChanged(Target target, bool active)
        {
            if (active)
            {
                targets.Add(target);
            }
            else
            {
                target.indicator?.Activate(false);
                target.indicator = null;
                targets.Remove(target);
            }
        }

        /// <summary>
        /// Get the indicator for the target.
        /// 1. If its not null and of the same required <paramref name="type"/> 
        ///     then return the same indicator;
        /// 2. If its not null but is of different type from <paramref name="type"/> 
        ///     then deactivate the old reference so that it returns to the pool 
        ///     and request one of another type from pool.
        /// 3. If its null then request one from the pool of <paramref name="type"/>.
        /// </summary>
        /// <param name="indicator"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        private Indicator GetIndicator(ref Indicator indicator, IndicatorType type)
        {
            if (indicator != null)
            {
                if (indicator.Type != type)
                {
                    indicator.Activate(false);
                    indicator = type == IndicatorType.BOX ? BoxObjectPool.current.GetPooledObject() : ArrowObjectPool.current.GetPooledObject();
                    indicator.Activate(true); // Sets the indicator as active.
                }
            }
            else
            {
                indicator = type == IndicatorType.BOX ? BoxObjectPool.current.GetPooledObject() : ArrowObjectPool.current.GetPooledObject();
                indicator.Activate(true); // Sets the indicator as active.
            }
            return indicator;
        }

        private void OnDestroy()
        {
            TargetStateChanged -= HandleTargetStateChanged;
        }
    }
}