package com.limegroup.gnutella;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.downloader.URLRemoteFileDesc;
import com.limegroup.gnutella.http.HTTPConstants;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.IntervalSet;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.IpPortImpl;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;

/**
 * A reference to a single file on a remote machine.  In this respect
 * RemoteFileDesc is similar to a URL, but it contains Gnutella-
 * specific data as well, such as the server's 16-byte GUID.<p>
 *
 * This class is serialized to disk as part of the downloads.dat file.  Hence
 * you must be very careful before making any changes.  Deleting or changing the
 * types of fields is DISALLOWED.  Adding field a F is acceptable as long as the
 * readObject() method of this initializes F to a reasonable value when
 * reading from older files where the fields are not present.  This is exactly
 * what we do with _urns and _browseHostEnabled.  On the other hand, older
 * version of LimeWire will simply discard any extra fields F if reading from a
 * newer serialized file.  
 */
public class RemoteFileDesc implements IpPort, Serializable, FileDetails {
    
    private static final Log LOG = LogFactory.getLog(RemoteFileDesc.class);
    
    private static final long serialVersionUID = 6619479308616716538L;
    
    private static final int COPY_INDEX = Integer.MAX_VALUE;

    /** bogus IP we assign to RFDs whose real ip is unknown */
    public static final String BOGUS_IP = "1.1.1.1";
    
	private final String _host;
	private final int _port;
	private final String _filename; 
	private final long _index;
	private final byte[] _clientGUID;
	private final int _speed;
	private final int _size;
	private final boolean _chatEnabled;
    private final int _quality;
    private final boolean _replyToMulticast;

    /**
     *  RemoteFileDesc can only be constructed with a single piece of metadata.
     *  However, historically RemoteFileDesc stored an array of metadata.  Hence
     *  we must be prepared to read this data from a serialized downloads.dat
     *  file.  In other words, _xmlDocs is typically null or a single non-null
     *  element, unless this was deserialized from an older version.  
	 * 
	 *  INVARIANT: _xmlDocs != null -> _xmlDocs.length != 0
	 */
    private LimeXMLDocument[] _xmlDocs;
	private Set /* of URN*/  _urns;

    /**
     * Boolean indicating whether or not the remote host has browse host 
     * enabled.
     */
	private boolean _browseHostEnabled;

    private boolean _firewalled;
    private String _vendor;
    private long _timestamp;
    
    /**
     * Whether or not the remote host supports HTTP/1.1
     * This is purposely NOT IMMUTABLE.  Before we connect,
     * we can only assume the remote host supports HTTP/1.1 by
     * looking at the set of URNs.  If any exist, we assume
     * HTTP/1.1 is supported (because URNs were added to Gnutella
     * after HTTP/1.1).  Once we connect, this value is set to
     * be whatever the host reports in the response line.
     *
     * When deserializing, this value may be wrong for older download.dat
     * files.  (Older versions will always set this to false, because
     * the field did not exist.)  To counter that, when deserializing,
     * if this is false, we set it to true if any URNs are present.
     */
    private boolean _http11;
    
    /**
     * The <tt>PushEndpoint</tt> for this RFD.
     * if null, the rfd is not behind a push proxy.
     */
    private transient PushEndpoint _pushAddr;
		

    /**
     * The list of available ranges.
     * This is NOT SERIALIZED.
     */
    private transient IntervalSet _availableRanges = null;
    
    /**
     * The last known queue status of the remote host
     * negative values mean free slots
     */
    private transient int _queueStatus = Integer.MAX_VALUE;
    
    /**
     * The number of times this download has failed while attempting
     * to transfer data.
     */
    private transient int _failedCount = 0;

    /**
     * The earliest time to retry this host in milliseconds since 01-01-1970
     */
    private transient volatile long _earliestRetryTime = 0;

    /**
     * The cached hash code for this RFD.
     */
    private transient int _hashCode = 0;

    /**
     * Whether or not THEX retrieval has failed with this host.
     */
    private transient boolean _THEXFailed = false;

    /**
     * The cached RemoteHostData for this rfd.
     */
    private transient RemoteHostData _hostData = null;
    
    /**
     * Whether or not this RFD is/was used for downloading.
     */
    private transient volatile boolean _isDownloading = false;
    
    /**
     * The creation time of this file.
     */
    private transient long _creationTime;
    
    /**
     * Whether to serialize the push proxies
     */
    private transient volatile boolean _serializeProxies = false;
    
    /**
     * A map of various properties we want to serialize.  Currently we use
     * this object only during de/serialization, but we keep it cached if we
     * ever create one
     */
    private Map propertiesMap; 
    
    /**
     * Constructs a new RemoteFileDesc exactly like the other one,
     * but with a different remote host.
     *
     * It is okay to use the same internal structures
     * for URNs because the Set is immutable.
     */
    public RemoteFileDesc(RemoteFileDesc rfd, IpPort ep) {
        this( ep.getAddress(),              // host
              ep.getPort(),                 // port
              COPY_INDEX,                   // index (unknown)
              rfd.getFileName(),            // filename
              rfd.getSize(),                // filesize
              rfd.getClientGUID(),          // client GUID
              0,                            // speed
              false,                        // chat capable
              2,                            // quality
              false,                        // browse hostable
              rfd.getXMLDocument(),              // xml doc
              rfd.getUrns(),                // urns
              false,                        // reply to MCast
              false,                        // is firewalled
              AlternateLocation.ALT_VENDOR, // vendor
              System.currentTimeMillis(),   // timestamp
              Collections.EMPTY_SET,        // push proxies
              rfd.getCreationTime(),       // creation time
              0,                            // firewalled transfer
              null);                       // no PE cause not firewalled
    }
    
    /**
     * Constructs a new RemoteFileDesc exactly like the other one,
     * but with a different push proxy host.  Will be handy when processing
     * head pongs.
     */
    public RemoteFileDesc(RemoteFileDesc rfd, PushEndpoint pe){
    	this( pe.getAddress(),              // host - ignored
                pe.getPort(),                 // port -ignored
                COPY_INDEX,                   // index (unknown)
                rfd.getFileName(),            // filename
                rfd.getSize(),                // filesize
                DataUtils.EMPTY_GUID,         // guid
                rfd.getSpeed(),                            // speed
                false,                        // chat capable
                rfd.getQuality(),                            // quality
                false,                        // browse hostable
                rfd.getXMLDocument(),              // xml doc
                rfd.getUrns(),                // urns
                false,                        // reply to MCast
                true,                        // is firewalled
                AlternateLocation.ALT_VENDOR, // vendor
                System.currentTimeMillis(),   // timestamp
                null,
                rfd.getCreationTime(),	// creation time
                0,
                pe);                // use existing PE
    }

	/** 
     * Constructs a new RemoteFileDesc with metadata.
     *
	 * @param host the host's ip
	 * @param port the host's port
	 * @param index the index of the file that the client sent
	 * @param filename the name of the file
	 * @param size the completed size of this file
	 * @param clientGUID the unique identifier of the client
	 * @param speed the speed of the connection
     * @param chat true if the location is chattable
     * @param quality the quality of the connection, where 0 is the
     *  worst and 3 is the best.  (This is the same system as in the
     *  GUI but on a 0 to N-1 scale.)
	 * @param browseHost specifies whether or not the remote host supports
	 *  browse host
	 * @param xmlDoc the <tt>LimeXMLDocument</tt> for the response
	 * @param urns the <tt>Set</tt> of <tt>URN</tt>s for the file
	 * @param replyToMulticast true if its from a reply to a multicast query
	 * @param firewalled true if the host is firewalled
	 * @param vendor the vendor of the remote host
	 * @param timestamp the time this RemoteFileDesc was instantiated
	 * @param proxies the push proxies for this host
	 * @param createTime the network-wide creation time of this file
	 * @throws <tt>IllegalArgumentException</tt> if any of the arguments are
	 *  not valid
     * @throws <tt>NullPointerException</tt> if the host argument is 
     *  <tt>null</tt> or if the file name is <tt>null</tt>
	 */
	public RemoteFileDesc(String host, int port, long index, String filename,
						  int size, byte[] clientGUID, int speed, 
						  boolean chat, int quality, boolean browseHost, 
						  LimeXMLDocument xmlDoc, Set urns,
						  boolean replyToMulticast, boolean firewalled, 
                          String vendor, long timestamp,
                          Set proxies, long createTime) {
        this(host, port, index, filename, size, clientGUID, speed, chat,
             quality, browseHost, xmlDoc, urns, replyToMulticast, firewalled,
             vendor, timestamp, proxies, createTime, 0, null);
    }

	/** 
     * Constructs a new RemoteFileDesc with metadata.
     *
	 * @param host the host's ip
	 * @param port the host's port
	 * @param index the index of the file that the client sent
	 * @param filename the name of the file
	 * @param clientGUID the unique identifier of the client
	 * @param speed the speed of the connection
     * @param chat true if the location is chattable
     * @param quality the quality of the connection, where 0 is the
     *  worst and 3 is the best.  (This is the same system as in the
     *  GUI but on a 0 to N-1 scale.)
     * @param xmlDocs the array of XML documents pertaining to this file
	 * @param browseHost specifies whether or not the remote host supports
	 *  browse host
	 * @param xmlDoc the <tt>LimeXMLDocument</tt> for the response
	 * @param urns the <tt>Set</tt> of <tt>URN</tt>s for the file
	 * @param replyToMulticast true if its from a reply to a multicast query
	 *
	 * @throws <tt>IllegalArgumentException</tt> if any of the arguments are
	 *  not valid
     * @throws <tt>NullPointerException</tt> if the host argument is 
     *  <tt>null</tt> or if the file name is <tt>null</tt>
	 */
	public RemoteFileDesc(String host, int port, long index, String filename,
						  int size, byte[] clientGUID, int speed, 
						  boolean chat, int quality, boolean browseHost, 
						  LimeXMLDocument xmlDoc, Set urns,
						  boolean replyToMulticast, boolean firewalled, 
                          String vendor, long timestamp,
                          Set proxies, long createTime, 
                          int FWTVersion) {
		this(host,port,index,filename,size, clientGUID,speed,chat,quality,browseHost,
                xmlDoc, urns, replyToMulticast, firewalled,vendor,timestamp,proxies,
                createTime, FWTVersion, null); // create pe if firewalled
	}
	
	public RemoteFileDesc(String host, int port, long index, String filename,
	        			int size,int speed,boolean chat, int quality, boolean browseHost,
	        			LimeXMLDocument xmlDoc, Set urns, boolean replyToMulticast,
	        			boolean firewalled, String vendor,long timestamp,long createTime,
	        			PushEndpoint pe) {
        this(host,port,index,filename,size,null,speed,chat,quality,browseHost,xmlDoc,urns,
                replyToMulticast,firewalled,vendor,timestamp,null,createTime,0,pe); // use exising pe
    }
    
    /**
     * Actual constructor.  If the firewalled flag is set and a PE object is passed it is used, if 
     * no PE object is passed a new one is created. 
     */
    private RemoteFileDesc (String host, int port, long index, String filename,
            int size, byte[] clientGUID, int speed,boolean chat, int quality, boolean browseHost,
            LimeXMLDocument xmlDoc, Set urns, boolean replyToMulticast,
            boolean firewalled, String vendor,long timestamp,Set proxies, long createTime,
            int FWTVersion, PushEndpoint pe) {
	    
	    if(!NetworkUtils.isValidPort(port)) {
			throw new IllegalArgumentException("invalid port: "+port);
		} 
		if((speed & 0xFFFFFFFF00000000L) != 0) {
			throw new IllegalArgumentException("invalid speed: "+speed);
		} 
		if(filename == null) {
			throw new NullPointerException("null filename");
		}
		if(filename.equals("")) {
			throw new IllegalArgumentException("cannot accept empty string file name");
		}
		if((size & 0xFFFFFFFF00000000L) != 0) {
			throw new IllegalArgumentException("invalid size: "+size);
		}
		if((index & 0xFFFFFFFF00000000L) != 0) {
			throw new IllegalArgumentException("invalid index: "+index);
		}
        if(host == null) {
            throw new NullPointerException("null host");
        }
        
	    _speed = speed;
		_host = host;
		_port = port;
		_index = index;
		_filename = filename;
		_size = size;
        _firewalled = firewalled;
		
		if (firewalled) {
            if (pe != null) 
                _pushAddr = pe;
            else {
                try {
                    _pushAddr = new PushEndpoint(clientGUID,proxies,
                        PushEndpoint.PLAIN, FWTVersion, 
                        new IpPortImpl(_host,_port));
                }catch (UnknownHostException uhe) {
                    throw new IllegalArgumentException("invalid host");
                }
            }
            
            _clientGUID = _pushAddr.getClientGUID();
        } else 
            _clientGUID = clientGUID;
        
        
		_chatEnabled = chat;
        _quality = quality;
		_browseHostEnabled = browseHost;
		_replyToMulticast = replyToMulticast;
        _vendor = vendor;
        _timestamp = timestamp;
        _creationTime = createTime;
		
	if(xmlDoc!=null) //not strictly needed
            _xmlDocs = new LimeXMLDocument[] {xmlDoc};
        else
            _xmlDocs = null;
		if(urns == null) {
			_urns = Collections.EMPTY_SET;
		}
		else {
			_urns = Collections.unmodifiableSet(urns);
		}
        _http11 = ( !_urns.isEmpty() );
	}

    private void readObject(ObjectInputStream stream) 
		throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        //Older downloads.dat files do not have _urns, so _urns will be null
        //(the default Java value).  Hence we also initialize
        //_browseHostEnabled.  See class overview for more details.
        if(_urns == null) {
            _urns = Collections.EMPTY_SET;
            _browseHostEnabled= false;
        } else {
            // It seems that the Urn Set has some java.io.Files
            // inserted into it. See:
            // http://bugs.limewire.com:8080/bugs/searching.jsp?disp1=l&disp2=c&disp3=o&disp4=j&l=141&c=188&m=694_223
            // Here we check for this case and remove the offending object.
            HashSet newUrns = null;
            Iterator iter = _urns.iterator();
            while(iter.hasNext()) {
                Object next = iter.next();
                if(!(next instanceof URN)) {
                    if(newUrns == null) {
                        newUrns = new HashSet();
                        newUrns.addAll(_urns);
                    }
                    newUrns.remove(next);
                }
            }
            if(newUrns != null) {
                _urns = Collections.unmodifiableSet(newUrns);
            }
        }
                
		// preserve the invariant that the LimeXMLDocument array either be
		// null or have at least one element
		if(_xmlDocs != null && _xmlDocs.length == 0) {
			_xmlDocs = null;
		}
        // http11 must be set manually, because older clients did not have this
        // field but did have urns.
        _http11 = ( _http11 || !_urns.isEmpty() );
        
        // if we saved any properties, read them now
        if (propertiesMap != null) {
            String http = (String)propertiesMap.get("_pushAddr");
            if (http != null) {
                try {
                    _pushAddr = new PushEndpoint(http);
                    if (!_firewalled) {
                        Assert.silent(false, "deserialized RFD had PE but wasn't firewalled, "+this+" "+_pushAddr);
                        _firewalled = true;
                    }
                } catch (IOException iox) {}
            }
            // currently, we do not need the map to exist during the life of the object
            // since we will not serialize pe unless told so this lifetime
            propertiesMap = null;
        }
    }
    
    public void setSerializeProxies() {
        _serializeProxies = true;
    }
    
    private void writeObject(ObjectOutputStream stream) throws IOException {
        if (_serializeProxies && _pushAddr != null) {
            if (propertiesMap == null)
                propertiesMap = new HashMap();
            
            // this will also update the PE in case it changed since last serialization
            propertiesMap.put("_pushAddr",_pushAddr.httpStringValue());
        }
        stream.defaultWriteObject();
    }
    
    /** 
     * Accessor for HTTP11.
     *
     * @return Whether or not we think this host supports HTTP11.
     */
    public boolean isHTTP11() {
        return _http11;
    }
    
    /**
     * Mutator for HTTP11.  Should be set after connecting.
     */
    public void setHTTP11(boolean http11) {
        _http11 = http11;
    }
    
    /**
     * Returns true if this is a partial source
     */
    public boolean isPartialSource() {
        return (_availableRanges != null);
    }
    
    /**
     * @return whether this rfd points to myself.
     */
    public boolean isMe() {
        return needsPush() ? 
                Arrays.equals(_clientGUID,RouterService.getMyGUID()) :
                    NetworkUtils.isMe(getHost(),getPort());
    }
    /**
     * Accessor for the available ranges.
     */
    public IntervalSet getAvailableRanges() {
        return (IntervalSet)_availableRanges.clone();
    }

    /**
     * Mutator for the available ranges.
     */
    public void setAvailableRanges(IntervalSet availableRanges) {
        this._availableRanges = availableRanges;
    }
    
    /**
     * updates the push address of the rfd to a new one.
     * This should be done only to update the set of push proxies,
     * features or FWT capability.
     */
    public void setPushAddress(PushEndpoint pe) {
        if (!Arrays.equals(pe.getClientGUID(),this._clientGUID))
                throw new IllegalArgumentException("different clientGUID");
        this._pushAddr=pe;
    }
    
    /**
     * Returns the current failed count.
     */
    public int getFailedCount() {
        return _failedCount;
    }
    
    /**
     * Increments the failed count by one.
     */
    public void incrementFailedCount() {
        _failedCount++;
    }
    
    /**
     * Resets the failed count back to zero.
     */
    public void resetFailedCount() {
        _failedCount = 0;
    }
    
    /**
     * Determines whether or not this RemoteFileDesc was created
     * from an alternate location.
     */
    public boolean isFromAlternateLocation() {
        return "ALT".equals(_vendor);
    }
    
    /**
     * @return true if this host is still busy and should not be retried
     */
    public boolean isBusy() {
        return isBusy(System.currentTimeMillis());
    }
    
    public boolean isBusy(long now) {
        return now < _earliestRetryTime;
    }

    /**
     * @return time to wait until this host will be ready to be retried
     * in seconds
     */
    public int getWaitTime(long now) {
        return (isBusy(now) ? 
                (int) (_earliestRetryTime - now)/1000 + 1:
                0 );
    }

    /**
     * Mutator for _earliestRetryTime. 
     * @param seconds number of seconds to wait before retrying
     */
    public void setRetryAfter(int seconds) {
        if(LOG.isDebugEnabled())
            LOG.debug("setting retry after to be [" + seconds + 
                      "] seconds for " + this);        
        _earliestRetryTime = System.currentTimeMillis() + seconds*1000;
    }
    
    /**
     * The creation time of this file.
     */
    public long getCreationTime() {
        return _creationTime;
    }

	/**
     * @return Returns the _THEXFailed.
     */
    public boolean hasTHEXFailed() {
        return _THEXFailed;
    }

    /**
     * Having THEX with this host is no good. We can get our THEX from anybody,
     * so we won't bother again. 
     */
    public void setTHEXFailed() {
        _THEXFailed = true;
    }
    
    /**
     * Sets this RFD as downloading.
     */
    public void setDownloading(boolean dl) {
        _isDownloading = dl;
    }
    
    /**
     * Determines if this RFD is downloading.
     *
     * @return whether or not this is downloading
     */
    public boolean isDownloading() { return _isDownloading; }

	/**
	 * Accessor for the host ip with this file.
	 *
	 * @return the host ip with this file
	 */
	public final String getHost() {return _host;}

	/**
	 * Accessor for the port of the host with this file.
	 *
	 * @return the file name for the port of the host
	 */
	public final int getPort() {return _port;}

	/**
	 * Accessor for the index this file, which can be <tt>null</tt>.
	 *
	 * @return the file name for this file, which can be <tt>null</tt>
	 */
	public final long getIndex() {return _index;}

	/**
	 * Accessor for the size in bytes of this file.
	 *
	 * @return the size in bytes of this file
	 */
	public final int getSize() {return _size;}
	
	public final long getFileSize() { return _size; }

	/**
	 * Accessor for the file name for this file, which can be <tt>null</tt>.
	 *
	 * @return the file name for this file, which can be <tt>null</tt>
	 */
	public final String getFileName() {return _filename;}

	/**
	 * Accessor for the client guid for this file, which can be <tt>null</tt>.
	 *
	 * @return the client guid for this file, which can be <tt>null</tt>
	 */
	public final byte[] getClientGUID() {return _clientGUID;}

	/**
	 * Accessor for the speed of the host with this file, which can be 
	 * <tt>null</tt>.
	 *
	 * @return the speed of the host with this file, which can be 
	 *  <tt>null</tt>
	 */
	public final int getSpeed() {return _speed;}	
    
    public final String getVendor() {return _vendor;}

	public final boolean chatEnabled() {return _chatEnabled;}
	public final boolean browseHostEnabled() {return _browseHostEnabled;}

	/**
	 * Returns the "quality" of the remote file in terms of firewalled status,
	 * whether or not the remote host has open slots, etc.
	 * 
	 * @return the current "quality" of the remote file in terms of the 
	 *  determined likelihood of the request succeeding
	 */
    public final int getQuality() {return _quality;}

	/**
	 * Returns the <tt>LimeXMLDocument</tt> for this <tt>RemoteFileDesc</tt>, 
	 * which can be <tt>null</tt>.
	 *
	 * @return the <tt>LimeXMLDocument</tt> for this <tt>RemoteFileDesc</tt>, 
	 * which can be <tt>null</tt>.
	 */
    public final LimeXMLDocument getXMLDocument() {
        if (_xmlDocs==null)
            return null;
        else
            return _xmlDocs[0];  //can be null
	}

	/**
	 * Accessor for the <tt>Set</tt> of URNs for this <tt>RemoteFileDesc</tt>.
	 *
	 * @return the <tt>Set</tt> of URNs for this <tt>RemoteFileDesc</tt>
	 */
	public final Set getUrns() {
		return _urns;
	}

	/**
	 * Accessor for the SHA1 URN for this <tt>RemoteFileDesc</tt>.
	 *
	 * @return the SHA1 <tt>URN</tt> for this <tt>RemoteFileDesc</tt>, or 
	 *  <tt>null</tt> if there is none
	 */
	public final URN getSHA1Urn() {
		Iterator iter = _urns.iterator(); 
		while(iter.hasNext()) {
			URN urn = (URN)iter.next();
			// defensively check against null values added.
			if(urn == null) continue;
			if(urn.isSHA1()) {
				return urn;
			}
		}
		return null;
	}

	/**
	 * Returns an <tt>URL</tt> instance for this <tt>RemoteFileDesc</tt>.
	 *
	 * @return an <tt>URL</tt> instance for this <tt>RemoteFileDesc</tt>
	 */
	public URL getUrl() {
		try {
			String fileName = "";
			URN urn = getSHA1Urn();
			if(urn == null) {
				fileName = "/get/"+_index+"/"+_filename;
			} else {
				fileName = HTTPConstants.URI_RES_N2R+urn.httpStringValue();
			}
			return new URL("http", _host, _port, fileName);
		} catch(MalformedURLException e) {
			return null;
		}
	}
	
    /**
     * Determines whether or not this RFD was a reply to a multicast query.
     *
     * @return <tt>true</tt> if this RFD was in reply to a multicast query,
     *  otherwise <tt>false</tt>
     */
	public final boolean isReplyToMulticast() {
	    return _replyToMulticast;
    }

    /**
     * Determines whether or not this host reported a private address.
     *
     * @return <tt>true</tt> if the address for this host is private,
     *  otherwise <tt>false</tt>.  If the address is unknown, returns
     *  <tt>true</tt>
     *
     * TODO:: use InetAddress in this class for the host so that we don't 
     * have to go through the process of creating one each time we check
     * it it's a private address
     */
	public final boolean isPrivate() {
        return NetworkUtils.isPrivateAddress(_host);
	}
    
    public boolean isFirewalled() {
        return _firewalled;
    }

    /**
     * Accessor for the <tt>Set</tt> of <tt>PushProxyInterface</tt>s for this
     * file -- can be empty, but is guaranteed to be non-null.
     *
     * @return the <tt>Set</tt> of proxy hosts that will accept push requests
     *  for this host -- can be empty
     */
    public final Set getPushProxies() {
    	if (_pushAddr!=null)
    		return _pushAddr.getProxies();
    	else
    		return Collections.EMPTY_SET;
    }

    /**
     * @return whether this RFD supports firewall-to-firewall transfer.
     * For this to be true we need to have some push proxies, indication that
     * the host supports FWT and we need to know that hosts' external address.
     */
    public final boolean supportsFWTransfer() {
        
        if (_host.equals(BOGUS_IP) ||
                !NetworkUtils.isValidAddress(_host) || 
                NetworkUtils.isPrivateAddress(_host))
            return false;
        
        return _pushAddr == null ? false : _pushAddr.supportsFWTVersion() > 0;
    }

    /**
     * Creates the _hostData lazily and uses as necessary
     */ 
    public final RemoteHostData getRemoteHostData() {
        if(_hostData == null)
            _hostData = new RemoteHostData(_host, _port, _clientGUID);
        return _hostData;
    }

    /**
     * @return true if I am not a multicast host and have a hash.
     * also, if I am firewalled I must have at least one push proxy,
     * otherwise my port and address need to be valid.
     */
    public final boolean isAltLocCapable() {
        boolean ret = getSHA1Urn() != null &&
               !_replyToMulticast;
        
        if (_firewalled)
        	ret = ret && 
				_pushAddr!=null &&
				_pushAddr.getProxies().size() > 0;
		else
             ret= ret &&  
			    NetworkUtils.isValidPort(_port) &&
                !NetworkUtils.isPrivateAddress(_host) &&
                NetworkUtils.isValidAddress(_host);
        
        return ret;
    }
    
    /**
     * 
     * @return whether a push should be sent tho this rfd.
     */
    public boolean needsPush() {
        
        //if replying to multicast, do a push.
        if ( isReplyToMulticast() )
            return true;
        //Return true if rfd is private or unreachable
        if (isPrivate()) {
            // Don't do a push for magnets in case you are in a private network.
            // Note to Sam: This doesn't mean that isPrivate should be true.
            if (this instanceof URLRemoteFileDesc) 
                return false;
            else  // Otherwise obey push rule for private rfds.
                return true;
        }
        else if (!NetworkUtils.isValidPort(getPort()))
            return true;
        
        else return isFirewalled();
    }
    
    /**
     * 
     * @return the push address.
     */
    public PushEndpoint getPushAddr() {
    	return _pushAddr;
    }

	/**
	 * Overrides <tt>Object.equals</tt> to return instance equality
	 * based on the equality of all <tt>RemoteFileDesc</tt> fields.
	 *
	 * @return <tt>true</tt> if all of fields of this 
	 *  <tt>RemoteFileDesc</tt> instance are equal to all of the 
	 *  fields of the specified object, and <tt>false</tt> if this
	 *  is not the case, or if the specified object is not a 
	 *  <tt>RemoteFileDesc</tt>.
	 *
	 * Dynamic values such as _http11, and _availableSources
	 * are not checked here, as they can change and still be considered
	 * the same "remote file".
	 * 
	 * The _host field may be equal for many firewalled locations; 
	 * therefore it is necessary that we distinguish those by their 
	 * client GUIDs
	 */
    public boolean equals(Object o) {
		if(o == this) return true;
        if (! (o instanceof RemoteFileDesc))
            return false;
        RemoteFileDesc other=(RemoteFileDesc)o;
        if (! (nullEquals(_host, other._host) && (_port==other._port)) )
            return false;

        if (_size != other._size)
            return false;
        
        if ( (_clientGUID ==null) != (other._clientGUID==null) )
            return false;
        
        if ( _clientGUID!= null &&
                ! ( Arrays.equals(_clientGUID,other._clientGUID)))
            return false;

        if (_urns.isEmpty() && other._urns.isEmpty())
            return nullEquals(_filename, other._filename);
        else
            return urnSetEquals(_urns, other._urns);
    }
    
    private boolean nullEquals(Object one, Object two) {
        return one == null ? two == null : one.equals(two);
    }
    
    private boolean urnSetEquals(Set one, Set two) {
        for (Iterator iter = one.iterator(); iter.hasNext(); ) {
            if (two.contains(iter.next())) {
                return true;
            }
        }
        return false;
    }

    private boolean byteArrayEquals(byte[] one, byte[] two) {
        return one == null ? two == null : Arrays.equals(one, two);
    }

	/**
	 * Overrides the hashCode method of Object to meet the contract of 
	 * hashCode.  Since we override equals, it is necessary to also 
	 * override hashcode to ensure that two "equal" RemoteFileDescs
	 * return the same hashCode, less we unleash unknown havoc on the
	 * hash-based collections.
	 *
	 * @return a hash code value for this object
	 */
	public int hashCode() {
	   if(_hashCode == 0) {
            int result = 17;
            result = (37* result)+_host.hashCode();
            result = (37* result)+_port;
			result = (37* result)+_size;
            result = (37* result)+_urns.hashCode();
            if (_clientGUID!=null)
                result = (37* result)+(new GUID(_clientGUID)).hashCode();
            _hashCode = result;
        }
		return _hashCode;
	}

    public String toString() {
        return  ("<"+getHost()+":"+getPort()+", "
				 +getFileName().toLowerCase()+">");
    }

    public String getAddress() {
        return getHost();
    }

    public InetAddress getInetAddress() {
        try {
            return InetAddress.getByName(getAddress());
        }catch(UnknownHostException bad){}
        return null;
    }
    
    public void setQueueStatus(int status) {
        _queueStatus = status;
    }
    
    public int getQueueStatus() {
        return _queueStatus;
    }

	public InetSocketAddress getSocketAddress() {
		InetAddress addr = getInetAddress();
		if (addr != null) {
			return new InetSocketAddress(addr, getPort());
		}
		return null;
	}
}
