context: // Copyright (c) The University of Dundee 2018-2019
  // This file is part of the Research Data Management Platform (RDMP).
  // RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
  // RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  // You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
  
  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Linq;
  using System.Reflection;
  using FAnsi.Discovery;
  using MapsDirectlyToDatabaseTable;
  using MapsDirectlyToDatabaseTable.Attributes;
  using MapsDirectlyToDatabaseTable.Versioning;
  using Rdmp.Core.CommandExecution.AtomicCommands;
  using Rdmp.Core.CommandLine.Interactive.Picking;
  using Rdmp.Core.Curation;
  using Rdmp.Core.Curation.Data;
  using Rdmp.Core.Curation.Data.DataLoad;
  using Rdmp.Core.Curation.Data.Pipelines;
  using Rdmp.Core.Repositories;
  using Rdmp.Core.Repositories.Construction;
  using ReusableLibraryCode.Checks;
  
  namespace Rdmp.Core.CommandExecution
  {
      public class CommandInvoker
      {
          private readonly IBasicActivateItems _basicActivator;
          private readonly IRDMPPlatformRepositoryServiceLocator _repositoryLocator;
          
          /// <summary>
          /// Delegates provided by <see cref="_basicActivator"/> for fulfilling constructor arguments of the key Type
          /// </summary>
          private List<CommandInvokerDelegate> _argumentDelegates;
  
          /// <summary>
          /// Called when the user attempts to run a command marked <see cref="ICommandExecution.IsImpossible"/>
          /// </summary>
          public event EventHandler<CommandEventArgs> CommandImpossible;
          
          /// <summary>
          /// Called when a command completes successfully
          /// </summary>
          public event EventHandler<CommandEventArgs> CommandCompleted;
  
          public CommandInvoker(IBasicActivateItems basicActivator)
          {
              _basicActivator = basicActivator;
              _repositoryLocator = basicActivator.RepositoryLocator;
  
              _argumentDelegates = _basicActivator.GetDelegates();
  
              
              AddDelegate(typeof(ICatalogueRepository),true,(p)=>_repositoryLocator.CatalogueRepository);
              AddDelegate(typeof(IDataExportRepository),true,(p)=>_repositoryLocator.DataExportRepository);
              AddDelegate(typeof(IBasicActivateItems),true,(p)=>_basicActivator);
              AddDelegate(typeof(IRDMPPlatformRepositoryServiceLocator),true,(p)=>_repositoryLocator);
              AddDelegate(typeof(DirectoryInfo), false,(p) => _basicActivator.SelectDirectory($"Enter Directory for '{p.Name}'"));
              AddDelegate(typeof(FileInfo), false,(p) => _basicActivator.SelectFile($"Enter File for '{p.Name}'"));
              
              // Is the Global Check Notifier the best here? 
              AddDelegate(typeof(ICheckNotifier),true,(p) => _basicActivator.GlobalErrorCheckNotifier);
              
              AddDelegate(typeof(string), false,(p) =>
              
                  _basicActivator.TypeText("Value needed for parameter", GetPromptFor(p), 1000, null, out string result, false)
                  ? result
                  : throw new OperationCanceledException());
  
              AddDelegate(typeof(Type), false,(p) => 
                  _basicActivator.SelectType($"Type needed for {p.Name} ", p.DemandIfAny?.TypeOf, out Type chosen)
                      ? chosen 
                      : throw new OperationCanceledException());
  
              AddDelegate(typeof(DiscoveredDatabase),false,(p)=>_basicActivator.SelectDatabase(true,GetPromptFor(p)));
              AddDelegate(typeof(DiscoveredTable),false,(p)=>_basicActivator.SelectTable(true,GetPromptFor(p)));
  
              AddDelegate(typeof(DatabaseEntity),false, (p) =>_basicActivator.SelectOne(GetPromptFor(p), GetAllObjectsOfType(p.Type)));
              AddDelegate(typeof(IPipeline), false, SelectPipeline);
              AddDelegate(typeof(IMightBeDeprecated),false, SelectOne<IMightBeDeprecated>);
              AddDelegate(typeof(IDisableable),false, SelectOne<IDisableable>);
              AddDelegate(typeof(INamed),false, SelectOne<INamed>);
              AddDelegate(typeof(IDeleteable[]), false, SelectMany<IDeleteable>);
              AddDelegate(typeof(IDeleteable),false, SelectOne<IDeleteable>);
              AddDelegate(typeof(ILoggedActivityRootObject),false, SelectOne<ILoggedActivityRootObject>);
              AddDelegate(typeof(ICollectSqlParameters), false, SelectOne<ICollectSqlParameters>);
              AddDelegate(typeof(IRootFilterContainerHost),false, SelectOne<IRootFilterContainerHost>);            
  
              AddDelegate(typeof(Enum),false,(p)=>_basicActivator.SelectEnum(GetPromptFor(p) , p.Type, out Enum chosen)?chosen:null);
  
  
              _argumentDelegates.Add(new CommandInvokerArrayDelegate(typeof(IMapsDirectlyToDatabaseTable),false,(p)=>
              {
                  IMapsDirectlyToDatabaseTable[] available = GetAllObjectsOfType(p.Type.GetElementType());                
                  var result = _basicActivator.SelectMany(GetPromptFor(p),p.Type.GetElementType(), available);
                  
                  if(result == null)
                      return null;
                  
                  var typedArray = Array.CreateInstance(p.Type.GetElementType(),result.Length);
                  for(int i=0;i<typedArray.Length;i++)
                      typedArray.SetValue(result[i],i);
                       
                  return typedArray;
              }));
                     
  
              AddDelegate(typeof(ICheckable), false,
                  (p)=>_basicActivator.SelectOne(GetPromptFor(p), 
                      _basicActivator.GetAll<ICheckable>()
                          .Where(p.Type.IsInstanceOfType)
                          .Cast<IMapsDirectlyToDatabaseTable>()
                          .ToArray())
                  );
  
              AddDelegate(typeof(IPatcher),false, (p) =>
                  {
                      if(!_basicActivator.SelectType("Select Patcher (if any)", typeof(IPatcher), out Type patcherType))
                          throw new OperationCanceledException();
  
                      if (patcherType == null)
                          return null;
                      
                      try
                      {
                          return Activator.CreateInstance(patcherType);
                      }
                      catch (Exception e)
                      {
                          throw new Exception($"Failed to call/find blank constructor of IPatcher Type '{patcherType}'",e);
                      }
                  }
              );
  
              _argumentDelegates.Add(new CommandInvokerValueTypeDelegate((p)=>
                  _basicActivator.SelectValueType(GetPromptFor(p), p.Type, null, out object chosen) 
                      ? chosen 
                      : throw new OperationCanceledException()));
  
          }
  
          private IPipeline SelectPipeline(RequiredArgument arg)
          {
              return (IPipeline)_basicActivator.SelectOne(GetPromptFor(arg), _basicActivator.RepositoryLocator.CatalogueRepository.GetAllObjects<Pipeline>().ToArray());
          }
  
          private string GetPromptFor(RequiredArgument p)
          {
              return $"Value needed for {p.Name} ({p.Type.Name})";
          }
  
          private void AddDelegate(Type type,bool isAuto, Func<RequiredArgument, object> func)
          {
              _argumentDelegates.Add(new CommandInvokerDelegate(type,isAuto,func));
          }
  
          
          public IEnumerable<Type> GetSupportedCommands()
          {
              return _basicActivator.RepositoryLocator.CatalogueRepository?.MEF?.GetAllTypes()?.Where(IsSupported) ?? throw new Exception("MEF property has not been initialized on the activator");
          }
  
          /// <summary>
          /// Constructs an instance of the <see cref="IAtomicCommand"/> and executes it.  Constructor parameters
          /// are populated from the (optional) <paramref name="picker"/> or the <see cref="IBasicActivateItems"/>
          /// </summary>
          /// <param name="type"></param>
          /// <param name="picker"></param>
          public void ExecuteCommand(Type type, CommandLineObjectPicker picker)
          {
              ExecuteCommand(GetConstructor(type,picker),picker);
          }
          private void ExecuteCommand(ConstructorInfo constructorInfo, CommandLineObjectPicker picker)
          {
              List<object> parameterValues = new List<object>();
              bool complainAboutExtraParameters = true;
  
              int idx = 0;
  
              //for each parameter on the constructor we want to invoke
              foreach (var parameterInfo in constructorInfo.GetParameters())
              {
                  var required = new RequiredArgument(parameterInfo);
                  
                  var argDelegate = GetDelegate(required);
  
                  //if it is an easy one to automatically fill e.g. IBasicActivateItems
                  if (argDelegate != null && argDelegate.IsAuto)
                      parameterValues.Add(argDelegate.Run(required));
                  else
                  //if the constructor argument is a picker, use the one passed in
                  if (parameterInfo.ParameterType == typeof(CommandLineObjectPicker))
                  {
                      if(picker == null)
                          throw new ArgumentException($"Type {constructorInfo.DeclaringType} contained a constructor which took an {parameterInfo.ParameterType} but no picker was passed");
  
                      parameterValues.Add(picker);
                      
                      //the parameters are expected to be consumed by the target constructors so it's not really a problem if there are extra
                      complainAboutExtraParameters = false;
                      continue;
                  }
                  else
                  //if we have argument values specified
                  if (picker != null)
                  {
                      //and the specified value matches the expected parameter type
                      if (picker.HasArgumentOfType(idx, parameterInfo.ParameterType))
                      {
                          //consume a value
                          parameterValues.Add(picker[idx].GetValueForParameterOfType(parameterInfo.ParameterType));
                          idx++;
                          continue;
                      }
                      else
                      // if user has not typed anything in for this parameter and it has a default value
                      if(picker.Length <= idx && parameterInfo.HasDefaultValue)
                      {
                          // then we should use the default value
                          parameterValues.Add(parameterInfo.DefaultValue);
                          idx++;
                          continue;
                      }
                      
                      throw new Exception($"Expected parameter at index {idx} to be a {parameterInfo.ParameterType} (for parameter '{parameterInfo.Name}') but it was {(idx >= picker.Length ? "Missing":picker[idx].RawValue)}");
                  }
                  else
                  {
                      parameterValues.Add(GetValueForParameterOfType(parameterInfo));
                  }
              }
  
              if(picker != null && idx < picker.Length && complainAboutExtraParameters)
                  throw new Exception("Unrecognised extra parameter " + picker[idx].RawValue);
  
              var instance = (IAtomicCommand)constructorInfo.Invoke(parameterValues.ToArray());
          
              if (instance.IsImpossible)
              {
                  CommandImpossible?.Invoke(this,new CommandEventArgs(instance));
                  return;
              }
  
              instance.Execute();
              CommandCompleted?.Invoke(this,new CommandEventArgs(instance));
          }
  
          public object GetValueForParameterOfType(PropertyInfo propertyInfo)
          {
              return GetValueForParameterOfType(new RequiredArgument(propertyInfo));
          }
  
          public object GetValueForParameterOfType(ParameterInfo parameterInfo)
          {
              return GetValueForParameterOfType(new RequiredArgument(parameterInfo));
          }
  
          public object GetValueForParameterOfType(RequiredArgument a)
          {
              return GetDelegate(a)?.Run(a);
          }
  
          private T SelectOne<T>(RequiredArgument parameterInfo)
          {
              return (T)_basicActivator.SelectOne(parameterInfo.Name,_basicActivator.GetAll<T>().Cast<IMapsDirectlyToDatabaseTable>().ToArray());
          }
          private T[] SelectMany<T>(RequiredArgument parameterInfo) 
          {
              return
                  _basicActivator.SelectMany(parameterInfo.Name, typeof(T), _basicActivator.GetAll<T>().Cast<IMapsDirectlyToDatabaseTable>().ToArray())
                  .Cast<T>().ToArray();
          }
  
          public bool IsSupported(ConstructorInfo c)
          {
              return c.GetCustomAttribute<UseWithCommandLineAttribute>() != null || c.GetParameters().All(IsSupported);
          }
  
          public bool IsSupported(ParameterInfo p)
          {
              return GetDelegate(new RequiredArgument(p)) != null;
          }
  
          public CommandInvokerDelegate GetDelegate(RequiredArgument required)
          {
              var match =  _argumentDelegates.FirstOrDefault(k=>k.CanHandle(required.Type));
  
              if(match != null)
                  return match;
  
              if(required.HasDefaultValue && required.DefaultValue != null)
                  return new CommandInvokerFixedValueDelegate(required.DefaultValue);
  
              return null;
  
          }
          public bool IsSupported(Type t)
          {
              bool acceptableType = typeof(IAtomicCommand).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface;
  
              if (!acceptableType)
                  return false;
  
              if (_basicActivator.GetIgnoredCommands().Contains(t))
                  return false;
  
              try
              {
                  var constructor = GetConstructor(t, new CommandLineObjectPicker(new string[0]{ },_basicActivator));
  
                  if (constructor == null)
                      return false;
  
                  return IsSupported(constructor);
  
              }
              catch (Exception)
              {
                  return false;
              }
          }
  
          /// <summary>
          /// Returns the first best constructor on the <paramref name="type"/> preferring those decorated with <see cref="UseWithObjectConstructorAttribute"/>
          /// </summary>
          /// <param name="type"></param>
          /// <returns></returns>
          public virtual ConstructorInfo GetConstructor(Type type)
          {
              return GetConstructor(type, null);
          }
  
          /// <summary>
          /// Returns the first best constructor on the <paramref name="type"/> preferring those decorated with <see cref="UseWithCommandLineAttribute"/>
          /// </summary>
          /// <param name="type">The type of command you want to fetch the constructor from</param>
          /// <param name="picker">The command line arguments that you want to use to hydrate the <paramref name="type"/> constructor</param>
          /// <returns></returns>
          public virtual ConstructorInfo GetConstructor(Type type, CommandLineObjectPicker picker)
          {
              var constructors = type.GetConstructors();
  
              if (constructors.Length == 0)
                  return null;
  
              ConstructorInfo[] importDecorated = null;
  
              //If we have a picker, look for a constructor that wants to run from the command line
              if(picker != null)
                  importDecorated = constructors.Where(c => Attribute.IsDefined(c, typeof(UseWithCommandLineAttribute))).ToArray();
  
              //otherwise look for a regular decorated constructor
  
gt:             if(importDecorated == null || !importDecorated.Any())