package ca.carleton.gcrc.couch.client.impl;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import ca.carleton.gcrc.couch.client.CouchContext;

import net.sf.json.JSON;
import net.sf.json.JSONObject;
import net.sf.json.util.JSONTokener;

public class ConnectionUtils {
	static public JSON getJsonFromInputStream(InputStream contentStream, String encoding) throws Exception {
		if( null == encoding ) {
			encoding = "UTF-8";
		}
		
		InputStreamReader isr = new InputStreamReader(contentStream, encoding);
		StringWriter sw = new StringWriter();
		int b = isr.read();
		while( b >= 0 ) {
			sw.write((char)b);
			b = isr.read();
		}
		sw.flush();
		
		JSONTokener jsonTokener = new JSONTokener(sw.toString());
		JSON json = null;
		Object obj = jsonTokener.nextValue();
		if( obj instanceof JSON ) {
			json = (JSON)obj;
		} else {
			throw new Exception("Unexpected returned object type: "+obj.getClass().getSimpleName());
		}
		
		return json;
	}
	
	static public <T> T getJsonFromInputStream(InputStream contentStream, String encoding, Class<T> clazz) throws Exception {
		JSON json = getJsonFromInputStream(contentStream, encoding);

		if( clazz.isAssignableFrom(json.getClass()) ) {
			T result = clazz.cast(json);
			return result;
		}
		
		throw new Exception("Unexpected returned object type: "+json.getClass().getSimpleName());
	}
	
	static public URL computeUrlWithParameter(URL url, UrlParameter parameter) throws Exception {
		List<UrlParameter> parameters = new ArrayList<UrlParameter>(1);
		parameters.add(parameter);
		return computeUrlWithParameters(url, parameters);
	}
	
	static public URL computeUrlWithParameters(URL url, List<UrlParameter> parameters) throws Exception {
		StringWriter sw = new StringWriter();
		sw.write(url.toExternalForm());
		
		boolean first = true;
		for(UrlParameter parameter : parameters) {
			if( first ) {
				sw.write("?");
				first = false;
			} else {
				sw.write("&");
			}
			
			sw.write( URLEncoder.encode(parameter.getKey(), "UTF-8") );
			sw.write("=");
			sw.write( URLEncoder.encode(parameter.getValue(), "UTF-8") );
		}
		
		sw.flush();
		
		return new URL( sw.toString() );
	}

	static public <T> T getJsonResource(CouchContext ctxt, URL url, Class<T> clazz) throws Exception {
		CouchContextBase context = CouchContextBase.getBase(ctxt);
		
		HttpURLConnection conn = (HttpURLConnection)url.openConnection();
		conn.setRequestMethod("GET");
		context.adjustConnection(conn);
		conn.setDoOutput(false);
		conn.setDoInput(true);
		conn.connect();
		String contentEncoding = conn.getContentEncoding();
		InputStream contentStream = conn.getInputStream();
		T result = getJsonFromInputStream(contentStream, contentEncoding, clazz);
		contentStream.close();
		conn.disconnect();

		return result;
	}

	static public JSONObject getJsonResource(CouchContext ctxt, URL url) throws Exception {
		return getJsonResource(ctxt, url, JSONObject.class);
	}

	static public JSONObject putJsonResource(CouchContext ctxt, URL url, JSONObject jsonObj) throws Exception {
		CouchContextBase context = CouchContextBase.getBase(ctxt);
		
		HttpURLConnection conn = (HttpURLConnection)url.openConnection();
		conn.setRequestMethod("PUT");
		context.adjustConnection(conn);
		conn.setDoInput(true);
		if( null == jsonObj ) {
			conn.setDoOutput(false);
		} else {
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
			OutputStream os = conn.getOutputStream();
			OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
			jsonObj.write(osw);
			osw.flush();
			os.close();
		}
		conn.connect();
		String contentEncoding = conn.getContentEncoding();
		InputStream contentStream = conn.getInputStream();
		JSONObject result = getJsonFromInputStream(contentStream, contentEncoding, JSONObject.class);
		contentStream.close();
		conn.disconnect();

		return result;
	}

	static public JSONObject postJsonResource(CouchContext ctxt, URL url, JSONObject jsonObj) throws Exception {
		CouchContextBase context = CouchContextBase.getBase(ctxt);
		
		HttpURLConnection conn = (HttpURLConnection)url.openConnection();
		conn.setRequestMethod("POST");
		context.adjustConnection(conn);
		conn.setDoInput(true);
		if( null == jsonObj ) {
			conn.setDoOutput(false);
		} else {
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
			OutputStream os = conn.getOutputStream();
			OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
			jsonObj.write(osw);
			osw.flush();
			os.close();
		}
		conn.connect();
		String contentEncoding = conn.getContentEncoding();
		InputStream contentStream = conn.getInputStream();
		JSONObject result = getJsonFromInputStream(contentStream, contentEncoding, JSONObject.class);
		contentStream.close();
		conn.disconnect();

		return result;
	}

	static public JSONObject deleteJsonResource(CouchContext ctxt, URL url) throws Exception {
		CouchContextBase context = CouchContextBase.getBase(ctxt);
		
		HttpURLConnection conn = (HttpURLConnection)url.openConnection();
		conn.setRequestMethod("DELETE");
		context.adjustConnection(conn);
		conn.setDoOutput(false);
		conn.setDoInput(true);
		conn.connect();
		String contentEncoding = conn.getContentEncoding();
		InputStream contentStream = conn.getInputStream();
		JSONObject result = getJsonFromInputStream(contentStream, contentEncoding, JSONObject.class);
		contentStream.close();
		conn.disconnect();

		return result;
	}

	static public JSONObject putSreamResource(CouchContext ctxt, URL url, InputStream is, String contentType, long size) throws Exception {
		CouchContextBase context = CouchContextBase.getBase(ctxt);
		
		if( null == contentType ) {
			contentType = "application/binary";
		}
		
		HttpURLConnection conn = (HttpURLConnection)url.openConnection();
		conn.setRequestMethod("PUT");
		context.adjustConnection(conn);
		conn.setDoInput(true);
		conn.setDoOutput(true);
		conn.setRequestProperty("Content-Type", contentType);
		OutputStream os = conn.getOutputStream();
		int b = is.read();
		while( b >= 0 ) {
			os.write(b);
			b = is.read();
		}
		os.flush();
		os.close();
		conn.connect();
		String contentEncoding = conn.getContentEncoding();
		InputStream contentStream = conn.getInputStream();
		JSONObject result = getJsonFromInputStream(contentStream, contentEncoding, JSONObject.class);
		contentStream.close();
		conn.disconnect();

		return result;
	}
	
	/**
	 * Analyze a CouchDb response and raises an exception if an error was returned
	 * in the response.
	 * @param response JSON response sent by server
	 * @param errorMessage Message of top exception
	 * @throws Exception If error is returned in response
	 */
	static public void captureReponseErrors(Object response, String errorMessage) throws Exception {
		if( null == response ) {
			throw new Exception("Capturing errors from null response");
		}
		if( false == (response instanceof JSONObject) ) {
			// Not an error
			return;
		}
		JSONObject obj = (JSONObject)response;
		if( obj.containsKey("error") ) {
			String serverMessage;
			try {
				serverMessage = obj.getString("error");
			} catch (Exception e) {
				serverMessage = "Unable to parse error response";
			}
			
			if( null == errorMessage ) {
				errorMessage = "Error returned by database: ";
			}
			throw new Exception(errorMessage+serverMessage);
		}
	}
}
