using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;

public abstract class FSM<EnumT>: MonoBehaviour
{
	protected Lookup<int, Action> mStateHashToUpdateMethod = null;
	protected Lookup<int, Action> mStateHashToEnterMethod = null;
	protected Lookup<int, Action> mStateHashToExitMethod = null;
	protected int mState = -1;
	protected EnumT mEnumState;

	public Animator stateAnimator = null;

	public int CurState
	{
		get { return mState; }
	}

	public EnumT CurEnumState
	{
		get { return mEnumState; }
	}

	public abstract int ToInt(EnumT state);

	protected virtual void Awake()
	{
		InitAnimator();
		DiscoverStateMethods();
	}

	protected virtual void InitAnimator()
	{
		if(stateAnimator == null)
		{
			stateAnimator = GetComponent<Animator>();
			if(stateAnimator == null && transform.childCount > 0)
			{
				stateAnimator = transform.GetChild(0).GetComponent<Animator>();
			}
		}
	}

	protected virtual void OnTransition(int oldState, int newState)
	{
		if(mStateHashToExitMethod != null && mStateHashToExitMethod.Contains(oldState))
		{
			foreach(Action action in mStateHashToExitMethod[oldState])
			{
				action.Invoke();
			}
		}

		if(mStateHashToEnterMethod != null && mStateHashToEnterMethod.Contains(newState))
		{
			foreach(Action action in mStateHashToEnterMethod[newState])
			{
				action.Invoke();
			}

		}
		if(mStateHashToUpdateMethod != null && mStateHashToUpdateMethod.Contains(newState))
		{
			foreach(Action action in mStateHashToUpdateMethod[newState])
			{
				action.Invoke();
			}

		}
	}

	protected virtual void StateUpdate()
	{
		if(mStateHashToUpdateMethod != null && mStateHashToUpdateMethod.Contains(mState))
		{
			foreach(Action action in mStateHashToUpdateMethod[mState])
			{
				action.Invoke();
			}
		}
	}

	protected void DiscoverStateMethods()
	{
		var components = gameObject.GetComponents<MonoBehaviour>();

		List<StateMethod> enterStateMethods = new List<StateMethod>();
		List<StateMethod> updateStateMethods = new List<StateMethod>();
		List<StateMethod> exitStateMethods = new List<StateMethod>();


		foreach(var component in components)
		{
			if(component == null) continue;

			var type = component.GetType();
			var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod);

			foreach(var method in methods)
			{
				object[] attributes;

				attributes = method.GetCustomAttributes(typeof(StateUpdateMethod), true);
				foreach(StateUpdateMethod attribute in attributes)
				{
					var parameters = method.GetParameters();
					if(parameters.Length == 0)
					{
						updateStateMethods.Add(CreateStateMethod(attribute.state, method, component));
					}
				}


				attributes = method.GetCustomAttributes(typeof(StateEnterMethod), true);
				foreach(StateEnterMethod attribute in attributes)
				{

					var parameters = method.GetParameters();
					if(parameters.Length == 0)
					{
						enterStateMethods.Add(CreateStateMethod(attribute.state, method, component));

					}
				}

				attributes = method.GetCustomAttributes(typeof(StateExitMethod), true);
				foreach(StateExitMethod attribute in attributes)
				{

					var parameters = method.GetParameters();
					if(parameters.Length == 0)
					{
						exitStateMethods.Add(CreateStateMethod(attribute.state, method, component));

					}
				}
			}
		}

		mStateHashToUpdateMethod = (Lookup<int, Action>)updateStateMethods.ToLookup<StateMethod, int, Action>(p => p.stateHash, p => p.method);
		mStateHashToEnterMethod = (Lookup<int, Action>)enterStateMethods.ToLookup<StateMethod, int, Action>(p => p.stateHash, p => p.method);
		mStateHashToExitMethod = (Lookup<int, Action>)exitStateMethods.ToLookup<StateMethod, int, Action>(p => p.stateHash, p => p.method);
		if(mStateHashToUpdateMethod.Count == 0)
			mStateHashToUpdateMethod = null;
		if(mStateHashToEnterMethod.Count == 0)
			mStateHashToEnterMethod = null;
		if(mStateHashToExitMethod.Count == 0)
			mStateHashToExitMethod = null;
	}

	protected StateMethod CreateStateMethod(int statehash, MethodInfo method, MonoBehaviour component)
	{
#if UNITY_EDITOR
		Array values = Enum.GetValues(typeof(EnumT));
		bool inEnums = false;
		foreach(EnumT eno in values)
		{
			if(ToInt(eno) == statehash)
			{
				inEnums = true;
				//Debug.Log(String.Format("{0}, {1}, {2}", typeof(EnumT), eno, statehash));
			}
		}
		if(!inEnums)
		{
			Debug.LogError(String.Format("Enum state {0}-{1} not found, method {2} will not be invoked!", typeof(EnumT).ToString(), statehash, method.Name));
		}
#endif
		StateMethod stateMethod = new StateMethod();
		stateMethod.stateHash = statehash;
		stateMethod.method = () =>
		{
			method.Invoke(component, null);
		};
		return stateMethod;
	}

	public virtual void EnterState(EnumT state, bool reenter = false)
	{
		int nuState = ToInt(state);
		if(!reenter && mState == nuState)
		{
			return;
		}
		OnTransition(mState, nuState);
		mState = nuState;
		mEnumState = state;
		AnimatorEnterState(nuState);
	}

	public virtual void UpdateFromAnimator()
	{
		if(stateAnimator == null) { return; }
		int stateId = stateAnimator.GetCurrentAnimatorStateInfo(0).fullPathHash;
		if(mState != stateId)
		{
			OnTransition(stateId, mState);
			mState = stateId;
		}
	}

	public virtual void AnimatorEnterState(int stateFullPathHash)
	{
		if(null != stateAnimator)
		{
			stateAnimator.Play(stateFullPathHash);
		}
	}
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class StateUpdateMethod: System.Attribute
{
	public int state;

	public StateUpdateMethod(string state = "", int istate = 0)
	{
		if(istate == 0)
			this.state = Animator.StringToHash(state);
		else
			this.state = istate;
	}
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class StateEnterMethod: System.Attribute
{
	public int state;

	public StateEnterMethod(string state = "", int istate = 0)
	{
		if(state == "")
			this.state = istate;
		else
			this.state = Animator.StringToHash(state);
	}
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class StateExitMethod: System.Attribute
{
	public int state;

	public StateExitMethod(string state = "", int istate = 0)
	{
		if(istate == 0)
			this.state = Animator.StringToHash(state);
		else
			this.state = istate;
	}
}

public class StateMethod
{
	public int stateHash;
	public Action method;
}
