package com.limegroup.gnutella.messages;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.LinkedList;

import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.statistics.DroppedSentMessageStatHandler;
import com.limegroup.gnutella.statistics.SentMessageStatHandler;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.NameValue;

/**
 * A Gnutella ping message.
 */

public class PingRequest extends Message {

    /**
     * various flags related to the SCP ggep field
     */
    public static final byte SCP_ULTRAPEER_OR_LEAF_MASK = 0x1;
    public static final byte SCP_LEAF = 0x0;
    public static final byte SCP_ULTRAPEER = 0x1;
   
    /**
     * GUID to send out for UDP pings.
     */
    public static final GUID UDP_GUID = new GUID();

    /**
     * With the Big Ping and Big Pong extensions pings may have a payload
     */
    private byte[] payload = null;
    
    /**
     * The GGEP blocks carried in this ping - parsed when necessary
     */
    private GGEP _ggep;
    
    /////////////////Constructors for incoming messages/////////////////
    /**
     * Creates a normal ping from data read on the network
     */
    public PingRequest(byte[] guid, byte ttl, byte hops) {
        super(guid, Message.F_PING, ttl, hops, 0);
    }

    /**
     * Creates an incoming group ping. Used only by boot-strap server
     */
    protected PingRequest(byte[] guid, byte ttl, byte hops, byte length) {
        super(guid, Message.F_PING, ttl, hops, length);
    }

    /**
     * Creates a big ping request from data read from the network
     * 
     * @param payload the headers etc. which the big pings contain.
     */
    public PingRequest(byte[] guid, byte ttl, byte hops, byte[] payload) {
        super(guid, Message.F_PING, ttl, hops, payload.length);
        this.payload = payload;
    }

    //////////////////////Constructors for outgoing Pings/////////////
    /**
     * Creates a normal ping with a new GUID
     *
     * @param ttl the ttl of the new Ping
     */
    public PingRequest(byte ttl) {
        super((byte)0x0, ttl, (byte)0);
        addBasicGGEPs();
    }
    
    /**
     * Creates a normal ping with a specified GUID
     *
     * @param ttl the ttl of the new Ping
     */
    public PingRequest(byte [] guid,byte ttl) {
        super(guid,(byte)0x0, ttl, (byte)0,0);
        addBasicGGEPs();
    }
    
    /**
     * Creates a ping with the specified GUID, ttl, and GGEP fields.
     */
    private PingRequest(byte[] guid, byte ttl, List /* of NameValue */ ggeps) {
        super(guid, (byte)0x0, ttl, (byte)0, 0);
        addGGEPs(ggeps);
    }

    /**
     * Creates a Query Key ping.
     */
    public static PingRequest createQueryKeyRequest() {
        List l = new LinkedList();
        l.add(new NameValue(GGEP.GGEP_HEADER_QUERY_KEY_SUPPORT));
        return new PingRequest(GUID.makeGuid(), (byte)1, l);
    }
    
    /**
     * Creates a TTL 1 Ping for faster bootstrapping, intended
     * for sending to UDP hosts.
     */
    public static PingRequest createUDPPing() {
        List l = new LinkedList();
        return new PingRequest(populateUDPGGEPList(UDP_GUID, l).bytes(), (byte)1, l);
    }
    
    /**
     * Creates a TTL 1 Ping for faster bootstrapping, intended
     * for sending to UHCs.
     */    
    public static PingRequest createUHCPing() {
        List ggeps = new LinkedList();
        GUID guid = populateUDPGGEPList(UDP_GUID,ggeps);
        ggeps.add(new NameValue(GGEP.GGEP_HEADER_UDP_HOST_CACHE));
        return new PingRequest(guid.bytes(),(byte)1,ggeps);
    }
    
    /**
     * @param l list to put the standard extentions we add to UDP pings
     * @return the guid to use for the ping
     */
    private static GUID populateUDPGGEPList(GUID g, List l) {
        GUID guid;
        if(ConnectionSettings.EVER_ACCEPTED_INCOMING.getValue()) {
            guid = g;
        } else {
            l.add(new NameValue(GGEP.GGEP_HEADER_IPPORT));
            guid = UDPService.instance().getSolicitedGUID();
        }
        byte[] data = new byte[1];
        if(RouterService.isSupernode())
            data[0] = SCP_ULTRAPEER;
        else
            data[0] = SCP_LEAF;
        l.add(new NameValue(GGEP.GGEP_HEADER_SUPPORT_CACHE_PONGS, data));
        
        return guid;
    }
    
    /**
     * Creates a TTL 1 Ping for faster bootstrapping, intended
     * for sending to the multicast network.
     */
    public static PingRequest createMulticastPing() {
        GUID guid = new GUID();
        byte[] data = new byte[1];
        if(RouterService.isSupernode())
            data[0] = 0x1;
        else
            data[0] = 0x0;
        List l = new LinkedList();
        l.add(new NameValue(GGEP.GGEP_HEADER_SUPPORT_CACHE_PONGS, data));
        return new PingRequest(guid.bytes(), (byte)1, l);
    }    
            

    /////////////////////////////methods///////////////////////////

    protected void writePayload(OutputStream out) throws IOException {
        if(payload != null) {
            out.write(payload);
        }
        // the ping is still written even if there's no payload
        SentMessageStatHandler.TCP_PING_REQUESTS.addMessage(this);
        //Do nothing...there is no payload!
    }

    public Message stripExtendedPayload() {
        if (payload==null)
            return this;
        else
            return new PingRequest(this.getGUID(), 
                                   this.getTTL(), 
                                   this.getHops());
    }

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

    public String toString() {
        return "PingRequest("+super.toString()+")";
    }

    /**
     * Accessor for whether or not this ping meets the criteria for being a
     * "heartbeat" ping, namely having ttl=0 and hops=1.
     * 
     * @return <tt>true</tt> if this ping apears to be a "heartbeat" ping,
     *  otherwise <tt>false</tt>
     */
    public boolean isHeartbeat() {
        return (getHops() == 1 && getTTL() == 0);
    }
    
    /**
     * Marks this ping request as requesting a pong carrying
     * an ip:port info.
     */
    public void addIPRequest() {
        List l = new LinkedList();
        l.add(new NameValue(GGEP.GGEP_HEADER_IPPORT));
        addGGEPs(l);
    }

    /**
     * Adds all basic GGEP information to the outgoing ping.
     * Currently adds a Locale field.
     */
    private void addBasicGGEPs() {
        List l = new LinkedList();
        l.add(new NameValue(GGEP.GGEP_HEADER_CLIENT_LOCALE, 
                            ApplicationSettings.LANGUAGE.getValue()));
        addGGEPs(l);
    }
    
    /**
     * Adds the specified GGEPs.
     */
     private void addGGEPs(List /* of NameValue */ ggeps) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (_ggep == null)
                _ggep = new GGEP(true);

            _ggep.putAll(ggeps);
            _ggep.write(baos);
            baos.write(0);            
            payload = baos.toByteArray();
            updateLength(payload.length);
        } catch(IOException e) {
            ErrorService.error(e);
        }
    }

    /**
     * get locale of this PingRequest 
     */
    public String getLocale() {
        if(payload != null) {
            try {
                parseGGEP();
                if(_ggep.hasKey(GGEP.GGEP_HEADER_CLIENT_LOCALE))
                	return _ggep.getString(GGEP.GGEP_HEADER_CLIENT_LOCALE);
            } catch(BadGGEPBlockException ignored) {
            } catch(BadGGEPPropertyException ignoredToo) {}
        }
        
        return ApplicationSettings.DEFAULT_LOCALE.getValue();
    }
    
    /**
     * Determines if this PingRequest has the 'supports cached pongs'
     * marking.
     */
    public boolean supportsCachedPongs() {
        if(payload != null) {
            try {
                parseGGEP();
                return _ggep.hasKey(GGEP.GGEP_HEADER_SUPPORT_CACHE_PONGS);
            } catch(BadGGEPBlockException ignored) {}
        }
        return false;
    }
    
    /**
     * Gets the data value for the SCP field, if one exists.
     * If none exist, null is returned.  Else, a byte[] of some
     * size is returned.
    */
    public byte[] getSupportsCachedPongData() {
        byte[] ret = null;

        if(payload != null) {
            try {
                parseGGEP();
                if(_ggep.hasKey(GGEP.GGEP_HEADER_SUPPORT_CACHE_PONGS)) {
                    ret = DataUtils.EMPTY_BYTE_ARRAY;
                    // this may throw, which is why we first set it to an empty value.
                    return _ggep.getBytes(GGEP.GGEP_HEADER_SUPPORT_CACHE_PONGS);
                }
            } catch(BadGGEPBlockException ignored) {
            } catch(BadGGEPPropertyException ignored) {
            }
        }

        return ret;
    }

    public boolean isQueryKeyRequest() {
        if (!(getTTL() == 0) || !(getHops() == 1))
            return false;

        if(payload != null) {
            try {
                parseGGEP();
                return _ggep.hasKey(GGEP.GGEP_HEADER_QUERY_KEY_SUPPORT);
            } catch (BadGGEPBlockException ignored) {}
        }

        return false;
    }
    
    /**
     * @return whether this ping wants a reply carrying IP:Port info.
     */
    public boolean requestsIP() {
       if(payload != null) {
           try {
               parseGGEP();
               return _ggep.hasKey(GGEP.GGEP_HEADER_IPPORT);
           } catch(BadGGEPBlockException ignored) {}
        }

       return false;
    }
    
    private void parseGGEP() throws BadGGEPBlockException {
        if(_ggep == null)
            _ggep = new GGEP(payload, 0, null);
    }
}
