/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * 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 net.java.sipmack.sip;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

import net.java.sipmack.common.Log;

/**
 * Title: Spark Phone
 * Description:JAIN-SIP Audio/Video phone application
 * New features: NAT / STUN
 *
 * @author Thiago Rocha Camargo (thiago@jivesoftware.com)
 */

public class NetworkAddressManager {
    private static NetworkAddressManager manager = null;

    private static InetAddress cachedLocalhost = null;

    private static int index = 0;

    private static List<InetAddressWrapper> addresses = new ArrayList<InetAddressWrapper>();

    private NetworkAddressManager() {
    }

    private void init() {
        setProperties();
    }

    /**
     * Initializes the address manager.
     */
    public static void start() {
        cacheLocalhost(null);
        index = 0;
        if (manager != null)
            return;
        manager = new NetworkAddressManager();
        manager.init();
        // Detect and output network configuration (Firewall and NAT type)

        // only used for debugging currently.
    }

    /**
     * Shuts down the address manager and the underlying stun lib and deletes
     * the manager.
     */
    public static void shutDown() {
        if (manager == null)
            return;
        manager = null;
    }

    /**
     * Returns an InetAddress instance representing the local host or null if no
     * IP address for the host could be found
     *
     * @return an InetAddress instance representing the local host or null if no
     *         IP address for the host could be found
     */
    public static InetAddress getLocalHost() {
        return getLocalHost(false);
    }

    /**
     * Returns a localhostAddress.
     *
     * @param anyAddressIsAccepted is 0.0.0.0 accepted as a return value.
     * @return the address that was detected the address of the localhost.
     */
    public static InetAddress getLocalHost(boolean anyAddressIsAccepted) {

        Log.debug("NETWORK DETECTION");

        if (cachedLocalhost != null) {
            Log.debug("SELECTED IP: " + cachedLocalhost);
            return cachedLocalhost;
        }

        Enumeration<NetworkInterface> ifaces = null;
        int i = 0;

        // Try to get the Preferred IP Address

        if (SIPConfig.getPreferredNetworkAddress() != null && !SIPConfig.getPreferredNetworkAddress().equals("")) {
            //Preferred is local host

            try {
                SIPConfig.setPreferredNetworkAddress(InetAddress.getLocalHost().getHostAddress());
            } catch (UnknownHostException e) {
                Log.error(e);
            }

        }

        if (SIPConfig.getPreferredNetworkAddress() != null && !SIPConfig.getPreferredNetworkAddress().equals("")) {

            try {
                ifaces = NetworkInterface.getNetworkInterfaces();
            } catch (SocketException e) {
                e.printStackTrace();
            }

            while (ifaces.hasMoreElements()) {

                NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
                Enumeration<InetAddress> iaddresses = iface.getInetAddresses();

                while (iaddresses.hasMoreElements()) {
                    InetAddress iaddress = iaddresses.nextElement();
                    if (iaddress.getHostAddress().equals(SIPConfig.getPreferredNetworkAddress())) {
                        addresses.add(new InetAddressWrapper(iaddress, i++));
                        break;
                    }
                }
            }

        }

        try {
            ifaces = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e) {
            e.printStackTrace();
        }

        // Try to get best IP ( not loopback, not linklocal and not sitelocal )

        while (ifaces.hasMoreElements()) {

            NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
            Enumeration<InetAddress> iaddresses = iface.getInetAddresses();

            while (iaddresses.hasMoreElements()) {
                InetAddress iaddress = (InetAddress) iaddresses.nextElement();
                if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress() && !iaddress.isSiteLocalAddress() && !(iaddress instanceof java.net.Inet6Address)) {
                    addresses.add(new InetAddressWrapper(iaddress, i++));
                }
            }
        }

        try {
            ifaces = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e) {
            e.printStackTrace();
        }

        // Try to get best IP ( not loopback and not linklocal )

        while (ifaces.hasMoreElements()) {

            NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
            Enumeration<InetAddress> iaddresses = iface.getInetAddresses();

            while (iaddresses.hasMoreElements()) {
                InetAddress iaddress = (InetAddress) iaddresses.nextElement();
                if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress() && !(iaddress instanceof java.net.Inet6Address)) {
                    addresses.add(new InetAddressWrapper(iaddress, i++));
                }
            }
        }

        // If Address list is empty, return localhost

        if (addresses.isEmpty())
            try {
                return InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }

        Collections.sort(addresses);

        cacheLocalhost(addresses.get(index).getInetAddress());
        setProperties();

        return addresses.get(addresses.size() - 1).getInetAddress();
    }

    /**
     * Caches localhost to make address discovery faster
     *
     * @param localhost localhost
     */
    public static boolean cacheLocalhost(InetAddress localhost) {
        cachedLocalhost = localhost;
        return true;
    }

    // EMIL: I don't see where this method is used. Should probbaly Take it away

    /**
     * Tries to obtain a mapped/public address for the specified address and
     * port. If the STUN lib fails, tries to retrieve localhost, if that fails
     * too, returns null.
     *
     * @param address the address to resolve
     * @param port    the port whose mapping we are interested in.
     * @return a public address corresponding to the specified port or null if
     *         all attempts to retrieve such an address have failed.
     */
    public static InetSocketAddress getPublicAddressFor(String address, int port) {
        return new InetSocketAddress(address, port);
    }

    /**
     * Tries to obtain a mapped/public address for the specified port. If the
     * STUN lib fails, tries to retrieve localhost, if that fails too, returns
     * null.
     *
     * @param port the port whose mapping we are interested in.
     * @return a public address corresponding to the specified port or null if
     *         all attempts to retrieve such an address have failed.
     */
    public static InetSocketAddress getPublicAddressFor(int port) {
        return new InetSocketAddress(getLocalHost(), port);
    }

    /**
     * Determines whether the address is the result of windows auto
     * configuration. (i.e. One that is in the 169.254.0.0 network)
     *
     * @param add the address to inspect
     * @return true if the address is autoconfigured by windows, false
     *         otherwise.
     */
    public static boolean isWindowsAutoConfiguredIPv4Address(InetAddress add) {
        return (add.getAddress()[0] & 0xFF) == 169
                && (add.getAddress()[1] & 0xFF) == 254;
    }

    /**
     * Determines whether the address is an IPv4 link local address. IPv4 link
     * local addresses are those in the following networks:
     * <p/>
     * 10.0.0.0 to 10.255.255.255 172.16.0.0 to 172.31.255.255 192.168.0.0 to
     * 192.168.255.255
     *
     * @param add the address to inspect
     * @return true if add is a link local ipv4 address and false if not.
     */
    public static boolean isLinkLocalIPv4Address(InetAddress add) {
        byte address[] = add.getAddress();
        if ((address[0] & 0xFF) == 10)
            return true;
        if ((address[0] & 0xFF) == 172 && (address[1] & 0xFF) >= 16
                && address[1] <= 31)
            return true;
        if ((address[0] & 0xFF) == 192 && (address[1] & 0xFF) == 168)
            return true;
        return false;
    }

    /**
     * Determines whether the address could be used in a VoIP session.
     * Attention, routable address as determined by this method are not only
     * globally routable addresses in the general sense of the term. Link local
     * addresses such as 192.168.x.x or fe80::xxxx are also considered usable.
     *
     * @param address the address to test.
     * @return true if the address could be used in a VoIP session.
     */
    public static boolean isRoutable(InetAddress address) {
        if (address instanceof Inet6Address) {
            return !address.isLoopbackAddress();
        } else {
            return (!address.isLoopbackAddress())
                    && (!isWindowsAutoConfiguredIPv4Address(address));
        }
    }

    private static void setProperties() {

        InetAddress selectedAddress = getLocalHost();

        NetworkAddressManager.cacheLocalhost(selectedAddress);

        SIPConfig.setPreferredNetworkAddress(selectedAddress.getHostAddress());
        SIPConfig.setIPAddress(selectedAddress.getHostAddress());
        SIPConfig.setPublicAddress(selectedAddress.getHostAddress());
        SIPConfig.setSystemProperties();

    }

    public static void resetIndex() {
        index = 0;
        cachedLocalhost = null;
    }

    public static boolean nextIndex() {
        if (index < addresses.size() - 1) {
            index++;
            cacheLocalhost(addresses.get(index).getInetAddress());
            setProperties();
            return true;
        }
        return false;
    }

    public static List<InetAddressWrapper> getInetAddresses() {
        return addresses;
    }

    public static class InetAddressWrapper implements Comparable<InetAddressWrapper> {

        private int value = 0;
        private InetAddress inetAddress = null;

        public InetAddressWrapper(InetAddress inetAddress, int value) {
            this.inetAddress = inetAddress;
            this.value = value;
        }

        public int getValue() {
            return value;
        }

        public InetAddress getInetAddress() {
            return inetAddress;
        }

        public int compareTo(InetAddressWrapper o) {

            InetAddressWrapper other = o;

            if (this.value < other.getValue()) return -1;
            else if (this.value == other.getValue()) return 0;
            else return 1;
        }
    }

    public static void main(String args[]) {

        NetworkAddressManager.start();

        boolean next = true;

        while (next) {
            System.out.println(NetworkAddressManager.getLocalHost());
            next = NetworkAddressManager.nextIndex();
        }

    }

}
