package com.limegroup.gnutella.uploader;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;

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

import com.limegroup.gnutella.CreationTimeCache;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.http.ConstantHTTPHeaderValue;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HTTPUtils;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.UploadSettings;
import com.limegroup.gnutella.util.BandwidthThrottle;

/**
 * An implementation of the UploadState interface for a normal upload situation,
 * i.e., the real uploader.  It should send the appropriate header information,
 * followed by the actual file.  
 */
public final class NormalUploadState extends UploadState {
    /** The amount of time that a send/wait cycle should take for throttled
     *  uploads.  This should be short enough to not be noticeable in the GUI,
     *  but long enough so that waits are not called so often as to be
     *  inefficient. */
    private static final Log LOG = LogFactory.getLog(NormalUploadState.class);
	
    private static final int BLOCK_SIZE=1024;
	
	private final int _index;
	private final String _fileName;
	private final int _fileSize;
	private InputStream _fis;
	private int _amountWritten;
    /** @see HTTPUploader#getUploadBegin */
	private int _uploadBegin;
    /** @see HTTPUploader#getUploadEnd */
    private int _uploadEnd;
    private int _amountRequested;
    
    /**
     * The task that periodically checks to see if the uploader has stalled.
     */
    private StalledUploadWatchdog _stalledChecker;

    /**
     * Throttle for the speed of uploads.  The rate will get dynamically
     * reset during the upload if the rate changes.
     */
    private static final BandwidthThrottle THROTTLE = 
        new BandwidthThrottle(getUploadSpeed(), false);
        
    /**
     * UDP throttle.
     */
    private static final BandwidthThrottle UDP_THROTTLE =
        new BandwidthThrottle(getUploadSpeed(), false);


	/**
	 * Constructs a new <tt>NormalUploadState</tt>, establishing all 
	 * invariants.
	 *
	 * @param uploaded the <tt>HTTPUploader</tt>
	 */
	public NormalUploadState(HTTPUploader uploader, 
                                    StalledUploadWatchdog watchdog) {
		super(uploader);

		LOG.debug("creating a normal upload state");

		
		
		_index = UPLOADER.getIndex();	
		_fileName = UPLOADER.getFileName();
		_fileSize = UPLOADER.getFileSize();

		_amountWritten = 0;
		_stalledChecker = watchdog; //new StalledUploadWatchdog();
 	}
 	
 	public static void setThrottleSwitching(boolean on) {
 	    THROTTLE.setSwitching(on);
    }
    
	public void writeMessageHeaders(OutputStream network) throws IOException {
		LOG.debug("writing message headers");
		try {
		    Writer ostream = new StringWriter();
			_fis =  UPLOADER.getInputStream();
			_uploadBegin =  UPLOADER.getUploadBegin();
			_uploadEnd =  UPLOADER.getUploadEnd();
			_amountRequested = UPLOADER.getAmountRequested();
			//guard clause
			if(_fileSize < _uploadBegin)
				throw new IOException("Invalid Range");
			
            
			// Initial OK	
			if( _uploadBegin==0 && _amountRequested==_fileSize ) {
				ostream.write("HTTP/1.1 200 OK\r\n");
			} else {
				ostream.write("HTTP/1.1 206 Partial Content\r\n");
			}
			
            HTTPUtils.writeHeader(HTTPHeaderName.SERVER, ConstantHTTPHeaderValue.SERVER_VALUE, ostream);
            HTTPUtils.writeHeader(HTTPHeaderName.CONTENT_TYPE, getMimeType(), ostream);
            HTTPUtils.writeHeader(HTTPHeaderName.CONTENT_LENGTH, _amountRequested, ostream);
            HTTPUtils.writeDate(ostream);
            HTTPUtils.writeContentDisposition(_fileName, ostream);
			
			// _uploadEnd is an EXCLUSIVE index internally, but HTTP uses an INCLUSIVE index.
			if (_uploadBegin != 0 || _amountRequested != _fileSize) {
			    ostream.write("Content-Range: bytes " + _uploadBegin  +
				    "-" + ( _uploadEnd - 1 )+ "/" + _fileSize + "\r\n");
			}
			
			writeAlts(ostream);
			writeRanges(ostream);
			writeProxies(ostream);
			
			if(FILE_DESC != null) {
				URN urn = FILE_DESC.getSHA1Urn();
				
                if (UPLOADER.isFirstReply()) {
                    // write the creation time if this is the first reply.
                    // if this is just a continuation, we don't need to send
                    // this information again.
                    // it's possible t do that because we don't use the same
                    // uploader for different files
                    CreationTimeCache cache = CreationTimeCache.instance();
                    if (cache.getCreationTime(urn) != null)
                        HTTPUtils.writeHeader(
                            HTTPHeaderName.CREATION_TIME,
                            cache.getCreationTime(urn).toString(),
                            ostream);
                }
            }
            
            // write x-features header once because the downloader is
            // supposed to cache that information anyway
            if (UPLOADER.isFirstReply())
                HTTPUtils.writeFeatures(ostream);

            // write X-Thex-URI header with root hash if we have already 
            // calculated the tigertree
            if (FILE_DESC.getHashTree()!=null)
                HTTPUtils.writeHeader(HTTPHeaderName.THEX_URI, FILE_DESC.getHashTree(), ostream);
            
			ostream.write("\r\n");
			
			_stalledChecker.activate(network);			
			network.write(ostream.toString().getBytes());
        } finally {
			// we do not need to check the return value because
			// if it was stalled, an IOException would have been thrown
			// causing us to fall out to the catch clause
			_stalledChecker.deactivate();
		} 
	}

	public void writeMessageBody(OutputStream ostream) throws IOException {
		LOG.debug("writing message body");
        try {            
            _fis.skip(_uploadBegin);
            upload(ostream);
        } catch(IOException e) {
            _stalledChecker.deactivate(); // no need to kill now
            throw e;
        }
	}

    /**
     * Upload the file, throttling the upload by making use of the
     * BandwidthThrottle class
     * @exception IOException If there is any I/O problem while uploading file
     */
    private void upload(OutputStream ostream) throws IOException {
        // construct the buffer outside of the loop, so we don't
        // have to reconstruct new byte arrays every BLOCK_SIZE.
        byte[] buf = new byte[BLOCK_SIZE];
        while (true) {
            BandwidthThrottle throttle =
                UPLOADER.isUDPTransfer() ? UDP_THROTTLE : THROTTLE;
            throttle.setRate(getUploadSpeed());

            int c = -1;
            // request the bytes from the throttle
            // BLOCKING (only if we need to throttle)
            int allowed = BLOCK_SIZE;
            if(!UPLOADER.isForcedShare())
                allowed = throttle.request(BLOCK_SIZE);
            int burstSent=0;
            try {
                c = _fis.read(buf, 0, allowed);
            } catch(NullPointerException npe) {
                // happens occasionally :(
                throw new IOException(npe.getMessage());
            }
            if (c == -1)
                return;
            //dont upload more than asked
            if( c > (_amountRequested - _amountWritten))
                c = _amountRequested - _amountWritten;
            _stalledChecker.activate(ostream);
            ostream.write(buf, 0, c);
			// we do not need to check the return value because
			// if it was stalled, an IOException would have been thrown
			// causing us to exit immediately.
			_stalledChecker.deactivate();
            
            _amountWritten += c;
            UPLOADER.setAmountUploaded(_amountWritten);
            burstSent += c;           
            //finish uploading if the desired amount 
            //has been uploaded
            if(_amountWritten >= _amountRequested)
                return;
        }
            
    }

	/** 
	 * Eventually this method should determine the mime type of a file fill 
	 * in the details of this later. Assume binary for now. 
	 */
	private String getMimeType() {
        return "application/binary";                  
	}
    
    /**
     * @return the bandwidth for uploads in bytes per second
     */
    private static float getUploadSpeed() {
	    // if the user chose not to limit his uploads
	    // by setting the upload speed to unlimited
	    // set the upload speed to 3.4E38 bytes per second.
	    // This is de facto not limiting the uploads
	    int uSpeed = UploadSettings.UPLOAD_SPEED.getValue();
	    float ret = ( uSpeed == 100 ) ? Float.MAX_VALUE : 
            // if the uploads are limited, take messageUpstream
            // for ultrapeers into account, - don't allow lower 
            // speeds than 1kb/s so uploads won't stall completely
            // if the user accidently sets his connection speed 
            // lower than his message upstream
            Math.max(
                // connection speed is in kbits per second
                ConnectionSettings.CONNECTION_SPEED.getValue() / 8.f 
                // upload speed is in percent
                * uSpeed / 100.f
                // reduced upload speed if we are an ultrapeer
                - RouterService.getConnectionManager()
                .getMeasuredUpstreamBandwidth()*1.f, 1.f )
	        // we need bytes per second
	        * 1024;
	    return ret;
    }
    
	public boolean getCloseConnection() {
	    return false;
	}

	// overrides Object.toString
	public String toString() {
		return "NormalUploadState:\r\n"+
		       "File Name:  "+_fileName+"\r\n"+
		       "File Size:  "+_fileSize+"\r\n"+
		       "File Index: "+_index+"\r\n"+
		       "File Desc:  "+FILE_DESC;
	}	
}

