package net.noderunner.amazon.s3.emulator;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

import net.noderunner.amazon.s3.Entry;
import net.noderunner.amazon.s3.Headers;
import net.noderunner.amazon.s3.Owner;
import net.noderunner.amazon.s3.S3Object;
import net.noderunner.http.servlet.ServletServer;


/**
 * Amazon S3 emulator that stores data in an internal sorted map.
 * Not highly scalable or reliable, but may be fine for testing your application.
 * 
 * Some browse methods are not complete.
 * Get Data/Put/Head are mostly complete.
 * What's supported is in the test suite.
 * 
 * @author Elias Ross
 */
public class Server extends HttpServlet {

	private static final long serialVersionUID = 1L;

	private transient Log log = LogFactory.getLog(getClass());
	private transient ServletServer ss;
	private boolean bucket = false;
	private SortedMap<Entry, S3Object> map = Collections.synchronizedSortedMap(new TreeMap<Entry, S3Object>());

	public Server() throws IOException {
		ss = new ServletServer(this);
		log.info("Server created " + this);
	}

	/**
	 * Closes socket, stops accepting requests.
	 */
	public void close() throws IOException {
		log("close");
		ss.close();
	}

	@Override
	protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		Entry e = entry(req);
		S3Object remove = map.remove(e);
		if (remove == null) {
			resp.sendError(404, "Not found " + e);
		} else {
			resp.sendError(HttpURLConnection.HTTP_NO_CONTENT, "Deleted");
		}

	}

	private Entry entry(HttpServletRequest req) {
		return new Entry(key(uri(req)));
	}

	/**
	 * Listening port.
	 */
	public int getPort() {
		return ss.getPort();
	}

	/**
	 * Starts accepting requests.
	 */
	public void start() {
		ss.start();
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		URI uri = uri(req);
		boolean debug = log.isDebugEnabled();
		if (debug)
    		log("doGet " + uri);
		if ("/".equals(uri.getPath())) {
			list(req, resp);
		} else {
			String key = uri.getPath().substring(1);
			Entry e = new Entry(key);
			S3Object obj = map.get(e);
    		if (debug)
    			log("map.get(" + key + ") = " + obj);
			if (obj == null) {
				resp.sendError(404, "Not here: " + e);
				return;
			}
			Headers h = new Headers();
			h = h.mergeMetadata(obj.getMetadata());
			for (Map.Entry<String, List<String>> me : h.getHeaders().entrySet()) {
				for (String v : me.getValue()) {
					resp.setHeader(me.getKey(), v);
				}
			}
			resp.getOutputStream().write(obj.getData());
		}

	}

	private void list(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		String prefix = req.getParameter("prefix");
		String marker = req.getParameter("marker");
		String delimiter = req.getParameter("delimiter");
		String maxKeysStr = req.getParameter("max-keys");
		if (log.isDebugEnabled())
    		log("list prefix=" + prefix + " delimiter=" + delimiter);
		int maxKeys = Integer.MAX_VALUE;
		if (maxKeysStr != null)
			maxKeys = Integer.parseInt(maxKeysStr);
		Writer w = new Writer();
		SortedMap<Entry, S3Object> submap = new TreeMap<Entry, S3Object>(map);
		if (prefix != null)
			submap = submap.tailMap(new Entry(prefix));
		int keyCount = 0;
		boolean truncated = false;
		String nextMarker = null;
		for (Entry e : submap.keySet()) {
			if (++keyCount > maxKeys) {
				truncated = true;
				break;
			}
			String key = e.getKey();
			String remain = key;
			nextMarker = key;
			if (prefix != null) {
				if (!key.startsWith(prefix)) 
    				break;
				remain = key.substring(prefix.length());
			}
			if (delimiter != null && remain.indexOf(delimiter) != -1)
				continue;
    		if (log.isDebugEnabled())
    			log("include key=" + key);
			w.start("Contents");
			w.start("Key").write(key).end();
			w.start("LastModified").write(e.getLastModified()).end();
			w.start("Size").write(e.getSize()).end();
			w.start("Owner");
			w.start("ID").write(e.getOwner().getId()).end()
					.start("DisplayName").write(e.getOwner().getDisplayName())
					.end();
			w.end();
			w.end();
		}

		Writer hw = new Writer();
		hw.start("ListBucketResult");
		hw.start("Name").write("localhost").end();
		hw.start("Prefix").write(s(prefix)).end();
		hw.start("Marker").write(s(marker)).end();
		if (delimiter != null) {
			hw.start("Delimiter").write(delimiter).end();
    		if (truncated) {
    			hw.start("NextMarker").write(nextMarker).end();
    		}
		}
		hw.start("IsTruncated").write(String.valueOf(truncated)).end();
		if (maxKeysStr != null)
			hw.start("MaxKeys").write(maxKeysStr).end();
		hw.write(w);
		hw.end();

		PrintWriter pw = resp.getWriter();
		pw.write(hw.toString());
		pw.flush();
		bucket = true;
	}

	private String s(String s) {
		if (s == null)
			return "";
		return s;
	}

	@Override
	protected void doHead(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		URI uri = uri(req);
		if (log.isDebugEnabled())
    		log("doHead " + uri);
		if (map.containsKey(entry(req))) {
    		log("found");
			resp.sendError(HttpURLConnection.HTTP_OK, "Found URI");
		} else {
    		log("not found");
			resp.sendError(404, "Not found");
		}
	}
	
	private URI uri(HttpServletRequest req) {
		try {
			return new URI(req.getRequestURI());
		} catch (URISyntaxException e1) {
			throw new RuntimeException(e1);
		}
	}
	
	private String key(URI uri) {
		return uri.getPath().substring(1);
	}

	@Override
	protected void doPut(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		URI uri = uri(req);
		log("doPut " + uri);
		if ("/".equals(uri.getPath())) {
			log("create bucket");
			bucket = true;
		} else {
			Entry e = new Entry(key(uri));
			e.setLastModified(new Date());
			e.setSize(req.getContentLength());
			e.setOwner(new Owner("id", "name"));
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			ServletInputStream is = req.getInputStream();
			byte b[] = new byte[128];
			while (true) {
				int len = is.read(b);
				if (len == -1)
					break;
				os.write(b, 0, len);
			}
			S3Object s3 = new S3Object(os.toByteArray());
			map.put(e, s3);
			Headers h = new Headers();
			@SuppressWarnings("unchecked")
			Enumeration<String> names = req.getHeaderNames();
			for (String n : Collections.list(names))
				h.put(n, req.getHeader(n));
			s3.setMetadata(h.extractMetadata());
			log("put '" + e + "' as: " + s3);
		}
	}

	@Override
	public void log(String s) {
		log.debug(s);
	}

	/**
	 * Returns a debug <code>String</code>.
	 */
	@Override
	public String toString()
	{
	    return super.toString() +
	        " bucket=" + this.bucket +
	        " ss=" + this.ss +
	        " map=" + this.map +
	        "";
	}

}
