using System;
using Assets.Scripts.Gameplay;
using Assets.Scripts.Terrain;
using UnityEngine;

namespace Assets.Scripts.CarController
{
    public class CarController : MonoBehaviour
    {
        public static CarController Instance;

        [Header("Car Settings")]
        public Transform CenterOfGravity;
        public AnimationCurve TorqueCurve;
        public AnimationCurve GearRatio;

        [Range(0, 1)] public float Throttle;
        [Range(0, 1)] public float Clutch;
        [Range(0, 5)] public int Gear;
        public float Rpm;
        public float Speed;

        //Car internal settings
        private readonly float _topSpeed = 130f;
        private readonly float _minRpm = 600f;
        private readonly float _maxRpm = 7500f;
        private readonly float CylinderBore = 0.115f;
        private readonly float _pistonStroke = 0.115f;
        private readonly float[] _gearSpeeds = { 0f, 30f, 70f, 110f, 150f, 200f, 280f };

        private Wheel[] _wheels;
        [HideInInspector] public float Acceleration;
        [HideInInspector] public float Steering;
        public float Brake;
        [HideInInspector] public float Handbrake;
        private Rigidbody _body;
        private Vector3 _oldPosition;

		public float MaxSpeed{get { return _topSpeed; }}
        private SteeringWheelController _wheel;

        void Awake()
        {
            Instance = this;

            _body = gameObject.GetComponent<Rigidbody>();
            _body.centerOfMass = CenterOfGravity.localPosition;

            _wheels = new Wheel[4];
            FindChildren(transform);
        }

        void FindChildren(Transform obj)
        {
            var wheels = string.Equals(obj.name, "Alloys01", StringComparison.CurrentCultureIgnoreCase);
            var wheelCount = 0;
            foreach (Transform child in obj)
            {
                if (child.childCount > 0) FindChildren(child);
                if (wheels)
                {
                    _wheels[wheelCount] = new Wheel()
                    {
                        Collider = child.gameObject.GetComponent<WheelCollider>(),
                        Trans = child,
                        Home = child.eulerAngles
                    };
                    wheelCount++;
                }
            }
        }

        void FixedUpdate()
        {
            SetAngle(_wheels[2], Steering);
            SetAngle(_wheels[3], Steering);

            var torque = CalculateTorque();
            if (Throttle <= 0) torque = 0f;
            var gearedTorque = CalculateGearFactor(torque);
            RotateWheels(gearedTorque);
            AddDownForce();
            LimitTopSpeed();

            if (Throttle > 0.1) return;
            _body.velocity *= 0.99f;
        }


        private void AddDownForce()
        {
            _body.AddForce(-transform.up * 150f * _body.velocity.magnitude);
        }

        void RotateWheels(float gearedTorque)
        {
            for (var i = 0; i < 4; ++i)
            {
                _wheels[i].Collider.motorTorque = (gearedTorque * (1 - Clutch)) * (1 - Brake);
            }
        }

        private float CalculateGearFactor(float torque)
        {
            var speed = _body.velocity.magnitude;
            speed *= 2.23693629f;

            var gearRatio = TorqueCurve.Evaluate(torque) * GearRatio.Evaluate(Gear);

            if (Gear == 0)
                gearRatio = 0f;
            else
            {
                if (Gear == -1) return gearRatio;

                var lowSpeed = Gear > 0 ? _gearSpeeds[Gear - 1] : 0;
                var highSpeed = _gearSpeeds[Gear];

                var dif = 1f;
                if (speed < lowSpeed)
                {
                    dif = 1 - Mathf.Abs((speed - lowSpeed) / highSpeed);
                }
                else if (speed > highSpeed)
                {
                    dif = highSpeed - speed;
                }
                gearRatio *= dif;
            }
            return gearRatio;
        }

        void SetAngle(Wheel wheel, float steering)
        {
            wheel.Collider.steerAngle = steering;
            wheel.Trans.localEulerAngles = wheel.Home + (wheel.Trans.transform.up * steering);
        }


        float CalculateTorque()
        {
            return CalculateEngineTorque();
        }

        float CalculateEngineTorque()
        {
            //This is dodgy. but will do for now. Basically I want the car to lift up if you push the throttle right up asap
            Rpm = (Throttle / 1f) * (_maxRpm - _minRpm) + _minRpm;

            /**
             * www.alternatewars.com/BBOW/Engineering/PistonEngine_Power.htm
             * Power = (n * ( p / 4 ) * D^2 * L) * p * N
            */
            float tempRpm = (Rpm < _minRpm) ? _minRpm: (Rpm > _maxRpm) ? _maxRpm: Rpm;
            float strokes = tempRpm / 60 / 2;         //per second
            float cylinders = 8;
            float pistonDiameter = CylinderBore;          //meters
            float pressure = 1000000f * Throttle;         /* number of firing strokes per second, 
                                                         * which equals the number of revolutions per second in the case
                                                         * of two-stroke engines and half the number of revolutions per second
                                                         * in the case of four stroke engines.
                                                    
             */
            //2f = 2 wheel drive? I'm going to do it, cause why not
           var torque = ((cylinders * (Mathf.PI / 2f)  * Mathf.Pow(pistonDiameter, 2f) * _pistonStroke) * pressure * strokes) * 0.00134102f;    //Dont hardcode values like this with no explanation of where they came from
            return torque;
        }

        private void LimitTopSpeed()
        {
            var speed = _body.velocity.magnitude;
            speed *= 2.23693629f;   //No idea what this magic number is
            if (speed > _topSpeed)
                _body.velocity = (_topSpeed / 2.23693629f) * _body.velocity.normalized;

            Speed = speed;
        }


        // Use this for initialization
        void Start () {
		    _wheel = SteeringWheelController.Instance;
        }
	
        // Update is called once per frame
        void Update ()
        {
            var distance = Vector3.Distance(transform.position, _oldPosition);
            if (distance < 5f) return;
            _oldPosition = transform.position;
            TerrainGenerator.Instance.CheckCarPosition(transform.position, _body.velocity.magnitude);
        }

        void OnTriggerExit(Collider other)
        {
            Steering = 0;
            if (string.Equals(other.name, "bounds", StringComparison.CurrentCultureIgnoreCase )) TerrainGenerator.Instance.RegenMap();
        }
    }
}
