package com.limegroup.gnutella.http;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.net.URLEncoder;

import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.settings.ChatSettings;
import com.limegroup.gnutella.statistics.BandwidthStat;
import com.limegroup.gnutella.util.StringUtils;

/**
 * This class supplies general facilities for handling HTTP, such as
 * writing headers, extracting header values, etc..
 */
public final class HTTPUtils {
	
	/**
	 * Constant for the carriage-return linefeed sequence that marks
	 * the end of an HTTP header
	 */
	private static final String CRLF = "\r\n";

	/**
	 * Cached colon followed by a space to avoid excessive allocations.
	 */
	private static final String COLON_SPACE = ": ";

	/**
	 * Cached colon to avoid excessive allocations.
	 */
	private static final String COLON = ":";
	
	/**
	 * Cached slash to avoid excessive allocations.
	 */
	private static final String SLASH = "/";

	/**
	 * Private constructor to ensure that this class cannot be constructed
	 */
	private HTTPUtils() {}
	
	/**
	 * Writes an single http header to the specified 
	 * <tt>OutputStream</tt> instance, with the specified header name 
	 * and the specified header value.
	 *
	 * @param name the <tt>HTTPHeaderName</tt> instance containing the
	 *  header name to write to the stream
	 * @param value the <tt>String</tt> instance containing the
	 *  header value to write to the stream
	 * @param os the <tt>OutputStream</tt> instance to write to
	 */
	public static void writeHeader(HTTPHeaderName name, String value, 
								   OutputStream os) 
		throws IOException {
		if(name == null) {
			throw new NullPointerException("null name in writing http header");
		} else if(value == null) {
			throw new NullPointerException("null value in writing http header: "+
										   name);
		} else if(os == null) {
			throw new NullPointerException("null os in writing http header: "+
										   name);
		}
		String header = createHeader(name, value);
		os.write(header.getBytes());
		BandwidthStat.HTTP_HEADER_UPSTREAM_BANDWIDTH.addData(header.length());
	}

	/**
	 * Writes an single http header to the specified 
	 * <tt>OutputStream</tt> instance, with the specified header name 
	 * and the specified header value.
	 *
	 * @param name the <tt>HTTPHeaderName</tt> instance containing the
	 *  header name to write to the stream
	 * @param value the <tt>HTTPHeaderValue</tt> instance containing the
	 *  header value to write to the stream
	 * @param out the <tt>Writer</tt> instance to write to
	 */
	public static void writeHeader(HTTPHeaderName name, String value, Writer out) 
	  throws IOException {
		if(name == null) {
			throw new NullPointerException("null name in writing http header");
		} else if(value == null) {
			throw new NullPointerException("null value in writing http header: "+
										   name);
		} else if(out == null) {
			throw new NullPointerException("null os in writing http header: "+
										   name);
		}
		String header = createHeader(name, value);
		out.write(header);
		BandwidthStat.HTTP_HEADER_UPSTREAM_BANDWIDTH.addData(header.length());
	}
	

	/**
	 * Writes an single http header to the specified 
	 * <tt>OutputStream</tt> instance, with the specified header name 
	 * and the specified header value.
	 *
	 * @param name the <tt>HTTPHeaderName</tt> instance containing the
	 *  header name to write to the stream
	 * @param name the <tt>HTTPHeaderValue</tt> instance containing the
	 *  header value to write to the stream
	 * @param os the <tt>OutputStream</tt> instance to write to
	 */
	public static void writeHeader(HTTPHeaderName name, HTTPHeaderValue value, OutputStream os) 
      throws IOException {
		if(name == null) {
			throw new NullPointerException("null name in writing http header");
		} else if(value == null) {
			throw new NullPointerException("null value in writing http header: "+
										   name);
		} else if(os == null) {
			throw new NullPointerException("null os in writing http header: "+
										   name);
		}
		String header = createHeader(name, value.httpStringValue());
		os.write(header.getBytes());
		BandwidthStat.HTTP_HEADER_UPSTREAM_BANDWIDTH.addData(header.length());
	}

	/**
	 * Writes an single http header to the specified 
	 * <tt>OutputStream</tt> instance, with the specified header name 
	 * and the specified header value.
	 *
	 * @param name the <tt>HTTPHeaderName</tt> instance containing the
	 *  header name to write to the stream
	 * @param name the <tt>HTTPHeaderValue</tt> instance containing the
	 *  header value to write to the stream
	 * @param out the <tt>Writer</tt> instance to write to
	 */
	public static void writeHeader(HTTPHeaderName name, HTTPHeaderValue value, 
								   Writer out) 
		throws IOException {
		if(name == null) {
			throw new NullPointerException("null name in writing http header");
		} else if(value == null) {
			throw new NullPointerException("null value in writing http header: "+
										   name);
		} else if(out == null) {
			throw new NullPointerException("null os in writing http header: "+
										   name);
		}
		String header = createHeader(name, value.httpStringValue());
		out.write(header);
		BandwidthStat.HTTP_HEADER_UPSTREAM_BANDWIDTH.addData(header.length());
	}

	/**
	 * Create a single http header String with the specified header name 
	 * and the specified header value.
	 *
	 * @param name the <tt>HTTPHeaderName</tt> instance containing the
	 *  header name 
	 * @param valueStr the value of the header, generally the httpStringValue
	 *  or a HttpHeaderValue, or just a String.
	 */
	private static String createHeader(HTTPHeaderName name, String valueStr) 
		throws IOException {
		if((name == null) || (valueStr == null)) {
			throw new NullPointerException("null value in creating http header");
		}
		String nameStr  = name.httpStringValue();
		if(nameStr == null) {
			throw new NullPointerException("null value in creating http header");
		}

		StringBuffer sb = new StringBuffer();
		sb.append(nameStr);
		sb.append(COLON_SPACE);
		sb.append(valueStr);
		sb.append(CRLF);
		return sb.toString();
	}

	/**
	 * Parses out the header value from the HTTP header string.
	 *
	 * @return the header value for the specified full header string, or
	 *  <tt>null</tt> if the value could not be extracted
	 */
	public static String extractHeaderValue(final String header) {
		int index = header.indexOf(COLON);
		if(index <= 0) return null;
		return header.substring(index+1).trim();
	}

	/**
     * Utility method for writing a header with an integer value.  This removes
     * the burden to the caller of converting integer HTTP values to strings.
     * 
	 * @param name the <tt>HTTPHeaderName</tt> of the header to write
	 * @param value the int value of the header
	 * @param writer the <tt>Writer</tt> instance to write the header to
	 * @throws IOException if an IO error occurs during the write
	 */
    public static void writeHeader(HTTPHeaderName name, int value, Writer writer) throws IOException {
        writeHeader(name, String.valueOf(value), writer);
    }
    
    /**
     * Utility method for writing a header with an integer value.  This removes
     * the burden to the caller of converting integer HTTP values to strings.
     * 
     * @param name the <tt>HTTPHeaderName</tt> of the header to write
     * @param value the int value of the header
     * @param stream the <tt>OutputStream</tt> instance to write the header to
     * @throws IOException if an IO error occurs during the write
     */
    public static void writeHeader(HTTPHeaderName name, int value, OutputStream stream) throws IOException {
        writeHeader(name, String.valueOf(value), stream);
    }
    
    /**
     * Writes the Content-Disposition header, assuming an 'attachment'.
     */
    public static void writeContentDisposition(String name, Writer writer) throws IOException {
        writeHeader(HTTPHeaderName.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + encode(name, "US-ASCII") + "\"",
                    writer);
    }
    
    /**
     * Utility method for writing the "Date" header, as specified in RFC 2616
     * section 14.18, to a <tt>Writer</tt>.
     * 
     * @param writer the <tt>Writer</tt> to write the header to
     * @throws IOException if a write error occurs
     */
    public static void writeDate(Writer writer) throws IOException {
        writeHeader(HTTPHeaderName.DATE, getDateValue(), writer);
    }
  
    /**
     * Utility method for writing the "Date" header, as specified in RFC 2616
     * section 14.18, to a <tt>OutputStream</tt>.
     * 
     * @param stream the <tt>OutputStream</tt> to write the header to
     * @throws IOException if a write error occurs
     */
    public static void writeDate(OutputStream stream) throws IOException {
        writeHeader(HTTPHeaderName.DATE, getDateValue(), stream);       
    }
    
    /**
     * Utility method for writing the currently supported features
     * to the <tt>Writer</tt>.
     */
    public static void writeFeatures(Writer writer) throws IOException {
        Set features = getFeaturesValue();
        // Write X-Features header.
        if (features.size() > 0) {
            writeHeader(HTTPHeaderName.FEATURES,
                    new HTTPHeaderValueCollection(features), writer);
        }
    }
    
    /**
     * Utility method for writing the currently supported features
     * to the <tt>OutputStream</tt>.
     */
    public static void writeFeatures(OutputStream stream) throws IOException {
        Set features = getFeaturesValue();
        // Write X-Features header.
        if (features.size() > 0) {
            writeHeader(HTTPHeaderName.FEATURES,
                    new HTTPHeaderValueCollection(features), stream);
        }
    }        
    
    /**
     * Utlity method for getting the currently supported features.
     */
    private static Set getFeaturesValue() {
        Set features = new HashSet(4);
        features.add(ConstantHTTPHeaderValue.BROWSE_FEATURE);
        if (ChatSettings.CHAT_ENABLED.getValue())
            features.add(ConstantHTTPHeaderValue.CHAT_FEATURE);
        
       	features.add(ConstantHTTPHeaderValue.PUSH_LOCS_FEATURE);
       	
       	if (!RouterService.acceptedIncomingConnection() && UDPService.instance().canDoFWT())
       	    features.add(ConstantHTTPHeaderValue.FWT_PUSH_LOCS_FEATURE);
        
        return features;
    }
    
    /**
     * Utility method for extracting the version from a feature token.
     */
    public static float parseFeatureToken(String token) throws
    	ProblemReadingHeaderException{
        int slashIndex = token.indexOf(SLASH);
        
        if (slashIndex == -1 || slashIndex >= token.length()-1)
            throw new ProblemReadingHeaderException("invalid feature token");
        
        String versionS = token.substring(slashIndex+1);
        
        try {
            return Float.parseFloat(versionS);
        }catch (NumberFormatException bad) {
            throw new ProblemReadingHeaderException(bad);
        }
    }
    
    /**
     * Utility method for getting the date value for the "Date" header in
     * standard format.
     * 
     * @return the current date as a standardized date string -- see 
     *  RFC 2616 section 14.18
     */
    private static String getDateValue() {
        DateFormat df = 
            new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US);
        df.setTimeZone(TimeZone.getTimeZone("GMT"));
        return df.format(new Date());
    }
    
    /**
     * Encodes a name using URLEncoder, using %20 instead of + for spaces.
     */
    private static String encode(String name, String encoding) throws IOException {
        return StringUtils.replace(URLEncoder.encode(name, encoding), "+", "%20");
    }
}
