package com.limegroup.gnutella.messages.vendor;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

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

import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.IncompleteFileDesc;
import com.limegroup.gnutella.PushEndpoint;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UploadManager;
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.downloader.DownloadWorker;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.settings.UploadSettings;
import com.limegroup.gnutella.util.CountingOutputStream;
import com.limegroup.gnutella.util.IntervalSet;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.MultiRRIterator;
import com.limegroup.gnutella.util.NetworkUtils;

/**
 * a response to an HeadPing.  It is a trimmed down version of the standard HEAD response,
 * since we are trying to keep the sizes of the udp packets small.
 * 
 * This message can also be used for punching firewalls if the ping requests so. 
 * Feature like this can be used to allow firewalled nodes to participate more 
 * in download meshes.
 * 
 * Since headpings will be sent by clients who have started to download a file whose download
 * mesh contains  this host, it needs to contain information that will help those clients whether 
 * this host is a good bet to start an http download from.  Therefore, the following information should
 * be included in the response:
 * 
 *  - available ranges of the file 
 *  - queue status
 *  - some altlocs (if space permits)
 * 
 * the queue status can be an integer representing how many people are waiting in the queue.  If 
 * nobody is waiting in the queue and we have slots available, the integer can be negative.  So if
 * we have 3 people on the queue we'd send the integer 3.  If we have nobody on the queue and 
 * two upload slots available we would send -2.  A value of 0 means all upload slots are taken but 
 * the queue is empty.  This information can be used by the downloaders to better judge chances of
 * successful start of the download. 
 * 
 * Format:
 * 
 * 1 byte - features byte
 * 2 byte - response code
 * 4 bytes - vendor id
 * 1 byte - queue status
 * n*8 bytes - n intervals (if requested && file partial && fits in packet)
 * the rest - altlocs (if requested) 
 */
public class HeadPong extends VendorMessage {
	
	private static final Log LOG = LogFactory.getLog(HeadPong.class);
	/**
	 * cache references to the upload manager and file manager for
	 * easier stubbing and testing.
	 */
	private static UploadManager _uploadManager 
		= RouterService.getUploadManager();
	
	private static FileManager _fileManager
		= RouterService.getFileManager();
	
	/**
	 * try to make packets less than this size
	 */
	private static final int PACKET_SIZE = 580;
	
	/**
	 * instead of using the HTTP codes, use bit values.  The first three 
	 * possible values are mutually exclusive though.  DOWNLOADING is
	 * possible only if PARTIAL_FILE is set as well.
	 */
	private static final byte FILE_NOT_FOUND= (byte)0x0;
	private static final byte COMPLETE_FILE= (byte)0x1;
	private static final byte PARTIAL_FILE = (byte)0x2;
	private static final byte FIREWALLED = (byte)0x4;
	private static final byte DOWNLOADING = (byte)0x8;
	
	private static final byte CODES_MASK=(byte)0xF;
	/**
	 * all our slots are full..
	 */
	private static final byte BUSY=(byte)0x7F;
	
	public static final int VERSION = 1;
	
	/**
	 * the features contained in this pong.  Same as those of the originating ping
	 */
	private byte _features;
	
	/**
	 * available ranges
	 */
	private IntervalSet _ranges;
	
	/**
	 * the altlocs that were sent, if any
	 */
	private Set _altLocs;
	
	/**
	 * the firewalled altlocs that were sent, if any
	 */
	private Set _pushLocs;
	
	/**
	 * the queue status, can be negative
	 */
	private int _queueStatus;
	
	/**
	 * whether the other host has the file at all
	 */
	private boolean _fileFound,_completeFile;
	
	/**
	 * the remote host
	 */
	private byte [] _vendorId;
	
	/**
	 * whether the other host can receive tcp
	 */
	private boolean _isFirewalled;
	
	/**
	 * whether the other host is currently downloading the file
	 */
	private boolean _isDownloading;
	
	/**
	 * creates a message object with data from the network.
	 */
	protected HeadPong(byte[] guid, byte ttl, byte hops,
			 int version, byte[] payload)
			throws BadPacketException {
		super(guid, ttl, hops, F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, version, payload);
		
		//we should have some payload
		if (payload==null || payload.length<2)
			throw new BadPacketException("bad payload");
		
		
		//if we are version 1, the first byte has to be FILE_NOT_FOUND, PARTIAL_FILE, 
		//COMPLETE_FILE, FIREWALLED or DOWNLOADING
		if (version == VERSION && 
				payload[1]>CODES_MASK) 
			throw new BadPacketException("invalid payload for version "+version);
		
		try {
    		DataInputStream dais = new DataInputStream(new ByteArrayInputStream(payload));
    		
    		//read and mask the features
    		_features = (byte) (dais.readByte() & HeadPing.FEATURE_MASK);
    		
    		//read the response code
    		byte code = dais.readByte();
    		
    		//if the other host doesn't have the file, stop parsing
    		if (code == FILE_NOT_FOUND) 
    			return;
    		else
    			_fileFound=true;
    		
    		//is the other host firewalled?
    		if ((code & FIREWALLED) == FIREWALLED)
    			_isFirewalled = true;
    		
    		//read the vendor id
    		_vendorId = new byte[4];
    		dais.readFully(_vendorId);
    		
    		//read the queue status
    		_queueStatus = dais.readByte();
    		
    		//if we have a partial file and the pong carries ranges, parse their list
    		if ((code & COMPLETE_FILE) == COMPLETE_FILE) 
    			_completeFile=true;
    		else {
    			//also check if the host is downloading the file
    			if ((code & DOWNLOADING) == DOWNLOADING)
    				_isDownloading=true;
    			
    			if ((_features & HeadPing.INTERVALS) == HeadPing.INTERVALS)
    				_ranges = readRanges(dais);
    		}
    		
    		//parse any included firewalled altlocs
    		if ((_features & HeadPing.PUSH_ALTLOCS) == HeadPing.PUSH_ALTLOCS) 
    			_pushLocs=readPushLocs(dais);
    		
    			
    		//parse any included altlocs
    		if ((_features & HeadPing.ALT_LOCS) == HeadPing.ALT_LOCS) 
    			_altLocs=readLocs(dais);
		} catch(IOException oops) {
			throw new BadPacketException(oops.getMessage());
		}
	}
	
	/**
	 * creates a message object as a response to a udp head request.
	 */
	public HeadPong(HeadPing ping) {
		super(F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, VERSION,
		 		derivePayload(ping));
		setGUID(new GUID(ping.getGUID()));
	}
	
	/**
	 * packs information about the shared file, queue status and altlocs into the body
	 * of the message.
	 * @param ping the original UDP head ping to respond to
	 */
	private static byte [] derivePayload(HeadPing ping)  {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		CountingOutputStream caos = new CountingOutputStream(baos);
		DataOutputStream daos = new DataOutputStream(caos);
		byte retCode=0;
		byte queueStatus;
		URN urn = ping.getUrn();
		FileDesc desc = _fileManager.getFileDescForUrn(urn);
		boolean didNotSendAltLocs=false;
		boolean didNotSendPushAltLocs = false;
		boolean didNotSendRanges = false;
		
		try {
    		byte features = ping.getFeatures();
    		features &= ~HeadPing.GGEP_PING; 
    		daos.write(features);
    		if (LOG.isDebugEnabled())
    			LOG.debug("writing features "+features);
    		
    		//if we don't have the file..
    		if (desc == null) {
    			LOG.debug("we do not have the file");
    			daos.write(FILE_NOT_FOUND);
    			return baos.toByteArray();
    		}
    		
    		//if we can't receive unsolicited tcp...
    		if (!RouterService.acceptedIncomingConnection())
    			retCode = FIREWALLED;
    		
    		//we have the file... is it complete or not?
    		if (desc instanceof IncompleteFileDesc) {
    			retCode = (byte) (retCode | PARTIAL_FILE);
    			
    			//also check if the file is currently being downloaded 
    			//or is waiting for sources.  This does not care for queued downloads.
    			IncompleteFileDesc idesc = (IncompleteFileDesc)desc;
    			if (idesc.isActivelyDownloading())
    				retCode = (byte) (retCode | DOWNLOADING);
    		}
    		else 
    			retCode = (byte) (retCode | COMPLETE_FILE);
    		
    		daos.write(retCode);
    		
    		if(LOG.isDebugEnabled())
    			LOG.debug("our return code is "+retCode);
    		
    		//write the vendor id
    		daos.write(F_LIME_VENDOR_ID);
    		
    		//get our queue status.
    		int queueSize = _uploadManager.getNumQueuedUploads();
    		
    		if (queueSize == UploadSettings.UPLOAD_QUEUE_SIZE.getValue())
    			queueStatus = BUSY;
    		else if (queueSize > 0) 
    			queueStatus = (byte) queueSize;
    		 else 	
    			//optimistic value
    			queueStatus =  (byte)
    				(_uploadManager.uploadsInProgress() - 
    						UploadSettings.HARD_MAX_UPLOADS.getValue() );
    		
    		//write out the return code and the queue status
    		daos.writeByte(queueStatus);
    		
    		if (LOG.isDebugEnabled())
    			LOG.debug("our queue status is "+queueStatus);
    		
    		//if we sent partial file and the remote asked for ranges, send them 
    		if (retCode == PARTIAL_FILE && ping.requestsRanges()) 
    			didNotSendRanges=!writeRanges(caos,desc);
    		
    		//if we have any firewalled altlocs and enough room in the packet, add them.
    		if (ping.requestsPushLocs()){
    			boolean FWTOnly = (features & HeadPing.FWT_PUSH_ALTLOCS) ==
    				HeadPing.FWT_PUSH_ALTLOCS;
                
                if (FWTOnly) {
                    AlternateLocationCollection push = RouterService.getAltlocManager().getPush(urn,true);
                    synchronized(push) {
                        didNotSendPushAltLocs = !writePushLocs(caos,push.iterator());
                    }
                } else {
                    AlternateLocationCollection push = RouterService.getAltlocManager().getPush(urn,true);
                    AlternateLocationCollection fwt = RouterService.getAltlocManager().getPush(urn,false);
                    synchronized(push) {
                        synchronized(fwt) {
                            didNotSendPushAltLocs = 
                                !writePushLocs(caos,
                                        new MultiRRIterator(new Iterator[]{push.iterator(),fwt.iterator()}));
                        }
                    }
                }
    		}
    		
    		//now add any non-firewalled altlocs in case they were requested. 
    		if (ping.requestsAltlocs()) {
                AlternateLocationCollection col = RouterService.getAltlocManager().getDirect(urn);
                synchronized(col) {
                    didNotSendAltLocs=!writeLocs(caos, col.iterator());
                }
            }
			
		} catch(IOException impossible) {
			ErrorService.error(impossible);
		}
		
		//done!
		byte []ret = baos.toByteArray();
		
		//if we did not add ranges or altlocs due to constraints, 
		//update the flags now.
		
		if (didNotSendRanges){
			LOG.debug("not sending ranges");
			ret[0] = (byte) (ret[0] & ~HeadPing.INTERVALS);
		}
		if (didNotSendAltLocs){
			LOG.debug("not sending altlocs");
			ret[0] = (byte) (ret[0] & ~HeadPing.ALT_LOCS);
		}
		if (didNotSendPushAltLocs){
			LOG.debug("not sending push altlocs");
			ret[0] = (byte) (ret[0] & ~HeadPing.PUSH_ALTLOCS);
		}
		return ret;
	}
	
	/**
	 * 
	 * @return whether the alternate location still has the file
	 */
	public boolean hasFile() {
		return _fileFound;
	}
	
	/**
	 * 
	 * @return whether the alternate location has the complete file
	 */
	public boolean hasCompleteFile() {
		return hasFile() && _completeFile;
	}
	
	/**
	 * 
	 * @return the available ranges the alternate location has
	 */
	public IntervalSet getRanges() {
		return _ranges;
	}
	
	/**
	 * 
	 * @return set of <tt>Endpoint</tt> 
	 * containing any alternate locations this alternate location returned.
	 */
	public Set getAltLocs() {
		return _altLocs;
	}
	
	/**
	 * 
	 * @return set of <tt>PushEndpoint</tt>
	 * containing any firewalled locations this alternate location returned.
	 */
	public Set getPushLocs() {
		return _pushLocs;
	}
	
	/**
	 * @return all altlocs carried in the pong as 
	 * set of <tt>RemoteFileDesc</tt>
	 */
	public Set getAllLocsRFD(RemoteFileDesc original){
		Set ret = new HashSet();
		
		if (_altLocs!=null)
			for(Iterator iter = _altLocs.iterator();iter.hasNext();) {
				IpPort current = (IpPort)iter.next();
				ret.add(new RemoteFileDesc(original,current));
			}
		
		if (_pushLocs!=null)
			for(Iterator iter = _pushLocs.iterator();iter.hasNext();) {
				PushEndpoint current = (PushEndpoint)iter.next();
				ret.add(new RemoteFileDesc(original,current));
			}
		
		return ret;
	}
	
    /**
     * updates the rfd with information in this pong
     */
    public void updateRFD(RemoteFileDesc rfd) {
        // if the rfd claims its busy, ping it again in a minute
        // (we're obviously using HeadPings, so its cheap to ping it sooner 
        // rather than later)
        if (isBusy())
            rfd.setRetryAfter(DownloadWorker.RETRY_AFTER_NONE_ACTIVE);
        rfd.setQueueStatus(getQueueStatus());
        rfd.setAvailableRanges(getRanges());
        rfd.setSerializeProxies();
    }
    
	/**
	 * 
	 * @return the remote vendor as string
	 */
	public String getVendor() {
		return new String(_vendorId);
	}
	
	/**
	 * 
	 * @return whether the remote is firewalled and will need a push
	 */
	public boolean isFirewalled() {
		return _isFirewalled;
	}
	
	public int getQueueStatus() {
		return _queueStatus;
	}
	
	public boolean isBusy() {
		return _queueStatus >= BUSY;
	}
	
	public boolean isDownloading() {
		return _isDownloading;
	}
    
    /**
     * @return whether the host that returned this pong supports ggep
     */
    public boolean isGGEPPong() {
        return (_features & HeadPing.GGEP_PING) != 0;
    }
    
    public String toString() {
        return "HeadPong: isGGEP "+ isGGEPPong()+
            " hasFile "+hasFile()+
            " hasCompleteFile "+hasCompleteFile()+
            " isDownloading "+isDownloading()+
            " isFirewalled "+isFirewalled()+
            " queue rank "+getQueueStatus()+
            " \nranges "+getRanges()+
            " \nalts "+getAltLocs()+
            " \npushalts "+getPushLocs();
    }
	
	//*************************************
	//utility methods
	//**************************************
	
	/**
	 * reads available ranges from an inputstream
	 */
	private final IntervalSet readRanges(DataInputStream dais)
		throws IOException{
		int rangeLength=dais.readUnsignedShort();
		byte [] ranges = new byte [rangeLength];
		dais.readFully(ranges);
		return IntervalSet.parseBytes(ranges);
	}
	
	/**
	 * reads firewalled alternate locations from an input stream
	 */
	private final Set readPushLocs(DataInputStream dais) 
		throws IOException, BadPacketException {
		int size = dais.readUnsignedShort();
		byte [] altlocs = new byte[size];
		dais.readFully(altlocs);
		Set ret = new HashSet();
		ret.addAll(NetworkUtils.unpackPushEPs(new ByteArrayInputStream(altlocs)));
		return ret;
	}
	
	/**
	 * reads non-firewalled alternate locations from an input stream
	 */
	private final Set readLocs(DataInputStream dais) 
		throws IOException, BadPacketException {
		int size = dais.readUnsignedShort();
		byte [] altlocs = new byte[size];
		dais.readFully(altlocs);
		Set ret = new HashSet();
		ret.addAll(NetworkUtils.unpackIps(altlocs));
		return ret;
	}
	
	
	/**
	 * @param daos the output stream to write the ranges to
	 * @return if they were written or not.
	 */
	private static final boolean writeRanges(CountingOutputStream caos,
			FileDesc desc) throws IOException{
		DataOutputStream daos = new DataOutputStream(caos);
		LOG.debug("adding ranges to pong");
		IncompleteFileDesc ifd = (IncompleteFileDesc) desc;
		byte [] ranges =ifd.getRangesAsByte();
		
		//write the ranges only if they will fit in the packet
		if (caos.getAmountWritten()+2 + ranges.length <= PACKET_SIZE) {
			LOG.debug("added ranges");
			daos.writeShort((short)ranges.length);
			caos.write(ranges);
			return true;
		} 
		else { //the ranges will not fit - say we didn't send them.
			LOG.debug("ranges will not fit :(");
			return false;
		}
	}
	
	private static final boolean writePushLocs(CountingOutputStream caos, Iterator pushlocs) 
    throws IOException {
	
        if (!pushlocs.hasNext())
            return false;

        //push altlocs are bigger than normal altlocs, however we 
        //don't know by how much.  The size can be between
        //23 and 47 bytes.  We assume its 47.
        int available = (PACKET_SIZE - (caos.getAmountWritten()+2)) / 47;
        
        // if we don't have any space left, we can't send any pushlocs
        if (available == 0)
            return false;
        
		if (LOG.isDebugEnabled())
			LOG.debug("trying to add up to "+available+ " push locs to pong");
		
        long now = System.currentTimeMillis();
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
        while (pushlocs.hasNext() && available > 0) {
            PushAltLoc loc = (PushAltLoc) pushlocs.next();

            if (loc.getPushAddress().getProxies().isEmpty()) {
                pushlocs.remove();
                continue;
            }
            
            if (loc.canBeSent(AlternateLocation.MESH_PING)) {
                baos.write(loc.getPushAddress().toBytes());
                available --;
                loc.send(now,AlternateLocation.MESH_PING);
            } else if (!loc.canBeSentAny())
                pushlocs.remove();
        }
		
		if (baos.size() == 0) {
			//altlocs will not fit or none available - say we didn't send them
			LOG.debug("did not send any push locs");
			return false;
		} else { 
			LOG.debug("adding push altlocs");
            ByteOrder.short2beb((short)baos.size(),caos);
			baos.writeTo(caos);
			return true;
		}
	}
	
	private static final boolean writeLocs(CountingOutputStream caos, Iterator altlocs) 
    throws IOException {
		
		//do we have any altlocs?
		if (!altlocs.hasNext())
			return false;
        
        //how many can we fit in the packet?
        int toSend = (PACKET_SIZE - (caos.getAmountWritten()+2) ) /6;
        
        if (toSend == 0)
            return false;
        
		if (LOG.isDebugEnabled())
			LOG.debug("trying to add up to "+ toSend +" locs to pong");
        
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int sent = 0;
        long now = System.currentTimeMillis();
		while(altlocs.hasNext() && sent < toSend) {
            DirectAltLoc loc = (DirectAltLoc) altlocs.next();
            if (loc.canBeSent(AlternateLocation.MESH_PING)) {
                loc.send(now,AlternateLocation.MESH_PING);
                baos.write(loc.getHost().getInetAddress().getAddress());
                ByteOrder.short2leb((short)loc.getHost().getPort(),baos);
                sent++;
            } else if (!loc.canBeSentAny())
                altlocs.remove();
        }
		
		LOG.debug("adding altlocs");
		ByteOrder.short2beb((short)baos.size(),caos);
		baos.writeTo(caos);
		return true;
			
	}
	
}
	
