/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.groovy.runtime;

import groovy.lang.Closure;
import groovy.transform.Monadic;
import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.function.Function;

/**
 * Runtime bind/map dispatcher for monadic comprehensions &mdash; the emit target
 * of the {@code DO} macro.
 * <p>
 * The macro runs at {@code SEMANTIC_ANALYSIS}, <em>before</em> type checking, so it
 * cannot know the carrier's bind-method name and cannot emit it directly. Instead it
 * emits {@code Comprehensions.bind(carrier) { x -> ... }} calls; this class resolves
 * the carrier-specific method at runtime (dynamic Groovy), while the
 * {@code groovy.typecheckers.MonadicChecker} type-checking extension specialises this
 * one signature under {@code @CompileStatic}.
 * <p>
 * This is runtime support invoked from generated bytecode, hence its placement in
 * core alongside the rest of the Groovy runtime; the {@code DO} macro and the type
 * checker are compile-time only and remain in their optional modules.
 * <p>
 * Participation is resolved first-match-wins:
 * <ol>
 *   <li>standard allow-list ({@link MonadicCarrierRegistry});</li>
 *   <li>structural ({@code flatMap}/{@code map} present);</li>
 *   <li>{@code @Monadic} opt-in (honours {@code bind}/{@code map} overrides).</li>
 * </ol>
 * A configured marker interface is a further opt-in mechanism that is not yet
 * implemented.
 * <p>
 * Surface generosity: the carrier's bind method may accept a {@link Function} or a
 * {@code Closure}; the closure is adapted to whichever the target declares. Monad
 * laws are not enforced &mdash; structural participation, algebraic-law obligation
 * on the implementer (the {@code @Reducer}/{@code @Associative} treatment).
 *
 * @since 6.0.0
 */
@Incubating
public final class Comprehensions {

    private Comprehensions() {}

    /** Bind (flatMap-shaped): {@code carrier.<bind>(x -> fn(x))} where {@code fn} yields the same carrier. */
    public static Object bind(Object carrier, Closure<?> fn) {
        return dispatch(carrier, fn, true);
    }

    /** Map: {@code carrier.<map>(x -> fn(x))} where {@code fn} yields a plain value. */
    public static Object map(Object carrier, Closure<?> fn) {
        return dispatch(carrier, fn, false);
    }

    private static Object dispatch(Object carrier, Closure<?> fn, boolean bindRole) {
        if (carrier == null) {
            throw new IllegalArgumentException(
                "Monadic comprehension carrier is null; null cannot participate as a carrier");
        }
        Class<?> type = carrier.getClass();
        String method = resolveMethodName(carrier, type, bindRole);
        if (method == null) {
            String role = bindRole ? "bind (flatMap-shaped)" : "map";
            String structural = bindRole ? "flatMap" : "map";
            throw new IllegalArgumentException(
                "Type " + type.getName() + " does not participate in monadic comprehensions: "
              + "no " + role + " method found (not in the standard carrier allow-list, "
              + "has no structural '" + structural + "' method, and is not annotated @Monadic)");
        }
        Object arg = adaptClosure(type, method, fn);
        return InvokerHelper.invokeMethod(carrier, method, arg);
    }

    private static String resolveMethodName(Object carrier, Class<?> type, boolean bindRole) {
        // 1. standard allow-list (Class- or name-keyed)
        String[] bindMap = MonadicCarrierRegistry.lookupBindMap(carrier);
        if (bindMap != null) {
            return bindRole ? bindMap[0] : bindMap[1];
        }
        // 2. structural
        String structural = bindRole ? "flatMap" : "map";
        if (findSingleArgMethod(type, structural) != null) {
            return structural;
        }
        // 3. @Monadic opt-in (matched by simple name, like @Reducer/@Associative)
        String monadic = monadicMethodName(type, bindRole);
        if (monadic != null && findSingleArgMethod(type, monadic) != null) {
            return monadic;
        }
        return null;
    }

    private static String monadicMethodName(Class<?> type, boolean bindRole) {
        for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) {
            String n = readMonadicMember(c.getDeclaredAnnotations(), bindRole);
            if (n != null) return n;
            for (Class<?> i : c.getInterfaces()) {
                String ni = readMonadicMember(i.getDeclaredAnnotations(), bindRole);
                if (ni != null) return ni;
            }
        }
        return null;
    }

    private static String readMonadicMember(Annotation[] annotations, boolean bindRole) {
        for (Annotation a : annotations) {
            if (!(a instanceof Monadic monadic)) continue;
            String configured = bindRole ? monadic.bind() : monadic.map();
            if (configured == null || configured.isEmpty()) {
                return bindRole ? "flatMap" : "map"; // opted in, structural defaults
            }
            return configured;
        }
        return null;
    }

    private static Method findSingleArgMethod(Class<?> type, String name) {
        for (Method m : type.getMethods()) {
            if (m.getParameterCount() == 1 && m.getName().equals(name)
                    && !m.isBridge() && !m.isSynthetic()) {
                return m;
            }
        }
        return null;
    }

    /**
     * Adapt the generator closure to whatever single-arg type the carrier's bind
     * method declares:
     * <ul>
     *   <li>a {@code Closure}-typed parameter receives the closure directly;</li>
     *   <li>a {@link Function}-typed parameter receives a thin wrapper;</li>
     *   <li>any other functional interface (for example Functional Java's
     *       {@code fj.F}) receives the closure coerced to that interface.</li>
     * </ul>
     */
    private static Object adaptClosure(Class<?> type, String method, final Closure<?> fn) {
        Method m = findSingleArgMethod(type, method);
        Class<?> pt = (m != null) ? m.getParameterTypes()[0] : null;
        if (pt == null || Closure.class.isAssignableFrom(pt)) {
            return fn;
        }
        if (pt == Function.class) {
            // Exact Function (not a supertype): Object is a supertype too, and a
            // structural Groovy carrier declaring 'flatMap(c)' lands with pt=Object;
            // the user's body typically calls c.call(x), which would fail against
            // a Function wrapper. Untyped Object falls through to the asType
            // branch below, which returns the Closure unchanged.
            return (Function<Object, Object>) fn::call;
        }
        // General SAM coercion: proxy the closure as the declared interface.
        // For pt == Object this returns the Closure unchanged.
        return DefaultGroovyMethods.asType(fn, pt);
    }
}
