package com.limegroup.gnutella.messages;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.limewire.io.BadGGEPPropertyException;
import org.limewire.io.GGEP;
import org.limewire.security.AddressSecurityToken;
import org.limewire.security.MACCalculatorRepositoryManager;
import org.limewire.service.ErrorService;
import org.limewire.util.ByteUtils;
import org.limewire.util.I18NConvert;
import org.limewire.util.StringUtils;
import org.xml.sax.SAXException;

import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UrnSet;
import com.limegroup.gnutella.messages.HUGEExtension.GGEPBlock;
import com.limegroup.gnutella.settings.MessageSettings;
import com.limegroup.gnutella.settings.SearchSettings;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLDocumentFactory;
import com.limegroup.gnutella.xml.SchemaNotFoundException;

/**
 * This class creates Gnutella query messages, either from scratch, or
 * from data read from the network.  Queries can contain query strings, 
 * XML query strings, URNs, etc.  The minimum speed field is now used
 * for bit flags to indicate such things as the firewalled status of
 * the querier.<p>
 * 
 * This class also has factory constructors for requeries originated
 * from this LimeWire.  These requeries have specially marked GUIDs
 * that allow us to identify them as requeries.
 */
public class QueryRequestImpl extends AbstractMessage implements QueryRequest {

    /**
     * The payload for the query -- includes the query string, the
     * XML query, any URNs, GGEP, etc.
     */
    private final byte[] PAYLOAD;

    /**
     * The "min speed" field.  This was originally used to specify
     * a minimum speed for returned results, but it was never really
     * used this way.  As of LimeWire 3.0 (02/2003), the bits of 
     * this field were changed to specify things like the firewall
     * status of the querier.
     */
    private final int MIN_SPEED;

    /**
     * The query string.
     */
    private final String QUERY;
    
    /**
     * The LimeXMLDocument of the rich query.
     */
    private final LimeXMLDocument XML_DOC;

    /**
     * The feature that this query is.
     */
    private int _featureSelector = 0;
    
    private boolean _isSecurityTokenRequired;
    
    /** If the query desires partial results */
    private boolean _partialResultsDesired;
    
    /**
     * Whether or not the GGEP header for Do Not Proxy was found and its
     * field is empty.
     */
    private boolean _doNotProxy = false;
    
    // HUGE v0.93 fields
    /** 
	 * Specific URNs requested.
	 */
    private final Set<URN> QUERY_URNS;

    /**
     * The Query Key associated with this query -- can be null.
     */
    private final AddressSecurityToken QUERY_KEY;

    /**
     * The flag in the 'M' GGEP extension - if non-null, the query is requesting
     * only certain types.
     */
    private Integer _metaMask = null;
    
    /**
     * If we're re-originated this query for a leaf.  This can be set/read
     * after creation.
     */
    private boolean originated = false;

	/**
	 * Cached hash code for this instance.
	 */
	private volatile int _hashCode = 0;
    
	/**
     * Cached illegal characters in search strings.
     */
    private static final char[] ILLEGAL_CHARS =
        SearchSettings.ILLEGAL_CHARS.getValue();


    /**
     * Cache the maximum length for queries, in bytes.
     */
    private static final int MAX_QUERY_LENGTH =
        SearchSettings.MAX_QUERY_LENGTH.getValue();

    /**
     * Cache the maximum length for XML queries, in bytes.
     */
    private static final int MAX_XML_QUERY_LENGTH =
        SearchSettings.MAX_XML_QUERY_LENGTH.getValue();

 
    
    /** Constructs a query. */
    QueryRequestImpl(byte[] guid, byte ttl, int minSpeed,
                        String query, String richQuery, 
                        Set<? extends URN> queryUrns,
                        AddressSecurityToken addressSecurityToken, boolean isFirewalled, 
                        Network network, boolean canReceiveOutOfBandReplies,
                        int featureSelector, boolean doNotProxy,
                        int metaFlagMask, boolean normalize,
                        boolean canDoFWT,
                        LimeXMLDocumentFactory limeXMLDocumentFactory) {
        // don't worry about getting the length right at first
        super(guid, Message.F_QUERY, ttl, /* hops */ (byte)0, /* length */ 0, 
              network);
        
        // make sure the query is normalized.
        // (this may have been normalized elsewhere, but it's okay to do it again)
        if(normalize && query != null)
            query = I18NConvert.instance().getNorm(query);
        
		if((query == null || query.length() == 0) &&
		   (richQuery == null || richQuery.length() == 0) &&
		   (queryUrns == null || queryUrns.size() == 0)) {
			throw new IllegalArgumentException("cannot create empty query");
		}		

        if(query != null && query.length() > MAX_QUERY_LENGTH) {
            throw new IllegalArgumentException("query too big: " + query);
        }        

        if(richQuery != null && richQuery.length() > MAX_XML_QUERY_LENGTH) {
            throw new IllegalArgumentException("xml too big: " + richQuery);
        }

        if(query != null && 
          !(queryUrns != null && queryUrns.size() > 0 &&
            query.equals(QueryRequestImpl.DEFAULT_URN_QUERY))
           && hasIllegalChars(query)) {
            throw new IllegalArgumentException("illegal chars: " + query);
        }

        if (featureSelector < 0)
            throw new IllegalArgumentException("Bad feature = " +
                                               featureSelector);
        _featureSelector = featureSelector;
        if ((metaFlagMask > 0) && (metaFlagMask < 4) || (metaFlagMask > 248))
            throw new IllegalArgumentException("Bad Meta Flag = " +
                                               metaFlagMask);
        if (metaFlagMask > 0)
            _metaMask = new Integer(metaFlagMask);

        // only set the minspeed if none was input...x
        if (minSpeed == 0) {
            // the new Min Speed format - looks reversed but
            // it isn't because of ByteOrder.short2leb
            minSpeed = SPECIAL_MINSPEED_MASK; 
            // set the firewall bit if i'm firewalled
            if (isFirewalled && !isMulticast())
                minSpeed |= SPECIAL_FIREWALL_MASK;
            // if i'm firewalled and can do solicited, mark the query for fw
            // transfer capability.
            if (isFirewalled && canDoFWT)
                minSpeed |= SPECIAL_FWTRANS_MASK;
            // THE DEAL:
            // if we can NOT receive out of band replies, we want in-band XML -
            // so set the correct bit.
            // if we can receive out of band replies, we do not want in-band XML
            // we'll hope the out-of-band reply guys will provide us all
            // necessary XML.
            
            if (!canReceiveOutOfBandReplies) 
                minSpeed |= SPECIAL_XML_MASK;
            else if (!SearchSettings.DISABLE_OOB_V2.getBoolean())
                minSpeed |= SPECIAL_OUTOFBAND_MASK;
        }

        MIN_SPEED = minSpeed;
		if(query == null) {
			this.QUERY = "";
		} else {
			this.QUERY = query;
		}
		if(richQuery == null || richQuery.equals("") ) {
			this.XML_DOC = null;
		} else {
		    LimeXMLDocument doc = null;
		    try {
		        doc = limeXMLDocumentFactory.createLimeXMLDocument(richQuery);
            } catch(SAXException ignored) {
            } catch(SchemaNotFoundException ignored) {
            } catch(IOException ignored) {
            }
            this.XML_DOC = doc;
		}
		
		Set<URN> tempQueryUrns = null;
		if(queryUrns != null) {
			tempQueryUrns = new UrnSet(queryUrns);
		} else {
			tempQueryUrns = URN.NO_URN_SET;
		}

        this.QUERY_KEY = addressSecurityToken;
        this._doNotProxy = doNotProxy;
		
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ByteUtils.short2leb((short)MIN_SPEED,baos); // write minspeed
            baos.write(QUERY.getBytes("UTF-8"));              // write query
            baos.write(0);                             // null

			
            // now write any & all HUGE v0.93 General Extension Mechanism 
			// extensions

			// this specifies whether or not the extension was successfully
			// written, meaning that the HUGE GEM delimiter should be
			// written before the next extension
            boolean addDelimiterBefore = false;
			
            byte[] richQueryBytes = null;
            if(XML_DOC != null) {
                assert richQuery != null;
                richQueryBytes = richQuery.getBytes("UTF-8");
			}
            
			// add the rich query
            addDelimiterBefore = 
			    writeGemExtension(baos, addDelimiterBefore, richQueryBytes);

			// add the urns
            addDelimiterBefore = 
			    writeGemExtensions(baos, addDelimiterBefore, 
								   tempQueryUrns == null ? null : 
								   tempQueryUrns.iterator());

            // add the GGEP Extension, if necessary....
            // *----------------------------
            // construct the GGEP block
            GGEP ggepBlock = new GGEP(true); // do COBS

            // add the query key?
            if (this.QUERY_KEY != null) {
                // get query key in byte form....
                ByteArrayOutputStream qkBytes = new ByteArrayOutputStream();
                this.QUERY_KEY.write(qkBytes);
                ggepBlock.put(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT,
                              qkBytes.toByteArray());
            }

            // add the What Is header
            if (_featureSelector > 0)
                ggepBlock.put(GGEPKeys.GGEP_HEADER_FEATURE_QUERY, _featureSelector);

            // add a GGEP-block if we shouldn't proxy
            if (_doNotProxy) {
                ggepBlock.put(GGEPKeys.GGEP_HEADER_NO_PROXY);
            }

            // add a meta flag
            if (_metaMask != null)
                ggepBlock.put(GGEPKeys.GGEP_HEADER_META, _metaMask.intValue());

            // mark oob query to require support of security tokens
            if (canReceiveOutOfBandReplies) {
                _isSecurityTokenRequired = true;
                ggepBlock.put(GGEPKeys.GGEP_HEADER_SECURE_OOB);
            }
            
            if (SearchSettings.desiresPartialResults()) {
                _partialResultsDesired = true;
                ggepBlock.put(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_PREFIX);
            }
            
            // if there are GGEP headers, write them out...
            if (!ggepBlock.isEmpty()) {
                ByteArrayOutputStream ggepBytes = new ByteArrayOutputStream();
                ggepBlock.write(ggepBytes);
                // write out GGEP
                addDelimiterBefore = writeGemExtension(baos, addDelimiterBefore,
                                                       ggepBytes.toByteArray());
            }
            // ----------------------------*

            baos.write(0);                             // final null
		} 
        catch(UnsupportedEncodingException uee) {
            //this should never happen from the getBytes("UTF-8") call
            //but there are UnsupportedEncodingExceptions being reported
            //with UTF-8.
            //Is there other information we want to pass in as the message?
            throw new IllegalArgumentException("could not get UTF-8 bytes for query :"
                                               + QUERY 
                                               + " with richquery :"
                                               + richQuery);
        }
        catch (IOException e) {
		    ErrorService.error(e);
		}

		PAYLOAD = baos.toByteArray();
		updateLength(PAYLOAD.length);

		this.QUERY_URNS = Collections.unmodifiableSet(tempQueryUrns);
    }


    /**
     * Build a new query with data snatched from network
     *
     * @param guid the message guid
	 * @param ttl the time to live of the query
	 * @param hops the hops of the query
	 * @param payload the query payload, containing the query string and any
	 *  extension strings
	 * @param network the network that this query came from.
	 * @throws <tt>BadPacketException</tt> if this is not a valid query
     */
    QueryRequestImpl(byte[] guid, byte ttl, byte hops, byte[] payload, Network network,
            LimeXMLDocumentFactory limeXMLDocumentFactory, MACCalculatorRepositoryManager manager) throws BadPacketException {
        super(guid, Message.F_QUERY, ttl, hops, payload.length, network);
		PAYLOAD=payload;
		
        QueryRequestPayloadParser parser = new QueryRequestPayloadParser(payload, manager);
        
		QUERY = parser.query;

	    LimeXMLDocument tempDoc = null;
	    try {
	        tempDoc = limeXMLDocumentFactory.createLimeXMLDocument(parser.richQuery);
        } catch(SAXException ignored) {
        } catch(SchemaNotFoundException ignored) {
        } catch(IOException ignored) {
        }
        this.XML_DOC = tempDoc;
		MIN_SPEED = parser.minSpeed;
        
		_featureSelector = parser.featureSelector;
        
        _doNotProxy = parser.doNotProxy;
        
        _metaMask = parser.metaMask;
        
        _isSecurityTokenRequired = parser.hasSecurityTokenRequest;
        
        _partialResultsDesired = parser.partialResultsDesired;
        
		if(parser.queryUrns == null) {
			QUERY_URNS =Collections.emptySet(); 
		}
		else {
			QUERY_URNS = Collections.unmodifiableSet(parser.queryUrns);
		}
        QUERY_KEY = parser.addressSecurityToken;
		if(QUERY.length() == 0 &&
		   parser.richQuery.length() == 0 &&
		   QUERY_URNS.size() == 0) {
			throw new BadPacketException("empty query");
		}       
        if(QUERY.length() > MAX_QUERY_LENGTH) {
            //throw BadPacketException.QUERY_TOO_BIG;
            throw new BadPacketException("query too big: " + QUERY);
        }        

        if(parser.richQuery.length() > MAX_XML_QUERY_LENGTH) {
            //throw BadPacketException.XML_QUERY_TOO_BIG;
            throw new BadPacketException("xml too big: " + parser.richQuery);
        }

        if(!(QUERY_URNS.size() > 0 && QUERY.equals(QueryRequestImpl.DEFAULT_URN_QUERY))
           && hasIllegalChars(QUERY)) {
            //throw BadPacketException.ILLEGAL_CHAR_IN_QUERY;
            throw new BadPacketException("illegal chars: " + QUERY);
        }
    }
    
    private static boolean hasIllegalChars(String query) {
        return StringUtils.containsCharacters(query,ILLEGAL_CHARS);
    }

    /**
     * Returns a new GUID appropriate for query requests.  If isRequery,
     * the GUID query is marked.
     */
    public static byte[] newQueryGUID(boolean isRequery) {
        if (isRequery)
            return GUID.makeGuidRequery();
        byte [] ret = GUID.makeGuid();
        if (MessageSettings.STAMP_QUERIES.getValue())
            GUID.timeStampGuid(ret);
        return ret;
	}

    @Override
    protected void writePayload(OutputStream out) throws IOException {
        out.write(PAYLOAD);
    }

    /**
     * Accessor fot the payload of the query hit.
     *
     * @return the query hit payload
     */
    public byte[] getPayload() {
        return PAYLOAD;
    }

    /** 
     * Returns the query string of this message.<p>
     *
     * The caller should not call the getBytes() method on the returned value,
     * as this seems to cause problems on the Japanese Macintosh.  If you need
     * the raw bytes of the query string, call getQueryByteAt(int).
     */
    public String getQuery() {
        return QUERY;
    }
    
	/**
	 * Returns the rich query LimeXMLDocument.
	 *
	 * @return the rich query LimeXMLDocument
	 */
    public LimeXMLDocument getRichQuery() {
        return XML_DOC;
    }
    
    /**
     * Helper method used internally for getting the rich query string.
     */
    public String getRichQueryString() {
        if( XML_DOC == null )
            return null;
        else
            return XML_DOC.getXMLString();
    }       
 
	/**
	 * Returns the <tt>Set</tt> of <tt>URN</tt> instances for this query.
	 *
	 * @return  the <tt>Set</tt> of <tt>URN</tt> instances for this query, which
	 * may be empty (not null) if no URNs were requested
	 */
    public Set<URN> getQueryUrns() {
		return QUERY_URNS;
    }
	
	/**
	 * Returns whether or not this query contains URNs.
	 *
	 * @return <tt>true</tt> if this query contains URNs,<tt>false</tt> otherwise
	 */
	public boolean hasQueryUrns() {
		return !QUERY_URNS.isEmpty();
	}

    /**
	 * Note: the minimum speed can be represented as a 2-byte unsigned
	 * number, but Java shorts are signed.  Hence we must use an int.  The
	 * value returned is always smaller than 2^16.
	 */
	public int getMinSpeed() {
		return MIN_SPEED;
	}


    /**
     * Returns true if the query source is a firewalled servent.
     */
    public boolean isFirewalledSource() {
        if ( !isMulticast() ) {
            if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
                if ((MIN_SPEED & SPECIAL_FIREWALL_MASK) > 0)
                    return true;
            }
        }
        return false;
    }
 
 
    /**
     * Returns true if the query source desires Lime meta-data in responses.
     */
    public boolean desiresXMLResponses() {
        if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
            if ((MIN_SPEED & SPECIAL_XML_MASK) > 0)
                return true;
        }
        return false;        
    }


    /**
     * Returns true if the query source can do a firewalled transfer.
     */
    public boolean canDoFirewalledTransfer() {
        if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
            if ((MIN_SPEED & SPECIAL_FWTRANS_MASK) > 0)
                return true;
        }
        return false;        
    }


    /**
     * Returns true if the query source can accept out-of-band replies for
     * any supported protocol version.
     * 
     * Use getReplyAddress() and getReplyPort() if this is true to know where to
     * it. Always send XML if you are sending an out-of-band reply.
     */
    public boolean desiresOutOfBandReplies() {
        return desiresOutOfBandRepliesV2() || desiresOutOfBandRepliesV3();
    }
    
    /**
     * Returns true if sender desires out-of-band replies for protocol version
     * 2.
     */
    public boolean desiresOutOfBandRepliesV2() {
        if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
            if ((MIN_SPEED & SPECIAL_OUTOFBAND_MASK) > 0)
                return true;
        }
        return false;
    }
    
    /**
     * Returns true if sender desires out-of-band replies for protocol version
     * 3.
     */
    public boolean desiresOutOfBandRepliesV3() {
        return isSecurityTokenRequired(); 
    }
    
    /**
     * Returns true if the query source does not want you to proxy for it.
     */
    public boolean doNotProxy() {
        return _doNotProxy;
    }
    
    /**
     * Returns true if this query is for 'What is new?' content, i.e. usually
     * the top 3 YOUNGEST files in your library.
     */
    public boolean isWhatIsNewRequest() {
        return _featureSelector == FeatureSearchData.WHAT_IS_NEW;
    }
    
    /**
     * Returns true if this is a feature query.
     */
    public boolean isFeatureQuery() {
        return _featureSelector > 0;
    }
    
    /**
     * @return whether this is a browse host query
     */
    public boolean isBrowseHostQuery() {
        return FileManager.INDEXING_QUERY.equals(getQuery());
    }

    /**
     * Returns 0 if this is not a "feature" query, else it returns the selector
     * of the feature query, e.g. What Is New returns 1.
     */
    public int getFeatureSelector() {
        return _featureSelector;
    }
    
    /**
     * Returns true if the query request has a security token request,
     * this implies the sender requests OOB replies, protocol version 3.
     */
    public boolean isSecurityTokenRequired() {
        return _isSecurityTokenRequired;
    }

    public boolean desiresPartialResults() {
        return _partialResultsDesired;
    }
    
    /** Returns the address to send a out-of-band reply to.  Only useful
     *  when desiresOutOfBandReplies() == true.
     */
    public String getReplyAddress() {
        return (new GUID(getGUID())).getIP();
    }

        
    /** Returns true if the input bytes match the OOB address of this query.
     */
    public boolean matchesReplyAddress(byte[] ip) {
        return (new GUID(getGUID())).matchesIP(ip);
    }

        
    /** Returns the port to send a out-of-band reply to.  Only useful
     *  when desiresOutOfBandReplies() == true.
     */
    public int getReplyPort() {
        return (new GUID(getGUID())).getPort();
    }


	/**
	 * Accessor for whether or not this is a requery from a LimeWire.
	 *
	 * @return <tt>true</tt> if it is an automated requery from a LimeWire,
	 *  otherwise <tt>false</tt>
	 */
	public boolean isLimeRequery() {
		return GUID.isLimeRequeryGUID(getGUID());
	}
    
    /**
     * @return true if this is likely a query for LimeWire.
     */
    public boolean isQueryForLW() {
        for (String term : SearchSettings.LIME_SEARCH_TERMS.getValue()) {
            if (getQuery().length() > 0 &&
                    getQuery().toLowerCase(Locale.US).contains(term))
                return true;

            if (getRichQuery() != null) {
                for (String keyword : getRichQuery().getKeyWords())
                    if (keyword.toLowerCase(Locale.US).contains(term))
                        return true;
            }
        }
        return false;
    }
        
    /**
     * Returns the AddressSecurityToken associated with this Request.  May very well be
     * null.  Usually only UDP QueryRequests will have non-null QueryKeys.
     */
    public AddressSecurityToken getQueryKey() {
        return QUERY_KEY;
    }

    /** @return true if the query has no constraints on the type of results
     *  it wants back.
     */
    public boolean desiresAll() {
        return (_metaMask == null);
    }

    /** @return true if the query desires 'Audio' results back.
     */
    public boolean desiresAudio() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & AUDIO_MASK) > 0);
        return true;
    }
    
    /** @return true if the query desires 'Video' results back.
     */
    public boolean desiresVideo() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & VIDEO_MASK) > 0);
        return true;
    }
    
    /** @return true if the query desires 'Document' results back.
     */
    public boolean desiresDocuments() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & DOC_MASK) > 0);
        return true;
    }
    
    /** @return true if the query desires 'Image' results back.
     */
    public boolean desiresImages() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & IMAGE_MASK) > 0);
        return true;
    }
    
    /** @return true if the query desires 'Programs/Packages' for Windows
     *  results back.
     */
    public boolean desiresWindowsPrograms() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & WIN_PROG_MASK) > 0);
        return true;
    }
    
    /** @return true if the query desires 'Programs/Packages' for Linux/OSX
     *  results back.
     */
    public boolean desiresLinuxOSXPrograms() {
        if (_metaMask != null) 
            return ((_metaMask.intValue() & LIN_PROG_MASK) > 0);
        return true;
    }
    
    /**
     * Returns the mask of allowed programs.
     */
    public int getMetaMask() {
        if (_metaMask != null)
            return _metaMask.intValue();
        return 0;
    }
    
    /** Marks this as being an re-originated query. */
    public void originate() {
        originated = true;
    }
    
    /** Determines if this is an originated query */
    public boolean isOriginated() {
        return originated;
    }
    
    @Override
    public Class<? extends Message> getHandlerClass() {
        return QueryRequest.class;
    }

    /**
     * @effects Writes given extension string to given stream, adding
     * delimiter if necessary, reporting whether next call should add
     * delimiter. ext may be null or zero-length, in which case this is noop
     */
    protected boolean writeGemExtension(OutputStream os, 
                                        boolean addPrefixDelimiter, 
                                        byte[] extBytes) throws IOException {
        if (extBytes == null || (extBytes.length == 0)) {
            return addPrefixDelimiter;
        }
        if(addPrefixDelimiter) {
            os.write(0x1c);
        }
        os.write(extBytes);
        return true; // any subsequent extensions should have delimiter 
    }
    
     /**
     * @effects Writes given extension string to given stream, adding
     * delimiter if necessary, reporting whether next call should add
     * delimiter. ext may be null or zero-length, in which case this is noop
     */
    protected boolean writeGemExtension(OutputStream os, 
                                        boolean addPrefixDelimiter, 
                                        String ext) throws IOException {
        if (ext != null)
            return writeGemExtension(os, addPrefixDelimiter, ext.getBytes());
        else
            return writeGemExtension(os, addPrefixDelimiter, new byte[0]);
    }
    
    /**
     * @effects Writes each extension string in exts to given stream,
     * adding delimiters as necessary. exts may be null or empty, in
     *  which case this is noop
     */
    protected boolean writeGemExtensions(OutputStream os, 
                                         boolean addPrefixDelimiter, 
                                         Iterator<?> iter) throws IOException {
        if (iter == null) {
            return addPrefixDelimiter;
        }
        while(iter.hasNext()) {
            addPrefixDelimiter = writeGemExtension(os, addPrefixDelimiter, 
                                                   iter.next().toString());
        }
        return addPrefixDelimiter; // will be true is anything at all was written 
    }
    
    /**
     * @effects utility function to read null-terminated byte[] from stream
     */
    protected static byte[] readNullTerminatedBytes(InputStream is) 
        throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((is.available()>0)&&(i=is.read())!=0) {
            baos.write(i);
        }
        return baos.toByteArray();
    }
    
	@Override
    public int hashCode() {
		if(_hashCode == 0) {
			int result = 17;
			result = (37*result) + QUERY.hashCode();
			if( XML_DOC != null )
			    result = (37*result) + XML_DOC.hashCode();
			result = (37*result) + QUERY_URNS.hashCode();
			if(QUERY_KEY != null) {
				result = (37*result) + QUERY_KEY.hashCode();
			}
			// TODO:: ADD GUID!!
			_hashCode = result;
		}
		return _hashCode;
	}

	// overrides Object.toString
	@Override
    public boolean equals(Object o) {
		if(o == this) return true;
		if(!(o instanceof QueryRequestImpl)) return false;
		QueryRequestImpl qr = (QueryRequestImpl)o;
		return (MIN_SPEED == qr.MIN_SPEED &&
				QUERY.equals(qr.QUERY) &&
				(XML_DOC == null ? qr.XML_DOC == null : 
				    XML_DOC.equals(qr.XML_DOC)) &&
				QUERY_URNS.equals(qr.QUERY_URNS) &&
				Arrays.equals(getGUID(), qr.getGUID()) &&
				Arrays.equals(PAYLOAD, qr.PAYLOAD));
	}


    @Override
    public String toString() {
 		return "<query: \""+getQuery()+"\", "+
            "ttl: "+getTTL()+", "+
            "hops: "+getHops()+", "+            
            "meta: \""+getRichQueryString()+"\", "+
            "urns: "+getQueryUrns().size()+">";
    }


    static byte[] patchInGGEP(byte[] payload, GGEP ggep, MACCalculatorRepositoryManager manager) throws BadPacketException {
        QueryRequestPayloadParser parser = new QueryRequestPayloadParser(payload, manager);
        HUGEExtension huge = parser.huge;
        if (huge != null) {
            // we write in the last modifiable block if available, so our
            // values are still there in the merged version that is read back 
            // from the network: this is not good
            GGEPBlock block = getLastBlock(huge.getGGEPBlocks());
            if (block != null) {
                GGEP merge = new GGEP(true);
                // first merge in original block and then ours, to make sure
                // our values prevail
                merge.merge(block.getGGEP());
                merge.merge(ggep);
                return insertBytes(payload, parser.hugeStart + block.getStartPos(), parser.hugeStart + block.getEndPos(), merge.toByteArray());
            }
        }
        if (isFirstNullByteAfterOffset(payload, payload.length - 1, 2)) {
            // if ggep is appended after query string keep 0 delimiter
            return insertGGEP(payload, payload.length, payload.length, ggep.toByteArray(), true);
        }
        else if (payload[payload.length - 1] != 0x1C) {
            return insertGGEP(payload, payload.length - 1, payload.length - 1, ggep.toByteArray(), true);   
        }
        else {
            return insertGGEP(payload, payload.length, payload.length, ggep.toByteArray(), false);
        }
    }
    
    /**
     * Returns true if byte at <code>index</code> is 0 and it's the first
     * one in the payload signifying the end of the query string.
     */
    private static boolean isFirstNullByteAfterOffset(byte[] payload, int index, int offset) {
        if (payload[index] != 0x00) {
            return false;
        }
        for (int i = offset; i < index; i++) {
            if (payload[i] == 0x00) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Return the last GGEPBlock in the list or null if there
     * is none or if the list is empty.
     */
    private static GGEPBlock getLastBlock(List<GGEPBlock> blocks) {
        return blocks.isEmpty() ? null : blocks.get(blocks.size() - 1);
    }
    
    private static byte[] insertGGEP(byte[] payload, int start, int end, byte[] ggepBytes, boolean prependDelimiter) {
        if (prependDelimiter) {
            byte[] ggepBlock = new byte[ggepBytes.length  +  1];
            // set HUGE delimiter
            ggepBlock[0] = 0x1C;
            System.arraycopy(ggepBytes, 0, ggepBlock, 1, ggepBytes.length);
            return insertBytes(payload, start, end, ggepBlock);
        }
        else {
            return insertBytes(payload, start, end, ggepBytes);
        }
    }
    
    private static byte[] insertBytes(byte[] payload, int start, int end, byte[] ggepBytes) {
        byte[] newPayload = new byte[payload.length + ggepBytes.length - (end - start)];
        
        System.arraycopy(payload, 0, newPayload, 0, start);
        System.arraycopy(ggepBytes, 0, newPayload, start, ggepBytes.length);
        
        if (end < payload.length) {
            System.arraycopy(payload, end, newPayload, start + ggepBytes.length, payload.length - end);
        }
        
        return newPayload;
    }
    
    static class QueryRequestPayloadParser {

        String query = "";
        String richQuery = "";
        int minSpeed = 0;
        Set<URN> queryUrns = null;
        Set<URN.Type> requestedUrnTypes = null;
        AddressSecurityToken addressSecurityToken = null;
        
        HUGEExtension huge;
        
        int featureSelector;
        
        boolean doNotProxy;
        
        boolean doNotProxyV3;
        
        Integer metaMask;
        
        boolean hasSecurityTokenRequest;
        
        boolean partialResultsDesired;
        
        int hugeStart;
        
        int hugeEnd;
        
        public QueryRequestPayloadParser(byte[] payload, MACCalculatorRepositoryManager manager) throws BadPacketException {
            try {
                PositionByteArrayInputStream bais = new PositionByteArrayInputStream(payload);
                short sp = ByteUtils.leb2short(bais);
                minSpeed = ByteUtils.ushort2int(sp);
                query = new String(readNullTerminatedBytes(bais), "UTF-8");
                 
                // handle extensions, which include rich query and URN stuff
                hugeStart = bais.getPos();
                byte[] extsBytes = readNullTerminatedBytes(bais);
                huge = new HUGEExtension(extsBytes);
                hugeEnd = bais.getPos();
                GGEP ggep = huge.getGGEP();

                if(ggep != null) {
                    try {
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT)) {
                            byte[] qkBytes = ggep.getBytes(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT);
                            addressSecurityToken = new AddressSecurityToken(qkBytes, manager);
                        }
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_FEATURE_QUERY))
                            featureSelector = ggep.getInt(GGEPKeys.GGEP_HEADER_FEATURE_QUERY);
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_NO_PROXY)) {
                            doNotProxy = true;
                        }
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_META)) {
                            metaMask = new Integer(ggep.getInt(GGEPKeys.GGEP_HEADER_META));
                            // if the value is something we can't handle, don't even set it
                            if ((metaMask.intValue() < 4) || (metaMask.intValue() > 248))
                                metaMask = null;
                        }
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_SECURE_OOB)) {
                            hasSecurityTokenRequest = true;
                        }
                        
                        if (ggep.hasKey(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_PREFIX))
                            partialResultsDesired = true;
                        
                    } catch (BadGGEPPropertyException ignored) {}
                }

                queryUrns = huge.getURNS();
                requestedUrnTypes = huge.getURNTypes();
                for(String currMiscBlock : huge.getMiscBlocks()) {
                    if(!richQuery.equals(""))
                        break;
                    if (currMiscBlock.startsWith("<?xml"))
                        richQuery = currMiscBlock;                
                }
            } catch(UnsupportedEncodingException uee) {
                //couldn't build query from network due to unsupportedencodingexception
                //so throw a BadPacketException 
                throw new BadPacketException(uee.getMessage());
            } catch (IOException ioe) {
                ErrorService.error(ioe);
            }
        }
        
        static class PositionByteArrayInputStream extends ByteArrayInputStream {

            public PositionByteArrayInputStream(byte[] buf) {
                super(buf);
            }
            
            public int getPos() {
                return pos;
            }
            
        }
        
    }
    
}
