using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Serialization;

namespace StardewModdingAPI.Utilities
{
    /// <summary>A set of multi-key bindings which can be triggered by the player.</summary>
    public class KeybindList
    {
        /*********
        ** Accessors
        *********/
        /// <summary>The individual keybinds.</summary>
        public Keybind[] Keybinds { get; }

        /// <summary>Whether any keys are bound.</summary>
        public bool IsBound { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="keybinds">The underlying keybinds.</param>
        /// <remarks>See <see cref="Parse"/> or <see cref="TryParse"/> to parse it from a string representation. You can also use this type directly in your config or JSON data models, and it'll be parsed by SMAPI.</remarks>
        public KeybindList(params Keybind[] keybinds)
        {
            this.Keybinds = keybinds.Where(p => p.IsBound).ToArray();
            this.IsBound = this.Keybinds.Any();
        }

        /// <summary>Construct an instance.</summary>
        /// <param name="singleKey">A single-key binding.</param>
        public KeybindList(SButton singleKey)
            : this(new Keybind(singleKey)) { }

        /// <summary>Parse a keybind list from a string, and throw an exception if it's not valid.</summary>
        /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
        /// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>
        public static KeybindList Parse(string input)
        {
            return KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors)
                ? parsed
                : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}");
        }

        /// <summary>Try to parse a keybind list from a string.</summary>
        /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
        /// <param name="parsed">The parsed keybind list, if valid.</param>
        /// <param name="errors">The errors that occurred while parsing the input, if any.</param>
        public static bool TryParse(string? input, [NotNullWhen(true)] out KeybindList? parsed, out string[] errors)
        {
            // empty input
            if (string.IsNullOrWhiteSpace(input))
            {
                parsed = new KeybindList();
                errors = Array.Empty<string>();
                return true;
            }

            // parse buttons
            var rawErrors = new List<string>();
            var keybinds = new List<Keybind>();
            foreach (string rawSet in input.Split(','))
            {
                if (string.IsNullOrWhiteSpace(rawSet))
                    continue;

                if (!Keybind.TryParse(rawSet, out Keybind? keybind, out string[] curErrors))
                    rawErrors.AddRange(curErrors);
                else
                    keybinds.Add(keybind);
            }

            // build result
            if (rawErrors.Any())
            {
                parsed = null;
                errors = rawErrors.Distinct().ToArray();
                return false;
            }
            else
            {
                parsed = new KeybindList(keybinds.ToArray());
                errors = Array.Empty<string>();
                return true;
            }
        }

        /// <summary>Get a keybind list for a single keybind.</summary>
        /// <param name="buttons">The buttons that must be down to activate the keybind.</param>
        public static KeybindList ForSingle(params SButton[] buttons)
        {
            return new KeybindList(
                new Keybind(buttons)
            );
        }

        /// <summary>Get the overall keybind list state relative to the previous tick.</summary>
        /// <remarks>States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'.</remarks>
        public SButtonState GetState()
        {
            bool wasPressed = false;
            bool isPressed = false;

            foreach (Keybind keybind in this.Keybinds)
            {
                switch (keybind.GetState())
                {
                    case SButtonState.Pressed:
                        isPressed = true;
                        break;

                    case SButtonState.Held:
                        wasPressed = true;
                        isPressed = true;
                        break;

                    case SButtonState.Released:
                        wasPressed = true;
                        break;
                }
            }

            if (wasPressed == isPressed)
            {
                return wasPressed
                    ? SButtonState.Held
                    : SButtonState.None;
            }

            return wasPressed
                ? SButtonState.Released
                : SButtonState.Pressed;
        }

        /// <summary>Get whether any of the button sets are pressed.</summary>
        public bool IsDown()
        {
            SButtonState state = this.GetState();
            return state is SButtonState.Pressed or SButtonState.Held;
        }

        /// <summary>Get whether the input binding was just pressed this tick.</summary>
        public bool JustPressed()
        {
            return this.GetState() == SButtonState.Pressed;
        }

        /// <summary>Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned.</summary>
        public Keybind? GetKeybindCurrentlyDown()
        {
            return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown());
        }

        /// <summary>Get a string representation of the input binding.</summary>
        /// <remarks>A keybind list is serialized to a string like <c>LeftControl + S, LeftAlt + S</c>, where each multi-key binding is separated with <c>,</c> and the keys within each keybind are separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
        public override string ToString()
        {
            return this.Keybinds.Length > 0
                ? string.Join(", ", this.Keybinds.Select(p => p.ToString()))
                : SButton.None.ToString();
        }
    }
}