package com.limegroup.gnutella.uploader;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

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

import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.BandwidthTrackerImpl;
import com.limegroup.gnutella.ByteReader;
import com.limegroup.gnutella.Constants;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UploadManager;
import com.limegroup.gnutella.Uploader;
import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.altlocs.AlternateLocationCollection;
import com.limegroup.gnutella.altlocs.DirectAltLoc;
import com.limegroup.gnutella.altlocs.PushAltLoc;
import com.limegroup.gnutella.http.HTTPConstants;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HTTPMessage;
import com.limegroup.gnutella.http.HTTPRequestMethod;
import com.limegroup.gnutella.http.HTTPUtils;
import com.limegroup.gnutella.http.ProblemReadingHeaderException;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.statistics.BandwidthStat;
import com.limegroup.gnutella.udpconnect.UDPConnection;
import com.limegroup.gnutella.util.CountingOutputStream;
import com.limegroup.gnutella.util.MultiRRIterator;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.StringUtils;

/**
 * Maintains state for an HTTP upload request.  This class follows the
 * State Pattern, delegating its writeResponse method to the appropriate
 * state.  All states except for CONNECTING, COMPLETE, and INTERRUPTED
 * have an associated state class that implements HTTPMessage.
 *
 * Care must be taken to call closeFileStreams whenever a chunk of the
 * transfer is finished, and to call stop when the entire HTTP/1.1
 * session is finished.
 *
 * A single HTTPUploader should be reused for multiple chunks of a single
 * file in an HTTP/1.1 session.  However, multiple HTTPUploaders
 * should be used for multiple files in a single HTTP/1.1 session.
 */
public final class HTTPUploader implements Uploader {
    
	private static final Log LOG = LogFactory.getLog(HTTPUploader.class);
    /**
     * The outputstream -- a CountingOutputStream so that we can
     * keep track of the amount of bytes written.
     * Currently track is only kept for writing a THEX tree, so that
     * progress of the tree and bandwidth measurement may be done.
     */
	private CountingOutputStream _ostream;
	private InputStream _fis;
	private Socket _socket;
	private int _totalAmountReadBefore;
	private int _totalAmountRead;
	private int _amountRead;
	// useful so we don't have to do _uploadEnd - _uploadBegin everywhere
    private int _amountRequested;
	private int _uploadBegin;
	private int _uploadEnd;
	private int _fileSize;
	private final int _index;
	private String _userAgent;
	private boolean _headersParsed;
	private final String _fileName;
	private final String _hostName;
	private int _stateNum = CONNECTING;
	private int _lastTransferStateNum;
	private HTTPMessage _state;
	private boolean _firstReply = true;
	private boolean _containedRangeRequest = false;
	
	private boolean _chatEnabled;
	private boolean _browseEnabled;
    private boolean _supportsQueueing = false;
    private final boolean _hadPassword;
    
    /**
     * True if this is a forcibly shared network file.
     */
    private boolean _isForcedShare = false;
    
    /**
     * whether the remote side indicated they want to receive
     * firewalled altlocs.
     */
    private boolean _wantsFalts = false;
    
    /**
     * the version of the FWT protocol the remote supports.  
     * Non-firewalled hosts should not send this feature.
     * INVARIANT: if this is greater than 0, _wantsFalts is set.
     */
    private int _FWTVersion = 0;

    /**
     * The Watchdog that will kill this uploader if it takes too long.
     */
    private final StalledUploadWatchdog STALLED_WATCHDOG;

	/**
	 * The URN specified in the X-Gnutella-Content-URN header, if any.
	 */
	private URN _requestedURN;

	/**
	 * The descriptor for the file we're uploading.
	 */
	private FileDesc _fileDesc;
    
    /**
     * Indicates that the client to which we are uploading is capable of
     * accepting Queryreplies in the response.
     */
    private boolean _clientAcceptsXGnutellaQueryreplies = false;

    /**
     * The address as described by the "X-Node" header.
     */
    private InetAddress _nodeAddress = null;

    /**
     * The port as described by the "X-Node" header.
     */
    private int _nodePort = -1;
    
    /**
     * The parameters passed to the HTTP Request.
     */
    private Map _parameters = null;

    private BandwidthTrackerImpl bandwidthTracker=null;
    
    /**
     * The alternate locations that have been written out (as good) locations.
     */
    private Set _writtenLocs;
    
    /**
     * The firewalled alternate locations that have been written out as good locations.
     */
    private Set _writtenPushLocs;
    
    /**
     * The maximum number of alts to write per http transfer.
     */
    private static final int MAX_LOCATIONS = 10;
    
    /**
     * The maximum number of firewalled alts to write per http transfer.
     */
    private static final int MAX_PUSH_LOCATIONS = 5;

	/**
	 * The <tt>HTTPRequestMethod</tt> to use for the upload.
	 */
	private HTTPRequestMethod _method;

	/**
	 * Consructor for a "normal" non-push upload.  Note that this can
	 * be a URN get request.
	 *
	 * @param method the <tt>HTTPRequestMethod</tt> for the request
	 * @param fileName the name of the file
	 * @param socket the <tt>Socket</tt> instance to serve the upload over
	 * @param index the index of the file in the set of shared files
	 * @param params the map of parameters in the http request.
     * @param dog the StalledUploadWatchdog to use for monitor stalls.
     * @param hadPassword the get line had a matching password.
     * to initialize this' bandwidth tracker so we have history
	 */
	public HTTPUploader(HTTPRequestMethod method,
	                    String fileName, 
                        Socket socket,
                        int index,
                        Map params,
                        StalledUploadWatchdog dog,
                        boolean hadPassword) {
        STALLED_WATCHDOG = dog;
		_socket = socket;
		_hostName = _socket.getInetAddress().getHostAddress();
		_fileName = fileName;
		_index = index;
		_writtenLocs = null;
        _hadPassword = hadPassword;
		reinitialize(method, params);
    }
    
    /**
     * Reinitializes this uploader for a new request method.
     *
     * @param method the HTTPRequestMethod to change to.
     * @param params the parameter list to change to.
     */
    public void reinitialize(HTTPRequestMethod method, Map params) {
        _method = method;
        _amountRequested = 0;
        _uploadBegin = 0;
        _uploadEnd = 0;
        _headersParsed = false;
        _stateNum = CONNECTING;
        _state = null;
        _nodePort = 0;
        _supportsQueueing = false;
        _requestedURN = null;
        _clientAcceptsXGnutellaQueryreplies = false;
        _parameters = params;
        _totalAmountReadBefore = 0;
        
        // If this is the first time we are initializing it,
        // create a new bandwidth tracker and set a few more variables.
        if( bandwidthTracker == null ) {
            bandwidthTracker = new BandwidthTrackerImpl();
            _totalAmountRead = 0;
            _amountRead = 0;
        }            
        // Otherwise, update the amount read.
        else {
            _totalAmountRead += _amountRead;
            _amountRead = 0;
        }
	}
	
	/**
	 * Sets the FileDesc for this HTTPUploader to use.
	 * 
	 * @param fd the <tt>FileDesc</tt> to use
	 * @throws IOException if the file cannot be read from the disk.
	 */
	public void setFileDesc(FileDesc fd) throws IOException {
		if (LOG.isDebugEnabled())
			LOG.debug("trying to set the fd for uploader "+this+ " with "+fd);
	    _fileDesc = fd;
	    _fileSize = (int)fd.getFileSize();
	    // initializd here because we'll only write locs if a FileDesc exists
	    // only initialize once, so we don't write out previously written locs
	    if( _writtenLocs == null )
	        _writtenLocs = new HashSet();
	    
	    if( _writtenPushLocs == null )
	        _writtenPushLocs = new HashSet(); 
	    
        // if there already was an input stream, close it.
        if( _fis != null ) {
        	if (LOG.isDebugEnabled())
        		LOG.debug(this+ " had an existing stream");
            try {
                _fis.close();
            } catch(IOException ignored) {}
        }
        _fis = _fileDesc.createInputStream();
        _isForcedShare = FileManager.isForcedShare(_fileDesc);
	}

	/**
	 * Initializes the OutputStream for this HTTPUploader to use.
	 * 
	 * @throws IOException if the connection was closed.
	 */
	public void initializeStreams() throws IOException {
	    _ostream = new CountingOutputStream(_socket.getOutputStream());
	}
	    
    
	/**
	 * Starts "uploading" the requested file.  The behavior of the upload,
	 * however, depends on the current upload state.  If the file was not
	 * found, for example, the upload sends a 404 Not Found message, whereas
	 * in the case of a normal upload, the file is transferred as expected.<p>
	 *
	 * This method also handles storing any newly discovered alternate 
	 * locations for this file in the corresponding <tt>FileDesc</tt>.  The
	 * new alternate locations are discovered through the requesting client's
	 * HTTP headers.<p>
	 *
	 * Implements the <tt>Uploader</tt> interface.
	 */
	public void writeResponse() throws IOException {
        _ostream.setIsCounting(_stateNum == THEX_REQUEST);
		try {
			_method.writeHttpResponse(_state, _ostream);
		} catch (IOException e) {
            // Only propogate the exception if they did not read
            // as much as they wanted to.
            if ( amountUploaded() < getAmountRequested() )
                throw e;
		}
		_firstReply = false;
	}

    /**
	 * Closes the outputstream, inputstream, and socket for this upload 
	 * connection if they are not null.
	 *
	 * Implements the <tt>Uploader</tt> interface.
	 */
	public void stop() {
		try {
			if (_ostream != null)
				_ostream.close();
		} catch (IOException e) {}
		try {
			if (_fis != null)
				_fis.close();
		} catch (IOException e) {}
		try {
			if (_socket != null) 
				_socket.close();
		} catch (IOException e) {}
	}
	
	/**
	 * Close the file input stream.
	 */
	public void closeFileStreams() {
        try {
            if( _fis != null )
                _fis.close();
        } catch(IOException e) {}
    }
    
	/**
	 * This method changes the appropriate state class based on
	 * the integer representing the state.  I'm not sure if this
	 * is a good idea, since it results in a case statement, that
	 * i was trying to avoid with.
	 *
	 * Implements the <tt>Uploader</tt> interface.
	 */
	public void setState(int state) {
		_stateNum = state;
		switch (state) {
		case UPLOADING:
			_state = new NormalUploadState(this, STALLED_WATCHDOG);
			break;
        case QUEUED:
            int pos=RouterService.getUploadManager().positionInQueue(_socket);
            _state = new QueuedUploadState(pos,this);
            break;
        case NOT_VALIDATED:
            _state = new LimitReachedUploadState(this, true);
            break;
		case LIMIT_REACHED:
			_state = new LimitReachedUploadState(this);
			break;
		case FREELOADER:     
			_state = new FreeloaderUploadState();
			break;
        case BROWSE_HOST:
            _state = new BrowseHostUploadState(this);
            break;
        case BROWSER_CONTROL:
            _state = new BrowserControlUploadState(this);
            break;
        case PUSH_PROXY:
            _state = new PushProxyUploadState(this);
            break;
        case UPDATE_FILE:
            _state = new UpdateFileState(this);
            break;
		case FILE_NOT_FOUND:
			_state = new FileNotFoundUploadState();
            break;
        case MALFORMED_REQUEST:
            _state = new MalformedRequestState();
            break;
        case UNAVAILABLE_RANGE:
            _state = new UnavailableRangeUploadState(this);
            break;
        case BANNED_GREEDY:
        	_state = new BannedUploadState();
        	break;
        case THEX_REQUEST:
        	_state = new THEXUploadState(this, STALLED_WATCHDOG);
        	break;
		case COMPLETE:
		case INTERRUPTED:
		case CONNECTING:
		    _state = null;
			break;
        default:
            Assert.that(false, "Invalid state: " + state);
		}
		
		if(_state != null)
		    _lastTransferStateNum = state;
	}
	
	/**
	 * Returns the output stream this uploader is writing to.
	 */
	OutputStream getOutputStream() {
        return _ostream;
    }
    
    /**
     * Returns the FileInputStream this uploader is reading from.
     */
	InputStream getInputStream() {
	    return _fis;
    }
    
    /**
     * Returns the InetAddress of the socket we're connected to.
     */
    public InetAddress getConnectedHost() {
        if(_socket == null)
            return null;
        else
            return _socket.getInetAddress();
    }
    
    /**
      * Determines if this is uploading to via a UDP transfer.
      */
    boolean isUDPTransfer() {
        return (_socket instanceof UDPConnection);
    }
    
    /**
     * Returns whether or not the current state wants
     * to close the connection.
     */
    public boolean getCloseConnection() {
        Assert.that(_state != null);
        return _state.getCloseConnection();
    }    
    
	/**
     * Returns the current HTTP Request Method.
     */
	public HTTPRequestMethod getMethod() {
        return _method;
    }
    
    /**
     * Returns the queued position if queued.
     */
    public int getQueuePosition() {
        if( _lastTransferStateNum != QUEUED || _stateNum == INTERRUPTED)
            return -1;
        else
            return RouterService.getUploadManager().positionInQueue(_socket);
    }

	/**
	 * Sets the number of bytes that have been uploaded for this upload.
	 * This is expected to restart from 0 for each chunk of an HTTP/1.1
	 * transfer.
	 *
	 * @param amount the number of bytes that have been uploaded
	 */
	void setAmountUploaded(int amount) {
		int newData = amount - _amountRead;
		if(newData > 0) {
            if (isForcedShare())
                BandwidthStat.HTTP_BODY_UPSTREAM_INNETWORK_BANDWIDTH.addData(newData);
            else
                BandwidthStat.HTTP_BODY_UPSTREAM_BANDWIDTH.addData(newData);
        }
		_amountRead = amount;
	}
    
	/**
	 * Returns whether or not this upload is in what is considered an "inactive"
	 * state, such as completed or aborted.
	 *
	 * @return <tt>true</tt> if this upload is in an inactive state,
	 *  <tt>false</tt> otherwise
	 */
	public boolean isInactive() {
        switch(_stateNum) {
        case COMPLETE:
        case INTERRUPTED:
            return true;
        default:
            return false;
        }
	}
	
	/**
	 * Returns the parameter list of this HTTPUploader.
	 */
	Map getParameters() {
	    return _parameters;
	}
	 
    /** The byte offset where we should start the upload. */
	public int getUploadBegin() {return _uploadBegin;}
    /** Returns the offset of the last byte to send <b>PLUS ONE</b>. */
    public int getUploadEnd() {return _uploadEnd;}
    
    /**
     * Set new upload begin & end values, modifying the amount requested.
     */
    public void setUploadBeginAndEnd(int begin, int end) {
        _uploadBegin = begin;
        _uploadEnd = end;
        _amountRequested = _uploadEnd - _uploadBegin;
    }
    
    /**
     * Whether or not the last request to this HTTPUploader contained
     * a 'Range: ' header, so we can truncate the requested range.
     */
    public boolean containedRangeRequest() {
        return _containedRangeRequest;
    }

	// implements the Uploader interface
	public int getFileSize() {
	    if(_stateNum == THEX_REQUEST)
	        return _fileDesc.getHashTree().getOutputLength();
	    else
	        return _fileSize;
    }
	
	// implements the Uploader interface
	public int getAmountRequested() {
	    if(_stateNum == THEX_REQUEST)
	        return _fileDesc.getHashTree().getOutputLength();
	    else
	        return _amountRequested;
    }

	// implements the Uploader interface
	public int getIndex() {return _index;}

	// implements the Uploader interface
	public String getFileName() {return _fileName;}

	// implements the Uploader interface
	public int getState() {return _stateNum;}
	
	// implements the Uploader interface
	public int getLastTransferState() { return _lastTransferStateNum; }

	// implements the Uploader interface
	public String getHost() {return _hostName;}

	// implements the Uploader interface
	public boolean isChatEnabled() {return _chatEnabled;}
	
	// implements the Uploader interface
	public boolean isBrowseHostEnabled() { return _browseEnabled; }

	// implements the Uploader interface
	public int getGnutellaPort() {return _nodePort;}
	
	//implements the Uploader interface
	public String getUserAgent() { return _userAgent; }
	
	//implements the Uploader interface
	public boolean isHeaderParsed() { return _headersParsed; }
	
	// is a forced network share?
	public boolean isForcedShare() { return _isForcedShare; }

    public boolean supportsQueueing() {
        return _supportsQueueing && isValidQueueingAgent();
	}
	
	public boolean isTHEXRequest() {
		return HTTPConstants.NAME_TO_THEX.equals(
				_parameters.get(UploadManager.SERVICE_ID));
	}
    	
    
    /**
     * Returns an AlternateLocationCollection of alternates that
     * have not been sent out already.
     */
    Set getNextSetOfAltsToSend() {
        AlternateLocationCollection coll = RouterService.getAltlocManager().getDirect(_fileDesc.getSHA1Urn());
        Set ret = null;
        long now = System.currentTimeMillis();
        synchronized(coll) {
            Iterator iter  = coll.iterator();
            for(int i = 0; iter.hasNext() && i < MAX_LOCATIONS;) {
                AlternateLocation al = (AlternateLocation)iter.next();
                if(_writtenLocs.contains(al))
                    continue;
                
                if (al.canBeSent(AlternateLocation.MESH_LEGACY)) {
                    _writtenLocs.add(al);
                    if(ret == null) ret = new HashSet();
                    ret.add(al);
                    i++;
                    al.send(now,AlternateLocation.MESH_LEGACY);
                } else if (!al.canBeSentAny()) 
                    iter.remove();
            }
        }
        return ret == null ? Collections.EMPTY_SET : ret;
     
    }
    
    Set getNextSetOfPushAltsToSend() {
        if (!_wantsFalts)
            return Collections.EMPTY_SET;
        
    	AlternateLocationCollection fwt = 
            RouterService.getAltlocManager().getPush(_fileDesc.getSHA1Urn(), true);
        
        AlternateLocationCollection push = _FWTVersion > 0 ? AlternateLocationCollection.EMPTY : 
            RouterService.getAltlocManager().getPush(_fileDesc.getSHA1Urn(), false);
    	
    	Set ret = null;
    	long now = System.currentTimeMillis();
    	synchronized(push) {
    	    synchronized (fwt) {
    	        Iterator iter  = 
    	        	new MultiRRIterator(new Iterator[]{fwt.iterator(),push.iterator()});
    	        for(int i = 0; iter.hasNext() && i < MAX_PUSH_LOCATIONS;) {
    	            PushAltLoc al = (PushAltLoc)iter.next();
    	            
    	            if(_writtenPushLocs.contains(al))
    	                continue;
    	            
    	            // it is possible to end up having a PE with all
    	            // proxies removed.  In that case we remove it explicitly
    	            if(al.getPushAddress().getProxies().isEmpty()) {
    	                iter.remove();
    	                continue;
    	            }
    	            
                    if (al.canBeSent(AlternateLocation.MESH_LEGACY)) {
                        al.send(now,AlternateLocation.MESH_LEGACY);
                        _writtenPushLocs.add(al);
                        
                        if(ret == null) ret = new HashSet();
                        ret.add(al);
                        i++;
                    } else if (!al.canBeSentAny())
                        iter.remove();
    	        }
    	    }
    	}

        return ret == null ? Collections.EMPTY_SET : ret;
    }
    
    /**
     * Blocks certain vendors from being queued, because of buggy
     * downloading implementations on their side.
     */
    private boolean isValidQueueingAgent() {
        if( _userAgent == null )
            return true;

        return !_userAgent.startsWith("Morpheus 3.0.2");
    }
    
    protected boolean isFirstReply () {
    	return _firstReply;
    }
    
    public InetAddress getNodeAddress() {return _nodeAddress; }
    
    public int getNodePort() {return _nodePort; }
    
    /**
     * The amount of bytes that this upload has transferred.
     * For HTTP/1.1 transfers, this number is the amount uploaded
     * for this specific chunk only.  Uses getTotalAmountUploaded
     * for the entire amount uploaded.
     *
	 * Implements the Uploader interface.
     */
	public int amountUploaded() {
	    if(_stateNum == THEX_REQUEST) {
	        if(_ostream == null)
	            return 0;
	        else
	            return _ostream.getAmountWritten();
	    } else
	        return _amountRead;
    }
	
	/**
	 * The total amount of bytes that this upload and all previous
	 * uploaders have transferred on this socket in this file-exchange.
	 *
	 * Implements the Uploader interface.
	 */
	public int getTotalAmountUploaded() {
	    if(_stateNum == THEX_REQUEST) {
	        if(_ostream == null)
	            return 0;
	        else
	            return _ostream.getAmountWritten();
	    } else {
			if ( _totalAmountReadBefore > 0 )
				return _totalAmountReadBefore + _amountRead;
			else
	        return _totalAmountRead + _amountRead;
    }
    }

	/**
	 * Returns the <tt>FileDesc</tt> instance for this uploader.
	 *
	 * @return the <tt>FileDesc</tt> instance for this uploader, or
	 *  <tt>null</tt> if the <tt>FileDesc</tt> could not be assigned
	 *  from the shared files
	 */
	public FileDesc getFileDesc() {return _fileDesc;}

    boolean getClientAcceptsXGnutellaQueryreplies() {
        return _clientAcceptsXGnutellaQueryreplies;
    }
    
    /**
     * Returns the content URN that the client asked for.
     */
    public URN getRequestedURN() {
        return _requestedURN;
    }

	/**
     * Reads the HTTP header sent by the requesting client -- note that the
	 * 'GET' portion of the request header has already been read.
	 *
	 * @param iStream the input stream to read the headers from.
	 * @throws <tt>IOException</tt> if the connection closes while reading
	 * @throws <tt>ProblemReadingHeaderException</tt> if any header is invalid
	 */
	public void readHeader(InputStream iStream) throws IOException {
        _uploadBegin = 0;
        _uploadEnd = 0;
        _containedRangeRequest = false;
		_clientAcceptsXGnutellaQueryreplies = false;
		_totalAmountReadBefore = 0;
        
		ByteReader br = new ByteReader(iStream);
        
        try {
        	while (true) {
        		// read the line in from the socket.
                String str = br.readLine();

                if ( (str==null) || (str.equals("")) ) 
                    break;


                if (isForcedShare())
                    BandwidthStat.HTTP_HEADER_DOWNSTREAM_INNETWORK_BANDWIDTH.addData(str.length());
                else 
                    BandwidthStat.
                        HTTP_HEADER_DOWNSTREAM_BANDWIDTH.addData(str.length());
                if (LOG.isDebugEnabled())
                	LOG.debug("HTTPUploader.readHeader(): str = " +  str);

                
        		// break out of the loop if it is null or blank

                if      ( readChatHeader(str)        ) ;
                else if ( readRangeHeader(str)       ) ;
                else if ( readUserAgentHeader(str)   ) ;
                else if ( readContentURNHeader(str)  ) ;
                else if ( readAltLocationHeader(str) ) ;
                else if ( readNAltLocationHeader(str)) ;
                else if ( readFAltLocationHeader(str)) ;
                else if ( readNFAltLocationHeader(str));
                else if ( readAcceptHeader(str)      ) ;
                else if ( readQueueVersion(str)      ) ;
                else if ( readNodeHeader(str)        ) ;
                else if ( readFeatureHeader(str)     ) ;
                else if ( readXDownloadedHeader(str) ) ;
        	}
        } catch(ProblemReadingHeaderException prhe) {
            // there was a problem reading the header.. gobble up
            // the rest of the input and rethrow the exception
            while(true) {
                String str = br.readLine();
                if( str == null || str.equals("") )
                 break;
            }
            
            // TODO: record stats for this
            throw prhe;
        } finally {
            // we want to ensure these are always set, regardless
            // of if an exception was thrown.
            
			//if invalid end-index, then upload up to the end of file
			//or mark as unknown to bet when file size is set.
			if( _uploadEnd <= 0 ||
			  _uploadEnd <= _uploadBegin || 
			  _uploadEnd > _fileSize) {
                _uploadEnd = _fileSize;
            }

            _amountRequested = _uploadEnd - _uploadBegin;
            
            _headersParsed = true;
        }
        
	}
	

    /**
     * Read the chat portion of a header.
     * @return true if it had a chat header.
     */
    private boolean readChatHeader(String str) throws IOException {
        if (str.toUpperCase().indexOf("CHAT:") == -1)
            return false;
    
		String sub;
		try {
			sub = str.substring(5);
		} catch (IndexOutOfBoundsException e) {
			throw new ProblemReadingHeaderException();
        }
		sub = sub.trim();
		int colon = sub.indexOf(":");
		String host  = sub.substring(0,colon);
		host = host.trim();
		String sport = sub.substring(colon+1);
		sport = sport.trim();

		int port; 
		try {
			port = java.lang.Integer.parseInt(sport);
		} catch (NumberFormatException e) {
			throw new ProblemReadingHeaderException();
        }
		_chatEnabled = true;
		_browseEnabled = true;
		_nodePort = port;
        
        return true;
    }
    
    /**
	 * Look for X-Downloaded header which represents number 
	 * of bytes for this file already downloaded by peer
	 *
     * @return true if it had a X-Downloaded header
     */
    private boolean readXDownloadedHeader(String str) throws IOException {
        
        if ( !HTTPHeaderName.DOWNLOADED.matchesStartOfString(str) )
            return false;
            
		try {
			str = HTTPUtils.extractHeaderValue(str);
			if ( str != null ) {
				_totalAmountReadBefore = Integer.parseInt(str);
			}
		} 
		catch (NumberFormatException e) {}

		return true;
    }
    
    /**
	 * Look for range header of form, "Range: bytes=", "Range:bytes=",
	 * "Range: bytes ", etc.  Note that the "=" is required by HTTP, but
     * old versions of BearShare do not send it.  The value following the
     * bytes unit will be in the form '-n', 'm-n', or 'm-'.
     *
     * @return true if it had a Range header
     */
    private boolean readRangeHeader(String str) throws IOException {
        // was: != 0, is == -1 (that okay?)
        if ( StringUtils.indexOfIgnoreCase(str, "Range:") == -1 )
            return false;
            
        _containedRangeRequest = true;
            
        //Set 'sub' to the value after the "bytes=" or "bytes ".  Note
        //that we don't validate the data between "Range:" and the
        //bytes.
		String sub;
		String second;
		try {
            int i=str.indexOf("bytes");    //TODO: use constant
            if (i<0)
                throw new ProblemReadingHeaderException(
                     "bytes not present in range");
            i+=6;                          //TODO: use constant
			sub = str.substring(i);
		} catch (IndexOutOfBoundsException e) {
			throw new ProblemReadingHeaderException();
		}
		// remove the white space
        sub = sub.trim();   
        char c;
		// get the first character
		try {
			c = sub.charAt(0);
		} catch (IndexOutOfBoundsException e) {
			throw new ProblemReadingHeaderException();
		}
		// - n  
        if (c == '-') {  
			// String second;
			try {
				second = sub.substring(1);
			} catch (IndexOutOfBoundsException e) {
				throw new ProblemReadingHeaderException();
			}
            second = second.trim();
			try {
                //A range request for "-3" means return the last 3 bytes
                //of the file.  (LW used to incorrectly return bytes
                //0-3.)  
                _uploadBegin = Math.max(0,
                                    _fileSize-Integer.parseInt(second));
				_uploadEnd = _fileSize;
			} catch (NumberFormatException e) {
				throw new ProblemReadingHeaderException();
			}
        }
        else {                
			// m - n or 0 -
            int dash = sub.indexOf("-");
            
            // If the "-" does not exist, the head is incorrectly formatted.
            if(dash == -1) {
                throw new ProblemReadingHeaderException();
            }
			String first = sub.substring(0, dash).trim();
			try {
				_uploadBegin = java.lang.Integer.parseInt(first);
			} catch (NumberFormatException e) {
				throw new ProblemReadingHeaderException();
			}
			try {
				second = sub.substring(dash+1);
			} catch (IndexOutOfBoundsException e) {
				throw new ProblemReadingHeaderException();
			}
            second = second.trim();
            if (!second.equals("")) 
				try {
                    //HTTP range requests are inclusive.  So "1-3" means
                    //bytes 1, 2, and 3.  But _uploadEnd is an EXCLUSIVE
                    //index, so increment by 1.
					_uploadEnd = java.lang.Integer.parseInt(second)+1;
            } catch (NumberFormatException e) {
				throw new ProblemReadingHeaderException();
			}
        }
        
        return true;
    }
    
    /**
     * Read the User-Agent field of the header
     *
     * @return true if the header had a UserAgent field
     */
    private boolean readUserAgentHeader(String str)
		throws FreeloaderUploadingException {
        if ( StringUtils.indexOfIgnoreCase(str, "User-Agent:") == -1 )
            return false;
        
		// check for netscape, internet explorer,
		// or other free riding downoaders
        //Allow them to browse the host though
		if (SharingSettings.ALLOW_BROWSER.getValue() == false
            && !(_stateNum == BROWSE_HOST)  
            && !(_stateNum == BROWSER_CONTROL)  
            && !(_stateNum == PUSH_PROXY)  
			&& !(_fileName.toUpperCase().startsWith("LIMEWIRE"))) {
			// if we are not supposed to read from them
			// throw an exception
			if( (str.indexOf("Mozilla") != -1) ||
			    (str.indexOf("Morpheus") != -1) ||
				(str.indexOf("DA") != -1) ||
				(str.indexOf("Download") != -1) ||
				(str.indexOf("FlashGet") != -1) ||
				(str.indexOf("GetRight") != -1) ||
				(str.indexOf("Go!Zilla") != -1) ||
				(str.indexOf("Inet") != -1) ||
				(str.indexOf("MIIxpc") != -1) ||
				(str.indexOf("MSProxy") != -1) ||
				(str.indexOf("Mass") != -1) ||
				(str.indexOf("MLdonkey") != -1) ||
				(str.indexOf("MyGetRight") != -1) ||
				(str.indexOf("NetAnts") != -1) ||
				(str.indexOf("NetZip") != -1) ||
				(str.indexOf("RealDownload") != -1) ||
				(str.indexOf("SmartDownload") != -1) ||
				(str.indexOf("Teleport") != -1) ||
				(str.indexOf("WebDownloader") != -1) ) {
                if (!_hadPassword)
                    throw new FreeloaderUploadingException();
                
                    
			}
		}
		_userAgent = str.substring(11).trim();
		
		return true;
    }
    
    /**
	 * Read the content URN header
	 *
     * @return true if the header had a contentURN field
     */
    private boolean readContentURNHeader(String str) {
        if ( ! HTTPHeaderName.GNUTELLA_CONTENT_URN.matchesStartOfString(str) )
            return false;

        _requestedURN = HTTPUploader.parseContentUrn(str);
		
		return true;
	}
	
	/**
	 * Read the Alternate Locations header
	 *
	 * @return true if the header had an alternate locations field
	 */
	private boolean readAltLocationHeader(String str) {
        if ( ! HTTPHeaderName.ALT_LOCATION.matchesStartOfString(str) )
            return false;
                
        if(_fileDesc != null) 
            parseAlternateLocations(str, true);
        return true;
    }

    private boolean readNAltLocationHeader(String str) {
        if (!HTTPHeaderName.NALTS.matchesStartOfString(str))
            return false;
        
        if(_fileDesc != null)
            parseAlternateLocations(str, false);
        return true;
    }
    
	private boolean readFAltLocationHeader(String str) {
        if ( ! HTTPHeaderName.FALT_LOCATION.matchesStartOfString(str) )
            return false;
        
        //also set the interested flag
        _wantsFalts=true;
        
        if(_fileDesc != null) 
            parseAlternateLocations(str, true);
        return true;
    }

    private boolean readNFAltLocationHeader(String str) {
        if (!HTTPHeaderName.BFALT_LOCATION.matchesStartOfString(str))
            return false;

        //also set the interested flag
        _wantsFalts=true;
        
        if(_fileDesc != null)
            parseAlternateLocations(str, false);
        return true;
    }
    
    
    

    /** 
     * Reads the Accept heder
     *
     * @return true if the header had an accept field
     */
    private boolean readAcceptHeader(String str) {
        if ( StringUtils.indexOfIgnoreCase(str, "accept:") == -1 )
            return false;
           
        if(StringUtils.indexOfIgnoreCase(str, Constants.QUERYREPLY_MIME_TYPE) != -1)
            _clientAcceptsXGnutellaQueryreplies = true;
            
        return true;
    }	

    private boolean readQueueVersion(String str) {
        if (! HTTPHeaderName.QUEUE_HEADER.matchesStartOfString(str))
            return false;
        
        //String s = HTTPUtils.extractHeaderValue(str);
        //we are not interested in the value at this point, the fact that the
        //header was sent implies that the uploader supports queueing. 
        _supportsQueueing = true;
        return true;
    }

    /** 
     * Reads the X-Node header
     *
     * @return true if the header had an node description value
     */
    private boolean readNodeHeader(final String str) {
        if ( !HTTPHeaderName.NODE.matchesStartOfString(str) )
            return false;
           
        StringTokenizer st = 
            new StringTokenizer(HTTPUtils.extractHeaderValue(str), ":");
        InetAddress tempAddr = null;
        int tempPort = -1;
        // we are expecting 2 tokens - only evalute if you see 2
        if (st.countTokens() == 2) {
            try {
                tempAddr = InetAddress.getByName(st.nextToken().trim());
                tempPort = Integer.parseInt(st.nextToken().trim());
                if (NetworkUtils.isValidPort(tempPort)) {
                    // everything checks out....
                    _nodeAddress = tempAddr;
                    _nodePort = tempPort;
                }
            }
            catch (UnknownHostException badHost) { // crappy host
            }
            catch (NumberFormatException nfe) {} // crappy port
        }
            
        return true;
    }	

	/**
	 * Reads the X-Features header
	 *
	 * @return true if the header had an node description value
	 */
	private boolean readFeatureHeader(String str) {
		if ( !HTTPHeaderName.FEATURES.matchesStartOfString(str) )
			return false;
        str = HTTPUtils.extractHeaderValue(str);
        if (LOG.isDebugEnabled())
        	LOG.debug("reading feature header: "+str);
        StringTokenizer tok = new StringTokenizer(str, ",");
        while (tok.hasMoreTokens()) {
            String feature = tok.nextToken();
            String protocol = "";
            int slash = feature.indexOf("/");
            if(slash == -1) {
                protocol = feature.toLowerCase().trim();
            } else {
                protocol = feature.substring(0, slash).toLowerCase().trim();
            }
            // not interested in the version ...

			if (protocol.equals(HTTPConstants.CHAT_PROTOCOL))
				_chatEnabled = true;
			else if (protocol.equals(HTTPConstants.BROWSE_PROTOCOL))
				_browseEnabled = true;
			else if (protocol.equals(HTTPConstants.QUEUE_PROTOCOL))
				_supportsQueueing = true;
			else if (protocol.equals(HTTPConstants.PUSH_LOCS))
            	_wantsFalts=true;
            else if (protocol.equals(HTTPConstants.FW_TRANSFER)){
                // for this header we care about the version
            	try {
            	    _FWTVersion = (int)HTTPUtils.parseFeatureToken(feature);
            	    _wantsFalts=true;
            	}catch(ProblemReadingHeaderException prhe){
            	    continue;
            	}
            }
			
		}
		return true;
	}
	
	/**
	 * This method parses the "X-Gnutella-Content-URN" header, as specified
	 * in HUGE v0.93.  This assigns the requested urn value for this 
	 * upload, which otherwise remains null.
	 *
	 * @param contentUrnStr the string containing the header
	 * @return a new <tt>URN</tt> instance for the request line, or 
	 *  <tt>null</tt> if there was any problem creating it
	 */
	private static URN parseContentUrn(final String contentUrnStr) {
		String urnStr = HTTPUtils.extractHeaderValue(contentUrnStr);
		
		if(urnStr == null)
		    return URN.INVALID;
		try {
			return URN.createSHA1Urn(urnStr);
		} catch(IOException e) {
		    return URN.INVALID;
		}		
	}
	
	/**
	 * Parses the alternate location header.  The header can contain only one
	 * alternate location, or it can contain many in the same header.
	 * This method will notify DownloadManager of new alternate locations
	 * if the FileDesc is an IncompleteFileDesc.
	 *
	 * @param altHeader the full alternate locations header
	 * @param alc the <tt>AlternateLocationCollector</tt> that reads alternate
	 *  locations should be added to
	 */
	private void parseAlternateLocations(final String altHeader, boolean isGood) {

		final String alternateLocations=HTTPUtils.extractHeaderValue(altHeader);

		URN sha1 =_fileDesc.getSHA1Urn(); 
		
		// return if the alternate locations could not be properly extracted
		if(alternateLocations == null) return;
		StringTokenizer st = new StringTokenizer(alternateLocations, ",");
        while(st.hasMoreTokens()) {
            try {
                // note that the trim method removes any CRLF character
                // sequences that may be used if the sender is using
                // continuations.
                AlternateLocation al = 
                AlternateLocation.create(st.nextToken().trim(),
                                         _fileDesc.getSHA1Urn());
                
                Assert.that(al.getSHA1Urn().equals(sha1));
                
                if (al.isMe())
                    continue;
                
                if(al instanceof PushAltLoc) 
                    ((PushAltLoc)al).updateProxies(isGood);
                // Note: if this thread gets preempted at this point,
                // the AlternateLocationCollectioin may contain a PE
                // without any proxies.
                if(isGood) 
                    RouterService.getAltlocManager().add(al, null);
                else
                    RouterService.getAltlocManager().remove(al, null);
                        
                if (al instanceof DirectAltLoc)
                 	_writtenLocs.add(al);
                else
                 	_writtenPushLocs.add(al); // no problem if we add an existing pushloc
            } catch(IOException e) {
                // just return without adding it.
                continue;
            }
        }
	}

	public void measureBandwidth() {
	    int written = _totalAmountRead + _amountRead;
	    if(_ostream != null)
	        written += _ostream.getAmountWritten();
        bandwidthTracker.measureBandwidth(written);
    }

    public float getMeasuredBandwidth() {
        float retVal = 0;
        try {
            retVal = bandwidthTracker.getMeasuredBandwidth();
        } catch (InsufficientDataException ide) {
            retVal = 0;
        }
        return retVal;
    }
    
    public float getAverageBandwidth() {
        return bandwidthTracker.getAverageBandwidth();
    }
    
    public boolean wantsFAlts() {
    	return _wantsFalts;
    }
    
    public int wantsFWTAlts() {
    	return _FWTVersion;
    }

	// overrides Object.toString
	public String toString() {
        return "<"+_hostName+":"+ _index +">";
//  		return "HTTPUploader:\r\n"+
//  		       "File Name: "+_fileName+"\r\n"+
//  		       "Host Name: "+_hostName+"\r\n"+
//  		       "Port:      "+_port+"\r\n"+
//  		       "File Size: "+_fileSize+"\r\n"+
//  		       "State:     "+_state;
		
	}
}










