using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Quadarax.Foundation.Core.Console;
using Quadarax.Foundation.Core.QConsole.Attributes;
using Quadarax.Foundation.Core.QConsole.Command.Base;
using Quadarax.Foundation.Core.QConsole.Configuration;
using Quadarax.Foundation.Core.QConsole.Context;

namespace Quadarax.Foundation.Core.QConsole
{
    public class Engine : IDisposable
    {
        #region *** Private Fields ***
        private bool _isDispoising;
        private bool _isExitSignal;
        private IList<AbstractCommand> _commands = new List<AbstractCommand>();
        private StartupConfiguration _configuration;
        private IList<ISelectionEntry> _selections = new List<ISelectionEntry>();
        #endregion

        #region *** Properties ***
        public string[] RawArguments { get; }
        public StartupConfiguration Configuration => _configuration;
        public AbstractCommand[] Commands => _commands.ToArray();
        public ISelectionEntry[] Selections => _selections.ToArray();

        public IEngineContext Context { get; }
        #endregion

        #region *** Constructor ***
        public Engine(StartupConfiguration configuration, IEngineContext context, string[] args)
        {
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
            Context = context ?? throw new ArgumentNullException(nameof(context));

            if (args == null)
                args = new string[0];

            // correction of arguments (fix " values)
            for (var i = 0; i < args.Length; i++)
            {
                if (args[i].Contains(" ") && args[i].Contains(configuration.CharacterNamedArgumentValueSeparator) ||
                    args[i].Contains(configuration.CharacterNamedArgumentSeparator) && args[i].Contains(configuration.CharacterNamedArgumentValueSeparator))
                {
                    var parts = args[i].Split(new[] {configuration.CharacterNamedArgumentValueSeparator},
                        StringSplitOptions.RemoveEmptyEntries);
                    if (parts.Length == 2)
                        args[i] = parts[0] + configuration.CharacterNamedArgumentValueSeparator + configuration.CharacterTextValueBraceSeparator + parts[1] +
                                  configuration.CharacterTextValueBraceSeparator;
                    else
                        args[i] = parts[0] + configuration.CharacterNamedArgumentValueSeparator + configuration.CharacterTextValueBraceSeparator + string.Join(":",parts.Skip(1).Take(parts.Length - 1)) +
                                  configuration.CharacterTextValueBraceSeparator;
                }
            }

            // Ensure if debug mode -> Enable debug mode and remove explicit argument
            if (args.Any(x => x.Trim().ToLower() == Constants.Console.DebugModeExplicitArg))
            {
                configuration.EnableDebugMode();
                var newArgs = new List<string>(args);
                newArgs.Remove(Constants.Console.DebugModeExplicitArg);
                args = newArgs.ToArray();
            }


            WriteDebugInfo($"Console '{_configuration.ConsoleName}' initialization started.");
            WriteDebugInfo($"Engine assembly version '{Assembly.GetExecutingAssembly().GetName().Version}'.");
            WriteDebugInfo($"IsInitFromXml is set to '{_configuration.IsInitFromXml}'.");

            ShowHeader();

            RawArguments = args;
            WriteDebugInfo($"Input {args.Length} arguments appended.");
            WriteDebugInfo($"Context set: {Context}");

            var commandClasses = GetTypesWithHelpAttribute(new CommandDefinitionAttribute());
            WriteDebugInfo($"Console found {commandClasses.Count()} command definitions.");

            var paramConstrTypes = new Type[] { typeof(Engine) };
            var paramConstrValues = new object[] { this };

            foreach (var commandClass in commandClasses)
            {
                var command = Construct<AbstractCommand>(commandClass, paramConstrTypes, paramConstrValues);
                _commands.Add(command);
                WriteDebugInfo($"Command definition '{command}' added - class '{command.GetType().FullName}', assembly '{command.GetType().Assembly.GetName().Name}'.");
            }

            WriteDebugInfo("Console initialized.");
        }

        public void Start()
        {
            WriteDebugInfo("Console starting.");
            var commandLineArguments = string.Join(" ", RawArguments);

            using (var writer = new ConsoleWriter(ConsoleColor.White))
            {
                while (!_isExitSignal)
                {
                    var input = string.Empty;
                    if (commandLineArguments.Length == 0 && Configuration.AllowInteractive)
                    {
                        // interactive command entry
                        writer.Write(_configuration.CharacterLineIntroduce);
                        input = System.Console.ReadLine();
                    }
                    else
                    {
                        // inline command entry
                        input = commandLineArguments;
                        _isExitSignal = true;
                    }

                    var args = input.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
                    if (args.Length == 0)
                    {
                        if (_isExitSignal == true && !Configuration.AllowInteractive &&
                            Configuration.HandlerNothingToDo != null)
                        {
                            Configuration.HandlerNothingToDo.Invoke(this);
                        }
                        continue;
                    }

                    var command = GetCommand(args[0]);
                    if (command == null)
                    {
                        using (var writerError = new ConsoleWriter(ConsoleColor.Red))
                        {
                            writerError.WriteLine($"Unknown command '{args[0]}'.");
                        }
                        continue;
                    }

                    var result = command.Execute(input);
                    if (result.IsSuccess)
                    {
                        using (var writerSucc = new ConsoleWriter(ConsoleColor.Green))
                        {
                            writerSucc.WriteLine("Done.");
                        }
                    }
                    else
                    {
                        using (var writerError = new ConsoleWriter(ConsoleColor.Red))
                        {
                            writerError.WriteLine("Error:");
                            var e = result.ThrownException;

                            if (e is AggregateException aggr)
                            {
                                foreach (var ex in aggr.InnerExceptions)
                                    DumpException(writerError, ex);
                            }
                            else
                                while (e != null)
                                {                                         
                                    writerError.WriteLine(e.Message);
                                    e = e.InnerException;
                                }
                        }
                    }
                }

                if (!Configuration.AllowInteractive && Configuration.WaitOnKeyInNonInteractiveMode)
                {
                    WriteDebugInfo("Waiting for the key by configuration settings...");
                    using (var writerGreen = new ConsoleWriter(ConsoleColor.Magenta))
                    {
                        writerGreen.WriteLine("Press any key to close...");
                        System.Console.ReadKey();
                    }
                }
            }
        }

        public void Stop()
        {
            _isExitSignal = true;
            WriteDebugInfo("Console stopped.");
        }

        public AbstractCommand GetCommand(string commandName)
        {
            return _commands.FirstOrDefault(x => x.Name.ToUpper() == commandName.ToUpper());            
        }

        public void AppendSelection(params ISelectionEntry[] selectionEntries)
        {
            if (selectionEntries == null)
                throw new ArgumentNullException(nameof(selectionEntries));
            foreach (var entry in selectionEntries)
            {
                _selections.Add(entry);
            }
            WriteDebugInfo($"{selectionEntries.Length} was added to global selections. Total count {_selections.Count} entries.");
        }

        public void ClearSelection()
        {
            _selections.Clear();
            WriteDebugInfo("Global selections was cleared.");
        }

        public ISelectionEntry GetSelection(int ordinal)
        {
            if (ordinal==0)
                throw new ArgumentOutOfRangeException(nameof(ordinal), "Ordinal value must be greater than 0.");
            if (_selections.Count < ordinal)
                throw new ArgumentOutOfRangeException(nameof(ordinal), $"Ordinal value must be less than {_selections.Count}.");

            return _selections[ordinal - 1];
        }
         
        #endregion

        #region *** Public Operations ***
        public void Dispose()
        {
            if (_isDispoising)
                return;
            _isDispoising = true;
            WriteDebugInfo("Dispoising called.");
            OnDispoising();
            WriteDebugInfo("Dispoising done.");
        }
        #endregion

        #region *** Protected Operations ***
        protected virtual void OnDispoising()
        {
        }

        protected virtual void OnShowHeader()
        {
            var headerText = string.IsNullOrEmpty(_configuration.ConsoleCopyright) ?  @"{0} * {1}" : @"{0} * {1} * {2}";
            using (var writer = new ConsoleWriter(ConsoleColor.Green))
            {
                writer.WriteLine(!string.IsNullOrEmpty(_configuration.ConsoleCopyright)
                    ? string.Format(headerText, _configuration.ConsoleName, _configuration.ConsoleVersion,
                        _configuration.ConsoleCopyright)
                    : string.Format(headerText, _configuration.ConsoleName, _configuration.ConsoleVersion));
            }
        }
        #endregion

        #region *** Private Operations ***
        private static T Construct<T>(Type instance, Type[] paramTypes, object[] paramValues)
        {            
            var ci = instance.GetConstructor(
                BindingFlags.Instance | BindingFlags.Public,
                null, paramTypes, null);
            if (ci == null)
                throw new Exception($"Command class '{instance.FullName}' has not public constuctor defined or has bad constructor's format.");
            return (T)ci.Invoke(paramValues);
        }
        private IEnumerable<Type> GetTypesWithHelpAttribute(Attribute attribute)
        {
           
            WriteDebugInfo($"Loading command assemblies ...");

            var defaultTypes = Assembly.GetExecutingAssembly().GetTypes().Where(type => type.GetCustomAttributes(attribute.GetType(), true).Length > 0).Distinct().ToList();
            
            var customTypes = new List<Type>();
            if (_configuration.CommandDefinitionAssebmly != null)
            {
                foreach (var assembly in _configuration.CommandDefinitionAssebmly)
                {
                    customTypes.AddRange(assembly.GetTypes().Where(type => type.GetCustomAttributes(attribute.GetType(), true).Length > 0).Distinct().ToArray());
                    WriteDebugInfo(assembly.GetName().Name + " was scanned for commands.");
                }
            }
            else
            {
                WriteDebugInfo("Custom CommandDefinitionAssembly is not defined. Skip.");
            }
            WriteDebugInfo($"Command definition obtaining process found {defaultTypes.Count} default definitions and {customTypes.Count} custom definitions.");
            defaultTypes.AddRange(customTypes);
            defaultTypes = defaultTypes.Distinct().ToList();

            foreach (var disabledCommand in _configuration.DisabledCommands)
            {
                var disabled = defaultTypes.FirstOrDefault(x => x.Name == disabledCommand);
                if (disabled==null)
                    continue;
                defaultTypes.Remove(disabled);
                WriteDebugInfo($"Commnad class '{disabled.Name}', fullname '{disabled.FullName}', assembly '{disabled.Assembly.GetName().Name}' was skipped by configuration.");
            }

            return defaultTypes.ToArray();
        }

        private void WriteDebugInfo(string text)
        {
            using (var debug = new DebugConsoleWriter())
            {
                debug.WriteDebugLine(text);
            }
        }

        private void DumpException(ConsoleWriter writer, Exception e)
        {
            while (e != null)
            {                                         
                writer.WriteLine("- " + e.Message);
                e = e.InnerException;
            }
        }


        private void ShowHeader()
        {
            OnShowHeader();
        }

        #endregion
    }
}
