using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.ScriptHandling;

/// <summary>
/// Interface for custom trigger arguments.
/// </summary>
public interface ICustomTriggerArgs;

/// <summary>
/// Static helper methods for working with <see cref="ICustomTriggerArgs"/>.
///
/// This class provides methods to get and set properties on custom trigger arguments. This allows for plugins and scripts
/// to modify the behavior of custom triggers without needing to know the specific types of the arguments, which means
/// they do not need to reference each other directly.
/// </summary>
public static class CustomTriggerArgsHelpers
{
    private record TypeFuncs(
        Dictionary<StringKey, Func<ICustomTriggerArgs, object?>> Getters,
        Dictionary<StringKey, Action<ICustomTriggerArgs, object?>> Setters);

    private static readonly Dictionary<Type, TypeFuncs> TypeFuncInfo = new();

    private static void PopulateForType(Type type)
    {
        if (TypeFuncInfo.ContainsKey(type))
            return;

        var getters = type.GetProperties().Where(p => p.CanRead && p.GetIndexParameters().Length == 0).ToDictionary(
            p => new StringKey(p.Name), p =>
            {
                var parameter = Expression.Parameter(typeof(ICustomTriggerArgs), "args");
                var typeCast = Expression.Convert(parameter, type);
                var propertyAccess = Expression.Property(typeCast, p);
                var convert = Expression.Convert(propertyAccess, typeof(object));
                return Expression.Lambda<Func<ICustomTriggerArgs, object?>>(convert, parameter).Compile();
            });
        var setters = type.GetProperties().Where(p => p.CanWrite && p.GetIndexParameters().Length == 0).ToDictionary(
            p => new StringKey(p.Name), p =>
            {
                var parameter = Expression.Parameter(typeof(ICustomTriggerArgs), "args");
                var typeCast = Expression.Convert(parameter, type);
                var valueParameter = Expression.Parameter(typeof(object), "value");
                var propertyAccess = Expression.Property(typeCast, p);
                var convert = Expression.Convert(valueParameter, p.PropertyType);
                var assign = Expression.Assign(propertyAccess, convert);
                return Expression.Lambda<Action<ICustomTriggerArgs, object?>>(assign, parameter, valueParameter)
                    .Compile();
            });

        TypeFuncInfo[type] = new TypeFuncs(getters, setters);
    }

    private static bool TryGetGetter(Type type, StringKey key,
        [NotNullWhen(true)] out Func<ICustomTriggerArgs, object?>? getter)
    {
        if (!TypeFuncInfo.ContainsKey(type))
            PopulateForType(type);
        return TypeFuncInfo[type].Getters.TryGetValue(key, out getter);
    }

    private static bool TryGetSetter(Type type, StringKey key,
        [NotNullWhen(true)] out Action<ICustomTriggerArgs, object?>? setter)
    {
        if (!TypeFuncInfo.ContainsKey(type))
            PopulateForType(type);
        return TypeFuncInfo[type].Setters.TryGetValue(key, out setter);
    }

    public static T Get<T>(this ICustomTriggerArgs args, StringKey key)
    {
        if (args is null)
            throw new ArgumentNullException(nameof(args));
        var type = args.GetType();
        if (!TryGetGetter(type, key, out var getter))
            throw new KeyNotFoundException($"Key '{key}' not found in {type.Name}.");
        var value = getter(args);
        if (value is T typedValue)
            return typedValue;
        throw new InvalidCastException(
            $"Value for key '{key}' in {type.Name} is of type {value?.GetType().Name}, expected {typeof(T).Name}.");
    }

    public static object? Get(this ICustomTriggerArgs args, StringKey key)
    {
        if (args is null)
            throw new ArgumentNullException(nameof(args));
        var type = args.GetType();
        if (!TryGetGetter(type, key, out var getter))
            throw new KeyNotFoundException($"Key '{key}' not found in {type.Name}.");
        return getter(args);
    }

    public static bool TryGet<T>(this ICustomTriggerArgs args, StringKey key, [NotNullWhen(true)] out T? value)
    {
        if (args is null)
            throw new ArgumentNullException(nameof(args));
        var type = args.GetType();
        if (!TryGetGetter(type, key, out var getter))
        {
            value = default;
            return false;
        }
        var result = getter(args);
        if (result is T typedValue)
        {
            value = typedValue;
            return true;
        }
        value = default;
        return false;
    }

    public static void Set(this ICustomTriggerArgs args, StringKey key, object? value)
    {
        if (args is null)
            throw new ArgumentNullException(nameof(args));
        var type = args.GetType();
        if (!TryGetSetter(type, key, out var setter))
            throw new KeyNotFoundException($"Key '{key}' not found in {type.Name}.");
        setter(args, value);
    }
}