/*
 * 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.netbeans.modules.payara.tooling.utils;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import org.netbeans.modules.payara.tooling.PayaraIdeException;
import org.netbeans.modules.payara.tooling.logging.Logger;
import java.io.PrintWriter;

/**
 * Networking utilities
 * <p/>
 * @author Tomas Kraus
 */
public class NetUtils {

    ////////////////////////////////////////////////////////////////////////////
    // Inner classes                                                          //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Comparator for {@link InetAddress} instances to be sorted.
     */
    public static class InetAddressComparator
            implements Comparator<InetAddress> {

        /**
         * Compares values of <code>InetAddr</code> instances.
         * <p/>
         * @param ip1 First <code>InetAddr</code> instance to be compared.
         * @param ip2 Second <code>InetAddr</code> instance to be compared.
         * @return A negative integer, zero, or a positive integer as the first
         *         argument is less than, equal to, or greater than the second.
         */
        @Override
        public int compare(final InetAddress ip1, final InetAddress ip2) {
            byte[] addr1 = ip1.getAddress();
            byte[] addr2 = ip2.getAddress();
            int result = addr2.length - addr1.length;
            if (result == 0) {
                for (int i = 0; result == 0 && i < addr1.length; i++) {
                    result = addr1[i] - addr2[i];
                }
            }
            return result;
        }
        
    }

    ////////////////////////////////////////////////////////////////////////////
    // Class attributes                                                       //
    ////////////////////////////////////////////////////////////////////////////

    /** Logger instance for this class. */
    private static final Logger LOGGER = new Logger(ServerUtils.class);

    /** Port check timeout [ms]. */
    public static final int PORT_CHECK_TIMEOUT = 2000;

    /** Comparator for {@link InetAddress} instances to be sorted. */
    private static final InetAddressComparator INET_ADDRESS_COMPARATOR
            = new InetAddressComparator();

    ////////////////////////////////////////////////////////////////////////////
    // Static methods                                                         //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Finds out if server is running on remote host by connecting to remote
     * host and port.
     * <p/>
     * @param host Server host.
     * @param port Server port.
     * @param timeout Network connection timeout [ms].
     * @return Returns <code>true</code> when server port is accepting
     *         connections or <code>false</code> otherwise.
     */
    public static boolean isPortListeningRemote(final String host,
            final int port, final int timeout) {
        final String METHOD = "isPortListeningRemote";
        if (null == host) {
            return false;
        }
        Socket socket = null;
        try {
            socket = new Socket();
            socket.connect(new InetSocketAddress(host, port), timeout);
            return true;
        } catch (IOException ex) {
            return false;
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ioe) {
                    LOGGER.log(Level.INFO, METHOD,
                            "closeError", ioe.getLocalizedMessage());
                }
            }
        }
    }

    /**
     * Finds out if server is running on remote host by connecting to remote
     * host and port.
     * <p/>
     * @param host Server host.
     * @param port Server port.
     * @return Returns <code>true</code> when server port is accepting
     *         connections or <code>false</code> otherwise.
     */
    public static boolean isPortListeningRemote(final String host,
            final int port) {
        return isPortListeningRemote(host, port, 0);
    }

    /**
     * Finds out if server is running on local host by binding to local port.
     * <p/>
     * @param host Server host or <code>null</code> value for address of the
     *             loopback interface. 
     * @param port Server port.
     * @return Returns <code>true</code> when server port is accepting
     *         connections or <code>false</code> otherwise.
     */
    public static boolean isPortListeningLocal(final String host,
            final int port) {
        final String METHOD = "isPortListeningLocal";
        ServerSocket socket = null;
        try {
            InetAddress ia = InetAddress.getByName(host);
            socket = new ServerSocket(port, 1, ia);
            return false;
        } catch (IOException ioe) {
            return true;
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ioe) {
                    LOGGER.log(Level.INFO, METHOD,
                            "closeError", ioe.getLocalizedMessage());
                }
            }
        }
    }

    /**
     * Determine whether an HTTP listener is secure or not..
     * <p/>
     * This method accepts a host name and port #.  It uses this information
     * to attempt to connect to the port, send a test query, analyze the
     * result to determine if the port is secure or is not secure (currently
     * only HTTP / HTTPS is supported).
     * it might emit a warning in the server log for Payara cases.
     * No Harm, just an annoying warning, so we need to use this call only
     * when really needed.
     * <p/>
     * @param hostname The host for the HTTP listener.
     * @param port     The port for the HTTP listener.
     * @throws IOException
     * @throws SocketTimeoutException
     * @throws ConnectException
     */
    public static boolean isSecurePort(String hostname, int port)
            throws IOException, ConnectException, SocketTimeoutException {
        return isSecurePort(hostname,port, 0);
    }

    /**
     * Determine whether an HTTP listener is secure or not..
     * <p/>
     * This method accepts a host name and port #.  It uses this information
     * to attempt to connect to the port, send a test query, analyze the
     * result to determine if the port is secure or is not secure (currently
     * only HTTP / HTTPS is supported).
     * it might emit a warning in the server log for Payara cases.
     * No Harm, just an annoying warning, so we need to use this call only
     * when really needed.
     * <p/>
     * @param hostname The host for the HTTP listener.
     * @param port     The port for the HTTP listener.
     * @param depth     Method calling depth.
     * @throws IOException
     * @throws SocketTimeoutException
     * @throws ConnectException
     */
    private static boolean isSecurePort(String hostname, int port, int depth) 
            throws IOException, ConnectException, SocketTimeoutException {
        final String METHOD = "isSecurePort";
        boolean isSecure = true;
        try (Socket socket = new Socket()) {
            try {
                LOGGER.log(Level.FINE, METHOD, "socket");
                socket.connect(new InetSocketAddress(hostname, port), PORT_CHECK_TIMEOUT);
                socket.setSoTimeout(PORT_CHECK_TIMEOUT);
            // This could be bug 70020 due to SOCKs proxy not having localhost
            } catch (SocketException ex) {
                String socksNonProxyHosts = System.getProperty("socksNonProxyHosts");
                if(socksNonProxyHosts != null && socksNonProxyHosts.indexOf("localhost") < 0) {
                    String localhost = socksNonProxyHosts.length() > 0 ? "|localhost" : "localhost";
                    System.setProperty("socksNonProxyHosts",  socksNonProxyHosts + localhost);
                    ConnectException ce = new ConnectException();
                    ce.initCause(ex);
                    throw ce; //status unknown at this point
                    //next call, we'll be ok and it will really detect if we are secure or not
                }
            }
            java.io.InputStream istream = socket.getInputStream();
            //This is the test query used to ping the server in an attempt to
            //determine if it is secure or not.
            String testQuery = "GET / HTTP/1.0";
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            pw.println(testQuery);
            pw.println();
            pw.flush();
            byte[] respArr = new byte[1024];
            while (istream.read(respArr) != -1) {
                String resp = new String(respArr);
                if (checkHelper(resp) == false) {
                    isSecure = false;
                    break;
                }
            }
        }
        return isSecure;
    }

    private static boolean checkHelper(String respText) {
        boolean isSecure = true;
        if (respText.startsWith("http/1.") || respText.startsWith("HTTP/1.")) {
            isSecure = false;
        } else if (respText.contains("<html")) {
            isSecure = false;
        } else if (respText.contains("</html")) {
            // New test added to resolve 106245
            // when the user has the IDE use a proxy (like webcache.foo.bar.com),
            // the response comes back as "d><title>....</html>".  It looks like
            // something eats the "<html><hea" off the front of the data that
            // gets returned.
            //
            // This test makes an allowance for that behavior. I figure testing
            // the likely "last bit" is better than testing a bit that is close
            // to the data that seems to get eaten.
            //
            isSecure = false;
        } else if (respText.contains("connection: ")) {
            isSecure = false;
        }
        return isSecure;
    }

    /**
     * Retrieve {@link Set} of IP addresses of this host.
     * <p/>
     * @return {@link Set} of IP addresses of this host.
     * @throws PayaraIdeException if addresses of this host could not
     *         be retrieved.
     */
    public static Set<InetAddress> getHostIPs() {
        final String METHOD = "getHostIPs";
        Set<InetAddress> addrs = new TreeSet<>(INET_ADDRESS_COMPARATOR);
        try {
            Enumeration<NetworkInterface> ifaces = NetworkInterface.
                    getNetworkInterfaces();
            while (ifaces.hasMoreElements()) {
                NetworkInterface iface = ifaces.nextElement();
                for (Enumeration<InetAddress> e = iface.getInetAddresses(); e.hasMoreElements(); ) {
                    InetAddress a = e.nextElement();
                    addrs.add(a);
                }
            }
        } catch (SocketException se) {
            addrs = null;
            throw new PayaraIdeException(LOGGER.excMsg(METHOD, "exception"));
        }
        return addrs;
    }

    /**
     * Retrieve {@link Set} of IPv4 addresses of this host.
     * <p/>
     * @return {@link Set} of IPv4 addresses of this host.
     */
    public static Set<Inet4Address> getHostIP4s() {
        Set<Inet4Address> addrs = new TreeSet<>(INET_ADDRESS_COMPARATOR);
        for (InetAddress a : getHostIPs()) {
            if (a instanceof Inet4Address) {
                addrs.add((Inet4Address) a);
            }
        }
        return addrs;
    }

    /**
     * Retrieve {@link Set} of IPv6 addresses of this host.
     * <p/>
     * @return {@link Set} of IPv6 addresses of this host.
     */
    public static Set<Inet6Address> getHostIP6s() {
        Set<Inet6Address> addrs = new TreeSet<>(INET_ADDRESS_COMPARATOR);
        for (InetAddress a : getHostIPs()) {
            if (a instanceof Inet6Address) {
                addrs.add((Inet6Address) a);
            }
        }
        return addrs;
    }

}
