package org.deftserver.web.http;

import static org.deftserver.web.http.HttpServerDescriptor.WRITE_BUFFER_SIZE;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;

import org.deftserver.io.IOLoop;
import org.deftserver.io.buffer.DynamicByteBuffer;
import org.deftserver.util.Closeables;
import org.deftserver.util.DateUtil;
import org.deftserver.util.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;

public class HttpResponse {
	
	private final static Logger logger = LoggerFactory.getLogger(HttpResponse.class);
	
	private final HttpProtocol protocol;
	private final SelectionKey key;
	
	private int statusCode = 200;	// default response status code
	
	private final Map<String, String> headers = new HashMap<String, String>();
	private boolean headersCreated = false;
	private DynamicByteBuffer responseData = DynamicByteBuffer.allocate(WRITE_BUFFER_SIZE);
	
	public HttpResponse(HttpProtocol protocol, SelectionKey key, boolean keepAlive) {
		this.protocol = protocol;
		this.key = key;
		headers.put("Server", "Deft/0.4.0-SNAPSHOT");
		headers.put("Date", DateUtil.getCurrentAsString());
		headers.put("Connection", keepAlive ? "Keep-Alive" : "Close");
	}
	
	public void setStatusCode(int sc) {
		statusCode = sc;
	}
	
	public void setHeader(String header, String value) {
		headers.put(header, value);
	}

	/**
	 * The given data data will be sent as the HTTP response upon next flush or when the response is finished.
	 *
	 * @return this for chaining purposes.
	 */
	public HttpResponse write(String data) {
		byte[] bytes = data.getBytes(Charsets.UTF_8);
		responseData.put(bytes);
		return this;
	}

	/**
	 * Explicit flush. 
	 * 
	 * @return the number of bytes that were actually written as the result of this flush.
	 */
	public long flush() {
		if (!headersCreated) {
			String initial = createInitalLineAndHeaders();			
			responseData.prepend(initial);
			headersCreated = true;
		}

		SocketChannel channel = (SocketChannel) key.channel();
		responseData.flip();	// prepare for write
		try {
			channel.write(responseData.getByteBuffer());
		} catch (IOException e) {
			logger.error("ClosedChannelException during channel.write(): {}", e.getMessage());
			Closeables.closeQuietly(key.channel());
		}
		long bytesFlushed = responseData.position();
		if (IOLoop.INSTANCE.hasKeepAliveTimeout(channel)) {
			protocol.prolongKeepAliveTimeout(channel);
		}
		if (responseData.hasRemaining()) { 
			responseData.compact();	// make room for more data be "read" in
			try {
				key.channel().register(key.selector(), SelectionKey.OP_WRITE);
			} catch (ClosedChannelException e) {
				logger.error("ClosedChannelException during flush(): {}", e.getMessage());
				Closeables.closeQuietly(key.channel());
			}
			key.attach(responseData);
		} else {
			responseData.clear();
		}
		return bytesFlushed;
	}
	
	/**
	 * Should only be invoked by third party asynchronous request handlers 
	 * (or by the Deft framework for synchronous request handlers). 
	 * If no previous (explicit) flush is invoked, the "Content-Length" and "Etag" header will be calculated and 
	 * inserted to the HTTP response.
	 * 
	 */
	public long finish() {
		long bytesWritten = 0;
		SocketChannel clientChannel = (SocketChannel) key.channel();
		if (clientChannel.isOpen()) {
			if (!headersCreated) {
				setEtagAndContentLength();
			}
			bytesWritten = flush();
		}
		
		// close (or register for read) iff 
		// (a) DBB is attached but all data is sent to wire (hasRemaining == false)
		// (b) no DBB is attached (never had to register for write)
		if (key.attachment() instanceof DynamicByteBuffer) {
			DynamicByteBuffer dbb = (DynamicByteBuffer) key.attachment();
			if (!(dbb).hasRemaining()) {
				protocol.closeOrRegisterForRead(key);
			} 
		} else {
			protocol.closeOrRegisterForRead(key);
		}
		return bytesWritten;
	}
	
	private void setEtagAndContentLength() {
		if (responseData.position() > 0) {
			setHeader("Etag", HttpUtil.getEtag(responseData.array()));
		}
		setHeader("Content-Length", String.valueOf(responseData.position()));
	}
	
	private String createInitalLineAndHeaders() {
		StringBuilder sb = new StringBuilder(HttpUtil.createInitialLine(statusCode));
		for (Map.Entry<String, String> header : headers.entrySet()) {
			sb.append(header.getKey());
			sb.append(": ");
			sb.append(header.getValue());
			sb.append("\r\n");
		}
		
		sb.append("\r\n");
		return sb.toString();
	}
	
	/**
	 * Experimental support.
	 * Before use, read https://github.com/rschildmeijer/deft/issues/75
	 */
	public long write(File file) {
		//setHeader("Etag", HttpUtil.getEtag(file));
		setHeader("Content-Length", String.valueOf(file.length()));
		long bytesWritten = 0;
		flush(); // write initial line + headers
		try {
			bytesWritten = new RandomAccessFile(file, "r").getChannel().transferTo(0, file.length(), (SocketChannel) key.channel());
			logger.debug("sent file, bytes sent: {}", bytesWritten);
		} catch (IOException e) {
			logger.error("Error writing (static file) response: {}", e.getMessage());
		}
		return bytesWritten;
	}
	
}

@@ -4974,29 +4974,51 @@
 s%0A%09%09
-try %7B%0A%09%09%09bytesWritten
+RandomAccessFile raf = null;%0A%09%09try %7B%0A%09%09%09raf
  = n
@@ -5047,16 +5047,39 @@
 le, %22r%22)
+;%0A%09%09%09bytesWritten = raf
 .getChan
@@ -5309,24 +5309,201 @@
 sage());%0A%09%09%7D
+ finally %7B%0A%09%09%09if (raf != null) %7B%0A%09%09%09%09try %7B%0A%09%09%09%09%09raf.close();%0A%09%09%09%09%7D catch (IOException e) %7B%0A%09%09%09%09%09logger.error(%22Error closing static file: %22, e.getMessage());%09%09%09%09%09%0A%09%09%09%09%7D%0A%09%09%09%7D%0A%09%09%7D%0A
 %0A%09%09return by

Implement #135 (java.io.IOException: Too many open files) (close #135) patch by ilmich review by rschildmeijer