using System;
using System.Reflection;
using UnityEngine;

namespace Woop.Unity
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Rigidbody2D))]
    public sealed class WoopCharacterController2D : MonoBehaviour
    {
        [SerializeField, Min(0f)] private float moveSpeed = 4f;
        [SerializeField] private bool normalizeDiagonal = true;
        [SerializeField] private bool faceHorizontalMovement = true;
        [SerializeField] private Transform visualRoot;
        [Header("Jump")]
        [SerializeField, Min(0f)] private float jumpHeight = 2.2f;
        [SerializeField, Min(0.1f)] private float jumpDuration = 0.65f;

        private Rigidbody2D body;
        private Vector2 moveInput;
        private float visualScaleMagnitude = 1f;
        private Vector3 visualRestLocalPosition;
        private float jumpElapsed;

        public float MoveSpeed
        {
            get => moveSpeed;
            set => moveSpeed = Mathf.Max(0f, value);
        }

        public Vector2 MoveInput => moveInput;
        public bool IsJumping { get; private set; }
        public float JumpAmount { get; private set; }

        public void Configure(Transform importedVisualRoot, float speed)
        {
            visualRoot = importedVisualRoot;
            MoveSpeed = speed;
        }

        private void Awake()
        {
            body = GetComponent<Rigidbody2D>();

            if (visualRoot == null)
                visualRoot = transform.Find("Visual");

            if (visualRoot != null)
            {
                visualScaleMagnitude = Mathf.Max(Mathf.Abs(visualRoot.localScale.x), 0.0001f);
                visualRestLocalPosition = visualRoot.localPosition;
            }
        }

        private void Update()
        {
            moveInput = ReadWasd();
            if (normalizeDiagonal && moveInput.sqrMagnitude > 1f)
                moveInput.Normalize();

            if (faceHorizontalMovement && visualRoot != null && Mathf.Abs(moveInput.x) > 0.001f)
            {
                Vector3 scale = visualRoot.localScale;
                scale.x = visualScaleMagnitude * (moveInput.x < 0f ? -1f : 1f);
                visualRoot.localScale = scale;
            }

            if (!IsJumping && ReadJumpPressed())
            {
                IsJumping = true;
                jumpElapsed = 0f;
            }

            UpdateJump();
        }

        private void FixedUpdate()
        {
            body.MovePosition(body.position + moveInput * (moveSpeed * Time.fixedDeltaTime));
        }

        private static Vector2 ReadWasd()
        {
            if (InputSystemBridge.IsAvailable)
                return InputSystemBridge.ReadWasd();

            float x = (Input.GetKey(KeyCode.D) ? 1f : 0f) - (Input.GetKey(KeyCode.A) ? 1f : 0f);
            float y = (Input.GetKey(KeyCode.W) ? 1f : 0f) - (Input.GetKey(KeyCode.S) ? 1f : 0f);
            return new Vector2(x, y);
        }

        private void UpdateJump()
        {
            if (!IsJumping || visualRoot == null)
                return;

            jumpElapsed += Time.deltaTime;
            float normalizedTime = Mathf.Clamp01(jumpElapsed / jumpDuration);
            JumpAmount = Mathf.Sin(normalizedTime * Mathf.PI);
            visualRoot.localPosition = visualRestLocalPosition + Vector3.up * (jumpHeight * JumpAmount);

            if (normalizedTime >= 1f)
            {
                IsJumping = false;
                JumpAmount = 0f;
                visualRoot.localPosition = visualRestLocalPosition;
            }
        }

        private static bool ReadJumpPressed()
        {
            if (InputSystemBridge.IsAvailable)
                return InputSystemBridge.ReadJumpPressed();

            return Input.GetKeyDown(KeyCode.Space);
        }

        private void OnDisable()
        {
            IsJumping = false;
            JumpAmount = 0f;
            if (visualRoot != null)
                visualRoot.localPosition = visualRestLocalPosition;
        }

        private static class InputSystemBridge
        {
            private static readonly Type KeyboardType = Type.GetType(
                "UnityEngine.InputSystem.Keyboard, Unity.InputSystem",
                throwOnError: false);
            private static readonly PropertyInfo CurrentProperty = KeyboardType?.GetProperty(
                "current",
                BindingFlags.Public | BindingFlags.Static);

            public static bool IsAvailable => KeyboardType != null && CurrentProperty != null;

            public static Vector2 ReadWasd()
            {
                object keyboard = CurrentProperty.GetValue(null);
                if (keyboard == null)
                    return Vector2.zero;

                float x = (ReadKey(keyboard, "dKey", "isPressed") ? 1f : 0f)
                    - (ReadKey(keyboard, "aKey", "isPressed") ? 1f : 0f);
                float y = (ReadKey(keyboard, "wKey", "isPressed") ? 1f : 0f)
                    - (ReadKey(keyboard, "sKey", "isPressed") ? 1f : 0f);
                return new Vector2(x, y);
            }

            public static bool ReadJumpPressed()
            {
                object keyboard = CurrentProperty.GetValue(null);
                return keyboard != null && ReadKey(keyboard, "spaceKey", "wasPressedThisFrame");
            }

            private static bool ReadKey(object keyboard, string keyName, string stateName)
            {
                object key = KeyboardType.GetProperty(keyName, BindingFlags.Public | BindingFlags.Instance)
                    ?.GetValue(keyboard);
                if (key == null)
                    return false;

                object value = key.GetType().GetProperty(stateName, BindingFlags.Public | BindingFlags.Instance)
                    ?.GetValue(key);
                return value is bool pressed && pressed;
            }
        }
    }
}
