// Animancer // Copyright 2020 Kybernetik //

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Animancer.FSM
{
    /// <summary>
    /// A simple Finite State Machine system that registers each state with a particular key.
    /// <para></para>
    /// This class allows states to be registered with a particular key upfront and then accessed later using that key.
    /// See <see cref="StateMachine{TState}"/> for a system that does not bother keeping track of any states other than
    /// the active one.
    /// </summary>
    [HelpURL(StateExtensions.APIDocumentationURL + "StateMachine_2")]
    public partial class StateMachine<TKey, TState> : StateMachine<TState>, IDictionary<TKey, TState>
        where TState : class, IState<TState>
    {
        /************************************************************************************************************************/

        /// <summary>The collection of states mapped to a particular key.</summary>
        public IDictionary<TKey, TState> Dictionary { get; set; }

        /// <summary>The current state.</summary>
        public TKey CurrentKey { get; private set; }

        /************************************************************************************************************************/

        /// <summary>
        /// Constructs a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/>.
        /// </summary>
        public StateMachine()
        {
            Dictionary = new Dictionary<TKey, TState>();
        }

        /// <summary>
        /// Constructs a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary`.
        /// </summary>
        public StateMachine(IDictionary<TKey, TState> dictionary)
        {
            Dictionary = dictionary;
        }

        /// <summary>
        /// Constructs a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/> and
        /// immediately uses the `defaultKey` to enter the `defaultState`.
        /// </summary>
        public StateMachine(TKey defaultKey, TState defaultState)
        {
            Dictionary = new Dictionary<TKey, TState>();
            ForceSetState(defaultKey, defaultState);
        }

        /// <summary>
        /// Constructs a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary` and
        /// immediately uses the `defaultKey` to enter the `defaultState`.
        /// </summary>
        public StateMachine(IDictionary<TKey, TState> dictionary, TKey defaultKey, TState defaultState)
        {
            Dictionary = dictionary;
            ForceSetState(defaultKey, defaultState);
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Attempts to enter the specified `state` and returns true if successful.
        /// <para></para>
        /// This method returns true immediately if the specified `state` is already the
        /// <see cref="StateMachine{TState}.CurrentState"/>. To allow directly re-entering the same state, use
        /// <see cref="TryResetState(TKey, TState)"/> instead.
        /// </summary>
        public bool TrySetState(TKey key, TState state)
        {
            if (CurrentState == state)
                return true;
            else
                return TryResetState(key, state);
        }

        /// <summary>
        /// Attempts to enter the specified state associated with the specified `key` and returns it if successful.
        /// <para></para>
        /// This method returns true immediately if the specified `key` is already the <see cref="CurrentKey"/>. To
        /// allow directly re-entering the same state, use <see cref="TryResetState(TKey)"/> instead.
        /// </summary>
        public TState TrySetState(TKey key)
        {
            if (Equals(CurrentKey, key))
                return CurrentState;
            else
                return TryResetState(key);
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Attempts to enter the specified `state` and returns true if successful.
        /// <para></para>
        /// This method does not check if the `state` is already the <see cref="StateMachine{TState}.CurrentState"/>.
        /// To do so, use <see cref="TrySetState(TKey, TState)"/> instead.
        /// </summary>
        public bool TryResetState(TKey key, TState state)
        {
            if (!CanSetState(state))
                return false;

            ForceSetState(key, state);
            return true;
        }

        /// <summary>
        /// Attempts to enter the specified state associated with the specified `key` and returns it if successful.
        /// <para></para>
        /// This method does not check if the `key` is already the <see cref="CurrentKey"/>. To do so, use
        /// <see cref="TrySetState(TKey)"/> instead.
        /// </summary>
        public TState TryResetState(TKey key)
        {
            TState state;
            if (Dictionary.TryGetValue(key, out state))
            {
                if (TryResetState(key, state))
                    return state;
            }

            return null;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Calls <see cref="IState{TState}.OnExitState"/> on the current state then changes to the specified key and
        /// state and calls <see cref="IState{TState}.OnEnterState"/> on it.
        /// <para></para>
        /// Note that this method does not check <see cref="IState{TState}.CanExitState"/> or
        /// <see cref="IState{TState}.CanEnterState"/>. To do that, you should use
        /// <see cref="TrySetState(TKey, TState)"/> instead.
        /// </summary>
        public void ForceSetState(TKey key, TState state)
        {
            CurrentKey = key;
            ForceSetState(state);
        }

        /// <summary>
        /// Uses <see cref="ForceSetState(TKey, TState)"/> to change to the state mapped to the `key`. If nothing is mapped,
        /// it changes to default(TState).
        /// </summary>
        public TState ForceSetState(TKey key)
        {
            TState state;
            Dictionary.TryGetValue(key, out state);
            ForceSetState(key, state);
            return state;
        }

        /************************************************************************************************************************/
        #region Dictionary Wrappers
        /************************************************************************************************************************/

        /// <summary>Gets or sets a particular state in the <see cref="Dictionary"/>.</summary>
        public TState this[TKey key] { get { return Dictionary[key]; } set { Dictionary[key] = value; } }

        /// <summary>Gets an <see cref="ICollection{T}"/> containing the keys of the <see cref="Dictionary"/>.</summary>
        public ICollection<TKey> Keys { get { return Dictionary.Keys; } }

        /// <summary>Gets an <see cref="ICollection{T}"/> containing the state of the <see cref="Dictionary"/>.</summary>
        public ICollection<TState> Values { get { return Dictionary.Values; } }

        /// <summary>Gets the number of states contained in the <see cref="Dictionary"/>.</summary>
        public int Count { get { return Dictionary.Count; } }

        /// <summary>Indicates whether the <see cref="Dictionary"/> is read-only.</summary>
        public bool IsReadOnly { get { return Dictionary.IsReadOnly; } }

        /// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
        public void Add(TKey key, TState state) { Dictionary.Add(key, state); }

        /// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
        public bool Remove(TKey key) { return Dictionary.Remove(key); }

        /// <summary>Removes all state from the <see cref="Dictionary"/>.</summary>
        public void Clear() { Dictionary.Clear(); }

        /// <summary>Determines whether the <see cref="Dictionary"/> contains a state with the specified `key`.</summary>
        public bool ContainsKey(TKey key) { return Dictionary.ContainsKey(key); }

        /// <summary>Gets the state associated with the specified `key` in the <see cref="Dictionary"/>.</summary>
        public bool TryGetValue(TKey key, out TState state) { return Dictionary.TryGetValue(key, out state); }

        /// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
        public void Add(KeyValuePair<TKey, TState> item) { Dictionary.Add(item); }

        /// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
        public bool Remove(KeyValuePair<TKey, TState> item) { return Dictionary.Remove(item); }

        /// <summary>Determines whether the <see cref="Dictionary"/> contains a specific value.</summary>
        public bool Contains(KeyValuePair<TKey, TState> item) { return Dictionary.Contains(item); }

        /// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
        public IEnumerator<KeyValuePair<TKey, TState>> GetEnumerator() { return Dictionary.GetEnumerator(); }

        /// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
        IEnumerator IEnumerable.GetEnumerator() { return Dictionary.GetEnumerator(); }

        /// <summary>
        /// Copies the elements of the <see cref="Dictionary"/> to an `array` starting at the specified `arrayIndex`.
        /// </summary>
        public void CopyTo(KeyValuePair<TKey, TState>[] array, int arrayIndex) { Dictionary.CopyTo(array, arrayIndex); }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/

        /// <summary>
        /// Returns the state associated with the specified `key`, or null if none is present.
        /// </summary>
        public TState GetState(TKey key)
        {
            TState state;
            TryGetValue(key, out state);
            return state;
        }

        /************************************************************************************************************************/

        /// <summary>Adds the specified `keys` and `states`. Both arrays must be the same size.</summary>
        public void AddRange(TKey[] keys, TState[] states)
        {
            Debug.Assert(keys.Length == states.Length, "Both arrays must be the same size.");

            for (int i = 0; i < keys.Length; i++)
            {
                Dictionary.Add(keys[i], states[i]);
            }
        }

        /************************************************************************************************************************/

        /// <summary>Sets the <see cref="CurrentKey"/> without actually changing the state.</summary>
        public void SetFakeKey(TKey key)
        {
            CurrentKey = key;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Returns a string describing the type of this state machine and its <see cref="CurrentKey"/> and
        /// <see cref="StateMachine{TState}.CurrentState"/>.
        /// </summary>
        public override string ToString()
        {
            return GetType().FullName + " -> " + CurrentKey + (CurrentState != null ? (" -> " + CurrentState.ToString()) : " -> null");
        }

        /************************************************************************************************************************/
    }
}