using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using Sirenix.OdinInspector;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UIElements;
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;

public class LockOnManager : MonoBehaviour{
    public class ActiveLockOnTarget{
        public GameObject gameObject;
        public float targetWeight;
        public float refVelocity;
        public CinemachineTargetGroup.Target cinemachineTarget;
    }

    public static LockOnManager Instance;
    
    // Lock On settings
    [Space(5)] public float lockOnRange = 40f;
    public float lockOnMaxAngle = 70f;
    [Range(0,1)] public float mainTargetWeight = .15f;
    [FormerlySerializedAs("smoothing")] public float smoothTime = 1f;

    // Lock On Tracking
    [Space(10)]
    
    [ShowInInspector]
    ActiveLockOnTarget mainTarget;
    
    public List<ActiveLockOnTarget> activeTargets = new List<ActiveLockOnTarget>();

    [ReadOnly] public CinemachineTargetGroup.Target lockonTarget;
    public CinemachineTargetGroup targetGroup;

    private List<GameObject> acceptedTargets = new List<GameObject>();

    // UI
    [ShowInInspector] public UIDocument lockOnDocument;
    private Label elementLabelName;
    private VisualElement elementRoot;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start(){
        // Register as singleton
        if (Instance == null) {
            Instance = this;
        } else {
            this.enabled = false;
            return;
        }


        // Quick check for things in lock-on target that aren't lock-onable
        if (mainTarget != null && mainTarget.gameObject.GetComponent<ILockOnTarget>() == null) {
            mainTarget.gameObject.AddComponent<GenericLockOnTarget>();
            Debug.LogWarning($"The object <b>{mainTarget.gameObject.name}</b> has no ILockOnTarget interface. This isn't hyper critical, but adding one as a GenericLockOnTarget anyways.");
        }

        elementRoot = lockOnDocument.rootVisualElement.Query<VisualElement>("LockOnGroup");
        elementLabelName = lockOnDocument.rootVisualElement.Query<Label>("LockOnName").First();

        // Add all nearby game objects to lock-on eligible list
        GameObject[] allGameObjects = GameObject.FindObjectsByType<GameObject>(0, 0);

        foreach (GameObject thisObject in allGameObjects) {
            if (thisObject.GetComponent<ILockOnTarget>() != null) {
                acceptedTargets.Add(thisObject);
            }
        }
    }

    void Update(){
        if (mainTarget != null && mainTarget.gameObject.GetComponent<ILockOnTarget>() == null) {
            mainTarget.gameObject.AddComponent<GenericLockOnTarget>();
            Debug.LogWarning($"The object <b>{mainTarget.gameObject.name}</b> has no ILockOnTarget interface. This isn't hyper critical, but adding one as a GenericLockOnTarget anyways.");
        }

        // Iterate through targets, pushing their Target Group weight towards their goal weight, or removing them if they get too low.
        for (int i = 0; i < activeTargets.Count; i++) {
            if (activeTargets[i].gameObject == this.gameObject) {
                continue;
            }

            activeTargets[i].cinemachineTarget.Weight =
                Mathf.SmoothDamp(
                    activeTargets[i].cinemachineTarget.Weight,
                    activeTargets[i].targetWeight,
                    ref activeTargets[i].refVelocity,
                    smoothTime * Time.deltaTime);
            
            if (activeTargets[i].cinemachineTarget.Weight < 0.0001f) {
                StartCoroutine(RemoveFromTargetAtFrameEnd(activeTargets[i]));
            }
        }
    }

    IEnumerator RemoveFromTargetAtFrameEnd(ActiveLockOnTarget target){
        yield return new WaitForEndOfFrame();

        activeTargets.Remove(target);
        targetGroup.Targets.Remove(target.cinemachineTarget);
    }

    public void AddNewTarget(GameObject targetObject, float targetWeight, bool isMain = false){
        // Check that the target doesn't already exist- if it does, just change it's weight/make it main
        foreach (ActiveLockOnTarget target in activeTargets) {
            if (target.gameObject == targetObject) {
                target.targetWeight = targetWeight;
                
                if (isMain) {
                    mainTarget = target;
                }
                
                return;
            }            
        }
        
        // If it doesn't exist in the list of targets, add it
        ActiveLockOnTarget newTarget = new ActiveLockOnTarget{
            gameObject = targetObject, 
            targetWeight = mainTargetWeight,
            cinemachineTarget = new CinemachineTargetGroup.Target{
                Object = targetObject.transform,
                Radius = 1f,
                Weight = 0f
            }
        };

        //Set as main
        if (isMain) {
            mainTarget = newTarget;
        }

        // Finalize
        activeTargets.Add(newTarget);
        targetGroup.Targets.Add(newTarget.cinemachineTarget);
    }

    public void QueueTargetRemoval(GameObject targetObject, bool deleteAfterRemoved = false){
        // Ostensibly removes targest by setting their target weight to 0. Update loop finds targets with no weight and reduces their impact on the camera
        // After it smooths their current weight to 0, it removes them
        activeTargets.Find(target => target.gameObject == targetObject).targetWeight = 0f;

        if (deleteAfterRemoved) {
            StartCoroutine(DeleteGameObjectPostRemoval(targetObject));
        }

        // Remove as main target if it is
        if (mainTarget == activeTargets.Find(target => target.gameObject == targetObject)) {
            mainTarget = null;
        }
    }

    IEnumerator DeleteGameObjectPostRemoval(GameObject targetObject){
        ActiveLockOnTarget thisTarget = activeTargets.Find(target => target.gameObject == targetObject);
        
        yield return new WaitForEndOfFrame();
        
        while (activeTargets.Contains(thisTarget)) {
            yield return null;
        }
        
        Destroy(thisTarget.gameObject);
    }
    
    public void ChangeLockOnTarget(){
        Transform cameraTransform = Camera.main.transform;
    
        // If there is no target, simply find the closest to the center of the camera
        GameObject closestTarget = null;
        float lowestDistanceToCenter = Mathf.Infinity;
        
        foreach (GameObject target in acceptedTargets) {
            // Skip the current target if one exists
            if (mainTarget != null && mainTarget.gameObject == target) {
                continue;
            }
            
            // Skip targets currently behind objects.
            Physics.Raycast(cameraTransform.position,
                cameraTransform.position.DirectionTo(target.transform.position), out RaycastHit hit);

            if (hit.transform != target.transform) {
                continue;
            }
            
            // Skips targets too far
            if (Vector3.Distance(transform.position, target.transform.position) > lockOnRange) {
                continue;
            }
            
            // Skip targets outside lock on angle 
            float angleFromCameraForward = Vector3.Angle(cameraTransform.forward, cameraTransform.position.DirectionTo(target.transform.position));
            if (angleFromCameraForward > lockOnMaxAngle) {
                continue;
            }                

            // Find how close this target is from the center of the screen
            Vector3 targetScreenPoint = Camera.main.WorldToScreenPoint(target.transform.position);
            float distanceFromScreenCenter = targetScreenPoint.Flatten(null, null, 0f).magnitude - new Vector3(Screen.width, Screen.height, 0f).magnitude / 2f;
            distanceFromScreenCenter = Mathf.Abs(distanceFromScreenCenter);

            // Debug.Log($"{target.name}: {distanceFromScreenCenter} pixels, {angleFromCameraForward} degrees");

            // Set the new target to closest to screen
            if (distanceFromScreenCenter < lowestDistanceToCenter) {
                lowestDistanceToCenter = distanceFromScreenCenter;
                closestTarget = target;
            }
        }

        // Catch exception from nothing being found
        if (!closestTarget) {
            Debug.LogWarning("Lock-on attempted, but no lock on target was found viable.");
            return;
        }
        
        // Remove the main target that currently exists, if there is one.
        if (mainTarget != null) {
            QueueTargetRemoval(mainTarget.gameObject);
        }
        
        // Begin tracking target, set as main 
        AddNewTarget(closestTarget.gameObject, mainTargetWeight, true);
    }

    // Used by outside sources such as input to cancel lock-on.
    public void RemoveMainTarget(){
        QueueTargetRemoval(mainTarget.gameObject);
    }

    void LateUpdate(){
        if (mainTarget != null) {
            // This is just test logic to get an image above a lock on.
            // TODO: Replace with something less silly
            Vector2 screenPos = RuntimePanelUtils.CameraTransformWorldToPanel(
                lockOnDocument.rootVisualElement.panel,
                mainTarget.gameObject.GetComponent<ILockOnTarget>().GetReticlePosition(),
                Camera.main
            );

            // Set name
            elementLabelName.text = mainTarget.gameObject.name;

            // Set position (add the width/height of the element)
            elementRoot.style.top = new StyleLength(screenPos.y - 25f); // Was elementRoot.resolvedStyle.height * .7f
            elementRoot.style.left = new StyleLength(screenPos.x - elementRoot.resolvedStyle.width / 2f);

            // Set enabled
            elementRoot.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.Flex);
        } else {
            elementRoot.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None);
        }
    }
}
