using System.Reflection;
using static System.Reflection.BindingFlags;

namespace InnovEnergy.Lib.Utils;

public static class Operators
{
    public static Func<T, T, T> CreateBinaryOpForProps<T>(this String op) 
    {
        var methodName = GetMethodNameForBinaryOp(op);

        var props = typeof(T)
                   .GetProperties(Instance | Public)
                   .Where(p => p.CanWrite)
                   .Select(p => 
                       (
                           prop: p,
                           op: GetOpMethod(methodName, p.PropertyType) ??
                               throw new ArgumentException($"Type {p.PropertyType.Name} " +
                                                           $"of property {p.Name} " +
                                                           $"has no suitable {op} operator defined."))
                   )
                   .ToArray();

        var ctr = typeof(T).GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0);
        
        if (ctr is null)
            throw new ArgumentException($"Type {typeof(T).Name} has no suitable parameterless constructor.");


        T Op(T left, T right)
        {
            // TODO: make this faster/nicer using Expression API (low prio)
            
            var result = ctr.Invoke(null);
            
            foreach (var (p, m) in props)
            {
                var l = p.GetValue(left);
                var r = p.GetValue(right);

                var s = m.Invoke(null, new[] { l, r });
                
                p.SetValue(result, s);
            }

            return (T) result;
        }

        return Op;
    }

    private static String GetMethodNameForBinaryOp(String op)
    {
        // from https://stackoverflow.com/a/29495075
        
        return op switch
        {
            "&"  => "op_BitwiseAnd",
            "|"  => "op_BitwiseOr",
            "+"  => "op_Addition",
            "-"  => "op_Subtraction",
            "/"  => "op_Division",
            "%"  => "op_Modulus",
            "*"  => "op_Multiply",
            "<<" => "op_LeftShift",
            ">>" => "op_RightShift",
            "^"  => "op_ExclusiveOr",
            "==" => "op_Equality",
            "!=" => "op_Inequality",
            ">"  => "op_GreaterThan",
            "<"  => "op_LessThan",
            ">=" => "op_GreaterThanOrEqual",
            "<=" => "op_LessThanOrEqual",
            _    => throw new ArgumentException("unknown operator", nameof(op))
        };
    }

    private static MethodInfo? GetOpMethod(String? methodName, Type type)
    {
        return type
              .GetMethods(Static | Public)
              .FirstOrDefault(m => m.Name == methodName
                                && m.ReturnType == type
                                && m.IsMonoidOp());
    }

    private static Boolean IsMonoidOp(this MethodInfo m)
    {
        var ps = m.GetParameters();

        return ps.Length == 2 &&
               ps.All(p => p.ParameterType == m.ReturnType);
    }    
}