using Godot;
using gobbos_logic;

public partial class Player : CharacterBody2D, IPlayer
{
    [Signal]
    public delegate void BlinkTimerTimeoutEventHandler();

    [Signal]
    public delegate void WallCastDelayTimeoutEventHandler();

    // Constants
    const double COYOTE_JUMP_WINDOW = 0.15;

    // private readonly Vector2 TILE_SIZE = new(8, 8);

    [Export]
    public AnimationPlayer Blinkplayer { get; set; }

    private double timeSinceLeavingGround = 0.0;

    public bool CanCoyoteJump => timeSinceLeavingGround < COYOTE_JUMP_WINDOW;

    public void ResetCoyoteTimer()
    {
        timeSinceLeavingGround = 0.0;
    }

    public float Acceleration { get; set; } = 4.0f;
    public float Friction { get; set; } = 4.0f;
    public float AirFriction { get; set; } = 0.1f;
    public float WallFriction { get; set; } = 1200.0f;
    public int Speed { get; set; } = 40;
    public int WallSpeed { get; set; } = 15;
    public float SlowMod { get; set; } = 0.5f;
    public int Gravity { get; set; } = 250;
    public int Jump { get; set; } = -95;
    public int WallJump { get; set; } = 55;
    public int LedgeJump { get; set; } = -70;
    public float FallMod { get; set; } = 1.5f;
    public float ParachuteMod { get; set; } = 0.2f;
    public bool OnLedge { get; set; } = false;
    public bool InParachute { get; set; } = false;
    public bool CanParachute { get; set; } = false;

    public Vector2 AnimDir { get; set; } = Vector2.Zero;
    public Vector2 LastDir { get; set; } = Vector2.Zero;
    public Vector2 MoveDir { get; set; } = Vector2.Zero;
    public Label StateLabel { get; set; }
    public RayCast2D WallCastTop { get; set; }
    public RayCast2D WallCastMid { get; set; }
    public RayCast2D WallCastLow { get; set; }
    public AnimationNodeStateMachinePlayback AnimStateMachine { get; set; }
    public AnimationNodeStateMachinePlayback GroundedStateMachine { get; set; }
    public AnimationNodeStateMachinePlayback AirborneStateMachine { get; set; }
    public AnimationNodeStateMachinePlayback SprintStateMachine { get; set; }
    public float LastVelocityY { get; set; } = 0.0f;
    public float WallCastLength { get; set; } = 3.8f;
    public Timer WallCastDelayTimer { get; set; }

    private RayCast2D cameraOffsetCast;
    private RayCast2D cameraDownCast;
    private Node2D modelContainer;
    private Timer blinkTimer;
    public AnimationTree animTree;
    private StateMachine stateMachine;

    public override void _Ready()
    {
        modelContainer = GetNode<Node2D>("ModelContainer");
        blinkTimer = GetNode<Timer>("BlinkTimer");
        animTree = GetNode<AnimationTree>("AnimationTree");
        cameraOffsetCast = GetNode<RayCast2D>("CameraOffsetCast");
        cameraDownCast = GetNode<RayCast2D>("CameraOffsetCast/CameraDownCast");
        stateMachine = GetNode<StateMachine>("StateMachine");
        animTree.Set("parameters/Airborne/Standard/blend_position", 1.0f);
        animTree.Set("parameters/Airborne/Parachute/blend_position", 1.0f);

        WallCastTop = GetNode<RayCast2D>("WallCastTop");
        WallCastMid = GetNode<RayCast2D>("WallCastMid");
        WallCastLow = GetNode<RayCast2D>("WallCastLow");
        WallCastDelayTimer = GetNode<Timer>("WallCastDelay");
        WallCastDelayTimer.Timeout += OnWallCastDelayTimeout;
        AnimStateMachine = (AnimationNodeStateMachinePlayback)animTree.Get("parameters/playback");
        GroundedStateMachine = (AnimationNodeStateMachinePlayback)animTree.Get("parameters/Grounded/playback");
        AirborneStateMachine = (AnimationNodeStateMachinePlayback)animTree.Get("parameters/Airborne/playback");
        SprintStateMachine = (AnimationNodeStateMachinePlayback)animTree.Get("parameters/Grounded/Sprint/playback");
        StateLabel = GetNode<Label>("StateLabel");
    }

    public override void _PhysicsProcess(double delta)
    {
        var floatDelta = (float)delta;
        Vector2 inputDir = new(Input.GetAxis("move_left", "move_right"), Input.GetAxis("move_up", "move_down"));

        if (inputDir.X != 0.0f)
        {
            MoveDir = new(Mathf.MoveToward(MoveDir.X, inputDir.X, 20 * floatDelta), MoveDir.Y);
            LastDir = new(inputDir.X, LastDir.Y);
        }
        else
        {
            MoveDir = new Vector2(
                Mathf.MoveToward(MoveDir.X, 0.0f, 20 * floatDelta),
                MoveDir.Y);
        }

        if (inputDir.Y != 0.0f)
        {
            MoveDir = new(MoveDir.X, Mathf.MoveToward(MoveDir.Y, inputDir.Y, 20 * floatDelta));
            LastDir = new(LastDir.X, inputDir.Y);
        }
        else
        {
            MoveDir = new(MoveDir.X, Mathf.MoveToward(MoveDir.Y, 0.0f, 20 * floatDelta));
        }

        AnimDir = new(Mathf.MoveToward(AnimDir.X, LastDir.X, 20 * floatDelta), AnimDir.Y);

        // Set wall cast positions based on input direction
        if (inputDir.X > 0)
        {
            WallCastTop.TargetPosition = new(WallCastLength, WallCastTop.TargetPosition.Y);
            WallCastMid.TargetPosition = new(WallCastLength, WallCastMid.TargetPosition.Y);
            WallCastLow.TargetPosition = new(WallCastLength, WallCastLow.TargetPosition.Y);
        }
        else if (inputDir.X < 0)
        {
            WallCastTop.TargetPosition = new(0.0f - WallCastLength, WallCastTop.TargetPosition.Y);
            WallCastMid.TargetPosition = new(0.0f - WallCastLength, WallCastMid.TargetPosition.Y);
            WallCastLow.TargetPosition = new(0.0f - WallCastLength, WallCastLow.TargetPosition.Y);
        }
        else
        {
            WallCastTop.TargetPosition = new(0.0f, WallCastTop.TargetPosition.Y);
            WallCastMid.TargetPosition = new(0.0f, WallCastMid.TargetPosition.Y);
            WallCastLow.TargetPosition = new(0.0f, WallCastLow.TargetPosition.Y);
        }

        // Update animation parameters
        animTree.Set("parameters/Airborne/Standard/0/blend_position", Velocity.Y);
        animTree.Set("parameters/Airborne/Standard/1/blend_position", Velocity.Y);
        animTree.Set("parameters/Airborne/Parachute/0/blend_position", Velocity.Y);
        animTree.Set("parameters/Airborne/Parachute/1/blend_position", Velocity.Y);

        // Create tweens for animation blending
        var idleBlend = GetTree().CreateTween();
        var walkBlend = GetTree().CreateTween();
        var airBlend = GetTree().CreateTween();
        var parachuteBlend = GetTree().CreateTween();

        idleBlend.TweenProperty(animTree, "parameters/Grounded/Idle/blend_position", AnimDir.X, 0.2f);
        walkBlend.TweenProperty(animTree, "parameters/Grounded/Walk/blend_position", AnimDir.X, 0.2f);
        airBlend.TweenProperty(animTree, "parameters/Airborne/Standard/blend_position", AnimDir.X, 0.2f);
        parachuteBlend.TweenProperty(animTree, "parameters/Airborne/Parachute/blend_position", AnimDir.X, 0.5f);

        TiltModel(floatDelta);
        CameraOffset();

        // Increment time since leaving ground if not on floor
        if (!IsOnFloor())
        {
            timeSinceLeavingGround += delta;
        }

        // Call StateMachine PhysicsUpdate to handle state-specific movement
        stateMachine?.PhysicsUpdate(floatDelta);

        MoveAndSlide();
    }

    private void CameraOffset()
    {
        var targetX = Mathf.Lerp(cameraOffsetCast.TargetPosition.X, AnimDir.X * 50.0f, 0.6f);
        cameraOffsetCast.TargetPosition = new(targetX, cameraOffsetCast.TargetPosition.Y);

        if (cameraOffsetCast.IsColliding())
        {
            cameraDownCast.Position = cameraOffsetCast.GetCollisionPoint();
            cameraDownCast.Position = new(cameraDownCast.Position.X, cameraDownCast.Position.Y + 1);
        }
        else
        {
            cameraDownCast.Position = cameraOffsetCast.GlobalPosition + cameraOffsetCast.TargetPosition;
        }
    }

    private void TiltModel(float delta)
    {
        var floorNormal = GetFloorNormal();
        var tilt = 0.0f;

        if (floorNormal != new Vector2(0.0f, -1.0f))
        {
            tilt = Mathf.Atan2(floorNormal.X, floorNormal.Y);
            tilt /= 10;
        }

        modelContainer.Rotation = Mathf.Lerp(modelContainer.Rotation, tilt, 5 * delta);
    }

    private void OnBlinkTimerTimeout()
    {
        Blinkplayer?.Play("blink");
        var randomWaitTime = GD.RandRange(1.0f, 7.0f);
        blinkTimer?.WaitTime = randomWaitTime;
    }

    private void OnWallCastDelayTimeout()
    {
        WallCastTop.Enabled = true;
        WallCastMid.Enabled = true;
        WallCastLow.Enabled = true;
    }
}
