using System.Reflection;

namespace Pilz.Plugins;

public class PluginManager<TPluginInterface, TPluginRuntimeInfo> where TPluginInterface : class where TPluginRuntimeInfo : PluginRuntimeInfo
{
    protected readonly List<TPluginRuntimeInfo> loadedPlugins = [];

    /// <summary>
    /// The default initialization parameters used when no parameters passed. Commonly used for dynamically loading assemblies.
    /// </summary>
    public virtual object?[] DefaultParameters { get; set; } = [];

    /// <summary>
    /// Returns a list of all currently loaded plugins.
    /// </summary>
    public virtual IEnumerable<TPluginRuntimeInfo> LoadedPlugins => loadedPlugins.AsReadOnly();

    protected void OnCurrentAppDomainAssemblyLoad(object? sender, AssemblyLoadEventArgs args)
    {
        LoadPlugins(args.LoadedAssembly);
    }

    /// <summary>
    /// Loads assembly from the given file paths and then loads the plugins from the assemblies.
    /// </summary>
    /// <param name="paths"></param>
    /// <param name="parameters"></param>
    /// <returns></returns>
    public virtual IEnumerable<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>> LoadPlugins(string[] paths, params object?[]? parameters)
    {
        var results = new List<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>>();

        foreach (var path in paths)
            results.Add(LoadPlugin(path, parameters));

        return results;
    }

    /// <summary>
    /// Loads plugins from the given assemblies.
    /// </summary>
    /// <param name="assemblies"></param>
    /// <param name="parameters"></param>
    /// <returns></returns>
    public virtual IEnumerable<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>> LoadPlugins(Assembly[] assemblies, params object?[]? parameters)
    {
        var results = new List<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>>();

        foreach (var assembly in assemblies)
            results.Add(LoadPlugins(assembly, parameters));

        return results;
    }

    /// <summary>
    /// Loads plugins from already loaded assemblies for the current <see cref="AppDomain.CurrentDomain"/>.
    /// </summary>
    /// <returns></returns>
    public virtual IEnumerable<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>> LoadOwnPlugins(params object?[]? parameters)
    {
        return LoadOwnPlugins(false, parameters);
    }

    /// <summary>
    /// Loads plugins from already loaded assemblies for the current <see cref="AppDomain.CurrentDomain"/>.
    /// </summary>
    /// <param name="listenAssemblyLoadContext">Do also load plugins from all yet not loaded assemblies by listening the event <see cref="AppDomain.AssemblyLoad"/></param>
    /// <param name="parameters"></param>>.
    /// <returns></returns>
    public virtual IEnumerable<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>> LoadOwnPlugins(bool listenAssemblyLoadContext, params object?[]? parameters)
    {
        var results = new List<PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>>();

        if (listenAssemblyLoadContext)
            AppDomain.CurrentDomain.AssemblyLoad += OnCurrentAppDomainAssemblyLoad;

        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            results.Add(LoadPlugins(assembly, parameters));

        return results;
    }

    /// <summary>
    /// Loads an assembly from the given file path and then loads plugins from the assembly.
    /// </summary>
    /// <param name="path"></param>
    /// <param name="parameters"></param>
    /// <returns></returns>
    public virtual PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo> LoadPlugin(string path, params object?[]? parameters)
    {
        var result = new PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>();
        var loadContext = new PluginLoadContext(path);

        if (File.Exists(path))
        {
            try
            {
                result.Assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(path)));
            }
            catch
            {
                result.Status = PluginLoadStatus.ErrorAtLoading;
            }
        }
        else
            result.Status = PluginLoadStatus.FileNotFound;

        if (result.Assembly != null)
            LoadPlugin(result, parameters);

        return result;
    }

    /// <summary>
    /// Load plugins from a given assembly.
    /// </summary>
    /// <param name="assembly"></param>
    /// <param name="parameters"></param>
    /// <returns></returns>
    public virtual PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo> LoadPlugins(Assembly assembly, params object?[]? parameters)
    {
        var result = new PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>
        {
            Assembly = assembly
        };

        LoadPlugin(result, parameters);

        return result;
    }

    /// <summary>
    /// Loads the plugin from a given type.
    /// <br/>Commonly used to ensure loading core plugins.
    /// </summary>
    /// <typeparam name="TPlugin"></typeparam>
    /// <param name="parameters"></param>
    /// <returns></returns>
    public virtual PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo> LoadPlugin<TPlugin>(params object?[]? parameters) where TPlugin : TPluginInterface
    {
        var result = new PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo>
        {
            Assembly = typeof(TPlugin).Assembly
        };

        LoadPlugin(result, typeof(TPlugin), parameters);

        return result;
    }

    protected virtual void LoadPlugin(PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo> result, params object?[]? parameters)
    {
        if (result.Assembly == null)
            result.Status = PluginLoadStatus.NoValidPlugin;
        else if (loadedPlugins.Any(n => n.Assembly == result.Assembly))
            result.Status = PluginLoadStatus.AlreadyLoaded;
        else
        {
            foreach (var type in result.Assembly.GetTypes())
                LoadPlugin(result, type, parameters);
        }

        if (result.PluginsInternal.Count == 0)
            result.Status = PluginLoadStatus.NoValidPlugin;
    }

    protected virtual void LoadPlugin(PluginLoadInfo<TPluginInterface, TPluginRuntimeInfo> result, Type type, params object?[]? parameters)
    {
        if (loadedPlugins.Any(n => n.Plugin != null && n.Plugin.GetType() == type))
            result.Status = PluginLoadStatus.AlreadyLoaded;
        else if (type.IsAssignableTo(typeof(TPluginInterface)))
        {
            var info = Activator.CreateInstance<TPluginRuntimeInfo>();
            info.Assembly = result.Assembly;
            result.PluginsInternal.Add(info);

            try
            {
                if (Activator.CreateInstance(type, GetConstructorFlags(), null, GetParameters(type, parameters), null) is TPluginInterface plugin)
                {
                    if (plugin is IPluginLateInitialization pluginLateInit)
                        pluginLateInit.LateInit();

                    info.Plugin = plugin;
                    info.Status = PluginStatus.Success;
                    loadedPlugins.Add(info);
                }
            }
            catch (Exception ex)
            {
                info.Exception = ex;
                info.Status = PluginStatus.ErrorAtInitializing;
            }
        }
        else
            result.Status = PluginLoadStatus.NoValidPlugin;
    }

    protected virtual object?[]? GetParameters(Type type, object?[]? parameters)
    {
        if (parameters == null || parameters.Length == 0)
            return DefaultParameters;
        return parameters;
    }

    protected virtual BindingFlags GetConstructorFlags()
    {
        return BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
    }
}
