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

import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Vector;

import org.json.JSONArray;
import org.json.JSONObject;

import ca.carleton.gcrc.couch.client.CouchClient;
import ca.carleton.gcrc.couch.client.CouchContext;
import ca.carleton.gcrc.couch.client.CouchDb;
import ca.carleton.gcrc.couch.client.CouchServerVersion;
import ca.carleton.gcrc.couch.client.CouchSession;
import ca.carleton.gcrc.couch.client.CouchUserDb;
import ca.carleton.gcrc.couch.client.ReplicationRequest;

public class CouchClientImpl implements CouchClient {

	private CouchContext context;
	private URL url;
	private URL uuidUrl;
	private CouchServerVersion version = null;
	private Queue<String> uuids = new LinkedList<String>();
	
	public CouchClientImpl(CouchContext context, URL url) throws Exception {
		this.context = context;
		this.url = url;
		
		this.uuidUrl = new URL(url, "_uuids");
	}

	@Override
	public CouchContext getContext() {
		return context;
	}

	@Override
	public URL getUrl() {
		return url;
	}

	@Override
	public CouchServerVersion getVersion() throws Exception {
		if( null != version ) {
			return version;
		} else {
			JSONObject obj = ConnectionUtils.getJsonResource(context, url);
			
			// Parse version
			Object jsonVersion = obj.get("version");
			if( null != jsonVersion && jsonVersion instanceof String ) {
				String[] versions = ((String)jsonVersion).split("\\.");
				if( versions.length >= 2 ) {
					int major = Integer.parseInt(versions[0]);
					int minor = Integer.parseInt(versions[1]);
					version = new CouchServerVersionImpl((String)jsonVersion,major,minor);
					
					return version;
				}
			}
			
			throw new Exception("Unable to parse server version: "+jsonVersion);
		}
	}

	public void setVersion(CouchServerVersion version) {
		this.version = version;
	}

	@Override
	public String getUuid() throws Exception {
		synchronized( uuids ) {
			if( uuids.size() > 0 ) {
				String uuid = uuids.remove();
				return uuid;
			}
		}
		
		// Make request
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameter(uuidUrl, new UrlParameter("count", "10"));
		JSONObject obj = ConnectionUtils.getJsonResource(context, effectiveUrl);
		
		// Parse
		Object uuidsObj = obj.get("uuids");
		if( null == uuidsObj ) {
			throw new Exception("Unable to parse UUIDs response");
		}
		if( false == (uuidsObj instanceof JSONArray) ) {
			throw new Exception("Unexpected class for array: "+uuidsObj.getClass().getSimpleName());
		}
		JSONArray uuidsArray = (JSONArray)uuidsObj;
		
		String uuid = null;
		synchronized( uuids ) {
			for(int loop=0, end=uuidsArray.length(); loop<end; ++loop) {
				uuid = uuidsArray.getString(loop);
				uuids.add(uuid);
			}
			
			uuid = uuids.remove();
		}
		
		return uuid;
	}

	@Override
	public String[] getUuids(int count) throws Exception {
		if( count < 1 ){
			return new String[0];
		}
		
		// Make request
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameter(uuidUrl, new UrlParameter("count", ""+count));
		JSONObject obj = ConnectionUtils.getJsonResource(context, effectiveUrl);
		
		// Parse
		Object uuidsObj = obj.get("uuids");
		if( null == uuidsObj ) {
			throw new Exception("Unable to parse UUIDs response");
		}
		if( false == (uuidsObj instanceof JSONArray) ) {
			throw new Exception("Unexpected class for array: "+uuidsObj.getClass().getSimpleName());
		}
		JSONArray uuidsArray = (JSONArray)uuidsObj;
		
		String[] uuids = new String[uuidsArray.length()];
		for(int loop=0, end=uuidsArray.length(); loop<end; ++loop) {
			String uuid = uuidsArray.getString(loop);
			uuids[loop] = uuid;
		}
		
		return uuids;
	}

	@Override
	public CouchSession getSession() throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, "_session/");
		
		CouchSessionImpl couchSession = new CouchSessionImpl(this, effectiveUrl);
		
		return couchSession;
	}

	@Override
	public void validateContext() throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, "_session");
		
		JSONObject response = ConnectionUtils.getJsonResource(context, effectiveUrl, JSONObject.class);
		
		boolean ok = response.optBoolean("ok",false);
		if( false == ok ){
			throw new Exception("Unable to log in with CouchDb server");
		}
	}

	@Override
	public List<String> listDatabases() throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, "_all_dbs");

		JSONArray arr = ConnectionUtils.getJsonResource(context, effectiveUrl, JSONArray.class);
		
		// Parse response
		List<String> result = new Vector<String>();
		try {
			for(int i=0,e=arr.length(); i<e; ++i) {
				result.add( arr.getString(i) );
			}
		} catch(Exception e) {
			throw new Exception("Error parsing database list",e);
		}
		
		return result;
	}

	@Override
	public CouchDb createDatabase(String dbName) throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, dbName+"/");
		
		JSONObject response = ConnectionUtils.putJsonResource(context, effectiveUrl, null);
		
		ConnectionUtils.captureReponseErrors(response, "Error while creating database "+dbName+": ");
		
		CouchDbImpl db = new CouchDbImpl(this, effectiveUrl);
		
		return db;
	}

	@Override
	public boolean databaseExists(String dbName) throws Exception {
		List<String> dbs = listDatabases();
		for(String db : dbs) {
			if( db.equals(dbName) ) {
				return true;
			}
		}
		return false;
	}

	@Override
	public CouchDb getDatabase(String dbName) throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, dbName+"/");
		
		CouchDbImpl couchDb = new CouchDbImpl(this, effectiveUrl);
		
		return couchDb;
	}

	@Override
	public void deleteDatabase(CouchDb couchDb) throws Exception {
		URL effectiveUrl = couchDb.getUrl();
		
		JSONObject response = ConnectionUtils.deleteJsonResource(context, effectiveUrl);
		
		ConnectionUtils.captureReponseErrors(response, "Error while deleting database "+effectiveUrl+": ");
	}

	@Override
	public CouchUserDb getUserDatabase() throws Exception {
		// Compute url
		URL effectiveUrl = new URL(url, "_users/");
		
		CouchUserDbImpl couchUserDb = new CouchUserDbImpl(this, effectiveUrl);
		
		return couchUserDb;
	}

	@Override
	public JSONObject replicate(
			ReplicationRequest replicationRequest
			) throws Exception {

		URL effectiveUrl = new URL(url, "_replicate");
		
		String source = CouchUtils.computeEffectiveDatabaseUrl(
			replicationRequest.getSourceServerUrl()
			,replicationRequest.getSourceUserName()
			,replicationRequest.getSourcePassword()
			,replicationRequest.getSourceDbName()
			);
		String target = CouchUtils.computeEffectiveDatabaseUrl(
			replicationRequest.getTargetServerUrl()
			,replicationRequest.getTargetUserName()
			,replicationRequest.getTargetPassword()
			,replicationRequest.getTargetDbName()
			);
		
		JSONObject request = new JSONObject();
		request.put("source",source);
		request.put("target",target);
		
		if( replicationRequest.isContinuous() ) {
			request.put("continuous", true);
		}
		
		if( replicationRequest.isCancel() ) {
			request.put("cancel", true);
		}
		
		if( null != replicationRequest.getFilter() ) {
			request.put("filter", replicationRequest.getFilter());
		}
		
		if( null != replicationRequest.getDocIds() ) {
			JSONArray arr = new JSONArray();
			
			for(String docId : replicationRequest.getDocIds()) {
				arr.put(docId);
			}
			
			request.put("doc_ids", arr);
		}
		
		// POST
		JSONObject response = ConnectionUtils.postJsonResource(getContext(), effectiveUrl, request);
		
		ConnectionUtils.captureReponseErrors(response, "Error while replicating "+source+" -> "+target);

		return response;
	}

	@Override
	public JSONArray activeTasks() throws Exception {

		URL effectiveUrl = new URL(url, "_active_tasks");
		
		JSONArray response = ConnectionUtils.getJsonResource(getContext(), effectiveUrl, JSONArray.class);
		
		ConnectionUtils.captureReponseErrors(response, "Error while obtaining active tasks");

		return response;
	}
}
