/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2024 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.binding.rest;

import static org.ow2.petals.binding.rest.RESTConstants.ComponentParameter.HTTP_HOST;
import static org.ow2.petals.binding.rest.RESTConstants.ComponentParameter.HTTP_PORT;
import static org.ow2.petals.binding.rest.RESTConstants.HTTPServer.DEFAULT_HTTP_SERVER_HOSTNAME;
import static org.ow2.petals.binding.rest.RESTConstants.HTTPServer.DEFAULT_HTTP_SERVER_PORT;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.jbi.JBIException;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.ow2.petals.binding.rest.config.RESTConsumesConfiguration;
import org.ow2.petals.binding.rest.config.RESTProvidesConfiguration;
import org.ow2.petals.binding.rest.exchange.incoming.RESTExternalListener;
import org.ow2.petals.binding.rest.exchange.incoming.RESTServer;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.component.framework.api.exception.PEtALSCDKException;
import org.ow2.petals.component.framework.bc.AbstractBindingComponent;
import org.ow2.petals.component.framework.bc.BindingComponentServiceUnitManager;
import org.ow2.petals.component.framework.jbidescriptor.generated.Consumes;
import org.ow2.petals.component.framework.jbidescriptor.generated.Provides;

/**
 * The petals-bc-rest Binding Component.
 * 
 * <br>
 * <b>NOTE : </b>This class has to be used only if the component developer
 * wants to customize the main component class. In general, using the
 * org.objectweb.petals.component.framework.bc.DefaultBindingComponent class is
 * enough. If so, change the value in the JBI descriptor file.
 * 
 * @author Nicolas Oddoux
 * 
 */
public class RESTComponent extends AbstractBindingComponent {

    private RESTServer server;
    
    /**
     * The asynchronous HTTP client used to send REST requests
     */
    private CloseableHttpAsyncClient httpClient;

    /**
     * Default REST request configuration of the HTTP Client ({@link #httpClient}) set at component level. Can be
     * overridden at request level through parameters defined at service operation level.
     */
    private RequestConfig defaultRequestConfig;

    private Map<Consumes, RESTConsumesConfiguration> consumesConfigs = new HashMap<>();

    private Map<Provides, RESTProvidesConfiguration> providesConfigs = new HashMap<>();

    public RESTConsumesConfiguration getConsumesConfig(final Consumes consumes) {
        return this.consumesConfigs.get(consumes);
    }

    public Map<Consumes, RESTConsumesConfiguration> getConsumesConfigs() {
        return this.consumesConfigs;
    }

    public void addConsumesConfig(final Consumes consumes, final RESTConsumesConfiguration consumesConfig) {
        this.consumesConfigs.put(consumes, consumesConfig);
    }

    public void removeConsumesConfig(final Consumes consumes) {
        this.consumesConfigs.remove(consumes);
    }

    public RESTProvidesConfiguration getProvidesConfig(final Provides provides) {
        return this.providesConfigs.get(provides);
    }

    public Map<Provides, RESTProvidesConfiguration> getProvidesConfigs() {
        return this.providesConfigs;
    }

    public void addProvidesConfig(final Provides provides, final RESTProvidesConfiguration providesConfig) {
        this.providesConfigs.put(provides, providesConfig);
    }

    public void removeProvidesConfig(final Provides provides) {
        this.providesConfigs.remove(provides);
    }

    public void addRESTService(final Consumes consumes) throws PEtALSCDKException {
        final RESTConsumesConfiguration consumesConfig = this.getConsumesConfig(consumes);
        this.server.addRESTService(consumesConfig);
    }

    public void startRESTService(final RESTExternalListener restExternalListener, final Consumes consumes)
            throws PEtALSCDKException {
        final RESTConsumesConfiguration consumesConfig = this.getConsumesConfig(consumes);
        this.server.startRESTService(restExternalListener, consumesConfig);
    }

    public void stopRESTService(final Consumes consumes) throws PEtALSCDKException {
        final RESTConsumesConfiguration consumesConfig = this.getConsumesConfig(consumes);
        this.server.stopRESTService(consumesConfig);
    }

    public void removeRESTService(final Consumes consumes) throws PEtALSCDKException {
        final RESTConsumesConfiguration consumesConfig = this.getConsumesConfig(consumes);
        this.server.removeRESTService(consumesConfig);
    }

    public CloseableHttpAsyncClient getHttpClient() {
        assert this.httpClient != null;
        return this.httpClient;
    }

    public RequestConfig getDefaultRequestConfig() {
        assert this.defaultRequestConfig != null;
        return this.defaultRequestConfig;
    }

    @Override
    protected void doInit() throws JBIException {
        final String httpHost = this.getParameterAsNotEmptyTrimmedString(HTTP_HOST, DEFAULT_HTTP_SERVER_HOSTNAME);
        final int httpPort = this.getParameterAsStrictPositiveInteger(HTTP_PORT, DEFAULT_HTTP_SERVER_PORT);
        this.getLogger().config("HTTP server configuration:");
        this.getLogger().config("\t- HTTP hostname: " + httpHost);
        this.getLogger().config("\t- HTTP port: " + httpPort);
        this.server = new RESTServer(this.getLogger(), httpHost, httpPort, this.getContext());

        // Build the HTTP Client with a default request configuration
        final int maxConnTotal = this.getJbiComponentDescriptor().getComponent().getProcessorMaxPoolSize().getValue();
        final int maxConnPerRoute = this.getMaxConnPerRoute();
        final int connectionTimeout = this.getConnectionTimeout();
        final int readTimeout = this.getReadTimeout();
        this.getLogger().config("Default HTTP client configuration:");
        this.getLogger().config("\t- Max connection total: " + maxConnTotal);
        this.getLogger().config("\t- Max connection per route: " + maxConnPerRoute);
        this.getLogger().config("\t- Connection timeout (ms): " + connectionTimeout);
        this.getLogger().config("\t- Read timeout (ms): " + readTimeout);
        this.defaultRequestConfig = RequestConfig.custom().setSocketTimeout(readTimeout)
                .setConnectTimeout(connectionTimeout).build();

        final HttpAsyncClientBuilder httpClientBuilder = this.httpClientBuilder();
        httpClientBuilder.setMaxConnTotal(maxConnTotal);
        httpClientBuilder.setMaxConnPerRoute(maxConnPerRoute);

        // The HTTP Client is thread safe!
        this.httpClient = httpClientBuilder.build();
    }

    public HttpAsyncClientBuilder httpClientBuilder() {
        assert this.defaultRequestConfig != null;
        return HttpAsyncClients.custom().setDefaultRequestConfig(this.defaultRequestConfig);
    }

    /**
     * @return The max number of connections per route, used for outgoing REST request.
     */
    private int getMaxConnPerRoute() {
        return this.getParameterAsPositiveInteger(RESTConstants.ComponentParameter.MAX_CONN_PER_ROUTE,
                RESTConstants.ComponentParameter.DEFAULT_MAX_CONN_PER_ROUTE);
    }

    /**
     * @return The connection timeout in milliseconds used for outgoing REST request.
     */
    private int getConnectionTimeout() {
        long value = this.getParameterAsPositiveLong(RESTConstants.ComponentParameter.CONNECTION_TIMEOUT,
                RESTConstants.ComponentParameter.DEFAULT_CONNECTION_TIMEOUT);
        return this.longAsInt(value, RESTConstants.ComponentParameter.CONNECTION_TIMEOUT);
    }

    /**
     * @return The read timeout in milliseconds used for outgoing REST request.
     */
    private int getReadTimeout() {
        long value = this.getParameterAsPositiveLong(RESTConstants.ComponentParameter.READ_TIMEOUT,
                RESTConstants.ComponentParameter.DEFAULT_READ_TIMEOUT);
        return this.longAsInt(value, RESTConstants.ComponentParameter.READ_TIMEOUT);
    }

    private int longAsInt(long value, String parameter) {
        if (value > Integer.MAX_VALUE) {
            this.getLogger().log(Level.WARNING, parameter + " was greater than Integer.MAX_VALUE (" + Integer.MAX_VALUE
                    + "): using the later instead");
            return Integer.MAX_VALUE;
        } else {
            return (int) value;
        }
    }

    @Override
    protected void doStart() throws JBIException {
        this.server.start();
        this.httpClient.start();
    }

    @Override
    protected void doStop() throws JBIException {
        this.server.stop();
    }

    @Override
    protected void doShutdown() throws JBIException {
        try {
            this.httpClient.close();
        } catch (IOException e) {
            throw new JBIException("Can't close http client", e);
        }
    }

    @Override
    protected BindingComponentServiceUnitManager createServiceUnitManager() {
        return new RESTSUManager(this);
    }
}
