/**
 * 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.camel.util.component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.impl.DefaultAsyncProducer;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base class for API based Producers
 */
public abstract class AbstractApiProducer<E extends Enum<E> & ApiName, T>
    extends DefaultAsyncProducer implements PropertiesInterceptor, ResultInterceptor {

    // API Endpoint
    protected final AbstractApiEndpoint<E, T> endpoint;

    // properties helper
    protected final ApiMethodPropertiesHelper<T> propertiesHelper;

    // method helper
    protected final ApiMethodHelper<?> methodHelper;

    // logger
    private final transient Logger log = LoggerFactory.getLogger(getClass());

    public AbstractApiProducer(AbstractApiEndpoint<E, T> endpoint, ApiMethodPropertiesHelper<T> propertiesHelper) {
        super(endpoint);
        this.propertiesHelper = propertiesHelper;
        this.endpoint = endpoint;
        this.methodHelper = endpoint.getMethodHelper();
    }

    @Override
    public boolean process(final Exchange exchange, final AsyncCallback callback) {
        // properties for method arguments
        final Map<String, Object> properties = new HashMap<>();
        properties.putAll(endpoint.getEndpointProperties());
        propertiesHelper.getExchangeProperties(exchange, properties);

        // let the endpoint and the Producer intercept properties
        endpoint.interceptProperties(properties);
        interceptProperties(properties);

        // decide which method to invoke
        final ApiMethod method = findMethod(exchange, properties);
        if (method == null) {
            // synchronous failure
            callback.done(true);
            return true;
        }

        // create a runnable invocation task to be submitted on a background thread pool
        // this way we avoid blocking the current thread for long running methods
        Runnable invocation = new Runnable() {
            @Override
            public void run() {
                try {
                    if (log.isDebugEnabled()) {
                        log.debug("Invoking operation {} with {}", method.getName(), properties.keySet());
                    }

                    Object result = doInvokeMethod(method, properties);

                    // producer returns a single response, even for methods with List return types
                    exchange.getOut().setBody(result);
                    // copy headers
                    exchange.getOut().setHeaders(exchange.getIn().getHeaders());

                    interceptResult(result, exchange);

                } catch (Throwable t) {
                    exchange.setException(ObjectHelper.wrapRuntimeCamelException(t));
                } finally {
                    callback.done(false);
                }
            }
        };

        endpoint.getExecutorService().submit(invocation);
        return false;
    }

    @Override
    public void interceptProperties(Map<String, Object> properties) {
        // do nothing by default
    }

    /**
     * Invoke the API method. Derived classes can override, but MUST call super.doInvokeMethod().
     * @param method API method to invoke.
     * @param properties method arguments from endpoint properties and exchange In headers.
     * @return API method invocation result.
     * @throws RuntimeCamelException on error. Exceptions thrown by API method are wrapped.
     */
    protected Object doInvokeMethod(ApiMethod method, Map<String, Object> properties) throws RuntimeCamelException {
        return ApiMethodHelper.invokeMethod(endpoint.getApiProxy(method, properties), method, properties);
    }

    @Override
    public final Object splitResult(Object result) {
        // producer never splits results
        return result;
    }

    @Override
    public void interceptResult(Object methodResult, Exchange resultExchange) {
        // do nothing by default
    }

    protected ApiMethod findMethod(Exchange exchange, Map<String, Object> properties) {

        ApiMethod method = null;
        final List<ApiMethod> candidates = endpoint.getCandidates();
        if (processInBody(exchange, properties)) {

            // filter candidates based on endpoint and exchange properties
            final Set<String> argNames = properties.keySet();
            final List<ApiMethod> filteredMethods = methodHelper.filterMethods(
                candidates,
                ApiMethodHelper.MatchType.SUPER_SET,
                argNames);

            // get the method to call
            if (filteredMethods.isEmpty()) {
                throw new RuntimeCamelException(String.format("Missing properties for %s, need one or more from %s",
                    endpoint.getMethodName(),
                    methodHelper.getMissingProperties(endpoint.getMethodName(), argNames))
                );
            } else if (filteredMethods.size() == 1) {
                // found an exact match
                method = filteredMethods.get(0);
            } else {
                method = ApiMethodHelper.getHighestPriorityMethod(filteredMethods);
                log.warn("Calling highest priority operation {} from operations {}", method, filteredMethods);
            }
        }

        return method;
    }

    // returns false on exception, which is set in exchange
    private boolean processInBody(Exchange exchange, Map<String, Object> properties) {
        final String inBodyProperty = endpoint.getInBody();
        if (inBodyProperty != null) {

            Object value = exchange.getIn().getBody();
            if (value != null) {
                try {
                    value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(
                        endpoint.getConfiguration().getClass().getDeclaredField(inBodyProperty).getType(),
                        exchange, value);
                } catch (Exception e) {
                    exchange.setException(new RuntimeCamelException(String.format(
                            "Error converting value %s to property %s: %s", value, inBodyProperty, e.getMessage()), e));

                    return false;
                }
            } else {
                // allow null values for inBody only if its a nullable option
                if (!methodHelper.getNullableArguments().contains(inBodyProperty)) {
                    exchange.setException(new NullPointerException(inBodyProperty));

                    return false;
                }
            }

            log.debug("Property [{}] has message body value {}", inBodyProperty, value);
            properties.put(inBodyProperty, value);
        }

        return true;
    }
}
