package com.limegroup.gnutella.handshaking;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;

import org.limewire.core.settings.ApplicationSettings;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.io.IpPort;
import org.limewire.util.Version;
import org.limewire.util.VersionFormatException;

import com.limegroup.gnutella.util.LimeWireUtils;

/**
 * Contains the necessary information to form a response to a connection
 * handshake. It contains a status code, a status message, and the headers to
 * use in the response.
 * <p>
 * There are only two ways to create a HandshakeResponse.
 * <ol>
 * <li>Create an instance which defaults the status code and status message to
 * be "200 OK". Only the headers used in the response need to be passed in.
 * <li>Create an instance with a custom status code, status message, and the
 * headers used in the response.
 * </ol>
 */
public class HandshakeResponse {

    /**
     * Version of LimeWire we consider old.
     */
    private static final Version OLD_LIME_VERSION;
    static {
        Version v = null;
        try {
            v = new Version("3.4.0");
        } catch (VersionFormatException impossible) {
        }
        OLD_LIME_VERSION = v;
    }

    /**
     * The "default" status code in a connection handshake indicating that the
     * handshake was successful and the connection can be established.
     */
    public static final int OK = 200;

    /**
     * The "default" status message in a connection handshake indicating that
     * the handshake was successful and the connection can be established.
     */
    public static final String OK_MESSAGE = "OK";

    /**
     * HTTP response code for the crawler.
     */
    public static final int CRAWLER_CODE = 593;

    /**
     * HTTP response message for the crawler.
     */
    public static final String CRAWLER_MESSAGE = "Hi";

    /**
     * The error code that a shielded leaf node should give to incoming
     * connections.
     */
    public static final int SHIELDED = 503;

    /**
     * The error code that a node with no slots should give to incoming
     * connections.
     */
    public static final int SLOTS_FULL = 503;

    /**
     * Default bad status code to be used while rejecting connections.
     */
    public static final int DEFAULT_BAD_STATUS_CODE = 503;

    /**
     * Default bad status message to be used while rejecting connections.
     */
    public static final String DEFAULT_BAD_STATUS_MESSAGE = "Service Not Available";

    /*
     * TODO: check about this error code...
     */
    public static final int LOCALE_NO_MATCH = 577;

    public static final String LOCALE_NO_MATCH_MESSAGE = "Service Not Available";

    /**
     * HTTP-like status code used when handshaking (e.g., 200, 401, 503).
     */
    private final int STATUS_CODE;

    /**
     * Message used with status code when handshaking (e.g., "OK, "Service Not
     * Available"). The status message together with the status code make up the
     * status line (i.e., first line) of an HTTP-like response to a connection
     * handshake.
     */
    private final String STATUS_MESSAGE;

    /**
     * Headers to use in the response to a connection handshake.
     */
    private final Properties HEADERS;

    /**
     * Is the GGEP header set?
     */
    private Boolean _supportsGGEP;

    /**
     * Cached boolean for whether or not this is considered a considered a
     * "good" leaf connection.
     */
    private final boolean GOOD_LEAF;

    /**
     * Cached boolean for whether or not this is considered a considered a
     * "good" ultrapeer connection.
     */
    private final boolean GOOD_ULTRAPEER;

    /**
     * Cached value for the number of Ultrapeers this Ultrapeer attempts to
     * connect to.
     */
    private final int DEGREE;

    /**
     * Cached value for whether or not this is a high degree connection.
     */
    private final boolean HIGH_DEGREE;

    /**
     * Cached value for whether or not this is an Ultrapeer connection that
     * supports Ultrapeer query routing.
     */
    private final boolean ULTRAPEER_QRP;

    /**
     * Cached value for the maximum TTL to use along this connection.
     */
    private final byte MAX_TTL;

    /**
     * Cached value for whether or not this connection supports dynamic
     * querying.
     */
    private final boolean DYNAMIC_QUERY;

    /**
     * Cached value for whether or not this connection reported X-Ultrapeer:
     * true in it's handshake headers.
     */
    private final boolean ULTRAPEER;

    /**
     * Cached value for whether or not this connection reported X-Ultrapeer:
     * false in it's handshake headers.
     */
    private final boolean LEAF;

    /**
     * Cached value for whether or not the connection reported Content-Encoding:
     * deflate
     */
    private final boolean DEFLATE_ENCODED;

    /**
     * Constant for whether or not this connection supports probe queries.
     */
    private final boolean PROBE_QUERIES;

    /**
     * Constant for whether or not this node supports pong caching.
     */
    private final boolean PONG_CACHING;

    /**
     * Constant for whether or not this node supports GUESS.
     */
    private final boolean GUESS_CAPABLE;

    /**
     * Constant for whether or not this is a crawler.
     */
    private final boolean IS_CRAWLER;

    /**
     * Constant for whether or not this node is a LimeWire (or derivative)
     */
    private final boolean IS_LIMEWIRE;
    private final boolean IS_LIME_CLONE;
    /**
     * Constant for whether or nor this node is an older limewire.
     */
    private final boolean IS_OLD_LIMEWIRE;

    /**
     * Constant for whether or not the client claims to do no requerying.
     */
    private final boolean NO_REQUERYING;

    /**
     * Locale
     */
    private final String LOCALE_PREF;

    /** The port the response says it's listening on. */
    private final int LISTEN_PORT;

    /** This connection's lime version */
    private final Version limeVersion;

    /**
     * Constant for the number of hosts to return in X-Try-Ultrapeer headers.
     */
    private static final int NUM_X_TRY_ULTRAPEER_HOSTS = 10;

    /**
     * Creates a <tt>HandshakeResponse</tt> which defaults the status code and
     * status message to be "200 Ok" and uses the desired headers in the
     * response.
     * 
     * @param headers the headers to use in the response.
     */
    // public for testing
    public HandshakeResponse(Properties headers) {
        this(OK, OK_MESSAGE, headers);
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance with the specified
     * response code and message and with no extra connection headers.
     * 
     * @param code the status code for the response
     * @param message the status message
     */
    protected HandshakeResponse(int code, String message) {
        this(code, message, new Properties());
    }

    /**
     * Creates a HandshakeResponse with the desired status code, status message,
     * and headers to respond with.
     * 
     * @param code the response code to use
     * @param message the response message to use
     * @param headers the headers to use in the response
     */
    HandshakeResponse(int code, String message, Properties headers) {
        STATUS_CODE = code;
        STATUS_MESSAGE = message;
        HEADERS = headers;
        DEGREE = extractIntHeaderValue(HEADERS, HeaderNames.X_DEGREE, 6);
        HIGH_DEGREE = getNumIntraUltrapeerConnections() >= 15;
        ULTRAPEER_QRP = isVersionOrHigher(HEADERS, HeaderNames.X_ULTRAPEER_QUERY_ROUTING, 0.1F);
        MAX_TTL = extractByteHeaderValue(HEADERS, HeaderNames.X_MAX_TTL, (byte) 4);
        DYNAMIC_QUERY = isVersionOrHigher(HEADERS, HeaderNames.X_DYNAMIC_QUERY, 0.1F);
        PROBE_QUERIES = isVersionOrHigher(HEADERS, HeaderNames.X_PROBE_QUERIES, 0.1F);
        NO_REQUERYING = isFalseValue(HEADERS, HeaderNames.X_REQUERIES);

        IS_LIME_CLONE = extractStringHeaderValue(headers, HeaderNames.USER_AGENT).toLowerCase(
                Locale.US).contains("lime") || extractStringHeaderValue(headers, HeaderNames.USER_AGENT).toLowerCase(
                Locale.US).contains("wire");
        
        IS_LIMEWIRE = extractStringHeaderValue(headers, HeaderNames.USER_AGENT).toLowerCase(
                Locale.US).startsWith("limewire/") || extractStringHeaderValue(headers, HeaderNames.USER_AGENT).toLowerCase(
                Locale.US).startsWith("wireshare/");

        GOOD_ULTRAPEER = isHighDegreeConnection() && isUltrapeerQueryRoutingConnection()
                && (getMaxTTL() < 5) && isDynamicQueryConnection();

        GOOD_LEAF = GOOD_ULTRAPEER && (IS_LIMEWIRE || (IS_LIME_CLONE && NO_REQUERYING));

        ULTRAPEER = isTrueValue(HEADERS, HeaderNames.X_ULTRAPEER);
        LEAF = isFalseValue(HEADERS, HeaderNames.X_ULTRAPEER);
        DEFLATE_ENCODED = isStringValue(HEADERS, HeaderNames.CONTENT_ENCODING,
                HeaderNames.DEFLATE_VALUE);
        PONG_CACHING = isVersionOrHigher(headers, HeaderNames.X_PONG_CACHING, 0.1F);
        GUESS_CAPABLE = isVersionOrHigher(headers, HeaderNames.X_GUESS, 0.1F);
        IS_CRAWLER = isVersionOrHigher(headers, HeaderNames.CRAWLER, 0.1F);
        Version version = null;
        if (IS_LIMEWIRE) {
            version = extractVersion(extractStringHeaderValue(headers, HeaderNames.USER_AGENT));
            IS_OLD_LIMEWIRE = version != null && version.compareTo(OLD_LIME_VERSION) < 0;
        } else
            IS_OLD_LIMEWIRE = false;
        limeVersion = version;

        String loc = extractStringHeaderValue(headers, HeaderNames.X_LOCALE_PREF);
        LOCALE_PREF = (loc.equals("")) ? ApplicationSettings.DEFAULT_LOCALE.get() : loc;

        LISTEN_PORT = extractIntHeaderValueAfter(headers, HeaderNames.LISTEN_IP, ":", -1);
    }

    private Version extractVersion(String userAgent) {
        StringTokenizer tok = new StringTokenizer(userAgent, "/. ");
        if (tok.countTokens() < 3)
            return null;
        tok.nextToken(); // limewire
        String v = tok.nextToken() + "." + tok.nextToken() + ".";
        if (tok.hasMoreTokens())
            v += tok.nextToken();
        else
            v += "0";
        try {
            return new Version(v);
        } catch (VersionFormatException vfe) {
            return null;
        }
    }

    private static final HandshakeResponse EMPTY_RESPONSE = new HandshakeResponse(new Properties());

    /**
     * Creates an empty response with no headers. This is useful, for example,
     * during connection handshaking when we haven't yet read any headers.
     * 
     * @return a new, empty <tt>HandshakeResponse</tt> instance
     */
    public static HandshakeResponse createEmptyResponse() {
        return EMPTY_RESPONSE;
    }

    /**
     * Constructs the response from the other host during connection
     * handshaking.
     * 
     * @return a new <tt>HandshakeResponse</tt> instance with the headers sent
     *         by the other host
     */
    public static HandshakeResponse createResponse(Properties headers) {
        return new HandshakeResponse(headers);
    }

    /**
     * Constructs the response from the other host during connection
     * handshaking. The returned response contains the connection headers sent
     * by the remote host.
     * 
     * @param line the status line received from the connecting host
     * @param headers the headers received from the other host
     * @return a new <tt>HandshakeResponse</tt> instance with the headers sent
     *         by the other host
     * @throws <tt>IOException</tt> if the status line could not be parsed
     */
    public static HandshakeResponse createRemoteResponse(String line, Properties headers)
            throws IOException {
        int code = extractCode(line);
        if (code == -1)
            throw new IOException("could not parse status code: " + line);

        String message = extractMessage(line);
        if (message == null)
            throw new IOException("could not parse status message: " + line);

        return new HandshakeResponse(code, message, headers);
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that accepts the
     * potential connection.
     * 
     * @param headers the <tt>Properties</tt> instance containing the headers to
     *        send to the node we're accepting
     */
    static HandshakeResponse createAcceptIncomingResponse(HandshakeResponse response,
            Properties headers, HandshakeServices handshakeServices) {
        return new HandshakeResponse(addXTryHeader(response, headers, handshakeServices));
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that accepts the
     * outgoing connection -- the final third step in the handshake. This passes
     * no headers, as all necessary headers have already been exchanged. The
     * only possible exception is the potential inclusion of X-Ultrapeer: false.
     * 
     * @param headers the <tt>Properties</tt> instance containing the headers to
     *        send to the node we're accepting
     */
    static HandshakeResponse createAcceptOutgoingResponse(Properties headers) {
        return new HandshakeResponse(headers);
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that responds to a
     * special crawler connection with connected leaves and Ultrapeers. See the
     * Files>>Development section on the GDF.
     */
    static HandshakeResponse createCrawlerResponse(HandshakeServices handshakeServices) {
        Properties headers = new Properties();

        // add our user agent
        headers.put(HeaderNames.USER_AGENT, LimeWireUtils.getHttpServer());
        headers.put(HeaderNames.X_ULTRAPEER, Boolean.toString(handshakeServices.isUltrapeer()));

        // add any leaves
        List<? extends IpPort> leaves = handshakeServices.getLeafNodes();
        headers.put(HeaderNames.LEAVES, createEndpointString(leaves, leaves.size()));

        // add any Ultrapeers
        List<? extends IpPort> ultrapeers = handshakeServices.getUltrapeerNodes();
        headers.put(HeaderNames.PEERS, createEndpointString(ultrapeers, ultrapeers.size()));

        return new HandshakeResponse(HandshakeResponse.CRAWLER_CODE,
                HandshakeResponse.CRAWLER_MESSAGE, headers);
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that rejects the
     * potential connection. This includes the X-Try-Ultrapeers header to tell
     * the remote host about other nodes to connect to. We return the hosts we
     * most recently knew to have free leaf or ultrapeer connection slots.
     * 
     * @param hr the <tt>HandshakeResponse</tt> containing the connection
     *        headers of the connecting host
     * @return a <tt>HandshakeResponse</tt> with the appropriate response
     *         headers
     */
    static HandshakeResponse createUltrapeerRejectIncomingResponse(HandshakeResponse hr,
            HandshakeStatus status, HandshakeServices handshakeServices) {
        return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(),
                addXTryHeader(hr, new Properties(), handshakeServices));
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that rejects the
     * potential connection. The returned <tt>HandshakeResponse</tt> DOES NOT
     * include the X-Try-Ultrapeers header because this is an outgoing
     * connection, and we should not send host data that the remote client does
     * not request.
     */
    static HandshakeResponse createRejectOutgoingResponse(HandshakeStatus status) {
        return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(),
                new Properties());
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that rejects the
     * potential connection to a leaf. We add hosts that we know about with free
     * connection slots to the X-Try-Ultrapeers header.
     * 
     * @param hr the <tt>HandshakeResponse</tt> containing the headers of the
     *        remote host
     * @return a new <tt>HandshakeResponse</tt> instance rejecting the
     *         connection and with the specified connection headers
     */
    static HandshakeResponse createLeafRejectIncomingResponse(HandshakeResponse hr,
            HandshakeStatus status, HandshakeServices handshakeServices) {
        return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage(),
                addXTryHeader(hr, new Properties(), handshakeServices));
    }

    /**
     * Creates a new <tt>HandshakeResponse</tt> instance that rejects an
     * outgoing leaf connection. This occurs when we, as a leaf, reject a
     * connection on the third stage of the handshake.
     * 
     * @return a new <tt>HandshakeResponse</tt> instance rejecting the
     *         connection and with no extra headers
     */
    static HandshakeResponse createLeafRejectOutgoingResponse(HandshakeStatus status) {
        return new HandshakeResponse(HandshakeResponse.SLOTS_FULL, status.getMessage());
    }

    static HandshakeResponse createLeafRejectLocaleOutgoingResponse() {
        return new HandshakeResponse(HandshakeResponse.LOCALE_NO_MATCH,
                HandshakeResponse.LOCALE_NO_MATCH_MESSAGE);
    }

    /**
     * Creates a new String of hosts, limiting the number of hosts to add to the
     * default value of 10. This is particularly used for the X-Try-Ultrapeers
     * header.
     * 
     * @param hosts a <tt>Collection</tt> of <tt>IpPort</tt> instances
     * @return a string of the form IP:port,IP:port,... from the given list of
     *         hosts
     * 
     *         Default access for testing.
     */
    static String createEndpointString(Collection<? extends IpPort> hosts) {
        return createEndpointString(hosts, NUM_X_TRY_ULTRAPEER_HOSTS);
    }

    /**
     * Utility method that takes the specified list of hosts and returns a
     * string of the form:
     * <p>
     * 
     * IP:port,IP:port,IP:port
     * 
     * @param hosts a <tt>Collection</tt> of <tt>IpPort</tt> instances
     * @return a string of the form IP:port,IP:port,... from the given list of
     *         hosts Default access for testing.
     */
    static String createEndpointString(Collection<? extends IpPort> hosts, int limit) {
        StringBuilder sb = new StringBuilder();
        int i = 0;
        Iterator<? extends IpPort> iter = hosts.iterator();
        while (iter.hasNext() && i < limit) {
            IpPort host = iter.next();
            sb.append(host.getAddress());
            sb.append(":");
            sb.append(host.getPort());
            if (iter.hasNext()) {
                sb.append(",");
            }
            i++;
        }
        return sb.toString();
    }

    /**
     * Utility method to extract the connection code from the connect string,
     * such as "200" in a "200 OK" message.
     * <p>
     * Default access for testing.
     * 
     * @param line the full connection string, such as "200 OK."
     * @return the status code for the connection string, or -1 if the code
     *         could not be parsed
     */
    static int extractCode(String line) {
        // get the status code and message out of the status line
        int statusMessageIndex = line.indexOf(" ");
        if (statusMessageIndex == -1)
            return -1;
        try {
            return Integer.parseInt(line.substring(0, statusMessageIndex).trim());
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /**
     * Utility method to extract the connection message from the connect string,
     * such as "OK" in a "200 OK" message.
     * <p>
     * Default access for testing.
     * 
     * @param line the full connection string, such as "200 OK."
     * @return the status message for the connection string
     */
    static String extractMessage(String line) {
        // get the status code and message out of the status line
        int statusMessageIndex = line.indexOf(" ");
        if (statusMessageIndex == -1)
            return null;
        return line.substring(statusMessageIndex).trim();
    }

    /**
     * Utility method for creating a set of headers with the X-Try-Ultrapeers
     * header set according to the headers from the remote host.
     * <p>
     * Default access for testing.
     * @param hr the <tt>HandshakeResponse</tt> of the incoming request
     * @return a new <tt>Properties</tt> instance with the X-Try-Ultrapeers
     *         header set according to the incoming headers from the remote host
     */
    static Properties addXTryHeader(HandshakeResponse hr, Properties headers,
            HandshakeServices handshakeServices) {
        Collection<IpPort> hosts = handshakeServices.getAvailableHosts(hr.isUltrapeer(), hr
                .getLocalePref(), 10);
        headers.put(HeaderNames.X_TRY_ULTRAPEERS, createEndpointString(hosts));
        return headers;
    }

    /**
     * Returns the response code.
     */
    public int getStatusCode() {
        return STATUS_CODE;
    }

    /**
     * Returns the status message.
     * 
     * @return the status message (e.g. "OK" , "Service Not Available" etc.)
     */
    public String getStatusMessage() {
        return STATUS_MESSAGE;
    }

    /**
     * Tells if the status returned was OK or not.
     * 
     * @return true, if the status returned was not the OK status, false
     *         otherwise
     */
    public boolean notOKStatusCode() {
        if (STATUS_CODE != OK)
            return true;
        else
            return false;
    }

    /**
     * Returns whether or not this connection was accepted -- whether or not the
     * connection returned Gnutella/0.6 200 OK.
     * 
     * @return <tt>true</tt> if the server returned Gnutella/0.6 200 OK,
     *         otherwise <tt>false</tt>
     */
    public boolean isAccepted() {
        return STATUS_CODE == OK;
    }

    /**
     * Returns the status code and status message together used in a status
     * line. (e.g., "200 OK", "503 Service Not Available")
     */
    public String getStatusLine() {
        return new String(STATUS_CODE + " " + STATUS_MESSAGE);
    }

    /**
     * Returns the headers as a <tt>Properties</tt> instance.
     */
    public Properties props() {
        return HEADERS;
    }

    /**
     * Accessor for an individual property.
     */
    public String getProperty(String prop) {
        return HEADERS.getProperty(prop);
    }

    /**
     * Returns the vendor string reported by this connection, i.e., the
     * USER_AGENT property, or null if it wasn't set.
     * 
     * @return the vendor string, or null if unknown
     */
    public String getUserAgent() {
        return HEADERS.getProperty(HeaderNames.USER_AGENT);
    }

    /**
     * Returns the maximum TTL that queries originating from us and sent from
     * this connection should have. If the max TTL header is not present, the
     * default TTL is assumed.
     * 
     * @return the maximum TTL that queries sent to this connection should have
     *         -- this will always be 5 or less
     */
    public byte getMaxTTL() {
        return MAX_TTL;
    }

    /**
     * Accessor for the X-Try-Ultrapeers header. If the header does not exist or
     * is empty, this returns the empty string.
     * 
     * @return the string of X-Try-Ultrapeer hosts, or the empty string if they
     *         do not exist
     */
    public String getXTryUltrapeers() {
        return extractStringHeaderValue(HEADERS, HeaderNames.X_TRY_ULTRAPEERS);
    }

    /**
     * This is a convenience method to see if the connection passed the
     * X-Try-Ultrapeer header. This simply checks the existence of the header --
     * if the header was sent but is empty, this still returns <tt>true</tt>.
     * 
     * @return <tt>true</tt> if this connection sent the X-Try-Ultrapeer header,
     *         otherwise <tt>false</tt>
     */
    public boolean hasXTryUltrapeers() {
        return headerExists(HEADERS, HeaderNames.X_TRY_ULTRAPEERS);
    }

    /**
     * Returns whether or not this host included leaf guidance, i.e., whether or
     * not the host wrote:
     * <p>
     * X-Ultrapeer-Needed: false
     * 
     * @return <tt>true</tt> if the other host returned X-Ultrapeer-Needed:
     *         false, otherwise <tt>false</tt>
     */
    public boolean hasLeafGuidance() {
        return isFalseValue(HEADERS, HeaderNames.X_ULTRAPEER_NEEDED);
    }

    /**
     * Returns the number of intra-Ultrapeer connections this node maintains.
     * 
     * @return the number of intra-Ultrapeer connections this node maintains
     */
    public int getNumIntraUltrapeerConnections() {
        return DEGREE;
    }

    // implements ReplyHandler interface -- inherit doc comment
    public boolean isHighDegreeConnection() {
        return HIGH_DEGREE;
    }

    /**
     * Returns whether or not we think this connection is from a LimeWire or a
     * derivative of LimeWire.
     */
    public boolean isLimeWire() {
        return IS_LIMEWIRE;
    }

    /**
     * @return true if we consider this an older version of LimeWire, false
     *         otherwise
     */
    public boolean isOldLimeWire() {
        return IS_OLD_LIMEWIRE;
    }

    /**
     * @return the version of this connection if its LimeWire. null if not
     *         LimeWire or could not be parsed.
     */
    public Version getLimeVersion() {
        return limeVersion;
    }

    /**
     * Returns whether or not this is connection passed the headers to be
     * considered a "good" leaf.
     * 
     * @return <tt>true</tt> if this is considered a "good" leaf, otherwise
     *         <tt>false</tt>
     */
    public boolean isGoodLeaf() {
        return GOOD_LEAF;
    }

    /**
     * Returns whether or not this connection is encoded in deflate.
     */
    public boolean isDeflateEnabled() {
        // this does NOT check the setting because we have already told the
        // outgoing side we support encoding, and they're expecting us to use it
        return DEFLATE_ENCODED;
    }

    /**
     * Returns whether or not this connection accepts deflate as an encoding.
     */
    public boolean isDeflateAccepted() {
        // Note that we check the ENCODE_DEFLATE setting, and NOT the
        // ACCEPT_DEFLATE setting. This is a trick to prevent the
        // HandshakeResponders from thinking they can encode
        // the via deflate if we do not want to encode in deflate.
        return ConnectionSettings.ENCODE_DEFLATE.getValue() && containsStringValue(HEADERS, // the
                                                                                            // headers
                                                                                            // to
                                                                                            // look
                                                                                            // through
                HeaderNames.ACCEPT_ENCODING,// the header to look for
                HeaderNames.DEFLATE_VALUE); // the value to look for
    }

    /**
     * Returns whether or not this is connection passed the headers to be
     * considered a "good" ultrapeer.
     * 
     * @return <tt>true</tt> if this is considered a "good" ultrapeer, otherwise
     *         <tt>false</tt>
     */
    public boolean isGoodUltrapeer() {
        return GOOD_ULTRAPEER;
    }

    /**
     * Returns whether or not this connection supports query routing between
     * Ultrapeers at 1 hop.
     * 
     * @return <tt>true</tt> if this is an Ultrapeer connection that exchanges
     *         query routing tables with other Ultrapeers at 1 hop, otherwise
     *         <tt>false</tt>
     */
    public boolean isUltrapeerQueryRoutingConnection() {
        return ULTRAPEER_QRP;
    }

    /**
     * Returns true iff this connection wrote "X-Ultrapeer: false". This does
     * NOT necessarily mean the connection is shielded.
     */
    public boolean isLeaf() {
        return LEAF;
    }

    /** Returns true iff this connection wrote "X-Ultrapeer: true". */
    public boolean isUltrapeer() {
        return ULTRAPEER;
    }

    /**
     * Returns whether or not this connection is to a client supporting GUESS.
     * 
     * @return <tt>true</tt> if the node on the other end of this connection
     *         supports GUESS, <tt>false</tt> otherwise
     */
    public boolean isGUESSCapable() {
        return GUESS_CAPABLE;
    }

    /**
     * Returns whether or not this connection is to a ultrapeer supporting
     * GUESS.
     * 
     * @return <tt>true</tt> if the node on the other end of this Ultrapeer
     *         connection supports GUESS, <tt>false</tt> otherwise
     */
    public boolean isGUESSUltrapeer() {
        return isGUESSCapable() && isUltrapeer();
    }

    /**
     * Returns true iff this connection is a temporary connection as per the
     * headers.
     */
    public boolean isTempConnection() {
        // get the X-Temp-Connection from either the headers received
        String value = HEADERS.getProperty(HeaderNames.X_TEMP_CONNECTION);
        // if X-Temp-Connection header is not received, return false, else
        // return the value received
        if (value == null)
            return false;
        else
            return Boolean.valueOf(value).booleanValue();
    }

    /**
     * Returns true if this supports GGEP'ed messages. GGEP'ed messages (e.g.,
     * big pongs) should only be sent along connections for which
     * supportsGGEP()==true.
     */
    public boolean supportsGGEP() {
        if (_supportsGGEP == null) {
            String value = HEADERS.getProperty(HeaderNames.GGEP);

            // Currently we don't care about the version number.
            _supportsGGEP = new Boolean(value != null);
        }
        return _supportsGGEP.booleanValue();
    }

    /**
     * Determines whether or not this node supports vendor messages.
     * 
     * @return <tt>true</tt> if this node supports vendor messages, otherwise
     *         <tt>false</tt>
     */
    public float supportsVendorMessages() {
        String value = HEADERS.getProperty(HeaderNames.X_VENDOR_MESSAGE);
        if ((value != null) && !value.equals("")) {
            try {
                return Float.parseFloat(value);
            } catch (NumberFormatException nfe) {
                return 0;
            }
        }
        return 0;
    }

    /**
     * Returns whether or not this node supports pong caching.
     * 
     * @return <tt>true</tt> if this node supports pong caching, otherwise
     *         <tt>false</tt>
     */
    public boolean supportsPongCaching() {
        return PONG_CACHING;
    }

    public String getVersion() {
        return HEADERS.getProperty(HeaderNames.X_VERSION);
    }

    /**
     * True if the remote host supports query routing (QRP). This is only
     * meaningful in the context of leaf-supernode relationships.
     */
    public boolean isQueryRoutingEnabled() {
        return isVersionOrHigher(HEADERS, HeaderNames.X_QUERY_ROUTING, 0.1F);
    }

    /**
     * Returns whether or not the node on the other end of this connection uses
     * dynamic querying.
     * 
     * @return <tt>true</tt> if this node uses dynamic querying, otherwise
     *         <tt>false</tt>
     */
    public boolean isDynamicQueryConnection() {
        return DYNAMIC_QUERY;
    }

    /**
     * Accessor for whether or not this connection supports TTL=1 probe queries.
     * These queries are treated separately from other queries. In particular,
     * if a second query with the same GUID is received, it is not considered a
     * duplicate.
     * 
     * @return <tt>true</tt> if this connection supports probe queries,
     *         otherwise <tt>false</tt>
     */
    public boolean supportsProbeQueries() {
        return PROBE_QUERIES;
    }

    /**
     * Determines whether or not this handshake is from the crawler.
     * 
     * @return <tt>true</tt> if this handshake is from the crawler, otherwise
     *         <tt>false</tt>
     */
    public boolean isCrawler() {
        return IS_CRAWLER;
    }

    /**
     * Access the locale preferences advertised by the client.
     */
    public String getLocalePref() {
        return LOCALE_PREF;
    }

    /** Accessor for the listening port. */
    public int getListeningPort() {
        return LISTEN_PORT;
    }

    /**
     * Convenience method that returns whether or not the given header exists.
     * 
     * @return <tt>true</tt> if the header exists, otherwise <tt>false</tt>
     */
    private static boolean headerExists(Properties headers, String headerName) {
        String value = headers.getProperty(headerName);
        return value != null;
    }

    /**
     * Utility method for checking whether or not a given header value is true.
     * 
     * @param headers the headers to check
     * @param headerName the header name to look for
     */
    private static boolean isTrueValue(Properties headers, String headerName) {
        String value = headers.getProperty(headerName);
        if (value == null)
            return false;

        return Boolean.valueOf(value).booleanValue();
    }

    /**
     * Utility method for checking whether or not a given header value is false.
     * 
     * @param headers the headers to check
     * @param headerName the header name to look for
     */
    private static boolean isFalseValue(Properties headers, String headerName) {
        String value = headers.getProperty(headerName);
        if (value == null)
            return false;
        return value.equalsIgnoreCase("false");
    }

    /**
     * Utility method for determining whether or not a given header is a given
     * string value. Case-insensitive.
     * 
     * @param headers the headers to check
     * @param headerName the headerName to look for
     * @param headerValue the headerValue to check against
     */
    private static boolean isStringValue(Properties headers, String headerName, String headerValue) {
        String value = headers.getProperty(headerName);
        if (value == null)
            return false;
        return value.equalsIgnoreCase(headerValue);
    }

    /**
     * Utility method for determining whether or not a given header contains a
     * given string value within a comma-delimited list. Case-insensitive.
     * 
     * @param headers the headers to check
     * @param headerName the headerName to look for
     * @param headerValue the headerValue to check against
     */
    private static boolean containsStringValue(Properties headers, String headerName,
            String headerValue) {
        String value = headers.getProperty(headerName);
        if (value == null)
            return false;

        // As a small optimization, we first check to see if the value
        // by itself is what we want, so we don't have to create the
        // StringTokenizer.
        if (value.equalsIgnoreCase(headerValue))
            return true;

        StringTokenizer st = new StringTokenizer(value, ",");
        while (st.hasMoreTokens()) {
            if (st.nextToken().equalsIgnoreCase(headerValue))
                return true;
        }
        return false;
    }

    /**
     * Utility method that checks the headers to see if the advertised version
     * for a specified feature is greater than or equal to the version we
     * require (<tt>minVersion</tt>.
     * 
     * @param headers the connection headers to evaluate
     * @param headerName the header name for the feature to check
     * @param minVersion the minimum version that we require for this feature
     * 
     * @return <tt>true</tt> if the version number for the specified feature is
     *         greater than or equal to <tt>minVersion</tt>, otherwise
     *         <tt>false</tt>.
     */
    private static boolean isVersionOrHigher(Properties headers, String headerName, float minVersion) {
        String value = headers.getProperty(headerName);
        if (value == null)
            return false;
        try {
            Float f = new Float(value);
            return f.floatValue() >= minVersion;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Helper method for returning an int header value. If the header name is
     * not found, or if the header value cannot be parsed, the default value is
     * returned.
     * 
     * @param headers the connection headers to search through
     * @param headerName the header name to look for
     * @param defaultValue the default value to return if the header value could
     *        not be properly parsed
     * @return the int value for the header
     */
    private static int extractIntHeaderValue(Properties headers, String headerName, int defaultValue) {
        String value = headers.getProperty(headerName);

        if (value == null)
            return defaultValue;
        try {
            return Integer.valueOf(value).intValue();
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * Helper method for returning an int header value in a header after a
     * certain character. If the header name is not found, or if the header
     * value cannot be parsed, the default value is returned.
     * 
     * @param headers the connection headers to search through
     * @param headerName the header name to look for
     * @param token the token after which the int is looked for
     * @param defaultValue the default value to return if the header value could
     *        not be properly parsed
     * @return the int value for the header
     */
    private static int extractIntHeaderValueAfter(Properties headers, String headerName,
            String token, int defaultValue) {
        String value = headers.getProperty(headerName);

        if (value == null)
            return defaultValue;

        int idx = value.indexOf(token) + 1;
        if (idx == 0 || idx == value.length())
            return defaultValue;

        try {
            return Integer.valueOf(value.substring(idx)).intValue();
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * Helper method for returning a byte header value. If the header name is
     * not found, or if the header value cannot be parsed, the default value is
     * returned.
     * 
     * @param headers the connection headers to search through
     * @param headerName the header name to look for
     * @param defaultValue the default value to return if the header value could
     *        not be properly parsed
     * @return the byte value for the header
     */
    private static byte extractByteHeaderValue(Properties headers, String headerName,
            byte defaultValue) {
        String value = headers.getProperty(headerName);

        if (value == null)
            return defaultValue;
        try {
            return Byte.valueOf(value).byteValue();
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * Helper method for returning a string header value. If the header name is
     * not found, or if the header value cannot be parsed, the default value is
     * returned.
     * 
     * @param headers the connection headers to search through
     * @param headerName the header name to look for
     * @return the string value for the header, or the empty string if the
     *         header could not be found
     */
    private static String extractStringHeaderValue(Properties headers, String headerName) {
        String value = headers.getProperty(headerName);

        if (value == null)
            return "";
        return value;
    }

    @Override
    public String toString() {
        return "<" + STATUS_CODE + ", " + STATUS_MESSAGE + ">" + HEADERS;
    }
}
