public static NewPlayerMovement Instance { get; private set; }

    [SerializeField, Range(0f, 100f)]
    float maxWalkSpeed = 10f, maxRunSpeed = 15f;

    float walkOrRunSpeed;

    [SerializeField, Range(0f, 100f)]
    float maxAcceleration = 10f, maxAirAcceleration = 1f;

    [SerializeField, Range(0f, 10f)]
    float jumpHeight = 2f, dashForce = 5f;

    [SerializeField, Range(0, 5)]
    int maxAirJumps = 0, maxDashes = 1;

    [SerializeField, Range(0f, 90f)]
    float maxGroundAngle = 25f, maxStairsAngle = 50f;

    [SerializeField, Range(0f, 100f)]
    float maxSnapSpeed = 100f;

    [SerializeField, Min(0f)]
    float probeDistance = 1f;


    [SerializeField]
    LayerMask probeMask = -1, stairsMask = -1, waterBubble = -1, bounceMask;
    //probeMask should be everything but IgnoreRaycast and Agent --"Agent" is Player-layer.

    [SerializeField]
    Transform playerInputSpace = default; //make player input relative to a gameobject, we want camera to be put in there

    int jumpPhase, dashPhase;
    bool desiredJump, desiredDash;
    public bool DesiredJump { get { return desiredJump; } }
    Vector2 playerInput;
    Vector3 velocity, connectionVelocity;

    Vector3 upAxis, rightAxis, forwardAxis;
    Rigidbody body, connectedBody, previousConnectedBody;

    Vector3 connectionWorldPosition, connectionLocalPosition;

    float minGroundDotProduct, minStairsDotProduct; //min dot products (threshholds)

    Vector3 contactNormal, steepNormal;
    int groundContactCount, steepContactCount;

    public bool OnGround => groundContactCount > 0; //shorthand for bool OnGround { get {return groundContactCount > 0;} } ... shorthand to define a single-statement readonly property.    
    bool OnSteep => steepContactCount > 0;

    int stepsSinceLastGrounded, stepsSinceLastJump;

    Vector3 gravity;
    public Vector3 UpAxis { get { return upAxis; } }

    Vector3 xAxis, zAxis;

    void OnValidate()
    {
        //Computes the min dot product for different kinds of thresh holds
        minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
        minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
    }

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(gameObject);
        }

        body = GetComponent<Rigidbody>();
        body.useGravity = false;

        OnValidate();
    }

    private void Start()
    {
        MovementUpgrades.SetStats(maxAirJumps, maxDashes, jumpHeight, dashForce);
        MovementUpgrades.OnUpgrade += _OnUpgrade;
    }

    //when this is called, update the stats
    private void _OnUpgrade(object sender, EventArgs e)
    {
        jumpHeight = MovementUpgrades.jumpHeight;
        dashForce = MovementUpgrades.dashForce;
        maxAirJumps = MovementUpgrades.maxJumps;
        maxDashes = MovementUpgrades.maxDashes;
    }

    public Vector2 GetSpeedAndInputs(out float speed, out float maxSpeed)
    {
        maxSpeed = walkOrRunSpeed;
        speed = velocity.magnitude;
        return playerInput;
    }

    public Vector3 GetForwardAxis()
    {
        // Debug.Log("forwardaxis: " + forwardAxis + " velocity direction???: " + velocity.normalized);
        return  forwardAxis;
    }

    public Vector3 GetRightAxis()
    {
        return xAxis;
    }

    void Update()
    {
        playerInput.x = Input.GetAxis("Horizontal");
        playerInput.y = Input.GetAxis("Vertical");
        playerInput = Vector2.ClampMagnitude(playerInput, 1f); 

        if (playerInputSpace) 
        {
            rightAxis = ProjectDirectionOnPlane(playerInputSpace.right, upAxis);
            forwardAxis = ProjectDirectionOnPlane(playerInputSpace.forward, upAxis);
        }
        else
        {
            rightAxis = ProjectDirectionOnPlane(Vector3.right, upAxis);
            forwardAxis = ProjectDirectionOnPlane(Vector3.forward, upAxis);
        }

        desiredJump |= Input.GetButtonDown("Jump");
        desiredDash |= Input.GetKeyDown(KeyCode.F);

        
    }

    void FixedUpdate()
    {
        EvaluateGravitySource();
        
        UpdateState();
        AdjustVelocity();

        if (desiredJump)
        {
            desiredJump = false;
            Jump(gravity);
        }

        if (desiredDash)
        {
            desiredDash = false;
            Dash();
        }
        
        if (OnGround && velocity.sqrMagnitude < 0.01f)
        {
            velocity += contactNormal * (Vector3.Dot(gravity, contactNormal) * Time.deltaTime);
        }
        else
        {
            velocity += gravity * Time.deltaTime;
        }

        body.velocity = velocity; //body is the rigidbody
        transform.up = upAxis;
        ClearState();
    }

    void AdjustVelocity() //changed
    {
        float acceleration;
        //Vector3 xAxis, zAxis;
        
        
        walkOrRunSpeed = (Input.GetKey(KeyCode.LeftShift)) ? maxRunSpeed : maxWalkSpeed;
        acceleration = OnGround ? maxAcceleration : maxAirAcceleration;

        xAxis = rightAxis;
        zAxis = forwardAxis;
        
        xAxis = ProjectDirectionOnPlane(xAxis, contactNormal);
        zAxis = ProjectDirectionOnPlane(zAxis, contactNormal);

        //Debug.Log("xAxis: " + xAxis + "zAxis: " + zAxis);

        Vector3 relativeVelocity = velocity - connectionVelocity;
        float currentX = Vector3.Dot(relativeVelocity, xAxis);
        float currentZ = Vector3.Dot(relativeVelocity, zAxis);

        //float acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
        float maxSpeedChange = acceleration * Time.deltaTime;

        float newX = Mathf.MoveTowards(currentX, playerInput.x * walkOrRunSpeed, maxSpeedChange);
        float newZ = Mathf.MoveTowards(currentZ, playerInput.y * walkOrRunSpeed, maxSpeedChange);

        velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
    }

    Vector3 ProjectDirectionOnPlane(Vector3 direction, Vector3 normal) //new, replaced the above function with this one. This method works with an arbitrary normal and also performs the normalization at the end
    {
        return (direction - normal * Vector3.Dot(direction, normal)).normalized;
    }

    private void EvaluateGravitySource()
    {
        gravity = CustomGravity.GetGravity(body.position, out upAxis);   
        //Debug.Log("Current gravity source: " + CustomGravity.GetGravitySource);
    }

    void UpdateState() //changed
    {
        //Now we also update stepsSinceLastGrounded to 0 before we do proceed to do more calculations
        //If stepsSinceLastJump, a different float, is greater than 1, then we reset he jumpPhase
        //stepsSinceLastGrounded += 1;
        //stepsSinceLastJump += 1;
        
        if (stepsSinceLastJump > 1)
        {
            jumpPhase = 0;
        }

        velocity = body.velocity;
        if (OnGround || SnapToGround() || CheckSteepContacts()) //if we are grounded, snapped to ground, or on a steepContact then we reset steps and jumpPhase
        {
            stepsSinceLastGrounded = 0;
            jumpPhase = 0;
            dashPhase = 0;
            if (groundContactCount > 1) //We also normalize contactNormal (essentially the jumping direction) if we are in a concave mesh - standing at a point where we are touching several points
            {
                contactNormal.Normalize();
            }
        }
        else
        {
            contactNormal = upAxis; //Changed Vector.up with upAxis , so that we can implement custom gravity
        }

        if (connectedBody) //New, from moving platform 
        {
            if (connectedBody.isKinematic || connectedBody.mass >= body.mass)
            {
                UpdateConnectionState();
            }
        }

    }

    void UpdateConnectionState()//New, from moving platform 
    {
        if (connectedBody == previousConnectedBody)
        {
            Vector3 connectionMovement = connectedBody.transform.TransformPoint(connectionLocalPosition) - connectionWorldPosition;
            connectionVelocity = connectionMovement / Time.deltaTime;
        }
        connectionWorldPosition = body.position;
        connectionLocalPosition = connectedBody.transform.InverseTransformPoint(connectionWorldPosition);
    }

    void ClearState()
    {
        groundContactCount = steepContactCount  = 0;
        contactNormal = steepNormal = connectionVelocity  = Vector3.zero;
        previousConnectedBody = connectedBody;
        connectedBody = null;
    }

    void Jump(Vector3 gravity, float overrideJumpSpeed = 1f) //changed, it now has a parameter
    {
        //The changes are that now we check whether PC is on ground, OnSteep or still airborn in which case the jumpDirection remains the contactNormal

        Vector3 jumpDirection; //we create a jumpDirection vector, which changes based on what player stands on/or if in air

        if (OnGround)
        {
            jumpDirection = contactNormal;
        }
        else if (OnSteep)
        {
            jumpDirection = steepNormal;
            jumpPhase = 0;
        }
        else if (maxAirJumps > 0 && jumpPhase <= maxAirJumps)
        {
            if (jumpPhase == 0)
            {
                jumpPhase = 1;
            }
            jumpDirection = contactNormal;
        }
        else
        {
            return;
        }

        stepsSinceLastJump = 0;
        jumpPhase += 1;
        float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight); //changed, instead of Physics.gravity.magnitude, we use our custom gravity which is passed in as a parameter

        jumpDirection = (jumpDirection + upAxis).normalized; //Also changed jumpDirection + Vector3.up with jumpDirection + upAxis, as upAxis will now change, as in the direction off the gravity will change
        float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
        if (alignedSpeed > 0f)
        {
            jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
        }
        velocity += jumpDirection * jumpSpeed * overrideJumpSpeed;

    }

    private void Dash()
    {
        Vector3 dashDirection;
        if(dashPhase <= maxDashes && !OnGround)
        {
            dashDirection = playerInputSpace.forward;
            dashDirection.Normalize();
        }
        else
        {
            return;
        }
        
        dashPhase += 1;
        velocity += dashDirection * dashForce;
    }

    void OnCollisionEnter(Collision collision)
    {
        EvaluateCollision(collision);
    }

    void OnCollisionStay(Collision collision)
    {
        EvaluateCollision(collision);
    }

    void EvaluateCollision(Collision collision)
    {
        int layer = collision.gameObject.layer;
        float minDot = GetMinDot(layer);
        for (int i = 0; i < collision.contactCount; i++)
        {
            Vector3 normal = collision.GetContact(i).normal;
            float upDot = Vector3.Dot(upAxis, normal); //new, checks what kind of contact we have
            //Debug.Log("normal and updot: " + normal + "" + upDot + "upaxis: " + upAxis);
            if (upDot >= minDot)
            {
                groundContactCount += 1;
                contactNormal += normal;
                connectedBody = collision.rigidbody;
                //Debug.Log("When does this call?"); //gets called when standing on flat ground
                if((bounceMask & (1 << layer)) != 0)
                {
                    BouncePlatform bouncePlatform = collision.gameObject.GetComponent<BouncePlatform>();
                    Vector3 jumpDirection = contactNormal;
                    float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight);

                    jumpDirection = (jumpDirection + upAxis).normalized;
                    body.AddForce(jumpDirection * jumpSpeed * bouncePlatform.GetBounceForce(), ForceMode.Impulse);
                }
            }
            //else if (upDot > -0.01f) {
            else
            {
                if (upDot > -0.01f)
                {
                    steepContactCount += 1;
                    steepNormal += normal;
                    //Debug.Log("and When does this call?"); //gets called when not on flat ground, but when walls/slopes etc.
                    if (groundContactCount == 0)
                    {
                        connectedBody = collision.rigidbody;
                    }
                }
            }
        }
    }

    public void ResetDashAndJump()
    {
        dashPhase = 0;
        jumpPhase = 0;
    }

    bool SnapToGround()
    {
        //This returns true if we want to snap PC to the ground, if we are running quickly over a small stump we can prevent PC from flying, and just snap to ground if velocity is high enough
        if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2)
        {
            return false;
        }
        float speed = velocity.magnitude; //we calculate the speed earlier then we used to, so we can return early if we haven't reached a desired speed
        if (speed > maxSnapSpeed) //if we do not move fast enough, then snap
        {
            return false;
        }
        if (!Physics.Raycast(body.position, -upAxis, out RaycastHit hit, probeDistance, probeMask)) //We changed the second parameter (which was Vector3.down) to -upAxis, so down relative to where the player is
        {
            return false;
        }
        float upDot = Vector3.Dot(upAxis, hit.normal); //new -- we also removed hit.normal.y to upDot, as we no longer could use that
        if (upDot < GetMinDot(hit.collider.gameObject.layer))
        {
            return false;
        }
        groundContactCount = 1;
        contactNormal = hit.normal;
        //float speed = velocity.magnitude;
        float dot = Vector3.Dot(velocity, hit.normal);
        if (dot > 0f)
        {
            velocity = (velocity - hit.normal * dot).normalized * speed; //the actual snapping code, it changes the velocity to align with the ground?? if I'm correct..
        }
        connectedBody = hit.rigidbody;
        return true;
    }

    float GetMinDot(int layer)
    {
        return (stairsMask & (1 << layer)) == 0 ? minGroundDotProduct : minStairsDotProduct;
    }

    bool CheckSteepContacts()
    {

        if (steepContactCount > 1)
        {
            steepNormal.Normalize();
            float upDot = Vector3.Dot(upAxis, steepNormal); //new, used in statement below
            if (upDot >= minGroundDotProduct) //replaced steepNormal.y with upDot
            {
                groundContactCount = 1;
                contactNormal = steepNormal;
                return true;
            }
        }
        return false;
    }