package org.limewire.io;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.limewire.util.ByteUtils;
import org.limewire.util.Decorator;

/**
 * Provides methods for network programming. 
 * <code>NetworkUtils</code>' methods check the validity of IP addresses, ports
 * and socket addresses. <code>NetworkUtils</code> includes both 
 * IPv4 and 
 * <a href="http://en.wikipedia.org/wiki/IPv6">IPv6</a> compliant methods.
 */
//2345678|012345678|012345678|012345678|012345678|012345678|012345678|012345678|
public final class NetworkUtils {
    
    /**
     * Netmask for Class C Networks
     */
    public static final int CLASS_C_NETMASK = 0xFFFFFF00;
    
    /**
     * Ensure that this class cannot be constructed.
     */
    private NetworkUtils() {}
    
    /**
     * Determines if the given addr or port is valid.
     * Both must be valid for this to return true.
     */
    public static boolean isValidAddressAndPort(byte[] addr, int port) {
        return isValidAddress(addr) && isValidPort(port);
    }
    
    /**
     * Determines if the given addr or port is valid.
     * Both must be valid for this to return true.
     */
    public static boolean isValidAddressAndPort(String addr, int port) {
        return isValidAddress(addr) && isValidPort(port);
    }
    
    /** Determines if the given IpPort is valid. */
    public static boolean isValidIpPort(IpPort ipport) {
        return isValidAddress(ipport.getAddress()) && isValidPort(ipport.getPort());
    }

    /**
     * Returns whether or not the specified port is within the valid range of
     * ports.
     * 
     * @param port
     *            the port number to check
     */
    public static boolean isValidPort(int port) {
        return (port > 0 && port <= 0xFFFF);
    }
	
    /**
     * Returns whether or not the specified address is valid.
     * 
     * This method is IPv6 compliant
     */
    public static boolean isValidAddress(byte[] address) {
        return !isAnyLocalAddress(address) 
            && !isInvalidAddress(address)
            && !isBroadcastAddress(address)
            && !isDocumentationAddress(address);
    }
    
    /**
     * Returns whether or not the specified IP is valid.
     */
    public static boolean isValidAddress(IP ip) {
        int msb = (ip.addr >> 24) & 0xFF;
        return (msb != 0x00 && msb != 0xFF);
    }
    
    /**
     * Returns whether or not the specified InetAddress is valid.
     */
    public static boolean isValidAddress(InetAddress address) {
        return !address.isAnyLocalAddress() 
            && !isInvalidAddress(address)
            && !isBroadcastAddress(address)
            && !isDocumentationAddress(address);
    }
    
    /**
     * Returns whether or not the specified host is a valid address.
     */
    public static boolean isValidAddress(String host) {
        try {
            return isValidAddress(InetAddress.getByName(host));
        } catch(UnknownHostException uhe) {
            return false;
        }
    }
	
    /**
     * @return true if the provided string is a dotted ipv4 address
     * of the format "a.b.c.d"
     */
    public static boolean isDottedIPV4(String s) {
        int octets = 0;
        while(octets < 3) {
            int dot = s.indexOf(".");
            if (dot == -1)
                return false;
            String octet = s.substring(0,dot);
            try {

                int parsed = Integer.parseInt(octet);
                if (parsed < 0 || parsed > 255)
                    return false;
            }
            catch (NumberFormatException bad) {
                return false;
            }
            octets++;
            s = s.substring(Math.min(dot+1,s.length()),s.length());
        }
        if (s.indexOf(".") != -1)
            return false;
        try {
            int parsed = Integer.parseInt(s);
            if (parsed < 0 || parsed > 255)
                return false;
        }
        catch (NumberFormatException bad) {
            return false;
        }
        return true;
    }
    
    /**
     * Determines if <code>hostAndPort</code> is either "host" or in
     * "host:port" format in which case the port is checked if it is within a
     * valid range.
     */
    public static boolean isAddress(String hostAndPort) {
        hostAndPort = hostAndPort.trim();
        int i = hostAndPort.indexOf(":");
        if (i == -1) {
            return hostAndPort.length() > 0;
        } else if (i > 0){
            try {
                final int port = Integer.parseInt(hostAndPort.substring(i + 1));
                return isValidPort(port);
            } catch(NumberFormatException e) {
            }            
        }
        return false;
    }

    /**
     * @return whether the IpPort is a valid external address.
     */
    static boolean isValidExternalIpPort(IpPort addr) {
        InetAddress address = addr.getInetAddress();
        return address != null 
            && isValidAddress(address) 
            && isValidPort(addr.getPort());
    }
    
    /**
     * Returns whether or not the specified InetAddress and Port is valid.
     */
    public static boolean isValidSocketAddress(SocketAddress address) {
        InetSocketAddress iaddr = (InetSocketAddress)address;
        
        return !iaddr.isUnresolved()
            && isValidAddress(iaddr.getAddress())
            && isValidPort(iaddr.getPort());
    }
    
    /**
     * Returns true if the InetAddress is any of our local machine addresses
     * 
     * This method is IPv6 compliant
     */
    public static boolean isLocalAddress(InetAddress addr) {
        // There are cases where InetAddress.getLocalHost() returns addresses
        // such as 127.0.1.1 (note the two 1) but if you iterate through all
        // NetworkInterfaces and look at every InetAddress then it's not there
        // and NetworkInterface.getByInetAddress(...) returns null 'cause it
        // cannot find an Interface for it. The following checks take care
        // of this case.
        if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()) {
            return true;
        }
        
        try {
            return NetworkInterface.getByInetAddress(addr) != null;
        } catch (SocketException err) {
            return false;
        }
    }
    
    /**
     * Returns true if the SocketAddress is any of our local machine addresses
     */
    public static boolean isLocalAddress(SocketAddress addr) {
        InetSocketAddress iaddr = (InetSocketAddress)addr;
        return !iaddr.isUnresolved() && isLocalAddress(iaddr.getAddress());
    }
    
    /**
     * Returns whether or not the two IP addresses share the same
     * first octet in their address.  
     * 
     * This method is IPv6 compliant but returns always false if
     * any of the two addresses in an IPv6 address.
     * 
     * @param addr0 the first address to compare
     * @param addr1 the second address to compare
     */
    public static boolean isCloseIP(InetAddress addr0, InetAddress addr1) {
        return isCloseIP(addr0.getAddress(), addr1.getAddress());
    }
    
    /**
     * Returns whether or not the two IP addresses share the same
     * first octet in their address.  
     * 
     * This method is IPv6 compliant but returns always false if
     * any of the two addresses in an IPv6 address.
     * 
     * @param addr0 the first address to compare
     * @param addr1 the second address to compare
     */
    public static boolean isCloseIP(byte[] addr0, byte[] addr1) {
        if ((isIPv4Address(addr0) && isIPv4Address(addr1)) 
                || (isIPv4MappedAddress(addr0) && isIPv4MappedAddress(addr1))) {
            return addr0[/* 0 */ addr0.length - 4] == addr1[/* 0 */ addr1.length - 4];                    
        }
        return false;
    }
    
    /**
     * Returns whether or not the two IP addresses share the same
     * first two octets in their address -- the most common
     * indication that they may be on the same network.
     *
     * Private networks are NOT CONSIDERED CLOSE.
     *
     * This method is IPv6 compliant but returns always false if
     * any of the two addresses in a true IPv6 address.
     * 
     * @param addr0 the first address to compare
     * @param addr1 the second address to compare
     */
    public static boolean isVeryCloseIP(byte[] addr0, byte[] addr1) {
        if ((isIPv4Address(addr0) && isIPv4Address(addr1)) 
                || (isIPv4MappedAddress(addr0) && isIPv4MappedAddress(addr1))) {
            
            return addr0[/* 0 */ addr0.length - 4] == addr1[/* 0 */ addr1.length - 4]
                && addr0[/* 1 */ addr0.length - 3] == addr1[/* 1 */ addr1.length - 3];
        }
        return false;
    }
    
    /**
     * Utility method for determining whether or not the given 
     * address is private taking an InetAddress object as argument
     * like the isLocalAddress(InetAddress) method. 
     *
     * This method is IPv6 compliant
     *
     * @return <tt>true</tt> if the specified address is private,
     *  otherwise <tt>false</tt>
     */
    static boolean isPrivateAddress(InetAddress address) {
        if (address.isAnyLocalAddress() 
                || address.isLoopbackAddress() 
                || address.isLinkLocalAddress() 
                || address.isSiteLocalAddress()
                || isUniqueLocalUnicastAddress(address)
                || isBroadcastAddress(address)
                || isInvalidAddress(address)
                || isDocumentationAddress(address)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Checks if the given address is a private address.
     * 
     * This method is IPv6 compliant
     * 
     * @param address the address to check
     */
    static boolean isPrivateAddress(byte[] address) {
        if (isAnyLocalAddress(address) 
                || isInvalidAddress(address)
                || isLoopbackAddress(address) 
                || isLinkLocalAddress(address) 
                || isSiteLocalAddress(address)
                || isUniqueLocalUnicastAddress(address)
                || isBroadcastAddress(address)
                || isDocumentationAddress(address)) {
            return true;
        }
        
        return false;
    }
    
    /** 
     * Returns the IP (given in BIG-endian) format as standard
     * dotted-decimal, e.g., 192.168.0.1<p> 
     *
     * @param ip the IP address in BIG-endian format
     * @return the IP address as a dotted-quad string
     */
     public static final String ip2string(byte[] ip) {
         return ip2string(ip, 0);
     }
         
    /** 
     * Returns the IP (given in BIG-endian) format of
     * buf[offset]...buf[offset+3] as standard dotted-decimal, e.g.,
     * 192.168.0.1<p> 
     *
     * @param ip the IP address to convert
     * @param offset the offset into the IP array to convert
     * @return the IP address as a dotted-quad string
     */
    public static final String ip2string(byte[] ip, int offset) {
        // xxx.xxx.xxx.xxx => 15 chars
        StringBuilder sbuf = new StringBuilder(16);   
        sbuf.append(ByteUtils.ubyte2int(ip[offset]));
        sbuf.append('.');
        sbuf.append(ByteUtils.ubyte2int(ip[offset+1]));
        sbuf.append('.');
        sbuf.append(ByteUtils.ubyte2int(ip[offset+2]));
        sbuf.append('.');
        sbuf.append(ByteUtils.ubyte2int(ip[offset+3]));
        return sbuf.toString();
    }
    
    /**
     * Determines if the given socket is from a local host.
     * 
     * This method is IPv6 compliant
     */
    public static boolean isLocalHost(Socket socket) {
        return isLocalAddress(socket.getInetAddress());
    }
    
    /**
     * Packs a Collection of IpPorts into a byte array.
     */
    public static byte[] packIpPorts(Collection<? extends IpPort> ipPorts) {
        byte[] data = new byte[ipPorts.size() * 6];
        int offset = 0;
        for(IpPort next : ipPorts) {
            byte[] addr = next.getInetAddress().getAddress();
            int port = next.getPort();
            System.arraycopy(addr, 0, data, offset, 4);
            offset += 4;
            ByteUtils.short2leb((short)port, data, offset);
            offset += 2;
        }
        return data;
    }
    
    /**
     * parses an ip:port byte-packed values.  
     * 
     * @return a collection of <tt>IpPort</tt> objects.
     * @throws InvalidDataException if an invalid IP is found or the size 
     * is not divisible by six
     */
    public static List<IpPort> unpackIps(byte [] data) throws InvalidDataException {
        return unpackIps(data, null);
    }
    
    
    /**
     * Parses an ip:port byte-packed values.
     * The decorator is consulted for each IpPort prior to inserted it into the list.  
     * 
     * @param data the packed IpPorts.
     * @param decorator A decorator that can optionally change the IpPort that is added into the returned list.
     * @return a collection of <tt>IpPort</tt> objects.
     * @throws InvalidDataException if an invalid Ip is found or the size 
     * is not divisble by six
     */
    public static List<IpPort> unpackIps(byte [] data, Decorator<IpPort, ? extends IpPort> decorator) throws InvalidDataException {
    	if (data.length % 6 != 0)
    		throw new InvalidDataException("invalid size");
    	
    	int size = data.length/6;
    	List<IpPort> ret = new ArrayList<IpPort>(size);
    	byte [] current = new byte[6];
    	for (int i=0;i<size;i++) {
    		System.arraycopy(data,i*6,current,0,6);
            IpPort ipp = NetworkUtils.getIpPort(current, java.nio.ByteOrder.LITTLE_ENDIAN);
            if(decorator != null) {
                ipp = decorator.decorate(ipp);
                if(ipp == null)
                    throw new InvalidDataException("decorator returned null");
            }
    		ret.add(ipp);
    	}
    	
    	return Collections.unmodifiableList(ret);
    }
    
    /**
     * Filters unique IPs based on a Class C Netmask
     */
    public static <T extends IpPort> Collection<T> filterOnePerClassC(Collection<T> c) {
        return filterUnique(c, CLASS_C_NETMASK);
    }
    
    /**
     * Filters unique IPs based on a netmask.
     */
    public static <T extends IpPort> Collection<T> filterUnique(Collection<T> c, int netmask) {
        ArrayList<T> ret = new ArrayList<T>(c.size());
        Set<Integer> ips = new HashSet<Integer>();
        for (T ip : c) {
            if (ips.add( getMaskedIP(ip.getInetAddress(), netmask) ))
                ret.add(ip);
            
        }
        ret.trimToSize();
        return ret;
    }
    
    /**
     * Returns the Class C Network part of the given InetAddress.
     * See {@link #getMaskedIP(InetAddress, int)} for more info.
     */
    public static int getClassC(InetAddress addr) {
        return getMaskedIP(addr, CLASS_C_NETMASK);
    }
    
    /**
     * Applies the netmask on the lower four bytes of the given 
     * InetAddress and returns it as an Integer.
     * 
     * This method is IPv6 compliant but shouldn't be called if
     * the InetAddress is neither IPv4 compatible nor mapped!
     */
    public static int getMaskedIP(InetAddress addr, int netmask) {
        byte[] address = addr.getAddress();
        return ByteUtils.beb2int(address, /* 0 */ address.length - 4) & netmask;
    }
    
    /**
     * Converts integer <code>ip</code> into byt array.
     * 
     * This method is not IPv6 compliant.
     */
    public static byte[] toByteAddress(int ip) {
        byte[] address = new byte[4];
        ByteUtils.int2beb(ip, address, 0);
        return address;
    }
    
    /**
     * @param decMask a netmask in decimal, like /24
     * @return an integer that can be and-ed for masking
     */
    public static int getHexMask(int decMask) {
        if (decMask < 0 || decMask > 32)
            throw new IllegalArgumentException("bad mask "+decMask);
        if (decMask == 0)
            return 0;
        return 0xFFFFFFFF << (32 - decMask);
    }
    
    /**
     * @return A non-loopback IPv4 address of a network interface on the local
     *         host.
     * @throws UnknownHostException
     */
    public static InetAddress getLocalAddress() throws UnknownHostException {
        InetAddress addr = InetAddress.getLocalHost();
        
        if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
            return addr;
        }
        
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            if (interfaces != null) {
                while (interfaces.hasMoreElements()) {
                    Enumeration<InetAddress> addresses = interfaces.nextElement().getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        addr = addresses.nextElement();
                        if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
                            return addr;
                        }
                    }
                }
            }
        } catch (SocketException se) {
        }

        throw new UnknownHostException(
                "localhost has no interface with a non-loopback IPv4 address");
    }
    
    /**
     * Returns the IP:Port as byte array.
     * 
     * This method is IPv6 compliant
     */
    public static byte[] getBytes(SocketAddress addr, java.nio.ByteOrder order) throws UnknownHostException {
        InetSocketAddress iaddr = (InetSocketAddress)addr;
        if (iaddr.isUnresolved()) {
            throw new UnknownHostException(iaddr.toString());
        }
        
        return getBytes(iaddr.getAddress(), iaddr.getPort(), order);
    }
    
    /**
     * Returns the IP:Port as byte array.
     * 
     * This method is IPv6 compliant
     */
    public static byte[] getBytes(IpPort ipp, java.nio.ByteOrder order) {
        return getBytes(ipp.getInetAddress(), ipp.getPort(), order);
    }
    
    /**
     * Returns the IP:Port as byte array.
     * 
     * This method is IPv6 compliant
     */
    public static byte[] getBytes(InetAddress addr, int port, java.nio.ByteOrder order) {
        if (!isValidPort(port))
            throw new IllegalArgumentException("Port out of range: " + port);
        if(!isValidAddress(addr))
            throw new IllegalArgumentException("invalid addr: " + addr);
        
        byte[] address = addr.getAddress();

        byte[] dst = new byte[address.length + 2];
        System.arraycopy(address, 0, dst, 0, address.length);
        if(order == java.nio.ByteOrder.BIG_ENDIAN)
            ByteUtils.short2beb((short)port, dst, dst.length-2);
        else // if order == LITTLE_ENDIAN
            ByteUtils.short2leb((short)port, dst, dst.length-2);
        return dst;
    }

    /**
     * Creates an IpPort out of network data. The ByteOrder is used to determine
     * the order of the port. Throws <code>InvalidDataException</code> if the
     * data is invalid.
     * 
     * This method is IPv6 compliant.
     */
    public static IpPort getIpPort(byte[] ipport, java.nio.ByteOrder order)
      throws InvalidDataException {
        if(ipport.length < 6)
            throw new InvalidDataException("length must be >= 6, is: " + ipport.length);
        
        short shortport;
        if(order == java.nio.ByteOrder.BIG_ENDIAN)
            shortport = ByteUtils.beb2short(ipport, ipport.length - 2);
        else // if order == LITTLE_ENDIAN
            shortport = ByteUtils.leb2short(ipport, ipport.length - 2);
        int port = ByteUtils.ushort2int(shortport);
        
        if (!NetworkUtils.isValidPort(port))
            throw new InvalidDataException("Bad Port: " + port);
        
        InetAddress host;
        try {
            byte[] ip = new byte[ipport.length - 2];
            System.arraycopy(ipport, 0, ip, 0, ip.length);
            host = InetAddress.getByAddress(ip);
        } catch(UnknownHostException uhe) {
            throw new InvalidDataException("bad host.");
        }
        
        if (!NetworkUtils.isValidAddress(host))
            throw new InvalidDataException("invalid addr: " + host);
        
        return new IpPortImpl(new InetSocketAddress(host, port));
    }
    
    
    /**
     * Returns true if both SocketAddresses are either IPv4 or IPv6 addresses
     * 
     * This method is IPv6 compliant
     */
    public static boolean isSameAddressSpace(SocketAddress a, SocketAddress b) {
        return isSameAddressSpace(
                    ((InetSocketAddress)a).getAddress(), 
                    ((InetSocketAddress)b).getAddress());
    }
    
    /**
     * Returns true if both InetAddresses are compatible (IPv4 and IPv4
     * or IPv6 and IPv6).
     * 
     * This method is IPv6 compliant
     */
    public static boolean isSameAddressSpace(InetAddress a, InetAddress b) {
        if (a == null || b == null) {
            return false;
        }
        
        // Both are either IPv4 or IPv6
        if ((a instanceof Inet4Address && b instanceof Inet4Address)
                || (a instanceof Inet6Address && b instanceof Inet6Address)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Returns the IPv6 address bytes of IPv6 and IPv4 IP addresses. 
     * @throws IllegalArgumentException if given a different address type
     */
    public static byte[] getIPv6AddressBytes(InetAddress address) {
        byte[] bytes = address.getAddress();
        switch (bytes.length) {
            case 16:
                // Return the IPv6 address
                return bytes;
            case 4:
                // Turn the IPv4 address into a IPv4 mapped IPv6
                // address
                byte[] result = new byte[16];
                result[10] = (byte) 0xff;
                result[11] = (byte) 0xff;
                System.arraycopy(bytes, 0, result, 12, bytes.length);
                return result;
            default:
                throw new IllegalArgumentException("unhandled address length");
        }
    }
    
    /**
     * Returns true if an IPv6 representation of <code>address</code> exists.
     */
    public static boolean isIPv6Compatible(InetAddress address) {
        int length = address.getAddress().length;
        return length == 4 || length == 16;
    }
    
    /**
     * Returns true if the given byte-array is an IPv4 address
     */
    private static boolean isIPv4Address(byte[] address) {
        return address.length == 4;
    }
    
    /**
     * Returns true if the given byte-array is an IPv4 compatible address.
     * They're used when IPv6 systems need to communicate with each other, 
     * but are separated by an IPv4 network.
     */
    static boolean isIPv4CompatibleAddress(byte[] address) { 
        // Is it a IPv4 compatible IPv6 address?
        // (copied from Inet6Address)
        if (address.length == 16 
                && (address[ 0] == 0x00) && (address[ 1] == 0x00) 
                && (address[ 2] == 0x00) && (address[ 3] == 0x00) 
                && (address[ 4] == 0x00) && (address[ 5] == 0x00) 
                && (address[ 6] == 0x00) && (address[ 7] == 0x00) 
                && (address[ 8] == 0x00) && (address[ 9] == 0x00) 
                && (address[10] == 0x00) && (address[11] == 0x00))  {   
            return true;
        }
        
        return false;  
    }
    
    /**
     * Returns true if the given byte-array is an IPv4 mapped address.
     * IPv4 mapped addresses indicate systems that do not support IPv6. 
     * They are limited to IPv4. An IPv6 host can communicate with an 
     * IPv4 only host using the IPv4 mapped IPv6 address.
     */
    static boolean isIPv4MappedAddress(byte[] address) {
        if (address.length == 16 
                && (address[ 0] == 0x00) && (address[ 1] == 0x00) 
                && (address[ 2] == 0x00) && (address[ 3] == 0x00) 
                && (address[ 4] == 0x00) && (address[ 5] == 0x00) 
                && (address[ 6] == 0x00) && (address[ 7] == 0x00) 
                && (address[ 8] == 0x00) && (address[ 9] == 0x00) 
                && (address[10] == (byte)0xFF) && (address[11] == (byte)0xFF)) {   
            return true;
        }
        
        return false;  
    }
    
    /**
     * Returns true if it's an IPv4 InetAddress and the first octed is 0x00.
     */
    private static boolean isInvalidAddress(InetAddress address) {
        return isInvalidAddress(address.getAddress());
    }
    
    /**
     * Returns true if it's an IPv4 InetAddress and the first octet is 0x00.
     */
    private static boolean isInvalidAddress(byte[] address) {
        if (isIPv4Address(address) || isIPv4MappedAddress(address)) {
            return address[/* 0 */ address.length - 4] == 0x00;
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is an any local address.
     */
    static boolean isAnyLocalAddress(byte[] address) {
        if (address.length == 4 || address.length == 16) {
            byte test = 0;
            for (int i = 0; i < address.length; i++) {
                test |= address[i];
            }
            
            return (test == 0x00);
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is a loopback address
     */
    static boolean isLoopbackAddress(byte[] address) {
        if (isIPv4Address(address) || isIPv4MappedAddress(address)) {
            return (address[/* 0 */ address.length - 4] & 0xFF) == 127;
        } else if (address.length == 16) {
            byte test = 0x00;
            for (int i = 0; i < 15; i++) {
                test |= address[i];
            }
            return (test == 0x00) && (address[15] == 0x01);
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is a link-local address
     */
    static boolean isLinkLocalAddress(byte[] address) {
        if (isIPv4Address(address) || isIPv4MappedAddress(address)) {
            return (address[/* 0 */ address.length - 4] & 0xFF) == 169
                && (address[/* 1 */ address.length - 3] & 0xFF) == 254;
            
        // FE80::/64
        } else if (address.length == 16) {
            return (address[0] & 0xFF) == 0xFE
                && (address[1] & 0xC0) == 0x80;
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is a site-local address.
     * IPv6 site-local addresses were deprecated in September 2004 
     * by RFC 3879 and replaced by RFC 4193 (Unique Local IPv6 Unicast
     * Addresses).
     */
    static boolean isSiteLocalAddress(byte[] address) {
        if (isIPv4Address(address) || isIPv4MappedAddress(address)) {
            return  (address[/* 0 */ address.length - 4] & 0xFF) == 10
                || ((address[/* 0 */ address.length - 4] & 0xFF) == 172
                &&  (address[/* 1 */ address.length - 3] & 0xF0) == 16)
                || ((address[/* 0 */ address.length - 4] & 0xFF) == 192
                &&  (address[/* 1 */ address.length - 3] & 0xFF) == 168);
            
        // Has been deprecated in September 2004 by RFC 3879 
        // FEC0::/10
        } else if (address.length == 16) {
            return (address[0] & 0xFF) == 0xFE
                && (address[1] & 0xC0) == 0xC0;
        }
        return false;
    }
    
    /**
     * Returns true if the given InetAddress is an Unique Local IPv6
     * Unicast Address. See RFC 4193 for more info.
     */
    public static boolean isUniqueLocalUnicastAddress(InetAddress address) {
        if (address instanceof Inet6Address) {
            return isUniqueLocalUnicastAddress(address.getAddress());
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is an Unique Local IPv6
     * Unicast Address. See RFC 4193 for more info.
     */
    private static boolean isUniqueLocalUnicastAddress(byte[] address) {
        // FC00::/7
        if (address.length == 16) {
            return (address[0] & 0xFE) == 0xFC;
        }
        return false;
    }

    /**
     * Returns true if the given InetAddress is a broadcast address.
     */
    public static boolean isBroadcastAddress(InetAddress address) {
        return isBroadcastAddress(address.getAddress());
    }
    
    /**
     * Returns true if the given byte-array is a broadcast address
     * 
     * This method is IPv6 compliant but returns always false if
     * the given address is neither a true IPv4, nor an IPv4-mapped
     * address.
     */
    private static boolean isBroadcastAddress(byte[] address) {
        if (isIPv4Address(address) || isIPv4MappedAddress(address)) {
            return (address[/* 0 */ address.length - 4] & 0xFF) == 0xFF;
        }
        
        return false;
    }
    
    /**
     * Returns true if the given InetAddress is a private IPv4-compatible
     * address.
     * 
     * It checks for a somewhat tricky and undefined case. An address such
     * as ::0000:192.168.0.1 is an IPv6 address, it's an IPv4-compatible
     * address but it's by IPv6 definition not a site-local (private) address.
     * On the other hand it's a private IPv4 address.
     */
    public static boolean isPrivateIPv4CompatibleAddress(InetAddress address) {
        if (address instanceof Inet6Address) {
            return isPrivateIPv4CompatibleAddress(address.getAddress());
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array is a private IPv4-compatible
     * address.
     */
    private static boolean isPrivateIPv4CompatibleAddress(byte[] address) {
        if (isIPv4CompatibleAddress(address)) {
            // Copy the lower four bytes and perform the
            // checks on it to determinate whether or not
            // it's a private IPv4 address
            byte[] ipv4 = new byte[4];
            System.arraycopy(address, 12, ipv4, 0, ipv4.length);
            return isPrivateAddress(ipv4);
        }
        return false;
    }
    
    /**
     * Returns true if the given InetAddress has a prefix that's used in
     * Documentation. It's a non-routeable IPv6 address. See <a href=
     * "http://www.ietf.org/rfc/rfc3849.txt">RFC 3849</a>  
     * for more information.
     */
    public static boolean isDocumentationAddress(InetAddress address) {
        if (address instanceof Inet6Address) {
            return isDocumentationAddress(address.getAddress());
        }
        return false;
    }
    
    /**
     * Returns true if the given byte-array has a prefix that's used in
     * Documentation. It's a non-routeable IPv6 address. See <a href=
     * "http://www.ietf.org/rfc/rfc3849.txt">RFC 3849</a> 
     * for more information.
     */
    private static boolean isDocumentationAddress(byte[] address) {
        // 2001:0DB8::/32
        if (address.length == 16) {
            return (address[0] & 0xFF) == 0x20
                && (address[1] & 0xFF) == 0x01
                && (address[2] & 0xFF) == 0x0D
                && (address[3] & 0xFF) == 0xB8;
        }
        return false;
    }

    /**
     * Parses port from string and checks if it's valid.
     * 
     * @throws IOException if the port could not be parsed or is invalid
     */
    static int parsePort(String portString) throws IOException {
        try {
            int port = Integer.parseInt(portString);
            if(!isValidPort(port)) {
                throw new IOException("invalid port: " + port);
            }
            return port;
        } catch(NumberFormatException invalid) {
            throw (IOException)new IOException().initCause(invalid);
        }
    }
    
    /**
     * Gets {@link InetAddress} and checks if it's valid.
     * @throws IOException if the address is not valid address
     */
    static InetAddress getAndCheckAddress(String addressString) throws IOException {
        InetAddress address = InetAddress.getByName(addressString);
        if (!isValidAddress(address))
            throw new IOException("invalid addr: " + address);
        
        return address;
    }
    
    /**
     * Returns the index of the ':' separator between ip and port and checks
     * if it's at a valid position.
     * @param ipPort
     * @return
     * @throws IOException if separator is not found or not at a valid position
     */
    static int getAndCheckIpPortSeparator(String ipPort) throws IOException {
        int separator = ipPort.indexOf(":");
        
        //see if this is a valid ip:port address; 
        if (separator <= 0 || separator!= ipPort.lastIndexOf(":") || separator == ipPort.length() - 1)
            throw new IOException("invalid separator in http: " + ipPort);
        
        return separator;
    }
    
    /**
     * Returns a Connectable of the ipport as described by "a.b.c.d:port".
     * 
     * @param ipPort a string representing an ip and porte 
     * @throws IOException parsing failed.
     */
    public static Connectable parseIpPort(String ipPort, boolean tlsCapable) throws IOException {
        int separator = getAndCheckIpPortSeparator(ipPort);
    	InetAddress address = getAndCheckAddress(ipPort.substring(0, separator));
    	int port = parsePort(ipPort.substring(separator+1));
    	return new ConnectableImpl(new InetSocketAddress(address, port), tlsCapable);
    }
    
    
    /**
     * Returns an  
     * @param http a string representing a port and an ip
     * @return an object implementing IpPort 
     * @throws IOException parsing failed.
     */
    public static IpPort parsePortIp(String http) throws IOException{
        int separator = getAndCheckIpPortSeparator(http);
        int port = parsePort(http.substring(0, separator));
        InetAddress address = getAndCheckAddress(http.substring(separator+1));
        return new IpPortImpl(address, port);
    }
}
