/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed 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.gradle.messaging.remote.internal;

import org.gradle.api.GradleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.*;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

public class TcpOutgoingConnector implements OutgoingConnector {
    private static final Logger LOGGER = LoggerFactory.getLogger(TcpOutgoingConnector.class);
    private ClassLoader classLoader;

    public TcpOutgoingConnector(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public <T> Connection<T> connect(URI destinationUri) {
        if (!destinationUri.getScheme().equals("tcp") || destinationUri.getHost() == null || !destinationUri.getHost().equals("localhost")) {
            throw new IllegalArgumentException(String.format("Cannot create connection to destination URI '%s'.",
                    destinationUri));
        }

        // Find all loop back addresses. Not all of them are necessarily reachable (eg when socket option IPV6_V6ONLY
        // is on - the default for debian and others), so we will try each of them until we can connect
        List<InetAddress> loopBackAddresses = findLocalAddresses();

        // Now try each address
        try {
            SocketException lastFailure = null;
            for (InetAddress address : loopBackAddresses) {
                LOGGER.debug("Trying to connect to address {}.", address);
                SocketChannel socketChannel;
                try {
                    socketChannel = SocketChannel.open(new InetSocketAddress(address, destinationUri.getPort()));
                } catch (SocketException e) {
                    LOGGER.debug("Cannot connect to address {}, skipping.", address);
                    lastFailure = e;
                    continue;
                }
                LOGGER.debug("Connected to address {}.", address);
                URI localAddress = new URI(String.format("tcp://localhost:%d", socketChannel.socket().getLocalPort()));
                return new SocketConnection<T>(socketChannel, localAddress, destinationUri, classLoader);
            }
            throw lastFailure;
        } catch (java.net.ConnectException e) {
            throw new ConnectException(String.format("Could not connect to server %s. Tried addresses: %s.",
                    destinationUri, loopBackAddresses), e);
        } catch (Exception e) {
            throw new GradleException(String.format("Could not connect to server %s. Tried addresses: %s.",
                    destinationUri, loopBackAddresses), e);
        }
    }

    /**
     * Never returns an empty list.
     */
    static List<InetAddress> findLocalAddresses() {
        try {
            List<InetAddress> addresses = new ArrayList<InetAddress>();
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface networkInterface = interfaces.nextElement();
                Enumeration<InetAddress> candidates = networkInterface.getInetAddresses();
                while (candidates.hasMoreElements()) {
                    InetAddress inetAddress = candidates.nextElement();
                    if (inetAddress.isLoopbackAddress()) {
                        addresses.add(inetAddress);
                    }
                }
            }
            if (addresses.isEmpty()) {
                addresses.add(InetAddress.getByName(null));
            }
            LOGGER.debug("Found loop-back addresses: {}.", addresses);
            return addresses;
        } catch (Exception e) {
            throw new GradleException("Could not determine the local loop-back addresses.", e);
        }
    }
}
