package com.limegroup.gnutella.messages.vendor;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import org.limewire.io.BadGGEPBlockException;
import org.limewire.io.BadGGEPPropertyException;
import org.limewire.io.GGEP;
import org.limewire.service.ErrorService;

import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.messages.BadPacketException;

/**
 * An UDP equivalent of the HEAD request method with a twist.
 * 
 * Eventually, it will be routed like a push request 
 * to firewalled alternate locations.
 * 
 * As long as the pinging host can receive solicited udp 
 * it can be firewalled as well.
 * 
 * Illustration of [firewalled] NodeA pinging firewalled host NodeB:
 * 
 * 
 * NodeA --------(PUSH_PING,udp)-------->Push
 *    <-------------------(udp)--------- Proxy
 *                                       /|\  | (tcp)
 *                                        |   |
 *                                        |  \|/
 *                                        NodeB
 * 
 */

public class HeadPing extends AbstractVendorMessage implements HeadPongRequestor {
    
    /*
     * Version 1: Initial revision.
     * Version 2: Signals support for understanding TLS info about push proxies of push altlocs.
     */
    
    /** The initial version; expected a binary (non-GGEP) HeadPong response. */
    private static final int EXPECTS_BINARY_RESPONSE_VERSION = 1;
    
    /** The current version. */
	public static final int VERSION = 2;
	
	/**
	 * requsted content of the pong
	 */
	public static final int PLAIN = 0x0;
	public static final int INTERVALS = 0x1;
	public static final int ALT_LOCS = 0x2;
	public static final int PUSH_ALTLOCS=0x4;
	public static final int FWT_PUSH_ALTLOCS=0x8;
	public static final int GGEP_PING=0x10;
	
	
	/** 
	 * a ggep field name containing the client guid of the node we would like
	 * this ping routed to.
	 */
	private static final String GGEP_PUSH = "PUSH";
    
	/**
	 * the feature mask.
	 */
	public static final int FEATURE_MASK=0x1F;

	/** The URN of the file being requested */
	private final URN _urn;
	
	/** The format of the response that we desire */
	private final byte _features;

	/** The GGEP fields in this pong, if any */
	private GGEP _ggep;
	/** 
	 * The client GUID of the host we wish this ping routed to.
	 * null if pinging directly.
	 */ 
	private final GUID _clientGUID;
	
	/**
	 * creates a message object with data from the network.
	 * @param network TODO
	 */
	protected HeadPing(byte[] guid, byte ttl, byte hops,
			 int version, byte[] payload, Network network)
			throws BadPacketException {
		super(guid, ttl, hops, F_LIME_VENDOR_ID, F_UDP_HEAD_PING, version, payload, network);
		
		//see if the payload is valid
		if (getVersion() == VERSION && (payload == null || payload.length < 42))
			throw new BadPacketException();
		
		_features = (byte) (payload [0] & FEATURE_MASK);
		
		//parse the urn string.
		String urnStr = new String(payload,1,41);
		if (!URN.isUrn(urnStr))
			throw new BadPacketException("udp head request did not contain an urn");		
		try {
			_urn = URN.createSHA1Urn(urnStr);
		} catch(IOException oops) {
			throw new BadPacketException("failed to parse an urn");
		}
		
		// parse the GGEP if any
		if ((_features  & GGEP_PING) == GGEP_PING) {
			if (payload.length < 43)
				throw new BadPacketException("no ggep was found.");
			try {
				_ggep = new GGEP(payload, 42, null);
			} catch (BadGGEPBlockException bpx) {
				throw new BadPacketException("invalid ggep block");
			}
		}
		
		// extract the client guid if any
		GUID clientGuid = null;
		if (_ggep != null) {
            if(_ggep.hasKey(GGEP_PUSH)) {
    			try {
    				clientGuid = new GUID(_ggep.getBytes(GGEP_PUSH));
    			} catch (BadGGEPPropertyException noGuid) {}
            }
        } 
		
		_clientGUID=clientGuid;
		
	}
	
	/**
	 * creates a new udp head request.
	 * @param sha1 the urn to get information about.
	 * @param features which features to include in the response
	 */

	public HeadPing(GUID g, URN sha1, int features) {
		this (g,sha1, null, features);
	}
	
	
	public HeadPing(GUID g, URN sha1, GUID clientGUID, int features) {
		super(F_LIME_VENDOR_ID, F_UDP_HEAD_PING, VERSION,
		 		derivePayload(sha1, clientGUID, features));
		_features = (byte)(features & FEATURE_MASK);
		_urn = sha1;
		_clientGUID = clientGUID;
        setGUID(g);
	}

	
	/**
	 * creates a plain udp head request
	 */
	public HeadPing (URN urn) {
		this(new GUID(GUID.makeGuid()),urn, PLAIN);
	}
	

    /**
     * creates a duplicate ping with ttl and hops appropriate for a new
     * vendor message
     */
    public HeadPing (HeadPing original) {
        super(F_LIME_VENDOR_ID,F_UDP_HEAD_PING,original.getVersion(),original.getPayload());
        _features = original.getFeatures();
        _urn = original.getUrn();
        _clientGUID = original.getClientGuid();
        setGUID(new GUID(original.getGUID()));
    }
	
	private static byte [] derivePayload(URN urn, GUID clientGUID, int features) {

		features = features & FEATURE_MASK;

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream daos = new DataOutputStream(baos);
		
		String urnStr = urn.httpStringValue();
		
		GGEP ggep = null;
		if (clientGUID != null) {
			features |= GGEP_PING; // make sure we indicate we'll have ggep.
			ggep = new GGEP();
			ggep.put(GGEP_PUSH,clientGUID.bytes());
		}
		
		try {
			daos.writeByte(features);
			daos.writeBytes(urnStr);
			if ( ggep != null) 
				ggep.write(daos);
		}catch (IOException huh) {
			ErrorService.error(huh);
		}
		
		return baos.toByteArray();
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#getUrn()
     */
	public URN getUrn() {
		return _urn;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#requestsRanges()
     */
	public boolean requestsRanges() {
		return (_features & INTERVALS) == INTERVALS;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#requestsAltlocs()
     */
	public boolean requestsAltlocs() {
		return (_features & ALT_LOCS) == ALT_LOCS;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#requestsPushLocs()
     */
	public boolean requestsPushLocs() {
		return (_features & PUSH_ALTLOCS) == PUSH_ALTLOCS;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#requestsFWTPushLocs()
     */
	public boolean requestsFWTOnlyPushLocs() {
		return (_features & FWT_PUSH_ALTLOCS) == FWT_PUSH_ALTLOCS;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#getFeatures()
     */
	public byte getFeatures() {
		return _features;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.messages.vendor.HeadPongRequestor#getClientGuid()
     */
	public GUID getClientGuid() {
		return _clientGUID;
	}

    public boolean isPongGGEPCapable() {
        return getVersion() > EXPECTS_BINARY_RESPONSE_VERSION;
    }
	

}
