using System;
using Cirno.Scripts.Utils;
using Godot;

namespace Cirno.Scripts.Misc;

public partial class CameraController : Camera2D
{
    [Export] public bool PixelSnap { get; set; } = true;
    [Export] public bool EnableSmoothing { get; set; } = true;

    [Export] public bool FollowTargeting { get; set; } = true;

    [Export] public float SmoothTime { get; set; } = 0.2f;
    
    [Export] public float MaxAimOffsetDistance { get; set; } = 64f;
    [Export] public float AimLerpSpeed { get; set; } = 0.8f; // How fast the offset adapts
    [Export] public float AimDeadzone { get; set; } = 0.2f;
    
    [ExportGroup("Name Strings")]
    [Export] public StringName CameraControllersGroupName { get; private set; } = "camera_controllers";
    [Export] public StringName DebugCamera1ActionName { get; private set; } = "debug_camera_1";
    [Export] public StringName DebugCamera2ActionName { get; private set; } = "debug_camera_2";

    [Export] public StringName AimUpName { get; private set; } = "aim_up";
    [Export] public StringName AimDownName { get; private set; } = "aim_down";
    [Export] public StringName AimLeftName { get; private set; } = "aim_left";
    [Export] public StringName AimRightName { get; private set; } = "aim_right";
    
    private CameraTarget _activeTarget;

    private Vector2 _previousPixelSnapDelta = Vector2.Zero;

    // The current camera velocity, for smooth damping.
    private Vector2 _currentVelocity = Vector2.Zero;

    // The current exact position of the camera
    private Vector2 _currentPosition;
    
    private Vector2 _currentAimOffset = Vector2.Zero;

    public override void _Ready()
    {
        AddToGroup(CameraControllersGroupName);
    }
    
    public override void _UnhandledInput(InputEvent @event)
    {
        if (Input.IsActionJustPressed(DebugCamera1ActionName))
        {
            PixelSnap = !PixelSnap;
            GD.Print($"Camera Pixel Snap: {PixelSnap}");
        }

        if (Input.IsActionJustPressed(DebugCamera2ActionName))
        {
            EnableSmoothing = !EnableSmoothing;
            GD.Print($"Camera Smoothing: {EnableSmoothing}");
        }
    }

    public override void _Process(double delta)
    {
        float deltaTime = (float)delta;
        // Update position
        Vector2 nextPosition;
        
        Vector2 target;
        if (FollowTargeting)
        {
            // Adding the offset
            Vector2 aimDirection = GetAimDirection(); // We'll define this next
            Vector2 desiredOffset = aimDirection * MaxAimOffsetDistance;
            _currentAimOffset = _currentAimOffset.Lerp(desiredOffset, AimLerpSpeed * deltaTime);
        
            // Clamp final offset
            if (_currentAimOffset.LengthSquared() > MaxAimOffsetDistance * MaxAimOffsetDistance)
                _currentAimOffset = _currentAimOffset.Normalized() * MaxAimOffsetDistance;

            target = _activeTarget.GlobalPosition + _currentAimOffset;
        }
        else
        {
            target = _activeTarget.GlobalPosition;
        }
        
        if (EnableSmoothing)
        {
            // Handle target movement with SmoothDamp
            // Replace this with any smooth follow / lerp of your choosing, but be careful to use `delta`
            // properly -- improper use can result in jitter. Don't do lerp(current, target, delta).
            var resX = MathFunctions.SmoothDamp(_currentPosition.X, target.X, _currentVelocity.X, SmoothTime,
                float.PositiveInfinity, deltaTime);
            var resY = MathFunctions.SmoothDamp(_currentPosition.Y, target.Y, _currentVelocity.Y, SmoothTime,
                float.PositiveInfinity, deltaTime);

            nextPosition = new Vector2(resX.Item1, resY.Item1);
            _currentVelocity = new Vector2(resX.Item2, resY.Item2);
        }
        else
        {
            // Set next camera position to the exact target.
            nextPosition = target;
        }

        _currentPosition = nextPosition;

        if (PixelSnap)
        {
            // IMPORTANT: perform pixel snap so that the camera movement doesn't interfere with the
            // sub-pixel "smooth movement" we do in the shader.
            var snappedPosition = (nextPosition + new Vector2(0.5f, 0.5f)).Floor();
            _previousPixelSnapDelta = snappedPosition - nextPosition;
            nextPosition = snappedPosition;
        }
        else
        {
            _previousPixelSnapDelta = Vector2.Zero;
        }
        
        // Set the camera position.
        GlobalPosition = nextPosition;
        // IMPORTANT: Work around godot bug where camera doesn't update immediately: https://github.com/godotengine/godot/issues/74203
        ForceUpdateScroll();
    }
    
    private Vector2 GetAimDirection()
    {
        Vector2 screenSize = GetViewportRect().Size;
        Vector2 aspectFix = new Vector2(1f, screenSize.X / screenSize.Y);  // Stretch Y to match X range

        
        // Check controller stick input
        Vector2 stickDir = new Vector2(
            Input.GetActionStrength(AimRightName) - Input.GetActionStrength(AimLeftName),
            Input.GetActionStrength(AimDownName) - Input.GetActionStrength(AimUpName)
        );

        float stickLen = stickDir.Length();
        if (stickLen > AimDeadzone)
        {
            float scaled = (stickLen - AimDeadzone) / (1f - AimDeadzone);
            return stickDir.Normalized() * Mathf.Clamp(scaled, 0f, 1f);
        }

        // Mouse input
        Vector2 screenCenter = GetViewportRect().Size / 2f;
        Vector2 mousePos = GetViewport().GetMousePosition();
        mousePos = mousePos.Clamp(Vector2.Zero, screenSize);
        Vector2 dir = (mousePos - screenCenter) * aspectFix;
        float dist = dir.Length();

        // Use a pixel-based deadzone for mouse
        float mouseDeadzone = AimDeadzone * 100f; // e.g. 0.2 * 100 = 20px

        if (dist > mouseDeadzone)
        {
            float scaled = (dist - mouseDeadzone) / (screenCenter.Length() - mouseDeadzone);
            return dir.Normalized() * Mathf.Clamp(scaled, 0f, 1f);
        }

        return Vector2.Zero;
    }


    public Vector2 GetPixelSnapDelta()
    {
        return _previousPixelSnapDelta;
    }

    public void RegisterTarget(CameraTarget target)
    {
        // assert(not _active_target)
        _activeTarget = target;
        _currentPosition = _activeTarget.GlobalPosition;
    }
}