package com.limegroup.gnutella;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.altlocs.AlternateLocationCollection;
import com.limegroup.gnutella.altlocs.DirectAltLoc;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.metadata.AudioMetaData;
import com.limegroup.gnutella.messages.BadGGEPPropertyException;
import com.limegroup.gnutella.messages.GGEP;
import com.limegroup.gnutella.messages.HUGEExtension;
import com.limegroup.gnutella.search.HostData;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.NameValue;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
/* heavy */
import com.limegroup.gnutella.util.I18NConvert;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;


/**
 * A single result from a query reply message.  (In hindsight, "Result" would
 * have been a better name.)  Besides basic file information, responses can
 * include metadata.
 *
 * Response was originally intended to be immutable, but it currently includes
 * mutator methods for metadata; these will be removed in the future.  
 */
public class Response {
    
    private static final Log LOG = LogFactory.getLog(Response.class);
    
    /**
     * The magic byte to use as extension separators.
     */
    private static final byte EXT_SEPARATOR = 0x1c;
    
    /**
     * The maximum number of alternate locations to include in responses
     * in the GGEP block
     */
    private static final int MAX_LOCATIONS = 10;
    
    /** Both index and size must fit into 4 unsigned bytes; see
     *  constructor for details. */
    private final long index;
    private final long size;

	/**
	 * The bytes for the name string, guaranteed to be non-null.
	 */
    private final byte[] nameBytes;

    /** The name of the file matching the search.  This does NOT
     *  include the double null terminator.
     */
    private final String name;

    /** The document representing the XML in this response. */
    private LimeXMLDocument document;

    /** 
	 * The <tt>Set</tt> of <tt>URN</tt> instances for this <tt>Response</tt>,
	 * as specified in HUGE v0.94.  This is guaranteed to be non-null, 
	 * although it is often empty.
     */
    private final Set urns;

	/**
	 * The bytes between the nulls for the <tt>Response</tt>, as specified
	 * in HUGE v0.94.  This is guaranteed to be non-null, although it can be
	 * an empty array.
	 */
    private final byte[] extBytes;
    
    /**
     * The cached RemoteFileDesc created from this Response.
     */
    private volatile RemoteFileDesc cachedRFD;
    
    /**
     * The container for extra GGEP data.
     */
    private final GGEPContainer ggepData;

	/**
	 * Constant for the KBPS string to avoid constructing it too many
	 * times.
	 */
	private static final String KBPS = "kbps";

	/**
	 * Constant for kHz to string to avoid excessive string construction.
	 */
	private static final String KHZ = "kHz";

    /** Creates a fresh new response.
     *
     * @requires index and size can fit in 4 unsigned bytes, i.e.,
     *  0 <= index, size < 2^32
     */
    public Response(long index, long size, String name) {
		this(index, size, name, null, null, null, null);
    }


    /**
     * Creates a new response with parsed metadata.  Typically this is used
     * to respond to query requests.
     * @param doc the metadata to include
     */
    public Response(long index, long size, String name, LimeXMLDocument doc) {
        this(index, size, name, null, doc, null, null);
    }

	/**
	 * Constructs a new <tt>Response</tt> instance from the data in the
	 * specified <tt>FileDesc</tt>.  
	 *
	 * @param fd the <tt>FileDesc</tt> containing the data to construct 
	 *  this <tt>Response</tt> -- must not be <tt>null</tt>
	 */
	public Response(FileDesc fd) {
		this(fd.getIndex(), fd.getFileSize(), fd.getFileName(), 
			 fd.getUrns(), null, 
			 new GGEPContainer(
			    getAsEndpoints(RouterService.getAltlocManager().getDirect(fd.getSHA1Urn())),
			    CreationTimeCache.instance().getCreationTimeAsLong(fd.getSHA1Urn())),
			 null);
	}

    /**
	 * Overloaded constructor that allows the creation of Responses with
     * meta-data and a <tt>Set</tt> of <tt>URN</tt> instances.  This 
	 * is the primary constructor that establishes all of the class's 
	 * invariants, does any necessary parameter validation, etc.
	 *
	 * If extensions is non-null, it is used as the extBytes instead
	 * of creating them from the urns and locations.
	 *
	 * @param index the index of the file referenced in the response
	 * @param size the size of the file (in bytes)
	 * @param name the name of the file
	 * @param urns the <tt>Set</tt> of <tt>URN</tt> instances associated
	 *  with the file
	 * @param doc the <tt>LimeXMLDocument</tt> instance associated with
	 *  the file
	 * @param endpoints a collection of other locations on this network
	 *        that will have this file
	 * @param extensions The raw unparsed extension bytes.
     */
    private Response(long index, long size, String name,
					 Set urns, LimeXMLDocument doc, 
					 GGEPContainer ggepData, byte[] extensions) {
        if( (index & 0xFFFFFFFF00000000L)!=0 )
            throw new IllegalArgumentException("invalid index: " + index);
        // see note in createFromStream about Integer.MAX_VALUE
        if (size > Integer.MAX_VALUE || size < 0)
            throw new IllegalArgumentException("invalid size: " + size);
            
        this.index=index;
        this.size=size;
        
		if (name == null)
			this.name = "";
		else
//			this.name = name;
			this.name = I18NConvert.instance().compose(name);

        byte[] temp = null;
        try {
            temp = this.name.getBytes("UTF-8");
        } catch(UnsupportedEncodingException namex) {
            //b/c this should never happen, we will show and error
            //if it ever does for some reason.
            ErrorService.error(namex);
        }
        this.nameBytes = temp;

		if (urns == null)
			this.urns = Collections.EMPTY_SET;
		else
			this.urns = Collections.unmodifiableSet(urns);
		
        if(ggepData == null)
            this.ggepData = GGEPContainer.EMPTY;
        else
		    this.ggepData = ggepData;
		
		if (extensions != null)
		    this.extBytes = extensions;
		else 
		    this.extBytes = createExtBytes(this.urns, this.ggepData);

		this.document = doc;
    }
  
    /**
     * Factory method for instantiating individual responses from an
	 * <tt>InputStream</tt> instance.
	 * 
	 * @param is the <tt>InputStream</tt> to read from
	 * @throws <tt>IOException</tt> if there are any problems reading from
	 *  or writing to the stream
     */
    public static Response createFromStream(InputStream is) 
		throws IOException {
        // extract file index & size
        long index=ByteOrder.uint2long(ByteOrder.leb2int(is));
        long size=ByteOrder.uint2long(ByteOrder.leb2int(is));
        
        if( (index & 0xFFFFFFFF00000000L)!=0 )
            throw new IOException("invalid index: " + index);
        // must use Integer.MAX_VALUE instead of mask because
        // this value is later converted to an int, so we want
        // to ensure that when it's converted it doesn't become
        // negative.
        if (size > Integer.MAX_VALUE || size < 0)
            throw new IOException("invalid size: " + size);        

        //The file name is terminated by a null terminator.  
        // A second null indicates the end of this response.
        // Gnotella & others insert meta-information between
        // these null characters.  So we have to handle this.
        // See http://gnutelladev.wego.com/go/
        //         wego.discussion.message?groupId=139406&
        //         view=message&curMsgId=319258&discId=140845&
        //         index=-1&action=view

        // Extract the filename
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int c;
        while((c=is.read())!=0) {
            if(c == -1)
                throw new IOException("EOF before null termination");
            baos.write(c);
        }
        String name = new String(baos.toByteArray(), "UTF-8");
        if(name.length() == 0) {
            throw new IOException("empty name in response");
        }

        // Extract extra info, if any
        baos.reset();
        while((c=is.read())!=0) {
            if(c == -1)
                throw new IOException("EOF before null termination");
            baos.write(c);
        }
        byte[] rawMeta = baos.toByteArray();
        if(rawMeta == null || rawMeta.length == 0) {
			if(is.available() < 16) {
				throw new IOException("not enough room for the GUID");
			}
            return new Response(index,size,name);
        } else {
			// now handle between-the-nulls
			// \u001c is the HUGE v0.93 GEM delimiter
            HUGEExtension huge = new HUGEExtension(rawMeta);

			Set urns = huge.getURNS();

			LimeXMLDocument doc = null;
            Iterator iter = huge.getMiscBlocks().iterator();
            while (iter.hasNext() && doc == null)
                doc = createXmlDocument(name, (String)iter.next());

			GGEPContainer ggep = GGEPUtil.getGGEP(huge.getGGEP());

			return new Response(index, size, name, 
			                    urns, doc, ggep, rawMeta);
        }
    }
    
	/**
	 * Constructs an xml string from the given extension sting.
	 *
	 * @param name the name of the file to construct the string for
	 * @param ext an individual between-the-nulls string (note that there
	 *  can be multiple between-the-nulls extension strings with HUGE)
	 * @return the xml formatted string, or the empty string if the
	 *  xml could not be parsed
	 */
	private static LimeXMLDocument createXmlDocument(String name, String ext) {
		StringTokenizer tok = new StringTokenizer(ext);
		// if there aren't the expected number of tokens, simply
		// return the empty string
		if(tok.countTokens() < 2)
			return null;
		
		String first  = tok.nextToken();
		String second = tok.nextToken();
		if (first != null)
		    first = first.toLowerCase();
		if (second != null)
		    second = second.toLowerCase();
		String length="";
		String bitrate="";
		boolean bearShare1 = false;        
		boolean bearShare2 = false;
		boolean gnotella = false;
		if(second.startsWith(KBPS))
			bearShare1 = true;
		else if (first.endsWith(KBPS))
			bearShare2 = true;
		if(bearShare1){
			bitrate = first;
		}
		else if (bearShare2){
			int j = first.indexOf(KBPS);
			bitrate = first.substring(0,j);
		}
		if(bearShare1 || bearShare2){
			while(tok.hasMoreTokens())
				length=tok.nextToken();
			//OK we have the bitrate and the length
		}
		else if (ext.endsWith(KHZ)){//Gnotella
			gnotella = true;
			length=first;
			//extract the bitrate from second
			int i=second.indexOf(KBPS);
			if(i>-1)//see if we can find the bitrate                
				bitrate = second.substring(0,i);
			else//not gnotella, after all...some other format we do not know
				gnotella=false;
		}
		
		// make sure these are valid numbers.
		try {
		    Integer.parseInt(bitrate);
		    Integer.parseInt(length);
		} catch(NumberFormatException nfe) {
		    return null;
		}
		
		if(bearShare1 || bearShare2 || gnotella) {//some metadata we understand
		    List values = new ArrayList(3);
		    values.add(new NameValue("audios__audio__title__", name));
		    values.add(new NameValue("audios__audio__bitrate__", bitrate));
		    values.add(new NameValue("audios__audio__seconds__", length));
		    return new LimeXMLDocument(values, AudioMetaData.schemaURI);
		}
		
		return null;
	}

	/**
	 * Helper method that creates an array of bytes for the specified
	 * <tt>Set</tt> of <tt>URN</tt> instances.  The bytes are written
	 * as specified in HUGE v 0.94.
	 *
	 * @param urns the <tt>Set</tt> of <tt>URN</tt> instances to use in
	 *  constructing the byte array
	 */
	private static byte[] createExtBytes(Set urns, GGEPContainer ggep) {
        try {
            if( isEmpty(urns) && ggep.isEmpty() )
                return DataUtils.EMPTY_BYTE_ARRAY;
            
            ByteArrayOutputStream baos = new ByteArrayOutputStream();            
            if( !isEmpty(urns) ) {
                // Add the extension for URNs, if any.
    			Iterator iter = urns.iterator();
    			while (iter.hasNext()) {
    				URN urn = (URN)iter.next();
                    Assert.that(urn!=null, "Null URN");
    				baos.write(urn.toString().getBytes());
    				// If there's another URN, add the separator.
    				if (iter.hasNext()) {
    					baos.write(EXT_SEPARATOR);
    				}
    			}
    			
    			// If there's ggep data, write the separator.
    		    if( !ggep.isEmpty() )
    		        baos.write(EXT_SEPARATOR);
            }
            
            // It is imperitive that GGEP is added LAST.
            // That is because GGEP can contain 0x1c (EXT_SEPARATOR)
            // within it, which would cause parsing problems
            // otherwise.
            if(!ggep.isEmpty())
                GGEPUtil.addGGEP(baos, ggep);
			
            return baos.toByteArray();
        } catch (IOException impossible) {
            ErrorService.error(impossible);
            return DataUtils.EMPTY_BYTE_ARRAY;
        }
    }
    
    /**
     * Utility method to know if a set is empty or null.
     */
    private static boolean isEmpty(Set set) {
        return set == null || set.isEmpty();
    }
    
    /**
     * Utility method for converting the non-firewalled elements of an
     * AlternateLocationCollection to a smaller set of endpoints.
     */
    private static Set getAsEndpoints(AlternateLocationCollection col) {
        if( col == null || !col.hasAlternateLocations() )
            return Collections.EMPTY_SET;
        
        long now = System.currentTimeMillis();
        synchronized(col) {
            Set endpoints = null;
            int i = 0;
            for(Iterator iter = col.iterator();
              iter.hasNext() && i < MAX_LOCATIONS;) {
            	Object o = iter.next();
            	if (!(o instanceof DirectAltLoc))
            		continue;
                DirectAltLoc al = (DirectAltLoc)o;
                if (al.canBeSent(AlternateLocation.MESH_RESPONSE)) {
                    IpPort host = al.getHost();
                    if( !NetworkUtils.isMe(host) ) {
                        if (endpoints == null)
                            endpoints = new HashSet();
                        
                        if (!(host instanceof Endpoint)) 
                        	host = new Endpoint(host.getAddress(),host.getPort());
                        
                        endpoints.add( host );
                        i++;
                        al.send(now, AlternateLocation.MESH_RESPONSE);
                    }
                } else if (!al.canBeSentAny())
                    iter.remove();
            }
            return endpoints == null ? Collections.EMPTY_SET : endpoints;
        }
    }    

    /**
     * Like writeToArray(), but writes to an OutputStream.
     */
    public void writeToStream(OutputStream os) throws IOException {
        ByteOrder.int2leb((int)index, os);
        ByteOrder.int2leb((int)size, os);
        for (int i = 0; i < nameBytes.length; i++)
            os.write(nameBytes[i]);
        //Write first null terminator.
        os.write(0);
        // write HUGE v0.93 General Extension Mechanism extensions
        // (currently just URNs)
        for (int i = 0; i < extBytes.length; i++)
            os.write(extBytes[i]);
        //add the second null terminator
        os.write(0);
    }

    /**
     * Sets this' metadata.
     * @param meta the parsed XML metadata 
     */	
    public void setDocument(LimeXMLDocument doc) {
        document = doc;
	}
	
    
    /**
     */
    public int getLength() {
        // must match same number of bytes writeToArray() will write
		return 8 +                   // index and size
		nameBytes.length +
		1 +                   // null
		extBytes.length +
		1;                    // final null
    }   
   

	/**
	 * Returns the index for the file stored in this <tt>Response</tt>
	 * instance.
	 *
	 * @return the index for the file stored in this <tt>Response</tt>
	 * instance
	 */
    public long getIndex() {
        return index;
    }

	/**
	 * Returns the size of the file for this <tt>Response</tt> instance
	 * (in bytes).
	 *
	 * @return the size of the file for this <tt>Response</tt> instance
	 * (in bytes)
	 */
    public long getSize() {
        return size;
    }

	/**
	 * Returns the name of the file for this response.  This is guaranteed
	 * to be non-null, but it could be the empty string.
	 *
	 * @return the name of the file for this response
	 */
    public String getName() {
        return name;
    }

    /**
     * Returns this' metadata.
     */
    public LimeXMLDocument getDocument() {
        return document;
    }

	/**
	 * Returns an immutable <tt>Set</tt> of <tt>URN</tt> instances for 
	 * this <tt>Response</tt>.
	 *
	 * @return an immutable <tt>Set</tt> of <tt>URN</tt> instances for 
	 * this <tt>Response</tt>, guaranteed to be non-null, although the
	 * set could be empty
	 */
    public Set getUrns() {
		return urns;
    }
    
    /**
     * Returns an immutabe <tt>Set</tt> of <tt>Endpoint</tt> that
     * contain the same file described in this <tt>Response</tt>.
     *
     * @return an immutabe <tt>Set</tt> of <tt>Endpoint</tt> that
     * contain the same file described in this <tt>Response</tt>,
     * guaranteed to be non-null, although the set could be empty
     */
    public Set getLocations() {
        return ggepData.locations;
    }
    
    /**
     * Returns the create time.
     */
    public long getCreateTime() {
        return ggepData.createTime;
    }    
    
    byte[] getExtBytes() {
        return extBytes;
    }
    
    /**
     * Returns this Response as a RemoteFileDesc.
     */
    public RemoteFileDesc toRemoteFileDesc(HostData data){
        if(cachedRFD != null &&
           cachedRFD.getPort() == data.getPort() &&
           cachedRFD.getHost().equals(data.getIP()))
            return cachedRFD;
        else {
            RemoteFileDesc rfd = new RemoteFileDesc(
                 data.getIP(),
                 data.getPort(),
                 getIndex(),
                 getName(),
                 (int)getSize(),
                 data.getClientGUID(),
                 data.getSpeed(),
                 data.isChatEnabled(),
                 data.getQuality(),
                 data.isBrowseHostEnabled(),
                 getDocument(),
                 getUrns(),
                 data.isReplyToMulticastQuery(),
                 data.isFirewalled(), 
                 data.getVendorCode(),
                 System.currentTimeMillis(),
                 data.getPushProxies(),
                 getCreateTime(),
                 data.getFWTVersionSupported()
                );
            cachedRFD = rfd;
            return rfd;
        }
    }

	/**
	 * Overrides equals to check that these two responses are equal.
	 * Raw extension bytes are not checked, because they may be
	 * extensions that do not change equality, such as
	 * otherLocations.
	 */
    public boolean equals(Object o) {
		if(o == this) return true;
        if (! (o instanceof Response))
            return false;
        Response r=(Response)o;
		return getIndex() == r.getIndex() &&
               getSize() == r.getSize() &&
			   getName().equals(r.getName()) &&
               ((getDocument() == null) ? (r.getDocument() == null) :
               getDocument().equals(r.getDocument())) &&
               getUrns().equals(r.getUrns());
    }


    public int hashCode() {
        //Good enough for the moment
        // TODO:: IMPROVE THIS HASHCODE!!
        return getName().hashCode()+(int)getSize()+(int)getIndex();
    }

	/**
	 * Overrides Object.toString to print out a more informative message.
	 */
	public String toString() {
		return ("index:        "+index+"\r\n"+
				"size:         "+size+"\r\n"+
				"name:         "+name+"\r\n"+
				"xml document: "+document+"\r\n"+
				"urns:         "+urns);
	}
	
    /**
     * Utility class for handling GGEP blocks in the per-file section
     * of QueryHits.
     */
    private static class GGEPUtil {
        
        /**
         * Private constructor so it never gets constructed.
         */
        private GGEPUtil() {}
        
        /**
         * Adds a GGEP block with the specified alternate locations to the 
         * output stream.
         */
        static void addGGEP(OutputStream out, GGEPContainer ggep)
          throws IOException {
            if( ggep == null || (ggep.locations.size() == 0 && ggep.createTime <= 0))
                throw new NullPointerException("null or empty locations");
            
            GGEP info = new GGEP();
            if(ggep.locations.size() > 0) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                try {
                    for(Iterator i = ggep.locations.iterator(); i.hasNext();) {
                        try {
                            Endpoint ep = (Endpoint)i.next();
                            baos.write(ep.getHostBytes());
                            ByteOrder.short2leb((short)ep.getPort(), baos);
                        } catch(UnknownHostException uhe) {
                            continue;
                        }
                    }
                } catch(IOException impossible) {
                    ErrorService.error(impossible);
                }   
                info.put(GGEP.GGEP_HEADER_ALTS, baos.toByteArray());
            }
            
            if(ggep.createTime > 0)
                info.put(GGEP.GGEP_HEADER_CREATE_TIME, ggep.createTime / 1000);
            
            
            info.write(out);
        }
        
        /**
         * Returns a <tt>Set</tt> of other endpoints described
         * in one of the GGEP arrays.
         */
        static GGEPContainer getGGEP(GGEP ggep) {
            if (ggep == null)
                return GGEPContainer.EMPTY;

            Set locations = null;
            long createTime = -1;
            
            // if the block has a ALTS value, get it, parse it,
            // and move to the next.
            if (ggep.hasKey(GGEP.GGEP_HEADER_ALTS)) {
                try {
                    locations = parseLocations(ggep.getBytes(GGEP.GGEP_HEADER_ALTS));
                } catch (BadGGEPPropertyException bad) {}
            }
            
            if(ggep.hasKey(GGEP.GGEP_HEADER_CREATE_TIME)) {
                try {
                    createTime = ggep.getLong(GGEP.GGEP_HEADER_CREATE_TIME) * 1000;
                } catch(BadGGEPPropertyException bad) {}
            }
            
            return (locations == null && createTime == -1) ?
                GGEPContainer.EMPTY : new GGEPContainer(locations, createTime);
        }
        
        private static Set parseLocations(byte[] locBytes) {
            Set locations = null;
            IPFilter ipFilter = IPFilter.instance();
 
            if (locBytes.length % 6 == 0) {
                for (int j = 0; j < locBytes.length; j += 6) {
                    int port = ByteOrder.ushort2int(ByteOrder.leb2short(locBytes, j+4));
                    if (!NetworkUtils.isValidPort(port))
                        continue;
                    byte[] ip = new byte[4];
                    ip[0] = locBytes[j];
                    ip[1] = locBytes[j + 1];
                    ip[2] = locBytes[j + 2];
                    ip[3] = locBytes[j + 3];
                    if (!NetworkUtils.isValidAddress(ip) ||
                        !ipFilter.allow(ip) ||
                        NetworkUtils.isMe(ip, port))
                        continue;
                    if (locations == null)
                        locations = new HashSet();
                    locations.add(new Endpoint(ip, port));
                }
            }
            return locations;
        }
    }
    
    /**
     * A container for information we're putting in/out of GGEP blocks.
     */
    static final class GGEPContainer {
        final Set locations;
        final long createTime;
        private static final GGEPContainer EMPTY = new GGEPContainer();
        
        private GGEPContainer() {
            this(null, -1);
        }
        
        GGEPContainer(Set locs, long create) {
            locations = locs == null ? Collections.EMPTY_SET : locs;
            createTime = create;
        }
        
        boolean isEmpty() {
            return locations.isEmpty() && createTime <= 0;
        }
    }
}

