using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UIElements;


namespace Cogwheel
{
    public class CommandsManager : ICommandsManager
    {
        public Dictionary<string, Command> Commands { get; set; } = new(StringComparer.OrdinalIgnoreCase);
        public GameObject Context { get; set; }
        public Dictionary<Type, Func<string, object>> CustomParsers { get; private set; } = new();

        public string CommandPattern { get; set; } = "(?<val>(\\([^\\)]+\\)))|\"(?<val>[^\"]+)\"|'(?<val>[^']+)'|(?<val>[^\\s]+)";
        public List<Assembly> Assemblies { get; set; } = new();

        private CogwheelConsole _console;
        
        public CommandsManager(CogwheelConsole console)
        {
            _console = console;
            
            RefreshCommandsList();
        }

        public void SetContext(GameObject context)
        {
            Context = context;
        }

        public void SetContext(string name)
        {
            Context = SearchSceneForContext(name);
        }

        public GameObject SearchSceneForContext(string name)
        {
            var go = GameObject.Find(name);
            if (go is null)
            {
                _console.LogError($"Couldn't find an object with the name '{name}' in the scene");
                return null;
            }
            
            _console.Log($"Context set to: '{go.name}' <{go.GetType()}>");
            return go;
        }
        
        public Command GetCommandByName(string commandName)
        {
            return Commands.Keys.Contains(commandName) ? Commands[commandName] : null;
        }

        public (Command, List<object>)? GetCommandAndArgsFromString(string input)
        {
            var splitString = Regex.Matches(input, CommandPattern).Select(m => m.Groups["val"].Value).ToArray();
            var commandName = splitString[0];
            List<object> args = new();
            for (var splitIndex = 1; splitIndex < splitString.Length; splitIndex++)
            {
                args.Add(splitString[splitIndex]);
            }
            
            if (Commands.TryGetValue(commandName, out var command))
            {
                return (command, args);
            }

            return null;
        }
        
        public bool RunCommand(string input)
        {
            if (string.IsNullOrWhiteSpace(input)) return false;
            var command = GetCommandAndArgsFromString(input);
            if (command is null)
            {
                _console.LogError($"Command '{input}' not found");
                return false;
            }

            if (command.Value.Item1.Method.IsStatic)
            {
                return ExecuteCommand(null, command.Value);
            }
            
            if (Context is null)
            {
                _console.LogError("SceneContext not set");
                return false;
            }

            foreach (var component in Context.GetComponents<MonoBehaviour>())
            {
                if (command.Value.Item1.Method.DeclaringType!.IsInstanceOfType(component))
                {
                    return ExecuteCommand(component, command.Value);
                }
            }
                
            _console.LogError($"Instance of '{command.Value.Item1.Method.DeclaringType}' not found in SceneContext");
            return false;
        }

        public string GetCommandUsage(Command command)
        {
            string paramUsage = string.Join(" ",
                command.Method.GetParameters().Select(param =>
                    $"<{(param.IsDefined(typeof(ParamArrayAttribute)) ? "params " : "")}{param}>"));

            return $"{command.Name}: {paramUsage}";
        }
        
        public bool ExecuteCommand(object obj, (Command command, List<object> args) command)
        {
            var parameters = command.command.Method.GetParameters();
            for (var parameterIndex = 0; parameterIndex < parameters.Length; parameterIndex++)
            {
                if (parameters[parameterIndex].IsDefined(typeof(ParamArrayAttribute), false))
                {
                    var paramList = Activator.CreateInstance(typeof(List<>).MakeGenericType(parameters[parameterIndex].ParameterType.GetElementType()!));
                    for (var argIndex = parameterIndex; argIndex < parameters.Length; argIndex++)
                    {
                        if (TryParseParameter(parameters[parameterIndex].ParameterType.GetElementType(),
                                (string)command.args[argIndex], out var val))
                        {
                            paramList.GetType().GetMethod("Add")?.Invoke(paramList, new[] { val });
                        }
                        else
                        {
                            _console.LogError($"Format exception: could not parse '{command.args[parameterIndex]}' as '{parameters[parameterIndex].ParameterType}'");
                            return false;
                        }
                    }

                    command.args = command.args.Take(parameterIndex).ToList();
                    command.args.Add(paramList.GetType().GetMethod("ToArray")?.Invoke(paramList, null));
                }
                else if (parameterIndex >= command.args.Count)
                {
                    if (parameters[parameterIndex].IsOptional)
                    {
                        command.args.Add(Type.Missing);
                    }
                    else
                    {
                        _console.LogError("Not enough args passed");
                        _console.Log(GetCommandUsage(command.command));
                        return false;
                    }
                }
                else
                {
                    if (TryParseParameter(parameters[parameterIndex].ParameterType, (string)command.args[parameterIndex], out var val))
                    {
                        command.args[parameterIndex] = val;
                    }
                    else
                    {
                        _console.LogError($"Format exception: could not parse '{command.args[parameterIndex]}' as '{parameters[parameterIndex].ParameterType}'");
                        return false;
                    }
                }
            }

            try
            {
                command.command.Method.Invoke(obj, command.args.ToArray());
                return true;
            }
            catch (Exception e)
            {
                _console.LogError(e.Message);
                throw;
            }
        }

        public bool TryParseParameter(Type parameterType, string parameterString, out object parsedValue)
        {
            if (parameterType == typeof(string))
            {
                parsedValue = parameterString;
                return true;
            }

            if (CustomParsers.ContainsKey(parameterType))
            {
                try
                {
                    parsedValue = CustomParsers[parameterType].Invoke(parameterString);
                    return true;
                }
                catch (Exception e)
                {
                    _console.LogError(e.Message);
                    parsedValue = null;
                    return false;
                }
            }
            else
            {
                var parseMethod = parameterType.GetMethod("Parse", new[] { typeof(string) });

                if (parseMethod is not null)
                {
                    try
                    {
                        parsedValue = parseMethod.Invoke(null, new object[] { parameterString });
                        return true;
                    }
                    catch (Exception e)
                    {
                        _console.LogError(e.Message);
                        parsedValue = null;
                        return false;
                    }
                }
            }
            
            parsedValue = null;
            return false;
        }

        public bool IsCommandContextValid(Command command)
        {
            if (Context is null) return false;

            List<Type> components = new();
            foreach (var component in Context.GetComponents<MonoBehaviour>())
            {
                components.Add(component.GetType());
            }

            if (components.Contains(command.Method.DeclaringType)) return true;
            
            components.Clear();
            return false;
        }

        private void RefreshCommandsList()
        {
            foreach (var type in Assembly.GetCallingAssembly().GetTypes())
            {
                var methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance);

                foreach (var method in methods)
                {
                    var attributes = method.GetCustomAttributes<CommandAttribute>();

                    foreach (var attribute in attributes)
                    {
                        var commandName = attribute.CommandName ?? method.Name;
                        var newCommand = new Command(commandName, method, attribute.Description);
                        Commands.TryAdd(newCommand.Name, newCommand);
                    }
                }
            }
        }
    }
}


