package com.limegroup.gnutella.messages;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;

import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.statistics.DroppedSentMessageStatHandler;
import com.limegroup.gnutella.statistics.ReceivedErrorStat;
import com.limegroup.gnutella.statistics.SentMessageStatHandler;
import com.limegroup.gnutella.util.NetworkUtils;

/**
 * A Gnutella push request, used to download files behind a firewall.
 */

public class PushRequest extends Message implements Serializable {
    private static final int STANDARD_PAYLOAD_SIZE=26;

    public static final long FW_TRANS_INDEX = Integer.MAX_VALUE - 2;
    
    /** The unparsed payload--because I don't care what's inside.
     *  NOTE: IP address is BIG-endian.
     */
    private byte[] payload;

    /**
     * Wraps a PushRequest around stuff snatched from the network.
     * @exception BadPacketException the payload length is wrong
     */
    public PushRequest(byte[] guid, byte ttl, byte hops,
             byte[] payload, int network) throws BadPacketException {
        super(guid, Message.F_PUSH, ttl, hops, payload.length, network);
        if (payload.length < STANDARD_PAYLOAD_SIZE) {
            ReceivedErrorStat.PUSH_INVALID_PAYLOAD.incrementStat();
            throw new BadPacketException("Payload too small: "+payload.length);
        }
        this.payload=payload;
		if(!NetworkUtils.isValidPort(getPort())) {
		    ReceivedErrorStat.PUSH_INVALID_PORT.incrementStat();
			throw new BadPacketException("invalid port");
		}
		String ip = NetworkUtils.ip2string(payload, 20);
		if(!NetworkUtils.isValidAddress(ip)) {
		    ReceivedErrorStat.PUSH_INVALID_ADDRESS.incrementStat();
		    throw new BadPacketException("invalid address: " + ip);
		}
    }

    /**
     * Creates a new PushRequest from scratch.
     *
     * @requires clientGUID.length==16,
     *           0 < index < 2^32 (i.e., can fit in 4 unsigned bytes),
     *           ip.length==4 and ip is in <i>BIG-endian</i> byte order,
     *           0 < port < 2^16 (i.e., can fit in 2 unsigned bytes),
     */
    public PushRequest(byte[] guid, byte ttl,
               byte[] clientGUID, long index, byte[] ip, int port) {
    	this(guid, ttl, clientGUID, index, ip, port, Message.N_UNKNOWN);
    }
    
    /**
     * Creates a new PushRequest from scratch.  Allows the caller to 
     * specify the network.
     *
     * @requires clientGUID.length==16,
     *           0 < index < 2^32 (i.e., can fit in 4 unsigned bytes),
     *           ip.length==4 and ip is in <i>BIG-endian</i> byte order,
     *           0 < port < 2^16 (i.e., can fit in 2 unsigned bytes),
     */
    public PushRequest(byte[] guid, byte ttl,
            byte[] clientGUID, long index, byte[] ip, int port, int network) {
    	super(guid, Message.F_PUSH, ttl, (byte)0, STANDARD_PAYLOAD_SIZE,network);
    	
    	if(clientGUID.length != 16) {
			throw new IllegalArgumentException("invalid guid length: "+
											   clientGUID.length);
		} else if((index&0xFFFFFFFF00000000l)!=0) {
			throw new IllegalArgumentException("invalid index: "+index);
		} else if(ip.length!=4) {
			throw new IllegalArgumentException("invalid ip length: "+
											   ip.length);
        } else if(!NetworkUtils.isValidAddress(ip)) {
            throw new IllegalArgumentException("invalid ip "+NetworkUtils.ip2string(ip));
		} else if(!NetworkUtils.isValidPort(port)) {
			throw new IllegalArgumentException("invalid port: "+port);
		}

        payload=new byte[STANDARD_PAYLOAD_SIZE];
        System.arraycopy(clientGUID, 0, payload, 0, 16);
        ByteOrder.int2leb((int)index,payload,16); //downcast ok
        payload[20]=ip[0]; //big endian
        payload[21]=ip[1];
        payload[22]=ip[2];
        payload[23]=ip[3];
        ByteOrder.short2leb((short)port,payload,24); //downcast ok
    }


    protected void writePayload(OutputStream out) throws IOException {
		out.write(payload);
		SentMessageStatHandler.TCP_PUSH_REQUESTS.addMessage(this);
    }

    public byte[] getClientGUID() {
        byte[] ret=new byte[16];
        System.arraycopy(payload, 0, ret, 0, 16);
        return ret;
    }

    public long getIndex() {
        return ByteOrder.uint2long(ByteOrder.leb2int(payload, 16));
    }

    public boolean isFirewallTransferPush() {
        return (getIndex() == FW_TRANS_INDEX);
    }

    public byte[] getIP() {
        byte[] ret=new byte[4];
        ret[0]=payload[20];
        ret[1]=payload[21];
        ret[2]=payload[22];
        ret[3]=payload[23];
        return ret;
    }

    public int getPort() {
        return ByteOrder.ushort2int(ByteOrder.leb2short(payload, 24));
    }

    public Message stripExtendedPayload() {
        //TODO: if this is too slow, we can alias parts of this, as as the
        //payload.  In fact we could even return a subclass of PushRequest that
        //simply delegates to this.
        byte[] newPayload=new byte[STANDARD_PAYLOAD_SIZE];
        System.arraycopy(payload, 0,
                         newPayload, 0,
                         STANDARD_PAYLOAD_SIZE);
        try {
            return new PushRequest(this.getGUID(), this.getTTL(), this.getHops(),
                                   newPayload, this.getNetwork());
        } catch (BadPacketException e) {
            Assert.that(false, "Standard packet length not allowed!");
            return null;
        }
    }

	// inherit doc comment
	public void recordDrop() {
		DroppedSentMessageStatHandler.TCP_PUSH_REQUESTS.addMessage(this);
	}

    public String toString() {
        return "PushRequest("+super.toString()+" "+
            NetworkUtils.ip2string(getIP())+":"+getPort()+")";
    }

    //Unit tests: tests/com/limegroup/gnutella/messages/PushRequestTest
}
