// 04.10.2021 11:00

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public enum PivotAxis
{
    Free = 10,
    X = 20,
    Y = 30,
    Z = 40,
    XY = 50,
    XZ = 60,
    YZ = 70,
}

public enum EEnablingBehaviour
{
    Transition = 0,
    Hold = 10,
    Jump = 20,
}

public class ThresholdedBillboard : MonoBehaviour
{

    #region Inspector Properties

    [Header("Config Values")]
    [Tooltip("Specifies the axis about which the object will rotate.")]
    [SerializeField]
    private PivotAxis pivotAxis = PivotAxis.XY;

    [Tooltip("Specifies the desired OnEnable behaviour.")]
    [SerializeField]
    private EEnablingBehaviour enablingBehaviour;

    [Tooltip("Specifies the delay in seconds before initializing.")]
    [SerializeField]
    private float initDelay = 0.03f;

    [Tooltip("Specifies how fast the object will rotate.")]
    [SerializeField]
    private float rotationSpeed = 2f;

    [Tooltip("Specifies the deviation at which the object will start rotating.")]
    [SerializeField]
    private float startBillboardingAngle = 20f;

    [Tooltip("Specifies the deviation at which the object will stop rotating.")]
    [SerializeField]
    private float stopBillboardingAngle = 2f;

    [Tooltip("Specifies if the rotation should happen after a delay.")]
    [SerializeField]
    private bool isDelayedModeActive = false;

    [Tooltip("Only for delayed Mode: Specifies the delay before the rotation should happen.")]
    [SerializeField]
    private float delayedModeDelay = 0.3f;

    [Header("Scene Objects")]
    [Tooltip("Specifies the target we will orient to. If no target is specified, the main camera will be used.")]
    [SerializeField]
    private Transform targetTransform;

    [Tooltip("Specifies transforms which will be set to the same rotation.")]
    [SerializeField]
    private List<Transform> linkedTransforms;

    #endregion

    #region Public Properties

    public PivotAxis PivotAxis
    {
        get { return pivotAxis; }
        set { pivotAxis = value; }
    }

    /// <summary>
    /// The target we will orient to. If no target is specified, the main camera will be used.
    /// </summary>
    public Transform TargetTransform => targetTransform;

    #endregion

    #region Private Properties

    private bool isInitialized = false;

    private Camera cam;

    private Quaternion rotationFrom;
    private Quaternion rotationTo;

    private bool delayedRotationIsPending = false;
    private float lastRotationRequiredTime = 0;

    #endregion

    #region Framework Functions

    void Awake()
    {
        this.cam = Camera.main;
    }

    void OnEnable()
    {
        this.initBillboardWithDelay();
    }

    void Update()
    {
        if (this.isInitialized)
        {
            this.rotateToTarget(false, false);
        }
    }

    void OnDisable()
    {
        this.isInitialized = false;
    }

    #endregion

    #region Events

    #endregion

    #region Public Functions

    public void OverrideEnablingBehaviour(EEnablingBehaviour newBehaviour)
    {
        this.enablingBehaviour = newBehaviour;
    }

    public void OverrideRotationSpeed(float newRotationSpeed)
    {
        this.rotationSpeed = newRotationSpeed;
    }

    public void OverrideStartBillboardingAngle(float newStartBillboardingAngle)
    {
        this.startBillboardingAngle = newStartBillboardingAngle;
    }

    public void OverrideStopBillboardingAngle(float newStopBillboardingAngle)
    {
        this.stopBillboardingAngle = newStopBillboardingAngle;
    }

    public void JumpToDesiredRotation()
    {
        this.rotateToTarget(false, true);
    }

    #endregion

    #region Private Functions

    private async void initBillboardWithDelay()
    {
        await Task.Delay(TimeSpan.FromSeconds(this.initDelay));

        if (this == null)
        {
            // Has been destroyed -> cancel
            return;
        }

        this.rotationFrom = Quaternion.identity;
        this.rotationTo = Quaternion.identity;

        if (this.enablingBehaviour == EEnablingBehaviour.Transition)
        {
            this.rotateToTarget(true, false);
        }

        if (this.enablingBehaviour == EEnablingBehaviour.Jump)
        {
            this.rotateToTarget(false, true);
        }

        this.isInitialized = true;
    }

    private void rotateToTarget(bool forceRotation, bool jumpToDesiredRotation)
    {
        if (this.targetTransform == null)
        {
            this.targetTransform = this.cam.transform;
        }

        // Get a Vector that points from the target to the main camera.
        Vector3 directionToTarget = targetTransform.position - transform.position;

        bool useCameraAsUpVector = true;

        // Adjust for the pivot axis.
        switch (pivotAxis)
        {
            case PivotAxis.X:
                directionToTarget.x = 0.0f;
                useCameraAsUpVector = false;
                break;

            case PivotAxis.Y:
                directionToTarget.y = 0.0f;
                useCameraAsUpVector = false;
                break;

            case PivotAxis.Z:
                directionToTarget.x = 0.0f;
                directionToTarget.y = 0.0f;
                break;

            case PivotAxis.XY:
                useCameraAsUpVector = false;
                break;

            case PivotAxis.XZ:
                directionToTarget.x = 0.0f;
                break;

            case PivotAxis.YZ:
                directionToTarget.y = 0.0f;
                break;

            case PivotAxis.Free:
            default:
                // No changes needed.
                break;
        }

        // If we are right next to the camera the rotation is undefined. 
        if (directionToTarget.sqrMagnitude < 0.001f)
        {
            return;
        }

        Quaternion rotationCurrent = Quaternion.identity;
        // Calculate and apply the rotation required to reorient the object
        if (useCameraAsUpVector)
        {
            rotationCurrent = Quaternion.LookRotation(-directionToTarget,  this.cam.transform.up);
        }
        else
        {
            rotationCurrent = Quaternion.LookRotation(-directionToTarget);
        }

        if (jumpToDesiredRotation)
        {
            this.transform.rotation = rotationCurrent;
            return;
        }

        float deviationToDesiredRotation = Mathf.Abs(Quaternion.Angle(rotationCurrent, transform.rotation));

        // Check if rotation target needs to be set
        if (deviationToDesiredRotation > this.startBillboardingAngle || forceRotation)
        {
            if (this.isDelayedModeActive)
            {
                // Delayed mode
                if (!this.delayedRotationIsPending)
                {
                    // Start "timer"
                    this.delayedRotationIsPending = true;
                    this.lastRotationRequiredTime = Time.time;
                }
                else
                {
                    // "Timer" running
                    if (Time.time > this.lastRotationRequiredTime + this.delayedModeDelay)
                    {
                        // Delaytime has passed -> start rotating
                        this.delayedRotationIsPending = false;

                        this.rotationFrom = transform.rotation;
                        this.rotationTo = rotationCurrent;
                    }
                }
            }
            else
            {
                // Non delayed mode -> start rotating
                this.rotationFrom = transform.rotation;
                this.rotationTo = rotationCurrent;
            }
        }

        // If delayed mode -> check if delayedRotationIsPending needs to be reset
        if (deviationToDesiredRotation < this.startBillboardingAngle && this.isDelayedModeActive)
        {
            this.delayedRotationIsPending = false;
        }

        if (this.rotationTo != Quaternion.identity)
        {
            // Currently rotating
            transform.rotation = Quaternion.Lerp(this.transform.rotation, this.rotationTo, Time.deltaTime * rotationSpeed);

            for (int i = 0; i < this.linkedTransforms.Count; i++)
            {
                this.linkedTransforms[i].rotation = this.transform.rotation;
            }

            // Check if should stop
            float deviationToTargetRotation = Mathf.Abs(Quaternion.Angle(this.transform.rotation, this.rotationTo));
            if (deviationToTargetRotation < this.stopBillboardingAngle)
            {
                this.rotationFrom = Quaternion.identity;
                this.rotationTo = Quaternion.identity;
            }
        }
    }

    #endregion

}