/*
 * 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.coap;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.net.ssl.SSLContext;

import org.apache.camel.CamelContext;
import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.Processor;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.spi.HeaderFilterStrategyAware;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestConsumerFactory;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.DefaultComponent;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.HostUtils;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.config.CertificateAuthenticationMode;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.TcpConfig;
import org.eclipse.californium.elements.tcp.netty.TcpServerConnector;
import org.eclipse.californium.elements.tcp.netty.TlsServerConnector;
import org.eclipse.californium.scandium.DTLSConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents the component that manages {@link CoAPEndpoint}.
 */
@Component("coap,coaps,coap+tcp,coaps+tcp")
public class CoAPComponent extends DefaultComponent implements RestConsumerFactory, HeaderFilterStrategyAware {
    static final int DEFAULT_PORT = 5684;
    private static final Logger LOG = LoggerFactory.getLogger(CoAPComponent.class);

    @Metadata
    private String configurationFile;
    @Metadata(label = "producer,advanced")
    private CoapClient client;
    @Metadata(label = "filter",
              description = "To use a custom org.apache.camel.spi.HeaderFilterStrategy to filter header to and from Camel message.")
    private HeaderFilterStrategy headerFilterStrategy;

    final Map<Integer, CoapServer> servers = new ConcurrentHashMap<>();

    public CoAPComponent() {
    }

    public CoapServer getServer(int port, CoAPEndpoint endpoint) throws IOException, GeneralSecurityException {
        lock.lock();
        try {
            CoapServer server = servers.get(port);
            if (server == null && port == -1) {
                server = getServer(DEFAULT_PORT, endpoint);
            }
            if (server == null) {
                CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
                Configuration config = loadConfiguration();
                builder.setConfiguration(config);

                // Configure TLS and / or TCP
                InetSocketAddress address = new InetSocketAddress(port);
                if (CoAPEndpoint.enableDTLS(endpoint.getUri())) {
                    doEnableDTLS(endpoint, config, address, builder);
                } else if (CoAPEndpoint.enableTCP(endpoint.getUri())) {
                    doEnableTCP(endpoint, config, address, builder);
                } else {
                    builder.setInetSocketAddress(address);
                }

                server = new CoapServer();
                server.addEndpoint(builder.build());

                servers.put(port, server);
                if (this.isStarted()) {
                    server.start();
                }
            }
            return server;
        } finally {
            lock.unlock();
        }
    }

    public Configuration loadConfiguration() throws IOException {
        Configuration config;
        if (configurationFile != null) {
            InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(getCamelContext(), configurationFile);
            config = Configuration.createStandardFromStream(is);
        } else {
            config = Configuration.createStandardWithoutFile();
        }
        return config;
    }

    private void doEnableTCP(
            CoAPEndpoint endpoint, Configuration config, InetSocketAddress address, CoapEndpoint.Builder coapBuilder)
            throws GeneralSecurityException, IOException {
        TcpServerConnector tcpConnector;
        // TLS + TCP
        if (endpoint.getUri().getScheme().startsWith("coaps")) {
            tcpConnector = doEnableTLSTCP(endpoint, config, address);
        } else {
            tcpConnector = new TcpServerConnector(address, config);
        }
        coapBuilder.setConnector(tcpConnector);
    }

    private TcpServerConnector doEnableTLSTCP(CoAPEndpoint endpoint, Configuration config, InetSocketAddress address)
            throws GeneralSecurityException, IOException {
        TcpServerConnector tcpConnector;
        SSLContext sslContext = endpoint.getSslContextParameters().createSSLContext(getCamelContext());
        if (endpoint.isClientAuthenticationRequired()) {
            config.set(TcpConfig.TLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.NEEDED);
        } else if (endpoint.isClientAuthenticationWanted()) {
            config.set(TcpConfig.TLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.WANTED);
        } else {
            config.set(TcpConfig.TLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.NONE);
        }
        tcpConnector = new TlsServerConnector(
                sslContext, address, config);
        return tcpConnector;
    }

    private static void doEnableDTLS(
            CoAPEndpoint endpoint, Configuration config, InetSocketAddress address, CoapEndpoint.Builder coapBuilder)
            throws IOException {
        DTLSConnector connector = endpoint.createDTLSConnector(address, false, config);
        coapBuilder.setConnector(connector);
    }

    @Override
    public HeaderFilterStrategy getHeaderFilterStrategy() {
        return headerFilterStrategy;
    }

    /**
     * To use a custom {@link org.apache.camel.spi.HeaderFilterStrategy} to filter header to and from Camel message.
     */
    @Override
    public void setHeaderFilterStrategy(HeaderFilterStrategy headerFilterStrategy) {
        this.headerFilterStrategy = headerFilterStrategy;
    }

    @Override
    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
        CoAPEndpoint endpoint = new CoAPEndpoint(uri, this);
        endpoint.setClient(client);
        if (headerFilterStrategy != null) {
            endpoint.setHeaderFilterStrategy(headerFilterStrategy);
        }
        setProperties(endpoint, parameters);
        return endpoint;
    }

    @Override
    public Consumer createConsumer(
            CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate, String consumes,
            String produces,
            RestConfiguration configuration, Map<String, Object> parameters)
            throws Exception {

        String path = basePath;
        if (uriTemplate != null) {
            // make sure to avoid double slashes
            if (uriTemplate.startsWith("/")) {
                path = path + uriTemplate;
            } else {
                path = path + "/" + uriTemplate;
            }
        }
        path = FileUtil.stripLeadingSeparator(path);

        RestConfiguration config = configuration;
        if (config == null) {
            config = CamelContextHelper.getRestConfiguration(getCamelContext(), "coap");
        }

        if (config.isEnableCORS()) {
            LOG.info("CORS configuration will be ignored as CORS is not supported by the CoAP component");
        }

        final String host = doGetHost(config);

        Map<String, Object> map = new HashMap<>();
        // setup endpoint options
        if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) {
            map.putAll(config.getEndpointProperties());
        }

        String scheme = config.getScheme() == null ? "coap" : config.getScheme();
        String query = URISupport.createQueryString(map);
        int port = 0;

        int num = config.getPort();
        if (num > 0) {
            port = num;
        }

        // prefix path with context-path if configured in rest-dsl configuration
        String contextPath = config.getContextPath();
        if (ObjectHelper.isNotEmpty(contextPath)) {
            contextPath = FileUtil.stripTrailingSeparator(contextPath);
            contextPath = FileUtil.stripLeadingSeparator(contextPath);
            if (ObjectHelper.isNotEmpty(contextPath)) {
                path = contextPath + "/" + path;
            }
        }

        String restrict = verb.toUpperCase(Locale.US);
        String url = String.format("%s://%s:%d/%s?coapMethodRestrict=%s", scheme, host, port, path, restrict);

        if (!query.isEmpty()) {
            url += "&" + query;
        }

        CoAPEndpoint endpoint = (CoAPEndpoint) camelContext.getEndpoint(url, parameters);

        // configure consumer properties
        Consumer consumer = endpoint.createConsumer(processor);
        if (config.getConsumerProperties() != null && !config.getConsumerProperties().isEmpty()) {
            setProperties(camelContext, consumer, config.getConsumerProperties());
        }
        return consumer;
    }

    private static String doGetHost(RestConfiguration config) throws UnknownHostException {
        String host = config.getHost();
        if (ObjectHelper.isEmpty(host)) {
            if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
                host = "0.0.0.0";
            } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
                host = HostUtils.getLocalHostName();
            } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
                host = HostUtils.getLocalIp();
            }
        }
        return host;
    }

    public String getConfigurationFile() {
        return configurationFile;
    }

    /**
     * Name of COAP configuration file to load and use. Will by default load from classpath, so use file: as prefix to
     * load from file system.
     */
    public void setConfigurationFile(String configurationFile) {
        this.configurationFile = configurationFile;
    }

    public CoapClient getClient() {
        return client;
    }

    /**
     * To use a shared client for the producers
     */
    public void setClient(CoapClient client) {
        this.client = client;
    }

    @Override
    protected void doStart() throws Exception {
        super.doStart();

        for (CoapServer s : servers.values()) {
            s.start();
        }
    }

    @Override
    protected void doStop() throws Exception {
        for (CoapServer s : servers.values()) {
            s.stop();
        }
        super.doStop();
    }
}
