using Godot;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

public partial class PlayerPhysicsController : CharacterBody2D {
    // [Export]
    // public float RunMultiplier { get; set; } = 1.84f;

    [Signal]
    public delegate void OnTurnbackJumpEventHandler(bool onJumpPoint, bool isOnWall);

    [ExportCategory("Accelerations and Impulses")]
    [Export]
    public float WalkAcceleration { get; set; } = 3f;
    [Export]
    public float StoppingDeceleration { get; set; } = 8f;
    [Export]
    public float CancellationAcceleration { get; set; } = 4f;
    [Export]
    public float AirAcceleration { get; set; } = 0.5f;

    [Export]
    public float JumpHeight { get; set; } = 5.5f;
    
    [ExportCategory("Max Speeds")]
    [Export]
    private float MaxFallSpeed { get; set; } = 8f;

    [Export]
    private float MaxRiseSpeed { get; set; } = 16f;

    [Export]
    private float MaxRunSpeed { get; set; } = 16f;
    [Export]
    private float MaxWalkSpeed { get; set; } = 8f;

    [ExportCategory("Objects")]
    [Export]
    public Label DebugTextLabel { get; set; }

    [Export]
    public AnimatedSprite2D Sprite { get; set; }

    [Export]
    public PredictiveCollisions Predictor { get; set; }

    [ExportCategory("Physics Tweaks")]
    [Export]
    public float Gravity { get; set; } = 30.625f;

    private float TilesToMeters => 9.81f / Gravity;

    private float PxToMeters => (1 / UnitToPx) * TilesToMeters;

    [Export]
    public float UnitToPx { get; set; } = 32;

        private float _timeOffGround = 0.0f;

    [Export]
    public float CoyoteTiming { get; set; } = 0.1f;
    
    [Export]
    public float Deadzone { get; set; } = 0.1f;




    private float _lv = 0.0f;

    private void RecalculateLv() {
        _lv = Mathf.Sqrt(Mathf.Pow(Gravity * 0.1f / 2, 2) + 2 * Gravity * JumpHeight);
    }

    public override void _Ready() {
        RecalculateLv();
        base._Ready();
    }

    
    public override void _Process(double delta) {
        var grounded = this.IsOnFloor();
        var absvel = new Vector2(Mathf.Abs(Velocity.X), Mathf.Abs(Velocity.Y));
        var xAxis = Input.GetAxis("move_left", "move_right");
        Sprite.FlipH = xAxis < 0;
        if (grounded && absvel.X > 0.1) {
            Sprite.Animation = "walk";
            Sprite.SpeedScale = absvel.X switch {
                < 10 => 1,
                < 150 => 2,
                < 600 => 4,
                _ => 4
            };
            if (!Sprite.IsPlaying())
                Sprite.Play();
        } else {
            Sprite.SpeedScale = 1;
        }
        
        if (grounded && absvel.X < 0.1) {
            Sprite.Animation = "idle";
            if (!Sprite.IsPlaying())
                Sprite.Play();
        }

        if (!grounded && Velocity.Y > 0.1) {
            Sprite.Animation = "fall";
        }

        if (!grounded && Velocity.Y < 0.1) {
            Sprite.Animation = "jump";
        }
    }

    public override void _PhysicsProcess(double delta) {
        var xAxis = Input.GetAxis("move_left", "move_right");
        var yAxis = Input.GetAxis("move_up", "move_down");
        var lookAxis = Input.GetAxis("look_up", "look_down");
        // var jumpStrength = -Input.GetActionStrength("jump");

        RecalculateLv();

        float velx = (Velocity.X / UnitToPx);
        float vely = (Velocity.Y / UnitToPx) + Gravity * (float)delta;

        if (Mathf.Abs(velx) < 0.1) velx = 0;

        var grounded = this.IsOnFloor();
        var coyoteGrounded = _timeOffGround < CoyoteTiming;
        if (grounded || coyoteGrounded) {
            if (grounded) _timeOffGround = 0;
            else _timeOffGround += (float)delta;

            if (Input.IsActionJustPressed("jump")) {
                vely = -_lv;
            }

            if (!grounded && coyoteGrounded && Input.IsActionJustReleased("jump") && vely < 0) {
                vely *= 1/4f;
            }

            var accel = WalkAcceleration;
            if (xAxis * Velocity.X < 0) accel = StoppingDeceleration;
            velx += xAxis * accel * (float)delta;
        } else {
            if (Input.IsActionJustReleased("jump") && vely < 0) {
                vely *= 1/4f;
            }

            if (Input.IsActionJustPressed("turn_back") && vely > 0) {
                var side = velx > 0 ? PredictiveCollisions.Location.BottomRight : PredictiveCollisions.Location.BottomLeft;
                if (Predictor.AnyHit(side) && !this.IsOnWall()) {
                    EmitSignal(SignalName.OnTurnbackJump, true, this.IsOnWall());
                    var hits = Predictor.GetLastCastHits(side);
                    _ = hits.First(x => {
                        if (x.Obj is null) return false;
                        var collider = x.AsGodotDictionary()["collider"].AsGodotObject();
                        if (collider.HasMeta("JumpPointAnimatable")) {
                            var animPath = collider.GetMeta("JumpPointAnimatable").AsNodePath();
                            var anim = collider.Call(MethodName.GetNode, animPath).As<AnimatedSprite2D>();
                            var frame = anim.Frame;
                            void anf() {
                                anim.Stop();
                                anim.Frame = frame;
                                anim.AnimationLooped -= anf;  
                            };
                            anim.AnimationLooped += anf;
                            anim.Play();
                        }
                        return true;
                    });
                    // has jump point
                    float theta = Mathf.Atan2(-yAxis, Mathf.Abs(xAxis));
                    float margin = 20 * (Mathf.Pi / 180f);
                    if (theta < margin) theta = margin;
                    if (theta > Mathf.Pi / 2 - margin) theta = Mathf.Pi/2 - margin;
                    Vector2 turnjump = new Vector2(-Mathf.Sign(velx) * Mathf.Cos(theta), -Mathf.Sin(theta) - 0.2f) * _lv;
                    GD.Print($"Theta: {theta * (180 / Mathf.Pi)}, turnjump: {turnjump}");
                    velx = turnjump.X;
                    vely = turnjump.Y;
                } else {
                    // does not have jump point
                    // float theta = Mathf.Atan2(-yAxis, Mathf.Abs(xAxis));
                    // float margin = 20 * (Mathf.Pi / 180f);
                    // if (theta < margin) theta = margin;
                    // if (theta > Mathf.Pi / 2 - margin) theta = Mathf.Pi/2 - margin;
                    // Vector2 turnjump = new Vector2(-Mathf.Sign(Velocity.X) * Mathf.Cos(theta), -Mathf.Sin(theta) - 0.2f) * _lv;
                    // GD.Print($"Theta: {theta * (180 / Mathf.Pi)}, turnjump: {turnjump}");
                    // velx = turnjump.X;
                    // vely = turnjump.Y;
                }
            }

            if (Input.IsActionJustPressed("jump") && Mathf.Abs(vely) < 2) {
                vely = -_lv;
            }

            velx += xAxis * AirAcceleration * (float)delta;
        }

        if (grounded && Mathf.Abs(xAxis) < Deadzone) {
            var autostop = -Mathf.Sign(velx) * StoppingDeceleration * (float)delta;
            if (Mathf.Abs(autostop) > Mathf.Abs(velx)) velx = 0f;
            else velx += autostop;
        }

        var maxXSpeed = Input.IsActionPressed("run") ? MaxRunSpeed : MaxWalkSpeed;
        var maxYSpeed = vely > 0 ? MaxFallSpeed : MaxRiseSpeed;
        var (absvelx, absvely) = (Mathf.Abs(velx), Mathf.Abs(vely));
        float xcancel = 0, ycancel = 0;
        if (absvelx > maxXSpeed) xcancel = -Math.Sign(velx) * CancellationAcceleration * (float)delta;
        if (absvely > maxYSpeed) ycancel = -Math.Sign(vely) * CancellationAcceleration * (float)delta;
        if (Mathf.Abs(xcancel) + 0.1 > absvelx) velx = 0;
        else velx += xcancel;
        if (Mathf.Abs(ycancel) + 0.1 > absvely) vely = 0;
        else vely += ycancel;

        DebugTextLabel.Text = 
            $"grounded: {grounded}\n" + 
            $"vely: {vely}\n" +
            $"velx: {velx}\n" +
            $"lv  : {_lv:F2} or {_lv/(float)delta:F2}\n" + 
            $"lcrc : {Predictor.AnyHit(PredictiveCollisions.Location.BottomLeft)}/{Predictor.AnyHit(PredictiveCollisions.Location.BottomRight)}";
        // Force = new Vector2(0, forcey);
        Velocity = new Vector2(velx, vely);
        Velocity *= UnitToPx;
        MoveAndSlide();
    }
}
