package org.wikiwebserver.handler.http;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.wikiwebserver.core.ConfigManager;

public class HTTPHeaders extends TreeMap<String, List<String>> {

    private static final long serialVersionUID = 1L;
    
    private static final String CRLF = "\r\n";
    private static final String DEFAULT_COOKIE_EXPIRES = 
    	ConfigManager.getString("http.cookie-expiration"); 
    
    public HTTPHeaders() {
        super(String.CASE_INSENSITIVE_ORDER);
    }
    
    public String getFirst(String key) {
        List<String> list = get(key);
        if (list == null || list.size() == 0) {
            return null;
        }
        return list.get(0);
    }
    
    public long getFirstDate(String key) {
        List<String> list = get(key);
        if (list == null || list.size() != 2) return 0;
        // Date is spread across two fields as it contains a comma
        return parseDate(list.get(0) + ", " + list.get(1));
    }    
    
    public void add(String key, String... values) {
        List<String> list = get(key);
        if (list == null) {
            set(key, values);
        } else {
            populate(list, values);
        }
    }
    
    public void set(String key, String... values) {
        List<String> list = new LinkedList<String>();
        populate(list, values);
        put(key, list);
    }  
    
    private void splitAndSet(String key, String values, String sep) {
        List<String> parts = new LinkedList<String>();
        int sidx = 0;
        int idx = values.indexOf(sep, sidx);
        while (idx > 0) {
            parts.add(values.substring(sidx, idx).trim());
            sidx = idx+1;
            idx = values.indexOf(sep, sidx);
        }
        parts.add(values.substring(sidx).trim());
        put(key, parts);
    }     
    
    public void setAll(HTTPHeaders headers) {
        if (headers != null) {
            this.putAll(headers);
        }
    }    
    
    private void populate(List<String> list, String... values) {
        for (String value : values) {
            if (value == null) continue;
            value = value.trim();
            /* Stupid HTTP spec means date formatting in cookie is illigal
            if (value.contains(",")) {
                throw new IllegalArgumentException("Value can not contain seperator character"); 
            } 
            */           
            list.add(value);
        }
    }
    
    public void setDate(String key, long millis) {
        // A date is two fields since it contains a comma
        splitAndSet(key, formatDate(millis), ",");
    }
    
    public Map<String, String> getRequestCookies() {
        Map<String, String> cookieMap = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
        String sentCookies = getFirst("Cookie");
        if (sentCookies != null) {
            StringTokenizer tokenizer = new StringTokenizer(sentCookies, ";");
            while (tokenizer.hasMoreTokens()) {
                String cookie = tokenizer.nextToken();
                int sep = cookie.indexOf('=');
                if (sep > -1) {
                    cookieMap.put(cookie.substring(0, sep).trim(), 
                                  cookie.substring(sep+1).trim());
                }
            }
        }
        return cookieMap;
    }    
    
    public void setResponseCookie(String key, String value) {

        String expires = DEFAULT_COOKIE_EXPIRES;
        if (value == null) {
            expires = "Sat, 01-Jan-2000 00:00:00 GMT";
            value = "";
        }
            
        add("Set-Cookie", key + "=" + value + "; expires=" + expires + "; path=/");
    }   
    
    public static HTTPHeaders readHeaders(HTTPInputStream reader) throws IOException {
        
        HTTPHeaders headers = new HTTPHeaders();
        
        String charset = "US-ASCII";
        String line = reader.readStringUntil(charset, CRLF);
        reader.skipBytes(2);
        while (line != null && line.length() > 0) {
            // A header may span multiple lines if prefixed with a space or tab
            String nextLine = reader.readStringUntil(charset, CRLF);
            reader.skipBytes(2);
            while (nextLine != null && nextLine.length() > 0 && 
                  (nextLine.charAt(0) == ' ' || nextLine.charAt(0) == '\t')) {
                
                line += nextLine;
                nextLine = reader.readStringUntil(charset, CRLF);
                reader.skipBytes(2);
            }

            int sep = line.indexOf(':');
            if (sep > 0) {
	            String key = line.substring(0, sep);
	            String valueList = "";
	            if (line.length() > sep+1) {
	                valueList = line.substring(sep+1);
	            }
	            headers.splitAndSet(key, valueList, ",");
            }
            line = nextLine;            
        }
        
        return headers;
    }

    
    public String toString() {
        
        StringBuilder httpForm = new StringBuilder(1024);
        for(Map.Entry<String, List<String>> field : entrySet()) {
            if (field.getKey() != null && field.getValue() != null) {
                String key = field.getKey();
                httpForm.append(key);
                httpForm.append(": ");
                
                Iterator<String> i = field.getValue().iterator();
                while (i.hasNext()) {
                    String value = i.next();;
                    if (value != null) {
                        // We should add a space after line breaks in values,
                        // but in reality values do not contain line breaks
                        // value = value.replace("\n", "\n ");
                        httpForm.append(value);
                    }
                    if (i.hasNext()) {
                        if (key.equalsIgnoreCase("Set-Cookie")) {
                            httpForm.append(CRLF);
                            httpForm.append(key);
                            httpForm.append(": ");
                        }                        
                        else httpForm.append(", ");
                    }
                }
                
                httpForm.append(CRLF);
            }
        }
        return httpForm.toString();
    }
    
    
    
  

    public static String formatDate(long time) { 
        
        if (time < 0) time = 0;
        
        // Work out day of week
        int dayOfWeek = (int) ((time / DAY_IN_MILLIS) % 7);
        
        // Find year
        int year = 49;
        for (; year>0; year--) {
            if (time > yearsToMillis[year]) break;
        }
        
        // Drop year data
        time -= yearsToMillis[year];
        
        // Find month
        int month = 0;
        for (; month<12; month++) { 
            // Add day for leap years
            if (month >= 2 && (year+1970) % 4 == 0) {
                if (time < monthsToMillis[month] + DAY_IN_MILLIS) break;
            }              
            if (time < monthsToMillis[month]) break;
        }
        month--;
        
        // Drop month
        if (month >= 2 && (year+1970) % 4 == 0) {
            // Drop extra day for leap years
            time -= monthsToMillis[month] + DAY_IN_MILLIS;
        } 
        else time -= monthsToMillis[month];


        int day = (int) (time / DAY_IN_MILLIS);
        time -= day * DAY_IN_MILLIS;              
        int hour = (int) (time / HOUR_IN_MILLIS);
        time -= hour * HOUR_IN_MILLIS;  
        int minute = (int) (time / MINUTE_IN_MILLIS);
        time -= minute * MINUTE_IN_MILLIS;  
        int second = (int) (time / SECOND_IN_MILLIS);
        time -= second * SECOND_IN_MILLIS;          
        
        return dayLabels[dayOfWeek] + ", " + twoDecimalPlaces(day+1) + " " +
               monthLabels[month] + " " + (year+1970) + " " + 
               twoDecimalPlaces(hour) + ":" + 
               twoDecimalPlaces(minute) + ":" + 
               twoDecimalPlaces(second) + " GMT";
    }
    
    public static long parseDate(String str) {
        
        try {
            int days = Integer.parseInt(str.substring(5, 7));
            int months = 0;
            String monthsS = str.substring(8, 11);
            for (months=0; months<12; months++) {
                if (monthLabels[months].equals(monthsS)) break;
            }
            int years = Integer.parseInt(str.substring(12, 16));
            if (years < 1970) return 0;
            
            int hours = Integer.parseInt(str.substring(17, 19));
            int minutes = Integer.parseInt(str.substring(20, 22));
            int seconds = Integer.parseInt(str.substring(23, 25));
            
            long millis = yearsToMillis[years - 1970] + 
                          monthsToMillis[months] + 
                          (days-1) * DAY_IN_MILLIS +
                          hours * HOUR_IN_MILLIS + 
                          minutes * MINUTE_IN_MILLIS + 
                          seconds * SECOND_IN_MILLIS;
            
            if (months > 1 && years % 4 == 0) {
                millis += DAY_IN_MILLIS;
            }
        
            return millis;
        }
        catch (Exception ex) {
            System.err.println("Failed to parse HTTP date: " + str);
            ex.printStackTrace();
        }     
        
        return -1;
    }  
    
    private static String twoDecimalPlaces(int i) {
        return (i > 9) ? "" + i : "0" + i;
    }     
    
    private static final int SECOND_IN_MILLIS = 1000;
    private static final int MINUTE_IN_MILLIS = 60  * SECOND_IN_MILLIS;
    private static final int HOUR_IN_MILLIS   = 60  * MINUTE_IN_MILLIS;
    private static final long DAY_IN_MILLIS   = 24  * HOUR_IN_MILLIS;
    private static final long YEAR_IN_MILLIS  = 365 * DAY_IN_MILLIS;
    
    private static int[]    daysInMonth;
    private static String[] dayLabels;
    private static String[] monthLabels;
    
    private static long[]   yearsToMillis;
    private static long[]   monthsToMillis;  
    
    static {
        // Populate year lookup
        yearsToMillis = new long[50];
        yearsToMillis[0] = 0;
        for (int i=1; i<50; i++) {
            yearsToMillis[i] = yearsToMillis[i-1] + YEAR_IN_MILLIS;
            if ((1970 + (i-1)) % 4 == 0) {
                yearsToMillis[i] += DAY_IN_MILLIS;
            }
        }
        monthLabels = new String[] {
                "Jan" , "Feb" , "Mar" , "Apr", "May", "Jun",
                "Jul" , "Aug", "Sep" , "Oct" , "Nov" , "Dec"
        };
        daysInMonth = new int[] {
                31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        };
        // Populate month lookup
        monthsToMillis = new long[12]; 
        monthsToMillis[0] = 0;
        for (int i=1; i<12; i++) {
            monthsToMillis[i] = monthsToMillis[i-1] + daysInMonth[i-1] * DAY_IN_MILLIS;
        }  
        dayLabels = new String[] {
                "Thu", "Fri", "Sat", "Sun", "Mon" , "Tue" , "Wed"
        };        
    }    
}

