// This code is part of the Fungus library (https://github.com/snozbot/fungus)
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)

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

namespace Fungus.EditorUtils
{
    [CustomEditor(typeof(InvokeMethod))]
    public class InvokeMethodEditor : CommandEditor
    {
        InvokeMethod targetMethod;

        public override void DrawCommandGUI()
        {
            base.DrawCommandGUI();

            targetMethod = target as InvokeMethod;

            if (targetMethod == null || targetMethod.TargetObject == null)
                return;

            SerializedObject objSerializedTarget = new SerializedObject(targetMethod);

            string component = ShowComponents(objSerializedTarget, targetMethod.TargetObject);

            // show component methods if selected
            if (!string.IsNullOrEmpty(component))
            {
                var method = ShowMethods(objSerializedTarget, targetMethod.TargetObject, component);

                // show method parameters if selected
                if (method != null)
                {
                    objSerializedTarget.ApplyModifiedProperties();
                    ShowParameters(objSerializedTarget, targetMethod.TargetObject, method);
                    ShowReturnValue(objSerializedTarget, method);
                }
            }
        }

        private string ShowComponents(SerializedObject objTarget, GameObject gameObject)
        {
            var targetComponentAssemblyName = objTarget.FindProperty("targetComponentAssemblyName");
            var targetComponentFullname = objTarget.FindProperty("targetComponentFullname");
            var targetComponentText = objTarget.FindProperty("targetComponentText");
            var objComponents = gameObject.GetComponents<Component>();
            var objTypesAssemblynames = (from objComp in objComponents select objComp.GetType().AssemblyQualifiedName).ToList();
            var objTypesName = (from objComp in objComponents select objComp.GetType().Name).ToList();

            int index = objTypesAssemblynames.IndexOf(targetComponentAssemblyName.stringValue);

            index = EditorGUILayout.Popup("Target Component", index, objTypesName.ToArray());

            if (index != -1)
            {
                targetComponentAssemblyName.stringValue = objTypesAssemblynames[index];
                targetComponentFullname.stringValue = objComponents.GetType().FullName;
                targetComponentText.stringValue = objTypesName[index];
            }
            else
            {
                targetComponentAssemblyName.stringValue = null;
            }

            objTarget.ApplyModifiedProperties();

            return targetComponentAssemblyName.stringValue;
        }

        private MethodInfo ShowMethods(SerializedObject objTarget, GameObject gameObject, string component)
        {
            MethodInfo result = null;

            var targetMethodProp = objTarget.FindProperty("targetMethod");
            var targetMethodTextProp = objTarget.FindProperty("targetMethodText");
            var methodParametersProp = objTarget.FindProperty("methodParameters");
            var showInheritedProp = objTarget.FindProperty("showInherited");
            var saveReturnValueProp = objTarget.FindProperty("saveReturnValue");
            var returnValueKeyProp = objTarget.FindProperty("returnValueVariableKey");

            var objComponent = gameObject.GetComponent(ReflectionHelper.GetType(component));
            var bindingFlags = BindingFlags.Default | BindingFlags.Public | BindingFlags.Instance;

            if (!showInheritedProp.boolValue)
            {
                bindingFlags |= BindingFlags.DeclaredOnly;
            }

            if (objComponent != null)
            {
                var objMethods = objComponent.GetType().GetMethods(bindingFlags);
                var methods = (from objMethod in objMethods where !objMethod.IsSpecialName select objMethod).ToList(); // filter out the getter/setter methods
                var methodText = (from objMethod in methods select objMethod.Name + FormatParameters(objMethod.GetParameters()) + ": " + objMethod.ReturnType.Name).ToList();
                int index = methodText.IndexOf(targetMethodTextProp.stringValue);

                index = EditorGUILayout.Popup("Target Method", index, methodText.ToArray());

                EditorGUILayout.PropertyField(showInheritedProp);

                if (index != -1)
                {
                    if (targetMethodTextProp.stringValue != methodText[index])
                    {
                        // reset
                        methodParametersProp.ClearArray();
                        methodParametersProp.arraySize = methods[index].GetParameters().Length;

                        saveReturnValueProp.boolValue = false;
                        returnValueKeyProp.stringValue = null;
                    }

                    targetMethodTextProp.stringValue = methodText[index];
                    targetMethodProp.stringValue = methods[index].Name;

                    result = methods[index];
                }
                else
                {
                    targetMethodTextProp.stringValue = null;
                    targetMethodProp.stringValue = null;
                }

                objTarget.ApplyModifiedProperties();
            }

            return result;
        }

        private void ShowParameters(SerializedObject objTarget, GameObject gameObject, MethodInfo method)
        {
            var methodParametersProp = objTarget.FindProperty("methodParameters");
            var objParams = method.GetParameters();

            if (objParams.Length > 0)
            {
                GUILayout.Space(20);
                EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel);
                EditorGUILayout.BeginVertical(EditorStyles.helpBox);

                for (int i = 0; i < objParams.Length; i++)
                {
                    var objParam = objParams[i];

                    GUILayout.BeginHorizontal();
                    string labelFormat = string.Format("{0}: {1}", objParam.ParameterType.Name, objParam.Name);

                    var objItemProp = methodParametersProp.GetArrayElementAtIndex(i);
                    var serObjValueProp = objItemProp.FindPropertyRelative("objValue");
                    var serVariableKeyProp = objItemProp.FindPropertyRelative("variableKey");
                    var serValueTypeAssemblynameProp = serObjValueProp.FindPropertyRelative("typeAssemblyname");
                    var serValueTypeFullnameProp = serObjValueProp.FindPropertyRelative("typeFullname");

                    serValueTypeAssemblynameProp.stringValue = objParam.ParameterType.AssemblyQualifiedName;
                    serValueTypeFullnameProp.stringValue = objParam.ParameterType.FullName;

                    bool isDrawn = true;

                    if (string.IsNullOrEmpty(serVariableKeyProp.stringValue))
                    {
                        isDrawn = DrawTypedPropertyInput(labelFormat, serObjValueProp, objParam.ParameterType);
                    }

                    if (isDrawn)
                    {
                        var vars = GetFungusVariablesByType(targetMethod.GetFlowchart().Variables, objParam.ParameterType);
                        var values = new string[] { "<Value>" };
                        var displayValue = values.Concat(vars).ToList();

                        int index = displayValue.IndexOf(serVariableKeyProp.stringValue);

                        if (index == -1)
                        {
                            index = 0;
                        }

                        if (string.IsNullOrEmpty(serVariableKeyProp.stringValue))
                        {
                            index = EditorGUILayout.Popup(index, displayValue.ToArray(), GUILayout.MaxWidth(80));
                        }
                        else
                        {
                            index = EditorGUILayout.Popup(labelFormat, index, displayValue.ToArray());
                        }

                        if (index > 0)
                        {
                            serVariableKeyProp.stringValue = displayValue[index];
                        }
                        else
                        {
                            serVariableKeyProp.stringValue = null;
                        }
                    }
                    else
                    {
                        var style = EditorStyles.label;
                        var prevColor = style.normal.textColor;
                        style.normal.textColor = Color.red;
                        EditorGUILayout.LabelField(new GUIContent(objParam.ParameterType.Name + " cannot be drawn, don´t use this method in the flowchart."), style);
                        style.normal.textColor = prevColor;
                    }

                    GUILayout.EndHorizontal();
                }
                EditorGUILayout.EndVertical();

                objTarget.ApplyModifiedProperties();
            }
        }

        private void ShowReturnValue(SerializedObject objTarget, MethodInfo method)
        {
            var saveReturnValueProp = objTarget.FindProperty("saveReturnValue");
            var returnValueKeyProp = objTarget.FindProperty("returnValueVariableKey");
            var returnValueTypeProp = objTarget.FindProperty("returnValueType");
            var callModeProp = objTarget.FindProperty("callMode");

            returnValueTypeProp.stringValue = method.ReturnType.FullName;

            if (method.ReturnType == typeof(IEnumerator))
            {
                GUILayout.Space(20);          
                EditorGUILayout.PropertyField(callModeProp);           
            }
            else if (method.ReturnType != typeof(void))
            {
                GUILayout.Space(20);
                EditorGUILayout.LabelField("Return Value", EditorStyles.boldLabel);
                EditorGUILayout.BeginVertical(EditorStyles.helpBox);
                saveReturnValueProp.boolValue = EditorGUILayout.Toggle("Save return value", saveReturnValueProp.boolValue);

                if (saveReturnValueProp.boolValue)
                {
                    var vars = GetFungusVariablesByType(targetMethod.GetFlowchart().Variables, method.ReturnType).ToList();
                    int index = vars.IndexOf(returnValueKeyProp.stringValue);
                    index = EditorGUILayout.Popup(method.ReturnType.Name, index, vars.ToArray());

                    if (index != -1)
                    {
                        returnValueKeyProp.stringValue = vars[index];                    
                    }
                }

                EditorGUILayout.EndVertical();           
            }
            else
            {
                saveReturnValueProp.boolValue = false;
            }

            objTarget.ApplyModifiedProperties();
        }

        private bool DrawTypedPropertyInput(string label, SerializedProperty objProperty, Type type)
        {
            SerializedProperty objectValue = null;

            if (type == typeof(int))
            {
                objectValue = objProperty.FindPropertyRelative("intValue");
                objectValue.intValue = EditorGUILayout.IntField(new GUIContent(label), objectValue.intValue);

                return true;
            }
            else if (type == typeof(bool))
            {
                objectValue = objProperty.FindPropertyRelative("boolValue");
                objectValue.boolValue = EditorGUILayout.Toggle(new GUIContent(label), objectValue.boolValue);

                return true;
            }
            else if (type == typeof(float))
            {
                objectValue = objProperty.FindPropertyRelative("floatValue");
                objectValue.floatValue = EditorGUILayout.FloatField(new GUIContent(label), objectValue.floatValue);

                return true;
            }
            else if (type == typeof(string))
            {
                objectValue = objProperty.FindPropertyRelative("stringValue");
                objectValue.stringValue = EditorGUILayout.TextField(new GUIContent(label), objectValue.stringValue);

                return true;
            }
            else if (type == typeof(Color))
            {
                objectValue = objProperty.FindPropertyRelative("colorValue");
                objectValue.colorValue = EditorGUILayout.ColorField(new GUIContent(label), objectValue.colorValue);

                return true;
            }
            else if (type == typeof(UnityEngine.GameObject))
            {
                objectValue = objProperty.FindPropertyRelative("gameObjectValue");
                objectValue.objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(label), objectValue.objectReferenceValue, typeof(UnityEngine.GameObject), true);

                return true;
            }
            else if (type == typeof(UnityEngine.Material))
            {
                objectValue = objProperty.FindPropertyRelative("materialValue");
                objectValue.objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(label), objectValue.objectReferenceValue, typeof(UnityEngine.Material), true);

                return true;
            }
            else if (type == typeof(UnityEngine.Sprite))
            {
                objectValue = objProperty.FindPropertyRelative("spriteValue");
                objectValue.objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(label), objectValue.objectReferenceValue, typeof(UnityEngine.Sprite), true);

                return true;
            }
            else if (type == typeof(UnityEngine.Texture))
            {
                objectValue = objProperty.FindPropertyRelative("textureValue");
                objectValue.objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(label), objectValue.objectReferenceValue, typeof(UnityEngine.Texture), true);

                return true;
            }
            else if (type == typeof(UnityEngine.Vector2))
            {
                objectValue = objProperty.FindPropertyRelative("vector2Value");
                objectValue.vector2Value = EditorGUILayout.Vector2Field(new GUIContent(label), objectValue.vector2Value);

                return true;
            }
            else if (type == typeof(UnityEngine.Vector3))
            {
                objectValue = objProperty.FindPropertyRelative("vector3Value");
                objectValue.vector3Value = EditorGUILayout.Vector3Field(new GUIContent(label), objectValue.vector3Value);

                return true;
            }
            else if (type.IsSubclassOf(typeof(UnityEngine.Object)))
            {
                objectValue = objProperty.FindPropertyRelative("objectValue");
                objectValue.objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(label), objectValue.objectReferenceValue, type, true);

                return true;
            }
            else if (type.IsEnum)
            {
                var enumNames = Enum.GetNames(type);
                objectValue = objProperty.FindPropertyRelative("intValue");
                objectValue.intValue = EditorGUILayout.Popup(label, objectValue.intValue, enumNames);

                return true;
            }

            return false;
        }

        private string[] GetFungusVariablesByType(List<Variable> variables, Type type)
        {
            string[] result = new string[0];

            if (type == typeof(int))
            {
                result = (from v in variables where v.GetType() == typeof(IntegerVariable) select v.Key).ToArray();
            }
            else if (type == typeof(bool))
            {
                result = (from v in variables where v.GetType() == typeof(BooleanVariable) select v.Key).ToArray();
            }
            else if (type == typeof(float))
            {
                result = (from v in variables where v.GetType() == typeof(FloatVariable) select v.Key).ToArray();
            }
            else if (type == typeof(string))
            {
                result = (from v in variables where v.GetType() == typeof(StringVariable) select v.Key).ToArray();
            }
            else if (type == typeof(Color))
            {
                result = (from v in variables where v.GetType() == typeof(ColorVariable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.GameObject))
            {
                result = (from v in variables where v.GetType() == typeof(GameObjectVariable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.Material))
            {
                result = (from v in variables where v.GetType() == typeof(MaterialVariable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.Sprite))
            {
                result = (from v in variables where v.GetType() == typeof(SpriteVariable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.Texture))
            {
                result = (from v in variables where v.GetType() == typeof(TextureVariable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.Vector2))
            {
                result = (from v in variables where v.GetType() == typeof(Vector2Variable) select v.Key).ToArray();
            }
            else if (type == typeof(UnityEngine.Vector3))
            {
                result = (from v in variables where v.GetType() == typeof(Vector3Variable) select v.Key).ToArray();
            }
            else if (type.IsSubclassOf(typeof(UnityEngine.Object)))
            {
                result = (from v in variables where v.GetType() == typeof(ObjectVariable) select v.Key).ToArray();
            }

            return result;
        }

        private string FormatParameters(ParameterInfo[] paramInfo)
        {
            string result = " (";

            for (int i = 0; i < paramInfo.Length; i++)
            {
                var pi = paramInfo[i];
                result += pi.ParameterType.Name; // " arg" + (i + 1); 

                if (i < paramInfo.Length - 1)
                {
                    result += ", ";
                }
            }

            return result + ")";
        }

        private object GetDefaultValue(Type t)
        {
            if (t.IsValueType)
                return Activator.CreateInstance(t);

            return null;
        }
    }
}