@Override
    public Object invokeReflectively(EvaluationContext ctx, Object[] params) {
        // use reflection to call the appropriate invoke method
        try {
            boolean isNamedParams = params.length > 0 && params[0] instanceof NamedParameter;
            if (!isCustomFunction()) {

                CandidateMethod cm = getCandidateMethod(ctx, params, isNamedParams);

                if (cm != null) {
                    Object result = cm.actualMethod.invoke(this, cm.actualParams);

                    if (result instanceof Either) {
                        @SuppressWarnings("unchecked")
                        Either<FEELEvent, Object> either = (Either<FEELEvent, Object>) result;
                        return getEitherResult(ctx,
                                               either,
                                               () -> Stream.of(cm.actualMethod.getParameters()).map(p -> p.getAnnotation(ParameterName.class).value()).collect(Collectors.toList()),
                                               () -> Arrays.asList(cm.actualParams));
                    }

                    return result;
                } else {
                    // CandidateMethod cm could be null also if reflection failed on Platforms not supporting
                    // getClass().getDeclaredMethods()
                    String ps = getClass().toString();
                    logger.error("Unable to find function '" + getName() + "( " + ps.substring(1, ps.length() - 1) +
                                         " )'");
                    ctx.notifyEvt(() -> new FEELEventBase(Severity.ERROR, "Unable to find function '" + getName() +
                            "( " + ps.substring(1, ps.length() - 1) + " )'", null));
                }
            } else {
                if (isNamedParams) {
                    // This is inherently frail because it expects that, if, the first parameter is NamedParameter
                    // and the function is a CustomFunction, then all parameters are NamedParameter
                    NamedParameter[] namedParams =
                            Arrays.stream(params).map(NamedParameter.class::cast).toArray(NamedParameter[]::new);
                    params = BaseFEELFunctionHelper.rearrangeParameters(namedParams,
                                                                        this.getParameters().get(0).stream().map(Param::getName).collect(Collectors.toList()));
                }
                Object result = invoke(ctx, params);
                if (result instanceof Either) {
                    @SuppressWarnings("unchecked")
                    Either<FEELEvent, Object> either = (Either<FEELEvent, Object>) result;

                    final Object[] usedParams = params;
                    Object eitherResult = getEitherResult(ctx,
                                                          either,
                                                          () -> IntStream.of(0, usedParams.length).mapToObj(i -> "arg"
                                                                  + i).collect(Collectors.toList()),
                                                          () -> Arrays.asList(usedParams));
                    return BaseFEELFunctionHelper.normalizeResult(eitherResult);
                }
                return BaseFEELFunctionHelper.normalizeResult(result);
            }
        } catch (Exception e) {
            logger.error("Error trying to call function " + getName() + ".", e);
            ctx.notifyEvt(() -> new FEELEventBase(Severity.ERROR, "Error trying to call function " + getName() + ".",
                                                  e));
        }
        return null;
    }
@Test
    void invokeReflectiveCustomFunction() {
        List<FEELFunction.Param> parameters = List.of(new FEELFunction.Param("foo", BuiltInType.UNKNOWN),
                                                  new FEELFunction.Param("person's age", BuiltInType.UNKNOWN));

        BaseNode left = new InfixOpNode(InfixOperator.EQ,
                                        new NameRefNode(BuiltInType.UNKNOWN, "foo"),
                                        new NullNode(""),
                                        "foo = null");
        BaseNode right = new InfixOpNode(InfixOperator.LT,
                                        new NameRefNode(BuiltInType.UNKNOWN, "person's age"),
                                        new NumberNode(BigDecimal.valueOf(18), "18"),
                                        "person's age < 18");
        BaseNode body = new InfixOpNode(InfixOperator.AND, left, right, "foo = null and person's age < 18");
        BaseFEELFunction toTest = new CustomFEELFunction("<anonymous>",
                                                         parameters,
                                                         body,
                                                         ctx);
       Object[] params = {new NamedParameter("foo", null),
       new NamedParameter("person's age", 16)};
        Object retrieved = toTest.invokeReflectively(ctx, params);
        assertNotNull(retrieved);
        assertInstanceOf(Boolean.class, retrieved);
        assertTrue((Boolean) retrieved);

        params = new Object[]{new NamedParameter("foo", null),
                new NamedParameter("person's age", 19)};
        retrieved = toTest.invokeReflectively(ctx, params);
        assertNotNull(retrieved);
        assertInstanceOf(Boolean.class, retrieved);
        assertFalse((Boolean) retrieved);
    }