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

namespace AsmodeeDigital.Common.Plugin.Utils
{
    /// <summary>
    /// State Machine that contains states and transitions.
    /// When a condition of a transition is verified, the new state becomes current state
    /// 3 optionals methods are associated to a state : OnEnter, OnUpdate, OnExit
    /// </summary>
    [Serializable]
    public class StateMachine
    {
        private List<ActionState> _listState = new List<ActionState>();
        private List<Transition> _listTransition = new List<Transition>();
        public ActionState CurrentState = null;
        public ActionState PreviousState = null;
        public bool IsDebug = false;
        public bool FirstUpdate { get; set; }
        public string FSMName { get; set; }
        public Action OnStateChanged = null;
        public bool Enabled = true;

        public StateMachine(string fsmName)
        {
            this.FSMName = fsmName;
        }

        /// <summary>
        /// Perform state replacement.
        /// Each time Update is called, each transition of the current state is verified and can change the current state
        /// </summary>
        public void Update()
        {
            if (!Enabled)
                return;

            if (CurrentState.ActionUpdate != null)
                CurrentState.ActionUpdate();

            FirstUpdate = false;

            foreach (Transition transition in _listTransition)
            {
                if ((transition.ActionStateStart == CurrentState ||
                    (transition.ActionStateEnd != CurrentState && transition.ActionStateStart == null))
                    && transition.Condition())
                {
                    if (IsDebug)
                    {
                        Debug.Log("FSM " + this.FSMName + " : " + CurrentState.Name + " -> " + transition.ActionStateEnd.Name);
                    }

                    if (transition.TransitionType == TransitionType.WithDuration)
                    {
                        if (CurrentState.ActionExit != null)
                            CurrentState.ActionExit();

                        PreviousState = transition.ActionStateStart;
                        CurrentState = transition.ActionStateEnd;
                        FirstUpdate = true;

                        if (OnStateChanged != null)
                            OnStateChanged.Invoke();

                        if (CurrentState.ActionEnter != null)
                            CurrentState.ActionEnter();
                    }
                    else
                    {
                        if (CurrentState.ActionExit != null)
                            CurrentState.ActionExit();

                        PreviousState = transition.ActionStateStart;
                        CurrentState = transition.ActionStateEnd;
                        FirstUpdate = true;

                        if (OnStateChanged != null)
                            OnStateChanged.Invoke();

                        if (CurrentState.ActionEnter != null)
                            CurrentState.ActionEnter();
                    }

                    break;
                }
            }
        }

        /// <summary>
        /// Add a new state without transitions methods
        /// </summary>
        /// <param name="name">Name of the state</param>
        /// <returns>Created state</returns>
        public ActionState AddActionState(string name)
        {
            return AddActionState(name, null, null, null);
        }

        /// <summary>
        /// Add a new state with only OnEnter method
        /// </summary>
        /// <param name="name">Name of the state</param>
        /// <param name="actionEnter">Method called when state become the new one</param>
        /// <returns>Created state</returns>
        public ActionState AddActionState(string name, Action actionEnter)
        {
            return AddActionState(name, actionEnter, null, null);
        }

        /// <summary>
        /// Add a new state with OnEnter, onUpdate and OnExt methods
        /// </summary>
        /// <param name="name">Name of the state</param>
        /// <param name="actionEnter">Method called when state become the new one</param>
        /// <param name="actionUpdate">Method called each time Update on the State Machine is called and this state is the current state</param>
        /// <param name="actionExit">Method called (once) when is not anymore the current state</param>
        /// <returns>Created state</returns>
        public ActionState AddActionState(string name, Action actionEnter, Action actionUpdate, Action actionExit)
        {
            ActionState actionState = new ActionState(name, actionEnter, actionUpdate, actionExit);
            _listState.Add(actionState);

            return actionState;
        }

        /// <summary>
        /// Add a universal transition to a state.
        /// This transition is evaluated whatever the current state
        /// </summary>
        /// <param name="actionStateEnd">State that is activated if the transition is valid</param>
        /// <param name="condition">Condition of the transition</param>
        /// <returns>Created transition</returns>
        public Transition AddTransition(ActionState actionStateEnd, Func<bool> condition)
        {
            return AddTransition(null, actionStateEnd, condition);
        }

        /// <summary>
        /// Add a transition from a state to an other
        /// </summary>
        /// <param name="actionStateStart">State to which the transition is added</param>
        /// <param name="actionStateEnd">State that is activated if the transition is valid</param>
        /// <param name="condition">Condition of the transition</param>
        /// <returns>Created transition</returns>
        public Transition AddTransition(ActionState actionStateStart, ActionState actionStateEnd, Func<bool> condition)
        {
            Transition transition = new Transition(actionStateStart, actionStateEnd, condition, TransitionType.Normal);
            _listTransition.Add(transition);

            return transition;
        }

        /// <summary>
        /// Add timed transition from a state to an other
        /// Once the start state became the current state, the end state will become the new state once the duration has expired
        /// </summary>
        /// <param name="actionStateStart">State to which the transition is added</param>
        /// <param name="actionStateEnd">State that is activated if the transition is valid</param>
        /// <param name="transitionDuration">Duration of the transition</param>
        /// <returns>Created transition</returns>
        public Transition AddTransition(ActionState actionStateStart, ActionState actionStateEnd, float transitionDuration)
        {
            Transition transition = new Transition(actionStateStart, actionStateEnd, transitionDuration, TransitionType.WithDuration);
            _listTransition.Add(transition);

            return transition;
        }

        /// <summary>
        /// Reset all states and transition of the State Machine
        /// </summary>
        public void Reset()
        {
            this._listState = new List<ActionState>();
            this._listTransition = new List<Transition>();
        }
    }

    /// <summary>
    /// State with associated methods (OnEnter, OnUpdate, OnExit) of a state machine
    /// </summary>
    [Serializable]
    public class ActionState
    {
        public string Name { get; set; }
        public Action ActionEnter { get; set; }
        public Action ActionUpdate { get; set; }
        public Action ActionExit { get; set; }

        public ActionState(string name, Action actionEnter, Action actionUpdate, Action actionExit)
        {
            this.Name = name;
            this.ActionEnter = actionEnter;
            this.ActionUpdate = actionUpdate;
            this.ActionExit = actionExit;
        }
    }

    /// <summary>
    /// Transition between two state of a state machine
    /// </summary>
    [Serializable]
    public class Transition
    {
        public ActionState ActionStateStart { get; set; }
        public ActionState ActionStateEnd { get; set; }
        public Func<bool> Condition { get; set; }
        public TransitionType TransitionType { get; set; }
        public float TransitionDuration { get; set; }

        public Transition(ActionState actionStateStart, ActionState actionStateEnd, Func<bool> condition, TransitionType transitionType)
        {
            ActionStateStart = actionStateStart;
            ActionStateEnd = actionStateEnd;
            Condition = condition;
            TransitionType = transitionType;
        }

        public Transition(ActionState actionStateStart, ActionState actionStateEnd, float transitionDuration, TransitionType transitionType)
        {
            ActionStateStart = actionStateStart;
            ActionStateEnd = actionStateEnd;
            TransitionDuration = transitionDuration;
            TransitionType = transitionType;
        }
    }

    /// <summary>
    /// Types of transitions
    /// </summary>
    public enum TransitionType
    {
        /// <summary>
        /// The transition is valid with a condition
        /// </summary>
        Normal,
        /// <summary>
        /// The transition is valid after a duration has expired
        /// </summary>
        WithDuration
    }
}