using BehaviorDesigner.Runtime;
using UnityEngine;

namespace PixelCrushers.DialogueSystem.BehaviorDesignerSupport
{

    /// <summary>
    /// This script synchronizes a behavior tree's shared variables with the
    /// Dialogue System's Lua environment. Attach it to the GameObject that
    /// contains the behavior tree. Synchronization occurs automatically at
    /// the beginning and end of conversations. You can also synchronize manually
    /// by calling SyncToLua() or SyncFromLua().
    /// 
    /// The Lua variables will have the name <em>gameObjectName_variableName</em>.
    /// All blank spaces and hyphens will be converted to underscores.
    /// For example, say an NPC named Private Hart has a behavior tree with a shared
    /// variable named Angry. The Lua variable will be <c>Variable["Private_Hart_Angry"]</c>.
    /// 
    /// Only bools, floats, ints, and strings are synchronized.
    /// 
    /// This script also adds some Lua functions:
    /// - bdSyncToLua(gameObject)
    /// - bdSyncFromLua(gameObject)
    /// where gameObject is the name of a GameObject with a behavior tree.
    /// </summary>
    [AddComponentMenu("Pixel Crushers/Dialogue System/Third Party/Behavior Designer/Behavior Tree Lua Bridge")]
    public class BehaviorTreeLuaBridge : MonoBehaviour
    {

        // The behavior tree on this GameObject:
        private Behavior behavior = null;

        private static bool hasRegisteredLuaFunctions = false;

        void Awake()
        {
            behavior = GetComponentInChildren<Behavior>();
            RegisterLuaFunctions();
        }

        /// <summary>
        /// When a conversation starts, sync to Lua. This makes the behavior
        /// tree data available to conversations in Conditions and User Scripts.
        /// </summary>
        /// <param name="actor">The other actor.</param>
        public void OnConversationStart(Transform actor)
        {
            SyncToLua();
        }

        /// <summary>
        /// When a conversation ends, sync from Lua back into the behavior
        /// tree. If the conversation has changed any values, the changes
        /// will be reflected in the behavior tree.
        /// </summary>
        /// <param name="actor">The other actor.</param>
        public void OnConversationEnd(Transform actor)
        {
            SyncFromLua();
        }

        /// <summary>
        /// Syncs the behavior tree's shared variables to the Dialogue System's
        /// Lua environment. 
        /// </summary>
        public void SyncToLua()
        {
            if (behavior == null) return;
            foreach (SharedVariable variable in behavior.GetBehaviorSource().Variables)
            {
                if (IsSyncableType(variable))
                {
                    DialogueLua.SetVariable(GetLuaVariableName(variable.Name), variable.GetValue());
                }
            }
        }

        /// <summary>
        /// Syncs the Dialogue System's Lua environment back into the behavior tree's 
        /// shared variables.
        /// </summary>
        public void SyncFromLua()
        {
            if (behavior == null) return;
            foreach (SharedVariable variable in behavior.GetBehaviorSource().Variables)
            {
                if (IsSyncableType(variable))
                {
                    Lua.Result result = DialogueLua.GetVariable(GetLuaVariableName(variable.Name));
                    if (result.HasReturnValue)
                    {
                        variable.SetValue(CastLuaResult(variable, result));
                    }
                }
            }
        }

        private bool IsSyncableType(SharedVariable variable)
        {
            return (variable is SharedBool) ||
                (variable is SharedFloat) ||
                    (variable is SharedInt) ||
                    (variable is SharedString);
        }

        private string GetLuaVariableName(string variableName)
        {
            return string.Format("{0}_{1}", name, variableName);
        }

        private object CastLuaResult(SharedVariable variable, Lua.Result result)
        {
            if (variable is SharedBool) return result.AsBool;
            if (variable is SharedFloat) return result.AsFloat;
            if (variable is SharedInt) return result.AsInt;
            if (variable is SharedString) return result.AsString;
            return null;
        }

        public static void RegisterLuaFunctions()
        {
            if (hasRegisteredLuaFunctions) return;
            hasRegisteredLuaFunctions = true;
            Lua.RegisterFunction("bdSyncToLua", null, SymbolExtensions.GetMethodInfo(() => bdSyncToLua(string.Empty)));
            Lua.RegisterFunction("bdSyncToFrom", null, SymbolExtensions.GetMethodInfo(() => bdSyncFromLua(string.Empty)));

            Lua.RegisterFunction("bdGetBool", null, SymbolExtensions.GetMethodInfo(() => bdGetBool(string.Empty, string.Empty)));
            Lua.RegisterFunction("bdGetFloat", null, SymbolExtensions.GetMethodInfo(() => bdGetFloat(string.Empty, string.Empty)));
            Lua.RegisterFunction("bdGetInt", null, SymbolExtensions.GetMethodInfo(() => bdGetInt(string.Empty, string.Empty)));
            Lua.RegisterFunction("bdGetString", null, SymbolExtensions.GetMethodInfo(() => bdGetString(string.Empty, string.Empty)));
            Lua.RegisterFunction("bdSetBool", null, SymbolExtensions.GetMethodInfo(() => bdSetBool(string.Empty, string.Empty, false)));
            Lua.RegisterFunction("bdSetFloat", null, SymbolExtensions.GetMethodInfo(() => bdSetFloat(string.Empty, string.Empty, (double)0)));
            Lua.RegisterFunction("bdSetInt", null, SymbolExtensions.GetMethodInfo(() => bdSetInt(string.Empty, string.Empty, (double)0)));
            Lua.RegisterFunction("bdSetString", null, SymbolExtensions.GetMethodInfo(() => bdSetString(string.Empty, string.Empty, string.Empty)));

            Lua.RegisterFunction("bdGetBoolGroup", null, SymbolExtensions.GetMethodInfo(() => bdGetBoolGroup(string.Empty, (double)0, string.Empty)));
            Lua.RegisterFunction("bdGetFloatGroup", null, SymbolExtensions.GetMethodInfo(() => bdGetFloatGroup(string.Empty, (double)0, string.Empty)));
            Lua.RegisterFunction("bdGetIntGroup", null, SymbolExtensions.GetMethodInfo(() => bdGetIntGroup(string.Empty, (double)0, string.Empty)));
            Lua.RegisterFunction("bdGetStringGroup", null, SymbolExtensions.GetMethodInfo(() => bdGetStringGroup(string.Empty, (double)0, string.Empty)));
            Lua.RegisterFunction("bdSetBoolGroup", null, SymbolExtensions.GetMethodInfo(() => bdSetBoolGroup(string.Empty, (double)0, string.Empty, false)));
            Lua.RegisterFunction("bdSetFloatGroup", null, SymbolExtensions.GetMethodInfo(() => bdSetFloatGroup(string.Empty, (double)0, string.Empty, (double)0)));
            Lua.RegisterFunction("bdSetIntGroup", null, SymbolExtensions.GetMethodInfo(() => bdSetIntGroup(string.Empty, (double)0, string.Empty, (double)0)));
            Lua.RegisterFunction("bdSetStringGroup", null, SymbolExtensions.GetMethodInfo(() => bdSetStringGroup(string.Empty, (double)0, string.Empty, string.Empty)));
        }

        public static void bdSyncToLua(string gameObjectName)
        {
            var bridge = FindBridge(gameObjectName);
            if (bridge != null) bridge.SyncToLua();
        }

        public static void bdSyncFromLua(string gameObjectName)
        {
            var bridge = FindBridge(gameObjectName);
            if (bridge != null) bridge.SyncFromLua();
        }

        public static T bdGetVariableValue<T>(string gameObjectName, string variableName, int group = -1)
        {
            var behavior = FindBehavior(gameObjectName, group);
            if (behavior != null)
            {
                foreach (SharedVariable variable in behavior.GetBehaviorSource().Variables)
                {
                    if (variable.Name == variableName)
                    {
                        var value = variable.GetValue();
                        if (value.GetType() == typeof(T)) return (T)value;
                    }
                }
            }
            return default(T);
        }

        public static void bdSetVariableValue<T>(string gameObjectName, string variableName, T variableValue, int group = -1)
        {
            var behavior = FindBehavior(gameObjectName, group);
            if (behavior != null)
            {
                foreach (SharedVariable variable in behavior.GetBehaviorSource().Variables)
                {
                    if (variable.Name == variableName)
                    {
                        variable.SetValue(variableValue);
                        return;
                    }
                }
            }
        }

        public static bool bdGetBool(string gameObjectName, string variableName)
        {
            return bdGetVariableValue<bool>(gameObjectName, variableName);
        }

        public static double bdGetFloat(string gameObjectName, string variableName)
        {
            return (double)bdGetVariableValue<float>(gameObjectName, variableName);
        }

        public static double bdGetInt(string gameObjectName, string variableName)
        {
            return (double)bdGetVariableValue<int>(gameObjectName, variableName);
        }

        public static string bdGetString(string gameObjectName, string variableName)
        {
            return bdGetVariableValue<string>(gameObjectName, variableName);
        }

        public static void bdSetBool(string gameObjectName, string variableName, bool variableValue)
        {
            bdSetVariableValue<bool>(gameObjectName, variableName, variableValue);
        }

        public static void bdSetFloat(string gameObjectName, string variableName, double variableValue)
        {
            bdSetVariableValue<float>(gameObjectName, variableName, (float)variableValue);
        }

        public static void bdSetInt(string gameObjectName, string variableName, double variableValue)
        {
            bdSetVariableValue<int>(gameObjectName, variableName, (int)variableValue);
        }

        public static void bdSetString(string gameObjectName, string variableName, string variableValue)
        {
            bdSetVariableValue<string>(gameObjectName, variableName, variableValue);
        }

        public static bool bdGetBoolGroup(string gameObjectName, double group, string variableName)
        {
            return bdGetVariableValue<bool>(gameObjectName, variableName, (int)group);
        }

        public static double bdGetFloatGroup(string gameObjectName, double group, string variableName)
        {
            return (double)bdGetVariableValue<float>(gameObjectName, variableName, (int)group);
        }

        public static double bdGetIntGroup(string gameObjectName, double group, string variableName)
        {
            return (double)bdGetVariableValue<int>(gameObjectName, variableName, (int)group);
        }

        public static string bdGetStringGroup(string gameObjectName, double group, string variableName)
        {
            return bdGetVariableValue<string>(gameObjectName, variableName, (int)group);
        }

        public static void bdSetBoolGroup(string gameObjectName, double group, string variableName, bool variableValue)
        {
            bdSetVariableValue<bool>(gameObjectName, variableName, variableValue, (int)group);
        }

        public static void bdSetFloatGroup(string gameObjectName, double group, string variableName, double variableValue)
        {
            bdSetVariableValue<float>(gameObjectName, variableName, (float)variableValue, (int)group);
        }

        public static void bdSetIntGroup(string gameObjectName, double group, string variableName, double variableValue)
        {
            bdSetVariableValue<int>(gameObjectName, variableName, (int)variableValue, (int)group);
        }

        public static void bdSetStringGroup(string gameObjectName, double group, string variableName, string variableValue)
        {
            bdSetVariableValue<string>(gameObjectName, variableName, variableValue, (int)group);
        }

        public static BehaviorTreeLuaBridge FindBridge(string gameObjectName)
        {
            var go = GameObject.Find(gameObjectName);
            if (go == null)
            {
                if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: BehaviorTreeLuaBridge can't find GameObject named " + gameObjectName);
                return null;
            }
            var bridge = go.GetComponent<BehaviorTreeLuaBridge>();
            if (bridge == null)
            {
                var behavior = go.GetComponentInChildren<Behavior>();
                if (behavior == null)
                {
                    if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: GameObject " + go.name + " doesn't have a Behavior tree in its hierarchy.", go);
                    return null;
                }
                bridge = go.AddComponent<BehaviorTreeLuaBridge>();
                bridge.behavior = behavior;
            }
            return bridge;
        }

        public static Behavior FindBehavior(string gameObjectName, int group = -1)
        {
            var go = GameObject.Find(gameObjectName);
            if (go == null)
            {
                if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: BehaviorTreeLuaBridge can't find GameObject named " + gameObjectName);
                return null;
            }
            foreach (var behavior in go.GetComponentsInChildren<Behavior>())
            {
                if (group == -1 || behavior.Group == group)
                {
                    return behavior;
                }
            }
            return null;
        }

    }

}