using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace BNG {
    public class SteeringWheel : GrabbableEvents {

        [Header("Rotation Limits")]
        [Tooltip("Maximum Z value in Local Euler Angles. Can be < -360. Ex : -450")]
        public float MinAngle = -360f;

        [Tooltip("Maximum Z value in Local Euler Angles. Can be > 360. Ex : 450")]
        public float MaxAngle = 360f;

        [Header("Rotation Object")]
        [Tooltip("The Transform to rotate on its Z axis.")]
        public Transform RotatorObject;

        [Header("Rotation Speed")]
        [Tooltip("How fast to move the wheel towards the target angle. 0 = Instant.")]
        public float RotationSpeed = 0f;

        [Header("Two-Handed Option")]
        [Tooltip("IF true both hands will effect the rotation of the steering wheel while grabbed with both hands. Set to false if you only want one hand to control the rotation.")]
        public bool AllowTwoHanded = true;

        [Header("Return to Center")]
        public bool ReturnToCenter = false;
        public float ReturnToCenterSpeed = 45;

        [Header("Debug Options")]
        public Text DebugText;

        [Header("Events")]
        [Tooltip("Called if the SteeringWheel changes angle. Returns the current angle in degrees, clamped between MinAngle / MaxAngle")]
        public FloatEvent onAngleChange;

        [Tooltip("Called every frame. Returns the current current rotation between -1, 1")]
        public FloatEvent onValueChange;

        [Header("Editor Option")]
        [Tooltip("If true will show an angle helper in editor mode (Gizmos must be enabled)")]
        public bool ShowEditorGizmos = true;

        /// <summary>
        /// Returns the angle of the rotation, taking RotationSpeed into account
        /// </summary>
        public float Angle {
            get {
                return Mathf.Clamp(smoothedAngle, MinAngle, MaxAngle);
            }
        }

        /// <summary>
        /// Always returns the target angle, not taking RotationSpeed into account
        /// </summary>
        public float RawAngle {
            get {
                return targetAngle;
            }
        }

        public float ScaleValue {
            get {
                return GetScaledValue(Angle, MinAngle, MaxAngle);
            }
        }

        public float ScaleValueInverted {
            get {
                return ScaleValue * -1;
            }
        }

        public float AngleInverted {
            get {
                return Angle * -1;
            }
        }

        public Grabber PrimaryGrabber {
            get {
                return GetPrimaryGrabber();
            }
        }
        public Grabber SecondaryGrabber {
            get {
                return GetSecondaryGrabber();
            }
        }

        protected Vector3 rotatePosition;
        protected Vector3 previousPrimaryPosition;
        protected Vector3 previousSecondaryPosition;

        protected float targetAngle;
        protected float previousTargetAngle;

        /// <summary>
        /// This angle is smoothed towards target angle in Update using RotationSpeed
        /// </summary>
        protected float smoothedAngle;

        void Update() {

            // Calculate rotation if being held or returning to center
            if (grab.BeingHeld && grab.BeingHeldWithTwoHands) {
                UpdateAngleCalculations();
            }
            else if (ReturnToCenter) {
                ReturnToCenterAngle();
            }

            // Apply the new angle
            ApplyAngleToSteeringWheel(Angle);

            // Call any events
            CallEvents();

            UpdatePreviewText();

            // Update the angle so we can compare it next frame
            UpdatePreviousAngle(targetAngle);
        }        

        public virtual void UpdateAngleCalculations() {

            float angleAdjustment = 0f;

            // Add first Grabber
            if (PrimaryGrabber) {
                rotatePosition = transform.InverseTransformPoint(PrimaryGrabber.transform.position);
                rotatePosition = new Vector3(rotatePosition.x, rotatePosition.y, 0);

                // Add in the angles to turn
                angleAdjustment += GetRelativeAngle(rotatePosition, previousPrimaryPosition); 

                previousPrimaryPosition = rotatePosition;
            }

            // Add second Grabber
            if (AllowTwoHanded && SecondaryGrabber != null) {
                rotatePosition = transform.InverseTransformPoint(SecondaryGrabber.transform.position);
                rotatePosition = new Vector3(rotatePosition.x, rotatePosition.y, 0);

                // Add in the angles to turn
                angleAdjustment += GetRelativeAngle(rotatePosition, previousSecondaryPosition);

                previousSecondaryPosition = rotatePosition;
            }

            // Divide by two if being held by two hands
            if(PrimaryGrabber != null && SecondaryGrabber != null) {
                angleAdjustment *= 0.5f;
            }

            // Apply the angle adjustment
            targetAngle -= angleAdjustment;

            // Update Smooth Angle
            // Instant Rotation
            if(RotationSpeed == 0) {
                smoothedAngle = targetAngle;
            }
            // Apply smoothing based on RotationSpeed
            else {
                smoothedAngle = Mathf.Lerp(smoothedAngle, targetAngle, Time.deltaTime * RotationSpeed);
            }

            // Scrub the final results
            if (MinAngle != 0 && MaxAngle != 0) {
                targetAngle = Mathf.Clamp(targetAngle, MinAngle, MaxAngle);
                smoothedAngle = Mathf.Clamp(smoothedAngle, MinAngle, MaxAngle);
            }
        }

        public float GetRelativeAngle(Vector3 position1, Vector3 position2) {

            // Are we turning left or right?
            if (Vector3.Cross(position1, position2).z < 0) {
                return -Vector3.Angle(position1, position2);
            }

            return Vector3.Angle(position1, position2);
        }

        public virtual void ApplyAngleToSteeringWheel(float angle) {
            RotatorObject.localEulerAngles = new Vector3(0, 0, angle);
        }

        public virtual void UpdatePreviewText() {
            if (DebugText) {
                // Invert the values for display. Inverted values are easier to read (i.e 5 = clockwise rotation of 5 degrees). 
                DebugText.text = String.Format("{0}\n{1}", (int)AngleInverted, (ScaleValueInverted).ToString("F2"));
            }
        }

        public virtual void CallEvents() {
            // Call events
            if (targetAngle != previousTargetAngle) {
                onAngleChange.Invoke(targetAngle);
            }

            onValueChange.Invoke(ScaleValue);
        }

        public override void OnGrab(Grabber grabber) {
            // Primary or secondary that grabbed us?
            if(grabber == SecondaryGrabber) {
                previousSecondaryPosition = transform.InverseTransformPoint(SecondaryGrabber.transform.position);

                // Discard the Z value
                previousSecondaryPosition = new Vector3(previousSecondaryPosition.x, previousSecondaryPosition.y, 0);
            }
            // Primary
            else {
                previousPrimaryPosition = transform.InverseTransformPoint(PrimaryGrabber.transform.position);

                // Discard the Z value
                previousPrimaryPosition = new Vector3(previousPrimaryPosition.x, previousPrimaryPosition.y, 0);
            }
        }

        public virtual void ReturnToCenterAngle() {

            bool wasUnderZero = smoothedAngle < 0;

            if (smoothedAngle > 0) {
                smoothedAngle -= Time.deltaTime * ReturnToCenterSpeed;
            }
            else if (smoothedAngle < 0) {
                smoothedAngle += Time.deltaTime * ReturnToCenterSpeed;
            }

            // Overshot
            if (wasUnderZero && smoothedAngle > 0) {
                smoothedAngle = 0;
            }
            else if (!wasUnderZero && smoothedAngle < 0) {
                smoothedAngle = 0;
            }

            // Snap if very close
            if (smoothedAngle < 0.02f && smoothedAngle > -0.02f) {
                smoothedAngle = 0;
            }

            // Set the target angle to our newly calculated angle
            targetAngle = smoothedAngle;
        }

        public Grabber GetPrimaryGrabber() {
            if (grab.HeldByGrabbers != null) {
                for (int x = 0; x < grab.HeldByGrabbers.Count; x++) {
                    Grabber g = grab.HeldByGrabbers[x];
                    if (g.HandSide == ControllerHand.Right) {
                        return g;
                    }
                }
            }

            return null;
        }

        public Grabber GetSecondaryGrabber() {
            if (grab.HeldByGrabbers != null) {
                for (int x = 0; x < grab.HeldByGrabbers.Count; x++) {
                    Grabber g = grab.HeldByGrabbers[x];
                    if (g.HandSide == ControllerHand.Left) {
                        return g;
                    }
                }
            }

            return null;
        }

        public virtual void UpdatePreviousAngle(float angle) {
            previousTargetAngle = angle;
        }

        /// <summary>
        /// Returns a value between -1 and 1
        /// </summary>
        /// <param name="value">Current value to compute against</param>
        /// <param name="min">Minimum value of range used for conversion. </param>
        /// <param name="max">Maximum value of range used for conversion. Must be greater then min</param>
        /// <returns>Value between -1 and 1</returns>
        public virtual float GetScaledValue(float value, float min, float max) {
            float range = (max - min) / 2f;
            float returnValue = ((value - min) / range) - 1;

            return returnValue;
        }                

#if UNITY_EDITOR
        public void OnDrawGizmosSelected() {
            if (ShowEditorGizmos && !Application.isPlaying) {

                Vector3 origin = transform.position;
                float rotationDifference = MaxAngle - MinAngle;

                float lineLength = 0.1f;
                float arcLength = 0.1f;

                //This is the color of the lines
                UnityEditor.Handles.color = Color.cyan;

                // Min / Max positions in World space
                Vector3 minPosition = origin + Quaternion.AngleAxis(MinAngle, transform.forward) * transform.up * lineLength;
                Vector3 maxPosition = origin + Quaternion.AngleAxis(MaxAngle, transform.forward) * transform.up * lineLength;

                //Draw the min / max angle lines
                UnityEditor.Handles.DrawLine(origin, minPosition);
                UnityEditor.Handles.DrawLine(origin, maxPosition);

                // Draw starting position line
                Debug.DrawLine(transform.position, origin + Quaternion.AngleAxis(0, transform.up) * transform.up * lineLength, Color.magenta);

                // Fix for exactly 180
                if (rotationDifference == 180) {
                    minPosition = origin + Quaternion.AngleAxis(MinAngle + 0.01f, transform.up) * transform.up * lineLength;
                }

                // Draw the arc
                Vector3 cross = Vector3.Cross(minPosition - origin, maxPosition - origin);
                if (rotationDifference > 180) {
                    cross = Vector3.Cross(maxPosition - origin, minPosition - origin);
                }

                UnityEditor.Handles.color = new Color(0, 255, 255, 0.1f);
                UnityEditor.Handles.DrawSolidArc(origin, cross, minPosition - origin, rotationDifference, arcLength);
            }
        }
#endif
    }
}