/*
 * Dijjer - A Peer to Peer HTTP Cache
 * Copyright (C) 2004,2005 Change.Tv, Inc
 *
 * 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
 */

package freenet.io.comm;

import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Comparator;

import freenet.io.WritableToDataOutputStream;
import freenet.support.io.InetAddressIpv6FirstComparator;
import freenet.support.transport.ip.HostnameSyntaxException;
import freenet.support.transport.ip.IPUtil;

/**
 * @author ian
 *
 * To change the template for this generated type comment go to Window - Preferences - Java - Code Generation - Code and
 * Comments
 */
public class Peer implements WritableToDataOutputStream {

    public static class LocalAddressException extends Exception {
    	private static final long serialVersionUID = -1;
	}

	public static final PeerComparator PEER_COMPARATOR = new PeerComparator();

	public static final String VERSION = "$Id: Peer.java,v 1.4 2005/08/25 17:28:19 amphibian Exp $";

    private final FreenetInetAddress addr;
	private final int _port;

	public Peer(DataInput dis) throws IOException {
		addr = new FreenetInetAddress(dis);
		_port = dis.readInt();
		if(_port > 65535 || _port < 0) throw new IOException("bogus port");
	}

	public Peer(DataInput dis, boolean checkHostnameOrIPSyntax) throws HostnameSyntaxException, IOException {
		addr = new FreenetInetAddress(dis, checkHostnameOrIPSyntax);
		_port = dis.readInt();
		if(_port > 65535 || _port < 0) throw new IOException("bogus port");
	}

	/**
	 * Create a Peer from an InetAddress and a port. The IP address is primary; that is
	 * to say, it will remain the same regardless of DNS changes. Don't do this if you
	 * are dealing with domain names whose IP may change.
	 */
	public Peer(InetAddress address, int port) {
		addr = new FreenetInetAddress(address);
		_port = port;
		if(_port > 65535 || _port < 0) throw new IllegalArgumentException("bogus port");
	}

	/**
	 * Create a Peer from a string. This may be an IP address or a domain name. If it
	 * is the latter, the name is primary rather than the IP address; 
	 * getHandshakeAddress() will do a new lookup on the name, and change the IP address
	 * if the domain name has changed.
	 * @param physical The string to be parsed, in the format [ ip or domain name ]:[ port number].
	 * @param allowUnknown If true, allow construction of the Peer even if the domain name
	 * lookup fails.
	 * @throws PeerParseException If the string is not valid e.g. if it doesn't contain a 
	 * port.
	 * @throws UnknownHostException If allowUnknown is not set, and a domain name which does
	 * not exist was passed in.
	 */
    public Peer(String physical, boolean allowUnknown) throws PeerParseException, UnknownHostException {
        int offset = physical.lastIndexOf(':'); // ipv6
        if(offset < 0) throw new PeerParseException();
        String host = physical.substring(0, offset);
        addr = new FreenetInetAddress(host, allowUnknown);
        String strport = physical.substring(offset+1);
        try {
            _port = Integer.parseInt(strport);
            if(_port < 0 || _port > 65535) throw new PeerParseException("Invalid port "+_port);
        } catch (NumberFormatException e) {
            throw new PeerParseException(e);
        }
	}

	/**
	 * Create a Peer from a string. This may be an IP address or a domain name. If it
	 * is the latter, the name is primary rather than the IP address; 
	 * getHandshakeAddress() will do a new lookup on the name, and change the IP address
	 * if the domain name has changed.
	 * @param physical The string to be parsed, in the format [ ip or domain name ]:[ port number].
	 * @param allowUnknown If true, allow construction of the Peer even if the domain name
	 * lookup fails.
	 * @param checkHostnameOrIPSyntax If true, validate the syntax of the given DNS hostname or IPv4
	 * IP address
	 * @throws HostnameSyntaxException If the string is not formatted as a proper DNS hostname
	 * or IPv4 IP address
	 * @throws PeerParseException If the string is not valid e.g. if it doesn't contain a 
	 * port.
	 * @throws UnknownHostException If allowUnknown is not set, and a domain name which does
	 * not exist was passed in.
	 */
    public Peer(String physical, boolean allowUnknown, boolean checkHostnameOrIPSyntax) throws HostnameSyntaxException, PeerParseException, UnknownHostException {
        int offset = physical.lastIndexOf(':'); // ipv6
        if(offset < 0) 
        	throw new PeerParseException("No port number: \""+physical+"\"");
        String host = physical.substring(0, offset);
        addr = new FreenetInetAddress(host, allowUnknown, checkHostnameOrIPSyntax);
        String strport = physical.substring(offset+1);
        try {
            _port = Integer.parseInt(strport);
            if(_port < 0 || _port > 65535) throw new PeerParseException("Invalid port "+_port);
        } catch (NumberFormatException e) {
            throw new PeerParseException(e);
        }
    }
    
    public Peer(FreenetInetAddress addr, int port) {
    	this.addr = addr;
    	if(addr == null) throw new NullPointerException();
    	this._port = port;
		if(_port > 65535 || _port < 0) throw new IllegalArgumentException("bogus port");
	}

	public boolean isNull() {
		return _port == 0;
	}
	
	// FIXME same issues as with FreenetInetAddress.laxEquals/equals/strictEquals
	public boolean laxEquals(Object o) {
		if (this == o) {
			return true;
		}
		if (!(o instanceof Peer)) {
			return false;
		}

		final Peer peer = (Peer) o;

		if (_port != peer._port) {
			return false;
		}
		if(!addr.laxEquals(peer.addr))
			return false;
		return true;
	}

	// FIXME same issues as with FreenetInetAddress.laxEquals/equals/strictEquals
	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (!(o instanceof Peer)) {
			return false;
		}

		final Peer peer = (Peer) o;

		if (_port != peer._port) {
			return false;
		}
		if(!addr.equals(peer.addr))
			return false;
		return true;
	}

	public boolean strictEquals(Object o) {
		if (this == o) {
			return true;
		}
		if(o == null) return false;
		if (!(o instanceof Peer)) {
			return false;
		}

		final Peer peer = (Peer) o;

		if (_port != peer._port) {
			return false;
		}
		if(!addr.strictEquals(peer.addr))
			return false;
		return true;
	}

	/**
	 * Get the IP address. Look it up if necessary, but return the last value if it
	 * has ever been looked up before; will not trigger a new lookup if it has been
	 * looked up before.
	 */
	public InetAddress getAddress() {
		return getAddress(true);
	}

	/**
	 * Get the IP address. Look it up if allowed to, but return the last value if it
	 * has ever been looked up before; will not trigger a new lookup if it has been
	 * looked up before.
	 */
	public InetAddress getAddress(boolean doDNSRequest) {
		return addr.getAddress(doDNSRequest);
	}
	
	public InetAddress getAddress(boolean doDNSRequest, boolean allowLocal) throws LocalAddressException {
		InetAddress a = addr.getAddress(doDNSRequest);
		if(a == null) return null;
		if(allowLocal || IPUtil.isValidAddress(a, false)) return a;
		throw new LocalAddressException();
	}
	
	/**
	 * Get the IP address, looking up the hostname if the hostname is primary, even if
	 * it has been looked up before. Typically called on a reconnect attempt, when the
	 * dyndns address may have changed.
	 */
	public InetAddress getHandshakeAddress() {
		return addr.getHandshakeAddress();
	}
	
	@Override
	public int hashCode() {
		return addr.hashCode() + _port;
	}

	public int getPort() {
		return _port;
	}

	@Override
	public String toString() {
		return addr.toString() + ':' + _port;
	}

	@Override
	public void writeToDataOutputStream(DataOutputStream dos) throws IOException {
		addr.writeToDataOutputStream(dos);
		dos.writeInt(_port);
	}

	public FreenetInetAddress getFreenetAddress() {
		return addr;
	}

	public boolean isRealInternetAddress(boolean lookup, boolean defaultVal, boolean allowLocalAddresses) {
		return addr.isRealInternetAddress(lookup, defaultVal, allowLocalAddresses);
	}

	/**
	 * Get the address:port string, but prefer numeric IPs - don't return the name.
	 */
	public String toStringPrefNumeric() {
		return addr.toStringPrefNumeric()+':'+_port;
	}

	public Peer dropHostName() {
		FreenetInetAddress newAddr = addr.dropHostname();
		if(newAddr == null) return null;
		if(addr != newAddr) {
			return new Peer(newAddr, _port);
		} else return this;
	}

	/** Is this peer using IPv6? */
	public boolean isIPv6(boolean defaultValue) {
		if(addr == null) return defaultValue;
		return addr.isIPv6(defaultValue);
	}

	public static class PeerComparator implements Comparator<Peer> {
			@Override
			public int compare(Peer p0, Peer p1) {
				boolean hasHostnameP0 = p0.getFreenetAddress().hasHostname();
				boolean hasHostnameP1 = p1.getFreenetAddress().hasHostname();
				boolean isIpv6P0 = p0.isIPv6(false); // default for "no address yet" 
				boolean isIpv6P1 = p1.isIPv6(false); // default for "no address yet" 
				if (hasHostnameP0 && !hasHostnameP1) {
					return -1;
				} else if (!hasHostnameP0 && hasHostnameP1) {
					return 1;
				} else if (hasHostnameP0) {
					return 0;
				}
				if (isIpv6P0 && !isIpv6P1) {
					return -1;
				} else if (!isIpv6P0 && isIpv6P1) {
					return 1;
				}
				return InetAddressIpv6FirstComparator.COMPARATOR
						.compare(p0.getAddress(), p1.getAddress());
			}
	}
}
