package org.wikiwebserver.handler.http;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.wikiwebserver.core.ConfigManager;


public class HTTPInputStream {
    
    private static final boolean DEBUG = ConfigManager.getBoolean("http.debug-read");   
        
    private static final String CRLF = "\r\n";
    private static final int READ_BUFFER =
    	ConfigManager.getInt("http.read-buffer-size"); 
    private static final int MAX_STRING_LENGTH = 
    	ConfigManager.getInt("http.max-string-scan-size"); 
    private static final int MAX_ARRAY_LENGTH = 
    	ConfigManager.getInt("http.max-data-scan-size"); 
       
    public HTTPInputStream(HTTPHandler conn, InputStream in) {
        if (!in.markSupported()) {
            throw new IllegalArgumentException("Input stream must support mark and reset");
        }
        this.conn = conn;   
        this.in = in;
    }
        
    HTTPRequest readRequest() throws Exception {
        
        this.request = new HTTPRequest();  
        if (conn.getHTTPConfig() != null) {
            String newID = conn.getHTTPConfig().newIdentity("RequestID");
            this.request.setRequestID(newID);
        }        
        
        this.valid = false;  
        
        // Read request line
        String req = "";
        while (req.length() == 0) {
            req = readStringUntil("US-ASCII", CRLF).trim();
            skipBytes(2);
            if (!active) {
                throw new EOFException("Connection closed during read");
            }
        }
        
        // Parse request line
        int sep = req.indexOf(' ');
        if (sep == -1) sep = req.length();
        // Method
        this.request.setMethod(req.substring(0, sep));
        if (sep < req.length()) {
            req = req.substring(sep).trim();
            sep = req.indexOf(' ');
            if (sep == -1) sep = req.length();
            // URI
            this.request.setUri(req.substring(0, sep));
            if (sep < req.length()) {
                req = req.substring(sep).toUpperCase();
                sep = req.indexOf("HTTP/");
                int period = req.indexOf(".");
                if (sep != -1 && period != -1) {
                    // Version
                    int major = Integer.parseInt(req.substring(sep+5, period));
                    int minor = Integer.parseInt(req.substring(period+1).trim());
                    this.request.setVersionMajor(major);
                    this.request.setVersionMinor(minor);
                }
            }
        }
        
        // Read and parse request headers
        HTTPHeaders headers =  HTTPHeaders.readHeaders(this);
        this.request.setHeaders(headers);
        
        this.valid = true; 
        
        return this.request;
    }
    
    void readURLQuery(HTTPRequest request) throws IOException {  
        if (request.getQuery() != null && request.getQuery().length() > 0) {
            FormData formData = FormData.parseURLEncodedFormData(request.getQuery());
            request.setFormData(formData);        
        }
    }

    public int read() throws IOException {
    	int i = in.read();
        if (i == -1) active = false;
        else updateCounters(conn, 1);
        return i;
    }   
    
    public int read(byte[] buffer, int off, int len) throws IOException {
    	int r = in.read(buffer, off, len);
        if (r == 0) active = false;
        else updateCounters(conn, r);
        return r;
    }    
    
    public int available() throws IOException {
    	return in.available();
    }
    
    public String readStringUntil(String charset, String terminator) throws IOException {
        return (String) readUntil(terminator, charset, MAX_STRING_LENGTH, null, false);
    }    
    
    public byte[] readBytesUntil(String terminator) throws IOException {
        return (byte[]) readUntil(terminator, null, MAX_ARRAY_LENGTH, null, false);
    } 
    
    public void streamUntil(String terminator, OutputStream out) throws IOException {
        readUntil(terminator, "US-ASCII", -1, out, false);
    }   
    
    public void skipUntil(String terminator) throws IOException {
        readUntil(terminator, "US-ASCII", -1, null, true);
    }     
    
    public String readString(String charset, int length) throws IOException {
        return (String) readUntil(null, charset, length, null, false);
    }  
    
    public byte[] readBytes(int amount) throws IOException {
        return (byte[]) readUntil(null, null, amount, null, false);
    }  
    
    public void skipBytes(int amount) throws IOException {
        //readUntil(null, null, amount, null, true);
        in.skip(amount);
        updateCounters(conn, amount);
    }      

    /**
     * @param end      A string which indicates reading should stop and the string
     *                 up to that point should be returned. The stream is left in
     *                 the position before the end string. If end is null then reading
     *                 should not stop.
     * @param charset  The character encoding the raw bytes of the stream represent
     *                 or null if this method should return the raw byte array.     *                 
     * @param limit    A number of bytes indicating when reading should stop and
     *                 the data should be returned.            
     * @param out      An OutputStream where bytes read from this InputStream should be
     *                 directed.
     * @param dropData When true indicates that data should be read but not stored.
     * 
     * @return         Either a String, byte array or null depending on parameters.
     * 
     * @throws IOException If there is a problem reading from the InputStream or 
     *                      writing to the OutputStream (where used)
     */
    private Object readUntil(String end, 
                             String charset, 
                             int limit, 
                             OutputStream out, 
                             boolean dropData) throws IOException {
        
        if (limit == 0) {
            if (charset == null) {
                return new byte[0];
            }
            else return new String();
        }      

        // Raw data
        ByteArrayOutputStream data = null;
        if (out == null) {
            data = new ByteArrayOutputStream();
        }       
        
        int bytesRead = 0;
        boolean finished = false;
        
        int s = 0;          
        byte[] buffer = new byte[READ_BUFFER-s];      
        
        in.mark(READ_BUFFER-s);        
        int r = in.read(buffer, s, READ_BUFFER-s); 
        
        // We must read at least the terminator size into the buffer
        // or there will be problems scanning for the end sequence
        while (end != null && r > 0 && r < end.length()) {
            // Wait for more data
            try { Thread.sleep(1); } catch (InterruptedException ex) {}
            in.reset();
            r = in.read(buffer, s, READ_BUFFER-s); 
        }
        
        while (r > 0) {
            
            int bytesOverrun = 0;
            
            // Termination criteria for end terminator             
            if (end != null) {
                String text = new String(buffer, 0, s+r, charset);
                int idx = text.indexOf(end);
                if (idx > -1) {
                    // Calculate how far we have read since start of terminator
                    bytesOverrun = text.length() - idx;
                    finished = true;                        
                }                   
            }
            
            // Termination criteria for read limit
            if (limit > 0 && (bytesRead+r) >= limit) {
                // Calculate how far we have read over the limit
                if (bytesRead+r-bytesOverrun > limit) {
                    bytesOverrun = bytesRead+r - limit;
                }
                finished = true;                
            }
            
            if (bytesOverrun > 0) {
                // Set r to the number of valid bytes
                r -= bytesOverrun;
                // Move back in stream
                in.reset();
                in.skip(r);                
            }        
            
            // Drop, stream or append raw data
            if (dropData);
            else if (out  != null)  out.write(buffer, s, r);
            else if (data != null) data.write(buffer, s, r);
            
            // Log the number of bytes read
            bytesRead += r;
            updateCounters(conn, r);
            
            if (finished) break;
  
            // Overlap buffers to aid searching for end characters
            if (end != null && s > 0) {
                s = end.length()-1;
                System.arraycopy(buffer, r-s, buffer, 0, s);
            }
            
            // Read next block
            in.mark(READ_BUFFER-s);          
            r = in.read(buffer, s, READ_BUFFER-s);
        }
        
        if (r == -1) active = false; // Signifies the end of stream

        if (out != null) return null;
        else if (charset == null) return data.toByteArray();
        else {
            String charData = new String(data.toByteArray(), charset);
            if (DEBUG) System.out.println(charData);
            return charData;
        }
    }

    public boolean isActive() {
        return active;
    }
    
    public boolean isValidRequest() {
        return valid;
    }     
    
    public void close() throws IOException {
        in.close();
    }

    private void updateCounters(HTTPHandler conn, int r) {
        if (request != null) {
            request.incrementNumBytesRead(r);  
        }
        
        if (conn.getHTTPConfig() != null && conn.getHTTPConfig().getHTTPMonitor() != null) {
            conn.getHTTPConfig().getHTTPMonitor().incrementNumBytesRead(r); 
        }    	
    }
    
    private boolean active = true;
    private boolean valid = false;
    
    private HTTPHandler conn;
    private HTTPRequest request;
    private InputStream in;

}

