/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: HostAddress.java,v 1.24 2004/08/23 12:14:32 gregork Exp $
 */
package phex.host;

import java.net.*;
import java.util.*;


import phex.common.Ip2CountryManager;
import phex.connection.*;
import phex.utils.*;

/**
 * Represents a host address.
 */
public class HostAddress
{
    public static final byte CLASS_A = 1;
    public static final byte CLASS_B = 2;
    public static final byte CLASS_C = 3;

    public static byte[] LOCAL_HOST_IP = new byte[]
    {
        127, 0, 0, 1
    };
    public static String LOCAL_HOST_NAME = "127.0.0.1";
    public static final int DEFAULT_PORT = 6346;

    /** Cache the hash code for the address */
    private int hash = 0;

    private String hostName;
    private int port;
    private byte[] hostIP;
    
    public String countryCode;

    public HostAddress( String aHostName, int aPort )
        throws IllegalArgumentException
    {
        hostName = aHostName;
        if ( !isPortInRange( aPort ) )
        {
            throw new IllegalArgumentException( "Port out of range: "
                + aPort );
        }
        port = aPort;
    }

    public HostAddress( byte[] aHostIP, int aPort )
    {
        hostIP = aHostIP;
        if ( !isPortInRange( aPort ) )
        {
            throw new IllegalArgumentException( "Port out of range: "
                + aPort );
        }
        port = aPort;
    }

    public HostAddress( String hostString )
        throws MalformedHostAddressException
    {
        int idx = hostString.indexOf( ':' );
        // no port given
        if ( idx < 0 || idx == hostString.length() - 1 )
        {
            hostName = hostString;
            port = DEFAULT_PORT;
            return;
        }
        if ( idx == 0 )
        {
            throw new MalformedHostAddressException( "No host name: "
                + hostString );
        }
        hostName = hostString.substring( 0, idx );
        String portString = hostString.substring( idx + 1 );
        try
        {
            port = Integer.parseInt( portString );
            if ( !isPortInRange( port ) )
            {
                throw new MalformedHostAddressException( "Port out of range: "
                    + portString );
            }
        }
        catch ( NumberFormatException exp )
        {
            throw new MalformedHostAddressException( "Can't parse port: "
                + portString );
        }
    }

    /**
     * updated the host address with the new data.
     */
    public void updateAddress( String aHostName, int aPort )
    {
        if ( !aHostName.equals( hostName ) )
        {
            hostName = aHostName;
            hostIP = null;
        }
        if ( !isPortInRange( aPort ) )
        {
            throw new IllegalArgumentException( "Port out of range: "
                + aPort );
        }
        port = aPort;
        hash = 0;
        countryCode = null;
    }

    /**
     * updated the host address with the new data.
     */
    public void updateAddress( byte[] aHostIP, int aPort )
    {
        hostIP = aHostIP;
        if ( !isPortInRange( aPort ) )
        {
            throw new IllegalArgumentException( "Port out of range: "
                + aPort );
        }
        port = aPort;
        hash = 0;
        countryCode = null;
    }

    /**
     * If a host name is known the host name is retuned otherwise the IP is
     * returned. No port information is appended.
     */
    public String getHostName()
    {
        if ( hostName == null )
        {
            return IPUtils.ip2string( hostIP );
        }
        return hostName;
    }
    
    /**
     * Returns whether the host name is the string representation of the IP or
     * not.
     * @return Returns true if the host name is the string representation of 
     * the IP, false otherwise.
     */
    public boolean isIpHostName()
    {
        if ( hostName == null )
        {
            return true;
        }
        if( hostIP == null )
        {
            hostIP = parseIP( hostName );
            return hostIP != null;
        }
        return hostName.equals( IPUtils.ip2string( hostIP ) );        
    }

    /**
     * Returns the full host name with port in the format hostname:port.
     */
    public String getFullHostName()
    {
        StringBuffer buffer = new StringBuffer( 21 );
        buffer.append( getHostName() );
        buffer.append( ':' );
        buffer.append( port );
        return buffer.toString();
    }

    public int getPort()
    {
        return port;
    }

    /**
     * The method returns the IP of the host.
     */
    public byte[] getHostIP() throws UnknownHostException
    {
        initHostIP();
        return hostIP;
    }
    
    public long getLongHostIP( )
        throws UnknownHostException
    {
        initHostIP();
        int v1 =  hostIP[3]        & 0xFF;
        int v2 = (hostIP[2] <<  8) & 0xFF00;
        int v3 = (hostIP[1] << 16) & 0xFF0000;
        int v4 = (hostIP[0] << 24);
        long ipValue = ((long)(v4|v3|v2|v1)) & 0x00000000FFFFFFFFl;
        return ipValue;
    }
    
    public boolean equals( byte[] testIp, int testPort )
    {
        if ( hostIP != null && testIp != null)
        {
            return Arrays.equals( hostIP, testIp ) && port == testPort;
        }
        else
        {
            return getHostName().equals( IPUtils.ip2string( testIp ) )
               && port == testPort ;
        }
    }

    public boolean equals( HostAddress address )
    {
        if ( address == null )
        {
            return false;
        }
        if ( hostIP != null && address.hostIP != null)
        {
            return Arrays.equals( hostIP, address.hostIP ) && port == address.port;
        }
        else
        {
            return getHostName().equals( address.getHostName() )
               && port == address.port ;
        }
    }

    public boolean equals( Object obj )
    {
        if ( obj instanceof HostAddress )
        {
            return equals( (HostAddress) obj );
        }
        return false;
    }

    public int hashCode()
    {
        if ( hash == 0 )
        {
            int h = 0;
            h = ((31 *h) + port);
            if ( hostIP != null )
            {
                int ipVal = IOUtil.deserializeInt( hostIP, 0 );
                h = 31*h + ipVal;
            }
            else
            {
                h = ((127 *h)+hostName.hashCode());
            }
            hash = h;
        }
        return hash;
    }
    
    /**
     * Returns the country code of the HostAddress. But only in case the host ip
     * has already been resolved. Otherwise no country code is returned since
     * the country code lookup would cost high amount of time.
     * @return the country code or null.
     */
    public String getCountryCode()
    {
        if ( countryCode == null && hostIP != null )
        {
            countryCode = Ip2CountryManager.getInstance().getCountryCode( this );
        }
        return countryCode; 
    }

    /**
     * Checks if the host address is the local one with the local port
     */
    public boolean isLocalHost( )
    {
        HostAddress localAddress = NetworkManager.getInstance().getLocalAddress();
        boolean isPortEqual = port == localAddress.getPort();

        try
        {
            initHostIP();
        }
        catch ( UnknownHostException exp )
        {
            return false;
        }
        if ( hostIP[0] == (byte) 127 )
        {
            return isPortEqual;
        }
        else
        {
            return localAddress.equals( this );
        }
    }

    /**
     * Checks ig the IP is a private IP.
     */
    public boolean isPrivateIP()
    {
        try
        {
            initHostIP();
        }
        catch ( UnknownHostException exp )
        {
            return false;
        }
        //10.*.*.* and 127.*.*.*
        if ( hostIP[0] == (byte)10 || hostIP[0] == (byte)127 )
        {
            return true;
        }
        //172.16.*.* - 172.31.*.*
        if ( hostIP[0] == (byte)172 && hostIP[1] >= (byte)16 && hostIP[1] <= (byte)31 )
        {
            return true;
        }
        //192.168.*.*
        if ( hostIP[0] == (byte)192 && hostIP[1] == (byte)168 )
        {
            return true;
        }
        return false;
    }
    
    public boolean isValidIP()
    {
        try
        {
            initHostIP();
        }
        catch ( UnknownHostException exp )
        {
            return false;
        }
        // Class A
        // |0|-netid-|---------hostid---------|
        //     7 bits                  24 bits
        //
        // Class B
        // |10|----netid-----|-----hostid-----|
        //            14 bits          16 bits
        //
        // Class C
        // |110|--------netid--------|-hostid-|
        //                    21 bits   8 bits
        byte clazz = getIPClass();
        boolean valid;
        switch( clazz )
        {
            case CLASS_A:
                valid = ((hostIP[1]&0xFF) + (hostIP[2]&0xFF) + (hostIP[3]&0xFF)) != 0;
                break;
            case CLASS_B:
                valid = ((hostIP[2]&0xFF) + (hostIP[3]&0xFF)) != 0;
                break;
            case CLASS_C:
                valid = (hostIP[3]&0xFF) != 0;
                break;
            default:
                valid = false;
                break;
        }
        return valid;
    }

    public byte getIPClass()
    {
        // Class A
        // |0|-netid-|---------hostid---------|
        //     7 bits                  24 bits
        //
        // Class B
        // |10|----netid-----|-----hostid-----|
        //            14 bits          16 bits
        //
        // Class C
        // |110|--------netid--------|-hostid-|
        //                    21 bits   8 bits

        try
        {
            initHostIP();
        }
        catch ( UnknownHostException exp )
        {
            return -1;
        }
        if ( (hostIP[0] & 0x80) == 0 )
        {
            return CLASS_A;
        }
        else if ( (hostIP[0] & 0xC0) == 0x80 )
        {
            return CLASS_B;
        }
        else if ( (hostIP[0] & 0xE0) == 0xC0 )
        {
            return CLASS_C;
        }
        else
        {
            return -1;
        }
    }

    /**
     * Does a DNS lookup of the IP.
     * @throws UnknownHostException in case of DNS lookup failure.
     */
    private void initHostIP()
        throws UnknownHostException
    {
        if( hostIP == null )
        {
            // does the parsing of an IP or determinse the IP over DNS
            // TODO3 BUG if socks5 proxy is used the ip can't be determined right!
            hostIP = InetAddress.getByName( hostName ).getAddress();
            hash = 0;
        }
    }

    public String toString()
    {
        return getFullHostName();
    }

    /**
     * Trys to parse the port of a host string. If no port could be parsed -1 is
     * returned.
     */
    public static int parsePort( String hostName )
    {
        int portIdx = hostName.indexOf( ':' );
        if ( portIdx == -1 )
        {
            return -1;
        }

        String portString = hostName.substring( portIdx + 1);
        char[] data = portString.toCharArray();
        int port = 0;
        for ( int i = 0; i < data.length; i++ )
        {
            char c = data[i];
            //      '0'       '9'
            if (c < 48 || c > 57)
            { // !digit
                break;
            }
            // shift left and add value
            port = port * 10 + c - '0';
        }
        // no port or out of range
        if ( !isPortInRange( port ) )
        {
            return -1;
        }
        return port;
    }

    /**
     * Validates a port value if it is in range ( 1 - 65535 )
     * @param port the port to verify in int value. Unsigned short ports must be
     * converted to singned int to let this function work correctly.
     * @return true if the port is in range, false otherwise.
     */
    public static boolean isPortInRange( int port )
    {
        return ( port & 0xFFFF0000 ) == 0 && port != 0;
    }

    /**
     * Trys to parse the given string. The String must represent a numerical IP
     * address in the format %d.%d.%d.%d. A possible attached port will be cut of.
     * @return the ip represented in a byte[] or null if not able to parse the ip.
     */
    public static byte[] parseIP( String hostIp )
    {
        /* The string (probably) represents a numerical IP address.
         * Parse it into an int, don't do uneeded reverese lookup,
         * leave hostName null, don't cache.  If it isn't an IP address,
         * (i.e., not "%d.%d.%d.%d") or if any element > 0xFF,
         * it is a hostname and we return null.
         * This seems to be 100% compliant to the RFC1123 spec.
         */
        int portSeparatorIdx = hostIp.indexOf( ':' );
        if ( portSeparatorIdx != -1 )
        {// cut of port.
            hostIp = hostIp.substring( 0, portSeparatorIdx );
        }

        char[] data = hostIp.toCharArray();
        int IP = 0x00;
        int hitDots = 0;

        for(int i = 0; i < data.length; i++)
        {
            char c = data[i];
            if (c < 48 || c > 57)
            { // !digit
                return null;
            }
            int b = 0x00;
            while(c != '.')
            {
                //      '0'       '9'
                if (c < 48 || c > 57)
                { // !digit
                    return null;
                }
                b = b * 10 + c - '0';

                if (++i >= data.length)
                {
                    break;
                }
                c = data[i];
            }
            if(b > 0xFF)
            { /* bogus - bigger than a byte */
                return null;
            }
            IP = (IP << 8) + b;
            hitDots++;
        }
        if(hitDots != 4 || hostIp.endsWith("."))
        {
            return null;
        }
        // create byte[] from int address
        byte[] addr = new byte[4];
        addr[0] = (byte) ((IP >>> 24) & 0xFF);
        addr[1] = (byte) ((IP >>> 16) & 0xFF);
        addr[2] = (byte) ((IP >>> 8) & 0xFF);
        addr[3] = (byte) (IP & 0xFF);
        return addr;
    }
    
    /**
     * Parses a unsigned int value to a byte array containing the IP
     * @param ip
     * @return
     */
    public static byte[] parseIntIP( String ip )
    {
        long IP = Long.parseLong( ip );
        
        // create byte[] from int address
        byte[] addr = new byte[4];
        addr[0] = (byte) ((IP >>> 24) & 0xFF);
        addr[1] = (byte) ((IP >>> 16) & 0xFF);
        addr[2] = (byte) ((IP >>> 8) & 0xFF);
        addr[3] = (byte) (IP & 0xFF);
        return addr;
    }
    
    public static String toIntValueString( byte[] ip )
    {
        int v1 =  ip[3]        & 0xFF;
        int v2 = (ip[2] <<  8) & 0xFF00;
        int v3 = (ip[1] << 16) & 0xFF0000;
        int v4 = (ip[0] << 24);
        long ipValue = ((long)(v4|v3|v2|v1)) & 0x00000000FFFFFFFFl;
        return String.valueOf( ipValue );
    }
}