using System.Linq.Expressions;
using System.Reflection;
using PkmnLib.Dynamic.AI.Explicit;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.ScriptHandling.Registry;

/// <summary>
/// A helper data class that's passed through all plugins when initializing a library, so they can
/// register their scripts and other data.
/// </summary>
public class ScriptRegistry
{
    private readonly Dictionary<(ScriptCategory category, StringKey name), Func<Script>> _scriptTypes = new();
    private readonly Dictionary<StringKey, Func<IItem, ItemScript>> _itemScriptTypes = new();
    private IBattleStatCalculator? _battleStatCalculator;
    private IDamageCalculator? _damageCalculator;
    private IMiscLibrary? _miscLibrary;
    private ICaptureLibrary? _captureLibrary;
    private IExperienceGainCalculator? _experienceGainCalculator;

    /// <summary>
    /// Automatically register all scripts in the given assembly that have the <see cref="ScriptAttribute"/>, and
    /// inherit from <see cref="Script"/>.
    /// </summary>
    public void RegisterAssemblyScripts(Assembly assembly)
    {
        var baseType = typeof(Script);
        foreach (var type in assembly.GetTypes().Where(t => baseType.IsAssignableFrom(t)))
        {
            var attribute = type.GetCustomAttribute<ScriptAttribute>();
            if (attribute == null)
                continue;

            RegisterScriptType(attribute.Category, attribute.Name, type);
        }
        var itemBaseType = typeof(ItemScript);
        foreach (var type in assembly.GetTypes().Where(t => itemBaseType.IsAssignableFrom(t)))
        {
            var attribute = type.GetCustomAttribute<ItemScriptAttribute>();
            if (attribute == null)
                continue;

            RegisterItemScriptType(attribute.Name, type);
        }
    }

    /// <summary>
    /// Register a script type with the given category and name.
    /// </summary>
    [PublicAPI]
    public void RegisterScriptType(ScriptCategory category, StringKey name, Type type)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));

        var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
            null, Type.EmptyTypes, null);
        if (constructor == null)
            return;

        // We create a lambda that creates a new instance of the script type.
        // This is more performant than using Activator.CreateInstance.
        _scriptTypes[(category, name)] = Expression.Lambda<Func<Script>>(Expression.New(constructor)).Compile();
    }

    /// <summary>
    /// Register an item script type with the given name.
    /// </summary>
    [PublicAPI]
    public void RegisterItemScriptType(StringKey name, Type type)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));

        var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
            null, [typeof(IItem)], null);
        if (constructor == null)
            throw new ArgumentException($"Type {type} does not have a constructor that takes an IItem.");

        // We create a lambda that creates a new instance of the script type.
        // This is more performant than using Activator.CreateInstance.
        var parameterExpression = Expression.Parameter(typeof(IItem), "item");
        var newExpression = Expression.New(constructor, parameterExpression);
        _itemScriptTypes[name] =
            Expression.Lambda<Func<IItem, ItemScript>>(newExpression, parameterExpression).Compile();
    }

    /// <summary>
    /// Register a battle stat calculator.
    /// </summary>
    public void RegisterBattleStatCalculator<T>(T battleStatCalculator) where T : IBattleStatCalculator =>
        _battleStatCalculator = battleStatCalculator;

    /// <summary>
    /// Register a damage calculator.
    /// </summary>
    public void RegisterDamageCalculator<T>(T damageCalculator) where T : IDamageCalculator =>
        _damageCalculator = damageCalculator;

    /// <summary>
    /// Register a misc library.
    /// </summary>
    public void RegisterMiscLibrary<T>(T miscLibrary) where T : IMiscLibrary => _miscLibrary = miscLibrary;

    /// <summary>
    /// Register a capture library.
    /// </summary>
    public void RegisterCaptureLibrary<T>(T captureLibrary) where T : ICaptureLibrary =>
        _captureLibrary = captureLibrary;

    /// <summary>
    /// Register an experience gain calculator.
    /// </summary>
    public void RegisterExperienceGainCalculator<T>(T experienceGainCalculator) where T : IExperienceGainCalculator =>
        _experienceGainCalculator = experienceGainCalculator;

    internal IReadOnlyDictionary<(ScriptCategory category, StringKey name), Func<Script>> ScriptTypes => _scriptTypes;
    internal IReadOnlyDictionary<StringKey, Func<IItem, ItemScript>> ItemScriptTypes => _itemScriptTypes;
    internal IBattleStatCalculator? BattleStatCalculator => _battleStatCalculator;
    internal IDamageCalculator? DamageCalculator => _damageCalculator;
    internal IMiscLibrary? MiscLibrary => _miscLibrary;
    internal ICaptureLibrary? CaptureLibrary => _captureLibrary;
    internal IExperienceGainCalculator? ExperienceGainCalculator => _experienceGainCalculator;
    public ExplicitAIHandlers ExplicitAIHandlers { get; } = new();
}