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

public class PlayerControl : MonoBehaviour
{
    public static int DASHABLE_LAYER = 8;
    public static int DEFAULT_LAYER = 0;
    public static int PLAYER_LAYER = 6;
    public static int PLAYER_DASHING_LAYER = 10;

    public Rigidbody2D rigidBody;
    public Collider2D dashExitCollider;
    public Collider2D groundCheckCollider;
    public PlayerAnimator animationMgr;
    public float gravityFactor = 4;
    public float maxFloorDistance = 0.2f;
    public float maxOverEdgeJumpTimeout = 0.3f;
    public float minTimeBetweenJumps = 0.4f;
    public float jumpForce = 30f;
    public float fallFaster = 1.5f;
    public float minAxisTrigger = 0.3f;
    public float targetMoveVelocity = 8f;
    public float moveAcceleration = 4f;
    public float targetMoveQuickStartVelocity = 6f;
    public float moveQuickStartAcceleration = 10f;
    public float moveAccelerationInAir = 2f;
    public float dashTime = 0.5f;
    public PhysicsMaterial2D frictionOnGround;
    public PhysicsMaterial2D frictionOnWall;
    public float dampingOnFloor = 0.5f;
    public float sampleDistance = 1f;
    public float dashSpeed = 20f;
    public float dashExitSpeed = 20f;
    public float dashUpReduction = 0.7f;
    public float preDashFreezeTime = 0.03f;
    public float dashCorrectTime = 0.08f;
    public float minFloorDistance = 1;
    public float minRoofDistance = 1;
    public AnimationCurve wobbleCurve;
    public float fluidifySpeedOn = 1f;
    public float fluidifySpeedOff = 1f;
    public float fallingFastTreshhold = 3f;
    public float exitThroughMaterialFactorUpDown = 1.7f;

    public bool enableUserControl = false;

    // Sound
    public ISoundEffect soundDash;
    public ISoundEffect soundJump;
    
    // State
    private float myAngle;

    // Next controls to trigger
    private bool buttonJump;
    private bool buttonRight;
    private bool buttonRightWasDown;
    private bool buttonLeft;
    private bool buttonLeftWasDown;
    private bool buttonDash;
    private bool buttonUp;
    private bool buttonDown;
    private bool wasDashing;
    private bool buttonsDoReset;

    private float lastTouchingGround;
    private bool isTouchingGround;
    private float lastJumpTime;
    private Vector3 normalizedGravityDir = Vector3.down;
    private float gravityDerivative = 0;
    private float timeSinceDashStart = -100;
    private bool readyToDash = true;
    private bool wentThroughMaterial = false;

    private Vector3 lastSafePosition;
    private float lastSafeRotation;
    private Vector3 dashDirection;
    private bool dashStarted;
    private float dashDerivateAngle;
    private float dashActive;
    private bool isLookRight = true;
    private bool isDashUpOrDown = false;

    // Memory
    private RaycastHit2D[] tmpCastResults;

    // Start is called before the first frame update
    void Start()
    {
        myAngle = 0;
        tmpCastResults = new RaycastHit2D[1];

        // Safe point
        lastSafePosition = transform.position;
        lastSafeRotation = myAngle;
    }

    // Update is called once per frame
    void Update()
    {
        // Check controls
        if(enableUserControl) {
            if(buttonsDoReset) {
                ResetInput();
            }
            buttonJump = buttonJump | Input.GetButtonDown("Jump");
            buttonDash = buttonDash | Input.GetButtonDown("Dash");
            buttonRight = buttonRight || Input.GetAxis("Horizontal") > minAxisTrigger;
            buttonLeft = buttonLeft || Input.GetAxis("Horizontal") < -minAxisTrigger;
            buttonUp = buttonUp || Input.GetAxis("Vertical") > minAxisTrigger;
            buttonDown = buttonDown || Input.GetAxis("Vertical") < -minAxisTrigger;
        }

        // Dash Shader
        if(wasDashing) {
            dashActive+=fluidifySpeedOn * Time.deltaTime;
        } else {
            dashActive-=fluidifySpeedOff * Time.deltaTime;
        }
        dashActive = Mathf.Clamp01(dashActive);
        //Debug.Log(dashActive);
        SetWobbleShader(dashActive);
    }

    public void ResetInput() {
        buttonsDoReset = false;
        buttonRight = false;
        buttonLeft = false;
        buttonUp = false;
        buttonDown = false;
    }

    public void AutoRunRight() {
        buttonRight = true;
    }

    public void AutoRunLeft() {
        buttonLeft = true;
    }

    public void AutoLookLeft() {
        isLookRight = false;
    }

    private void ToggleDashColliders (bool collisionsOn) {
        if(collisionsOn) {
            gameObject.layer = PLAYER_LAYER;
        } else {
            gameObject.layer = PLAYER_DASHING_LAYER;
        }
    }

    private float CalculateMoveForceFactor(float currentVelocity, float targetVelocity) {
        return 1-Mathf.Clamp01((currentVelocity+targetVelocity)/targetVelocity/2);
    }

    private void SetWobbleShader (float percent) {
        Shader.SetGlobalFloat("_WobbleActive", wobbleCurve.Evaluate(percent));
    }

    private void FixedUpdate() {
        // Orientation (from previous frame) - will be refreshed later on in this method
        Vector3 right = ExtraMath.RotateRight(-normalizedGravityDir);
        Vector3 left = ExtraMath.RotateRight(normalizedGravityDir);

        // Check if Dash Mode
        bool isDashing = Time.timeSinceLevelLoad - timeSinceDashStart < dashTime;
        bool preDashFreeze = Time.timeSinceLevelLoad - timeSinceDashStart < preDashFreezeTime;
        bool canCorrectTime = Time.timeSinceLevelLoad - timeSinceDashStart < dashCorrectTime;
        bool canDashForward = false;
        if(!preDashFreeze) {
            if(isDashing && (!dashStarted || canCorrectTime)) {
                // Check the direction
                Vector3 dir = Vector3.zero;
                dashDerivateAngle = 0;
                bool upDash = false;
                int countUp = 0;
                int countRight = 0;
                if(buttonRight) {
                    dir += right;
                    dashDerivateAngle += -gravityDerivative;
                    countRight++;
                }
                if(buttonLeft) {
                    dir += left;
                    dashDerivateAngle += gravityDerivative;
                    countRight --;
                }
                if(buttonDown) {
                    dir += normalizedGravityDir;
                    countUp--;
                }
                if(buttonUp) {
                    dir += -normalizedGravityDir;
                    upDash = true;
                    countUp++;
                }
                if(dir.sqrMagnitude < 0.5f) {
                    // Default dash direction = the one we are moving in
                    if(isLookRight) {
                        dir += right;
                        countRight++;
                    } else {
                        dir += left;
                        countRight--;
                    }
                }

                // Animate
                if(countUp == 1 && countRight == 0) {
                    animationMgr.StartDashUp();
                }
                if(countUp == 1 && (countRight == 1 || countRight == -1)) {
                    animationMgr.StartDashUpRight();
                }
                if(countUp == 0 && (countRight == 1 || countRight == -1)) {
                    animationMgr.StartDashRight();
                }
                if(countUp == -1 && (countRight == 1 || countRight == -1)) {
                    animationMgr.StartDashDownRight();
                }
                if(countUp == -1 && countRight == 0) {
                    animationMgr.StartDashDown();
                }
                if(countRight > 0) {
                    isLookRight = true;
                }
                if(countRight < 0) {
                    isLookRight = false;
                }

                dashDirection = dir.normalized;
                isDashUpOrDown = countUp > 0;// Only count up for extra dash

                if(upDash) {
                    dashDirection *= dashUpReduction;
                }
                // Do the dash
                dashStarted = true;
            }
            if(wasDashing || isDashing) {
                // Can dash forward still?
                if(Physics2D.Raycast(transform.position, dashDirection, dashSpeed * Time.fixedDeltaTime, 1 << DEFAULT_LAYER).collider == null) {
                    canDashForward = true;

                    // Correct slighly upwards if needed
                    RaycastHit2D hit = Physics2D.Raycast(transform.position, -transform.up, minFloorDistance, 1 << DEFAULT_LAYER);
                    if(hit.collider != null) {
                        transform.position = transform.position + (minFloorDistance - hit.distance)*transform.up;
                    }

                    // Correct slighly downwards if needed
                    hit = Physics2D.Raycast(transform.position, transform.up, minRoofDistance, 1 << DEFAULT_LAYER);
                    if(hit.collider != null) {
                        transform.position = transform.position + (minRoofDistance - hit.distance)*transform.up;
                    }
                }

                // If we were dashing, check if we can exit the dash!
                if(dashExitCollider.IsTouchingLayers(1 << DASHABLE_LAYER)) {
                    // Cannot exit!
                    if(canDashForward) {
                        isDashing = true;
                        if(!wentThroughMaterial) {
                            // Enter material!
                            wentThroughMaterial = true;
                        }
                    } else {
                        // NEED TO EXIT => reset player
                        transform.position = lastSafePosition;
                        myAngle = lastSafeRotation;
                    }
                }
                if(!canDashForward) {
                    isDashing = false;
                    timeSinceDashStart = Time.timeSinceLevelLoad - (dashTime + 1);
                }
            }
            if(wasDashing && !isDashing) {
                if(isDashUpOrDown && wentThroughMaterial) {
                    dashDirection = dashDirection*exitThroughMaterialFactorUpDown;
                }

                // Exit speed
                rigidBody.velocity = dashDirection * dashExitSpeed;

                // Re-enable colliders
                ToggleDashColliders(true);

                // Animate
                animationMgr.ResetDashing();
            }
        }


        // Sample gravity
        Vector2 gravity = GravitySampler.Sample(transform.position);
        float gravityMagnitude = Mathf.Clamp01(gravity.magnitude);

        // Apply gravity
        float gravityMultiplier = 1f;
        if(Vector2.Dot(gravity, rigidBody.velocity) > 0) {
            gravityMultiplier = fallFaster;
        }
        if(!isDashing) {
            rigidBody.AddForce(gravity * gravityFactor * gravityMultiplier);
        }

        // Set player rotation
        transform.rotation = Quaternion.AngleAxis(myAngle, Vector3.forward);

        // Check whether we are on the floor?
        if(gravityMagnitude < 0.2f) {
            // We are in "space" (not on the floor)
            isTouchingGround = false;
        } else {
            // Update primary gravity direction! (so we know the direction of gravity in space)
            normalizedGravityDir = gravity.normalized;

            // Also calculate the secondary derivative of the gravity
            Vector2 gravityRight = GravitySampler.Sample((Vector2)transform.position + ExtraMath.RotateRight(normalizedGravityDir) * sampleDistance).normalized;
            Vector2 gravityLeft = GravitySampler.Sample((Vector2)transform.position + -ExtraMath.RotateRight(normalizedGravityDir) * sampleDistance).normalized;
            gravityDerivative = (Vector3.SignedAngle(gravity, gravityRight, Vector3.forward) + Vector3.SignedAngle(gravityLeft, gravity, Vector3.forward))/2;

            // Check whether there is floor?
            int amountHits = groundCheckCollider.Cast(gravity.normalized, tmpCastResults, maxFloorDistance, true);
            isTouchingGround = amountHits > 0;
        }
        if(isTouchingGround) {
            lastTouchingGround = Time.timeSinceLevelLoad;
            if(!isDashing) {
                readyToDash = true;
            }

            if(!isDashing) {
                lastSafePosition = transform.position;
                lastSafeRotation = myAngle;
            }
        }

        // Re-orient player based on gravity
        float destinationAngle = Vector3.SignedAngle(Vector3.up, -normalizedGravityDir, Vector3.forward);
        float angleDiff = ExtraMath.NormalizeAngle((destinationAngle - myAngle)*Mathf.Deg2Rad)*Mathf.Rad2Deg;

        myAngle += angleDiff * gravityMagnitude;

        bool isOrWasTouchingFloor = Time.timeSinceLevelLoad - lastTouchingGround < maxOverEdgeJumpTimeout;

        // Handle jump
        if(buttonJump) {
            buttonJump = false;
            // Cannot jump while dashing
            if(!isDashing) {
                // Did we jump already recently?
                if(Time.timeSinceLevelLoad - lastJumpTime >= minTimeBetweenJumps) {
                    // Is touching floor? (or was touching floor?)
                    if(isOrWasTouchingFloor) {
                        // Ok, so jump for real
                        lastJumpTime = Time.timeSinceLevelLoad;
                        rigidBody.AddForce(jumpForce* -normalizedGravityDir, ForceMode2D.Impulse);
                        animationMgr.TriggerJump();

                        soundJump.Trigger();
                    }
                }
            }
        }

        if(wasDashing && !isDashing) {
            // End of Dash!
            readyToDash = wentThroughMaterial;
        }

        // Orientation
        right = ExtraMath.RotateRight(-normalizedGravityDir);
        left = ExtraMath.RotateRight(normalizedGravityDir);

        // Dash!
        if(buttonDash) {
            buttonDash = false;

            if(!isDashing && readyToDash) {
                isDashing = true;
                timeSinceDashStart = Time.timeSinceLevelLoad;
                wentThroughMaterial = false;
                readyToDash = false;

                // Disable dash colliders
                ToggleDashColliders(false);

                // Control Dash
                rigidBody.velocity = Vector3.zero;

                dashStarted = false;// First freeze to collect button input!

                soundDash.Trigger();
            }
        }

        if(isDashing && dashStarted) {
            // We already checked that we can dash forward (raycast at the start!)
            Vector3 oldPosition = transform.position;
            transform.position = transform.position + dashDirection * dashSpeed * Time.fixedDeltaTime;

            Debug.DrawLine(oldPosition, transform.position, Color.cyan, 4f);

            // Follow planet angle
            dashDirection = Quaternion.AngleAxis(dashDerivateAngle * dashSpeed * Time.fixedDeltaTime, Vector3.forward) * dashDirection;
        }

        // Horizontal velocity
        float velocityRight = Vector3.Dot(right, rigidBody.velocity);
        float velocityLeft = Vector3.Dot(left, rigidBody.velocity);

        bool isMoving = false;
        bool isRunning = false;

        // Handle right arrow
        if(buttonRight) {
            if(!isDashing) {
                isMoving = true;
                isLookRight = true;
                if(isOrWasTouchingFloor) {
                    if(!buttonRightWasDown && velocityRight < 0) {
                        // Quick accelerate
                        rigidBody.AddForce(right*CalculateMoveForceFactor(velocityRight, targetMoveQuickStartVelocity)*moveQuickStartAcceleration, ForceMode2D.Impulse);
                    }
                    // Accelerate
                    rigidBody.AddForce(right*CalculateMoveForceFactor(velocityRight, targetMoveVelocity)*moveAcceleration);
                    isRunning = true;
                } else {
                    // In Air
                    rigidBody.AddForce(right*CalculateMoveForceFactor(velocityRight, targetMoveVelocity)*moveAccelerationInAir);
                }
            }

            buttonRightWasDown = buttonRight;
        }

        // Handle left arrow
        if(buttonLeft) {
            if(!isDashing) {
                isMoving = true;
                isLookRight = false;
                if(isOrWasTouchingFloor) {
                    if(!buttonLeftWasDown && velocityLeft < 0) {
                        // Quick accelerate
                        rigidBody.AddForce(left*CalculateMoveForceFactor(velocityLeft, targetMoveQuickStartVelocity)*moveQuickStartAcceleration, ForceMode2D.Impulse);
                    }
                    // Accelerate
                    rigidBody.AddForce(left*CalculateMoveForceFactor(velocityLeft, targetMoveVelocity)*moveAcceleration);
                    isRunning = true;
                } else {
                    // In Air
                    rigidBody.AddForce(left*CalculateMoveForceFactor(velocityLeft, targetMoveVelocity)*moveAccelerationInAir);
                }
            }

            buttonLeftWasDown = buttonLeft;
        }


        // Notify animator
        animationMgr.SetCanDash(readyToDash);
        animationMgr.SetRunning(isRunning);
        animationMgr.SetInAir(!isTouchingGround);
        animationMgr.SetFallingFast(!isTouchingGround && rigidBody.velocity.magnitude > fallingFastTreshhold);
        animationMgr.SetIsLookRight(isLookRight);

        // Finalize
        wasDashing = isDashing;

        // Update friction
        if(isTouchingGround && !isDashing && !isMoving) {
            // Split velocity
            Vector2 verticalVelocity = Vector2.Dot(rigidBody.velocity, normalizedGravityDir)*normalizedGravityDir;
            Vector2 otherVelocity = rigidBody.velocity - verticalVelocity;

            rigidBody.velocity = verticalVelocity + otherVelocity * dampingOnFloor;
        }

        if(!preDashFreeze) {
            buttonsDoReset = true;
        }
    }

    private void OnDrawGizmos() {
        Vector3 dir = ExtraMath.RotateRight(normalizedGravityDir);
        Vector3 position = transform.position;
        for(int i=0;i<10;i++) {
            Vector3 newPosition = position + dir;
            Debug.DrawLine(position, newPosition, Color.green);
            position = newPosition;
            dir = Quaternion.AngleAxis(gravityDerivative, Vector3.forward) * dir;
        }
    }
}
