/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.transport.impl;

import java.util.logging.Logger;
import org.teleal.cling.model.Constants;
import org.teleal.cling.transport.spi.InitializationException;
import org.teleal.cling.transport.spi.NetworkAddressFactory;
import org.teleal.common.util.OS;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Iterator;

/**
 * Supports only IPv4! Ignores the "vmnet*" interfaces!
 */
public class NetworkAddressFactoryImpl implements NetworkAddressFactory {

    public static final int DEFAULT_TCP_HTTP_LISTEN_PORT = 8081;

    private static Logger log = Logger.getLogger(NetworkAddressFactoryImpl.class.getName());

    protected Set<String> useInterfaces = new HashSet();
    protected Set<String> useAddresses = new HashSet();

    protected List<NetworkInterface> networkInterfaces = new ArrayList();
    protected List<InetAddress> bindAddresses = new ArrayList();

    public NetworkAddressFactoryImpl() throws InitializationException {

        String useInterfacesString = System.getProperty(SYSTEM_PROPERTY_NET_IFACES);
        if (useInterfacesString != null) {
            String[] userInterfacesStrings = useInterfacesString.split(",");
            useInterfaces.addAll(Arrays.asList(userInterfacesStrings));
        }

        String useAddressesString = System.getProperty(SYSTEM_PROPERTY_NET_ADDRESSES);
        if (useAddressesString != null) {
            String[] useAddressesStrings = useAddressesString.split(",");
            useAddresses.addAll(Arrays.asList(useAddressesStrings));
        }

        // TODO: Linux issue: http://mail.openjdk.java.net/pipermail/net-dev/2008-December/000497.html
        if (OS.checkForLinux()) {
            Properties props = System.getProperties();
            props.setProperty("java.net.preferIPv6Stack", "true");
            System.setProperties(props);
        }

        discoverNetworkInterfaces();
        discoverBindAddresses();

        if (networkInterfaces.size() == 0 || bindAddresses.size() == 0) {
            throw new InitializationException("Could not discover any bindable network interfaces and/or addresses");
        }
    }

    public InetAddress getMulticastGroup() {
        try {
            return InetAddress.getByName(Constants.IPV4_UPNP_MULTICAST_GROUP);
        } catch (UnknownHostException ex) {
            throw new RuntimeException(ex);
        }
    }

    public int getMulticastPort() {
        return Constants.UPNP_MULTICAST_PORT;
    }

    public int getStreamListenPort() {
        return DEFAULT_TCP_HTTP_LISTEN_PORT;
    }

    public NetworkInterface[] getNetworkInterfaces() {
        return networkInterfaces.toArray(new NetworkInterface[networkInterfaces.size()]);
    }

    public InetAddress[] getBindAddresses() {
        return bindAddresses.toArray(new InetAddress[bindAddresses.size()]);
    }

    public byte[] getHardwareAddress(InetAddress inetAddress) {
        try {
            return NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress();
        } catch (SocketException ex) {
            throw new RuntimeException(ex);
        }
    }

    public InterfaceAddress getInterfaceAddress(InetAddress inetAddress) {
        for (NetworkInterface networkInterface : networkInterfaces) {
            for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
                if (interfaceAddress != null && interfaceAddress.getAddress().equals(inetAddress)) {
                    return interfaceAddress;
                }
            }
        }
        return null;
    }

    public InetAddress getLocalInterfaceAddress(NetworkInterface networkInterface, boolean isIPv6, InetAddress remoteAddress) {

        // First try to find a local IP that is in the same subnet as the remote IP
        InetAddress localIPInSubnet = getBindAddressInSubnetOf(remoteAddress);
        if (localIPInSubnet != null) return localIPInSubnet;

        // There are two reasons why we end up here:
        //
        // - Windows Vista returns a 64 or 128 CIDR prefix if you ask it for the network prefix length of an IPv4 address!
        //
        // - We are dealing with genuine IPv6 addresses
        //
        // - Something is really wrong on the LAN and we received a multicast datagram from a source we can't reach via IP
        log.finer("Could not find local bind address in same subnet as: " + remoteAddress.getHostAddress());
        
        // Next, just take the given interface (which is really totally random) and get the first address that we like
        for (InetAddress interfaceAddress: Collections.list(networkInterface.getInetAddresses())) {
            if (isIPv6 && interfaceAddress instanceof Inet6Address)
                return interfaceAddress;
            if (!isIPv6 && interfaceAddress instanceof Inet4Address)
                return interfaceAddress;
        }
        throw new IllegalStateException("Can't find any IPv4 or IPv6 address on interface: " + networkInterface.getDisplayName());
    }

    protected InetAddress getBindAddressInSubnetOf(InetAddress inetAddress) {

        for (NetworkInterface iface : networkInterfaces) {
            for (InterfaceAddress ifaceAddress : iface.getInterfaceAddresses()) {

                if (!bindAddresses.contains(ifaceAddress.getAddress())) {
                    continue;
                }

                if (isInSubnet(
                        inetAddress.getAddress(),
                        ifaceAddress.getAddress().getAddress(),
                        ifaceAddress.getNetworkPrefixLength())
                        ) {
                    return ifaceAddress.getAddress();
                }
            }

        }

        return null;
    }

    protected boolean isInSubnet(byte[] ip, byte[] network, short prefix) {
        if (ip.length != network.length) {
            return false;
        }

        if (prefix / 8 > ip.length) {
            return false;
        }

        int i = 0;
        while (prefix >= 8 && i < ip.length) {
            if (ip[i] != network[i]) {
                return false;
            }
            i++;
            prefix -= 8;
        }
        final byte mask = (byte) ~((1 << 8 - prefix) - 1);

        return (ip[i] & mask) == (network[i] & mask);
    }

    protected void discoverNetworkInterfaces() throws InitializationException {
        try {

            Enumeration<NetworkInterface> interfaceEnumeration = NetworkInterface.getNetworkInterfaces();
            for (NetworkInterface iface : Collections.list(interfaceEnumeration)) {
                //displayInterfaceInformation(iface);

                log.finer("Analyzing network interface: " + iface.getDisplayName());
                if (isUsableNetworkInterface(iface)) {
                    log.fine("Discovered usable network interface: " + iface.getDisplayName());
                    networkInterfaces.add(iface);
                } else {
                    log.finer("Ignorning non-usable network interface: " + iface.getDisplayName());
                }
            }

        } catch (Exception ex) {
            throw new InitializationException("Could not not analyze local network interfaces: " + ex, ex);
        }
    }

    protected boolean isUsableNetworkInterface(NetworkInterface iface) throws Exception {
        if (!iface.isUp()) {
            log.finer("Skipping network interface (down): " + iface.getDisplayName());
            return false;
        }

        if (iface.getName().startsWith("vmnet")) {
            log.finer("Skipping network interface (VMWare interface: " + iface.getDisplayName());
            return false;
        }

        if (!iface.supportsMulticast()) {
            log.finer("Skipping network interface (no multicast support): " + iface.getDisplayName());
            return false;
        }

        if (iface.isLoopback()) {
            log.finer("Skipping network interface (ignoring loopback): " + iface.getDisplayName());
            return false;
        }

        if (useInterfaces.size() > 0 && !useInterfaces.contains(iface.getName())) {
            log.finer("Skipping unwanted network interface (-D" + SYSTEM_PROPERTY_NET_IFACES + "): " + iface.getName());
            return false;
        }

        return true;
    }

    protected void discoverBindAddresses() throws InitializationException {
        try {

            for (NetworkInterface networkInterface : networkInterfaces) {

                log.finer("Discovering addresses of interface: " + networkInterface.getDisplayName());
                for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
                    if (interfaceAddress == null) {
                        log.warning("Network has a null interface address: " + networkInterface.getDisplayName());
                        continue;
                    }
                    InetAddress inetAddress = interfaceAddress.getAddress();

                    if (isUsableAddress(networkInterface, inetAddress)) {
                        log.fine("Discovered usable network interface address: " + inetAddress.getHostAddress());
                        bindAddresses.add(inetAddress);
                    } else {
                        log.finer("Ignoring non-usable network interface address: " + inetAddress.getHostAddress());
                    }
                }

            }

        } catch (Exception ex) {
            throw new InitializationException("Could not not analyze local network interfaces: " + ex, ex);
        }
    }

    protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) {
        // TODO return address instanceof Inet4Address;
        return address instanceof Inet4Address && !address.isLoopbackAddress();
    }

    static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
        System.out.printf("Parent Info:%s\n", netint.getParent());
        System.out.printf("Display name: %s\n", netint.getDisplayName());
        System.out.printf("Name: %s\n", netint.getName());

        Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();

        for (InetAddress inetAddress : Collections.list(inetAddresses)) {
            System.out.printf("InetAddress: %s\n", inetAddress);
        }

        List<InterfaceAddress> ias = netint.getInterfaceAddresses();

        Iterator<InterfaceAddress> iias = ias.iterator();
        while (iias.hasNext()) {
            InterfaceAddress ia = iias.next();

            System.out.println(" Interface Address");
            System.out.println("  Address: " + ia.getAddress());
            System.out.println("  Broadcast: " + ia.getBroadcast());
            System.out.println("  Prefix length: " + ia.getNetworkPrefixLength());
        }

        Enumeration<NetworkInterface> subIfs = netint.getSubInterfaces();

        for (NetworkInterface subIf : Collections.list(subIfs)) {
            System.out.printf("\tSub Interface Display name: %s\n", subIf.getDisplayName());
            System.out.printf("\tSub Interface Name: %s\n", subIf.getName());
        }
        System.out.printf("Up? %s\n", netint.isUp());
        System.out.printf("Loopback? %s\n", netint.isLoopback());
        System.out.printf("PointToPoint? %s\n", netint.isPointToPoint());
        System.out.printf("Supports multicast? %s\n", netint.supportsMulticast());
        System.out.printf("Virtual? %s\n", netint.isVirtual());
        System.out.printf("Hardware address: %s\n", Arrays.toString(netint.getHardwareAddress()));
        System.out.printf("MTU: %s\n", netint.getMTU());
        System.out.printf("\n");

    }

}
