using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;

namespace RebootKit.Engine.Console {
    [AttributeUsage(AttributeTargets.Method)]
    public class RCCMD : Attribute {
        public string name;
        public string description;

        public RCCMD(string name, string description) {
            this.name = name;
            this.description = description;
        }
    }

    public class ConsoleService : IService {
        static readonly Logger s_logger = new Logger(nameof(ConsoleService));

        public struct ConsoleCommand {
            public string name;
            public string description;
            public Action<string[]> action;
        }

        readonly List<ConsoleCommand> m_ConsoleCommands = new List<ConsoleCommand>();

        FileStream m_LogFileStream;
        TextWriter m_LogFileWriter;

        bool m_IsLoading;

        public ConsoleService() {
            ConfigVar.StateChanged += OnCVarStateChanged;

#if UNITY_EDITOR
            string logFilePath = Application.persistentDataPath + "/rr_logs_editor.txt";
#else
            string logFilePath = Application.persistentDataPath + "/rr_logs.txt";
#endif

            try {
                m_LogFileStream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write);
                m_LogFileWriter = new StreamWriter(m_LogFileStream);

                m_LogFileWriter.WriteLine("============================");
                m_LogFileWriter.WriteLine("Starting new log");
                m_LogFileWriter.WriteLine($" > Game: {Application.productName}");
                m_LogFileWriter.WriteLine($" > Version: {Application.version}");
                m_LogFileWriter.WriteLine($" > Date: {DateTime.Now}");
                m_LogFileWriter.WriteLine("============================");
                m_LogFileWriter.Flush();

            } catch (Exception e) {
                s_logger.Error($"Failed to open log file at {logFilePath}: {e.Message}");
            }

            s_logger.Info("Waking up");

            Load();

            RegisterCommands();
        }

        public void Dispose() {
            s_logger.Info("Shutting down");

            ConfigVar.StateChanged -= OnCVarStateChanged;

            m_LogFileWriter?.Dispose();
            m_LogFileStream?.Dispose();

            m_LogFileStream = null;
            m_LogFileWriter = null;
        }

        void OnCVarStateChanged(ConfigVar cvar) {
            if (m_IsLoading) {
                return;
            }

            if ((cvar.flags & CVarFlags.ReadOnly) == 0) {
                Save();
            }
        }

        public event Action<string> OnOutputMessage = _ => { };

        public void WriteToOutput(string message) {
            if (m_LogFileWriter != null) {
                m_LogFileWriter.WriteLine(message);
                m_LogFileWriter.Flush();
            }

            OnOutputMessage?.Invoke(message);
        }

        string[] ParseCommandInput(string text) {
            return text.Split(' ');
        }

        // @NOTE: Input must be in format: "command arg1 arg2 arg3", one command = one call
        public void Execute(string input) {
            if (input.Length == 0) {
                return;
            }

            string[] arguments = ParseCommandInput(input);
            if (arguments.Length == 0) {
                return;
            }

            string commandName = arguments[0];

            foreach (ConsoleCommand command in m_ConsoleCommands) {
                if (command.name.Equals(commandName)) {
                    command.action(arguments);
                    return;
                }
            }

            foreach (ConfigVar cvar in ConfigVarsContainer.All()) {
                if (cvar.name.Equals(commandName)) {
                    if (arguments.Length == 2) {
                        cvar.ParseFromString(arguments[1]);
                    }

                    WriteToOutput($"<b>{cvar.name}</b> - {cvar}\n");
                    return;
                }
            }

            WriteToOutput($"ERROR: Command/CVar `{commandName}` not found.");
        }

        public void RegisterCommand(string name, string description, Action<string[]> action) {
            if (IsCommandRegistered(name)) {
                s_logger.Error($"`{name}` command is already registered");
                return;
            }

            m_ConsoleCommands.Add(new ConsoleCommand {
                name = name,
                description = description,
                action = action
            });
            s_logger.Info($"Registered command: {name}");
        }

        public static ConsoleCommand[] GenerateCommandsToRegister() {
            IEnumerable<MethodInfo> methods = AppDomain.CurrentDomain.GetAssemblies()
                                                       .SelectMany(assembly => assembly.GetTypes())
                                                       .SelectMany(type => type.GetMethods(BindingFlags.NonPublic |
                                                                                BindingFlags.Public |
                                                                                BindingFlags.Static))
                                                       .Where(method => method.GetCustomAttributes(typeof(RCCMD), false)
                                                                              .Length >
                                                                        0);

            List<ConsoleCommand> commands = new List<ConsoleCommand>();
            foreach (MethodInfo method in methods) {
                RCCMD attribute = (RCCMD) method.GetCustomAttributes(typeof(RCCMD), false)[0];

                if (!method.IsStatic) {
                    s_logger.Error($"Command `{attribute.name}` is not static, skipping");
                    continue;
                }

                if (method.GetParameters().Length != 1) {
                    s_logger.Error($"Command `{attribute.name}` has invalid number of parameters, skipping");
                    continue;
                }

                if (method.GetParameters()[0].ParameterType != typeof(string[])) {
                    s_logger.Error($"Command `{attribute.name}` has invalid parameter type, skipping");
                    continue;
                }

                Action<string[]> action = (Action<string[]>) Delegate.CreateDelegate(typeof(Action<string[]>), method);

                commands.Add(new ConsoleCommand {
                    name = attribute.name,
                    description = attribute.description,
                    action = action
                });
            }

            return commands.ToArray();
        }

        public void RegisterCommands() {
            ConsoleCommand[] commands = GenerateCommandsToRegister();
            foreach (ConsoleCommand command in commands) {
                RegisterCommand(command.name, command.description, command.action);
            }
        }

        bool IsCommandRegistered(string name) {
            foreach (ConsoleCommand command in m_ConsoleCommands) {
                if (command.name.Equals(name)) {
                    return true;
                }
            }

            return false;
        }

        [RCCMD("help", "Prints help message with all commands and cvars")]
        public static void PrintHelpCommand(string[] args) {
            StringBuilder message = new StringBuilder();

            message.AppendLine("Available commands:");
            foreach (ConsoleCommand command in RR.Console.m_ConsoleCommands) {
                message.Append("    ");
                message.Append(command.name);
                message.Append(" - ");
                message.Append(command.description);
                message.AppendLine();
            }

            message.AppendLine("Available cvars:");
            foreach (ConfigVar cvar in ConfigVarsContainer.All()) {
                message.Append("    ");
                message.Append(cvar.name);
                message.Append(" - ");
                message.Append(cvar.description);
                message.AppendLine();
            }

            RR.Console.WriteToOutput(message.ToString());
        }

        [RCCMD("cvars", "Prints all cvars")]
        public static void PrintCVars(string[] args) {
            StringBuilder message = new StringBuilder();

            foreach (ConfigVar cvar in ConfigVarsContainer.All()) {
                message.AppendLine($"{cvar.name} - {cvar}");
            }

            RR.Console.WriteToOutput(message.ToString());
        }

        void Save() {
            string path = Application.persistentDataPath + "/" + RConsts.k_CVarsFilename;

            s_logger.Info("Saving cvars to file: " + path);

            StringBuilder sb = new StringBuilder();

            foreach (ConfigVar cvar in ConfigVarsContainer.All()) {
                if ((cvar.flags & CVarFlags.ReadOnly) == 0) {
                    sb.AppendFormat("{0} {1}\n", cvar.name, cvar);
                }
            }

            File.WriteAllText(path, sb.ToString());
        }

        void Load() {
            string path = Application.persistentDataPath + "/" + RConsts.k_CVarsFilename;
            if (!File.Exists(path)) {
                s_logger.Info("CVar file not found, skipping load");
                return;
            }

            m_IsLoading = true;
            ExecuteFile(path);
            m_IsLoading = false;
        }

        bool ExecuteFile(string path) {
            if (!File.Exists(path)) {
                s_logger.Error($"Cannot load file '{path}', file not found");
                return false;
            }

            s_logger.Info($"Executing file '{path}'");

            string[] lines = File.ReadAllLines(path);
            foreach (string line in lines) {
                WriteToOutput(path + " > " + line);
                Execute(line);
            }

            return true;
        }
    }
}