using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace StardewModdingAPI.Framework.Input
{
    /// <summary>Manages controller state.</summary>
    internal class GamePadStateBuilder : IInputStateBuilder<GamePadStateBuilder, GamePadState>
    {
        /*********
        ** Fields
        *********/
        /// <summary>The maximum direction to ignore for the left thumbstick.</summary>
        private const float LeftThumbstickDeadZone = 0.2f;

        /// <summary>The maximum direction to ignore for the right thumbstick.</summary>
        private const float RightThumbstickDeadZone = 0.9f;

        /// <summary>The underlying controller state.</summary>
        private GamePadState? State;

        /// <summary>The current button states.</summary>
        private readonly IDictionary<SButton, ButtonState>? ButtonStates;

        /// <summary>The left trigger value.</summary>
        private float LeftTrigger;

        /// <summary>The right trigger value.</summary>
        private float RightTrigger;

        /// <summary>The left thumbstick position.</summary>
        private Vector2 LeftStickPos;

        /// <summary>The left thumbstick position.</summary>
        private Vector2 RightStickPos;


        /*********
        ** Accessors
        *********/
        /// <summary>Whether the gamepad is currently connected.</summary>
        [MemberNotNullWhen(true, nameof(GamePadStateBuilder.ButtonStates))]
        public bool IsConnected { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="state">The initial state.</param>
        public GamePadStateBuilder(GamePadState state)
        {
            this.State = state;
            this.IsConnected = state.IsConnected;

            if (!this.IsConnected)
                return;

            GamePadDPad pad = state.DPad;
            GamePadButtons buttons = state.Buttons;
            GamePadTriggers triggers = state.Triggers;
            GamePadThumbSticks sticks = state.ThumbSticks;
            this.ButtonStates = new Dictionary<SButton, ButtonState>
            {
                [SButton.DPadUp] = pad.Up,
                [SButton.DPadDown] = pad.Down,
                [SButton.DPadLeft] = pad.Left,
                [SButton.DPadRight] = pad.Right,

                [SButton.ControllerA] = buttons.A,
                [SButton.ControllerB] = buttons.B,
                [SButton.ControllerX] = buttons.X,
                [SButton.ControllerY] = buttons.Y,
                [SButton.LeftStick] = buttons.LeftStick,
                [SButton.RightStick] = buttons.RightStick,
                [SButton.LeftShoulder] = buttons.LeftShoulder,
                [SButton.RightShoulder] = buttons.RightShoulder,
                [SButton.ControllerBack] = buttons.Back,
                [SButton.ControllerStart] = buttons.Start,
                [SButton.BigButton] = buttons.BigButton
            };
            this.LeftTrigger = triggers.Left;
            this.RightTrigger = triggers.Right;
            this.LeftStickPos = sticks.Left;
            this.RightStickPos = sticks.Right;
        }

        /// <inheritdoc />
        public GamePadStateBuilder OverrideButtons(IDictionary<SButton, SButtonState> overrides)
        {
            if (!this.IsConnected)
                return this;

            foreach (var pair in overrides)
            {
                bool changed = true;

                bool isDown = pair.Value.IsDown();
                switch (pair.Key)
                {
                    // left thumbstick
                    case SButton.LeftThumbstickUp:
                        this.LeftStickPos.Y = isDown ? 1 : 0;
                        break;
                    case SButton.LeftThumbstickDown:
                        this.LeftStickPos.Y = isDown ? -1 : 0;
                        break;
                    case SButton.LeftThumbstickLeft:
                        this.LeftStickPos.X = isDown ? -1 : 0;
                        break;
                    case SButton.LeftThumbstickRight:
                        this.LeftStickPos.X = isDown ? 1 : 0;
                        break;

                    // right thumbstick
                    case SButton.RightThumbstickUp:
                        this.RightStickPos.Y = isDown ? 1 : 0;
                        break;
                    case SButton.RightThumbstickDown:
                        this.RightStickPos.Y = isDown ? -1 : 0;
                        break;
                    case SButton.RightThumbstickLeft:
                        this.RightStickPos.X = isDown ? -1 : 0;
                        break;
                    case SButton.RightThumbstickRight:
                        this.RightStickPos.X = isDown ? 1 : 0;
                        break;

                    // triggers
                    case SButton.LeftTrigger:
                        this.LeftTrigger = isDown ? 1 : 0;
                        break;
                    case SButton.RightTrigger:
                        this.RightTrigger = isDown ? 1 : 0;
                        break;

                    // buttons
                    default:
                        if (this.ButtonStates.ContainsKey(pair.Key))
                            this.ButtonStates[pair.Key] = isDown ? ButtonState.Pressed : ButtonState.Released;
                        else
                            changed = false;
                        break;
                }

                if (changed)
                    this.State = null;
            }

            return this;
        }

        /// <inheritdoc />
        public IEnumerable<SButton> GetPressedButtons()
        {
            if (!this.IsConnected)
                yield break;

            // buttons
            foreach (Buttons button in this.GetPressedGamePadButtons())
                yield return button.ToSButton();

            // triggers
            if (this.LeftTrigger > 0.2f)
                yield return SButton.LeftTrigger;
            if (this.RightTrigger > 0.2f)
                yield return SButton.RightTrigger;

            // left thumbstick direction
            if (this.LeftStickPos.Y > GamePadStateBuilder.LeftThumbstickDeadZone)
                yield return SButton.LeftThumbstickUp;
            if (this.LeftStickPos.Y < -GamePadStateBuilder.LeftThumbstickDeadZone)
                yield return SButton.LeftThumbstickDown;
            if (this.LeftStickPos.X > GamePadStateBuilder.LeftThumbstickDeadZone)
                yield return SButton.LeftThumbstickRight;
            if (this.LeftStickPos.X < -GamePadStateBuilder.LeftThumbstickDeadZone)
                yield return SButton.LeftThumbstickLeft;

            // right thumbstick direction
            if (this.RightStickPos.Length() > GamePadStateBuilder.RightThumbstickDeadZone)
            {
                if (this.RightStickPos.Y > 0)
                    yield return SButton.RightThumbstickUp;
                if (this.RightStickPos.Y < 0)
                    yield return SButton.RightThumbstickDown;
                if (this.RightStickPos.X > 0)
                    yield return SButton.RightThumbstickRight;
                if (this.RightStickPos.X < 0)
                    yield return SButton.RightThumbstickLeft;
            }
        }

        /// <inheritdoc />
        public GamePadState GetState()
        {
            this.State ??= new GamePadState(
                leftThumbStick: this.LeftStickPos,
                rightThumbStick: this.RightStickPos,
                leftTrigger: this.LeftTrigger,
                rightTrigger: this.RightTrigger,
                buttons: this.GetPressedGamePadButtons().ToArray()
            );

            return this.State.Value;
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get the pressed gamepad buttons.</summary>
        private IEnumerable<Buttons> GetPressedGamePadButtons()
        {
            if (!this.IsConnected)
                yield break;

            foreach (var pair in this.ButtonStates)
            {
                if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
                    yield return button;
            }
        }
    }
}