package com.archway.fw.server;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.isomorphic.base.Reflection;
import com.isomorphic.datasource.BasicDataSource;
import com.isomorphic.datasource.DSRequest;
import com.isomorphic.datasource.DSResponse;
import com.isomorphic.datasource.DataSource;
import com.isomorphic.log.Logger;
import com.isomorphic.rpc.RPCManager;
import com.isomorphic.rpc.RPCManagerCompletionCallback;


// Archway-specific custom DataSource impl, to cope with the requirement that DSRequests be
// sent in batches

public class BatchDataSource extends BasicDataSource implements RPCManagerCompletionCallback {
	
	private Logger log = new Logger(BatchDataSource.class.getName());
	
	public DSResponse executeFetch(DSRequest req) throws Exception {
		// Fetch is different from the write ops - just pass the call straight through to the
		// archServerMethod, like a DMI
		Object apiObject = getAPIObject(req);
		String apiMethod = getAPIMethod(req);
		return executeFetchMethod(apiObject, apiMethod, req);
	}

	public DSResponse executeAdd(DSRequest req) throws Exception {
		return processRequest(req);
	}

	public DSResponse executeCustom(DSRequest req) throws Exception {
		return processRequest(req);
	}

	public DSResponse executeRemove(DSRequest req) throws Exception {
		return processRequest(req);
	}

	public DSResponse executeUpdate(DSRequest req) throws Exception {
		return processRequest(req);
	}

	public DSResponse processRequest(DSRequest req) throws Exception {
		req.getRPCManager().registerCallback(this);
		if (isLastInBatch(req)) {
			Object apiObject = getAPIObject(req);
			String apiMethod = getAPIMethod(req);
			List batch = executeMethod(apiObject, apiMethod, req);
			DSResponse response = new DSResponse();
			response.setData(batch);    // this is actually a List of DSResponses that we'll 
			                            // unpick later
			response.setStatus(-1001);  // Our special status meaning "Contains all responses"
			clearBatchCache(req);
			return response;
		}
		
		addToBatch(req);
		DSResponse empty = new DSResponse();
		empty.setStatus(-1000);  // Our special status meaning "Not yet processed"
		return empty;
	}
	
	private boolean isLastInBatch(DSRequest req) throws Exception {
		String thisKey = getBatchKey(req);
		DSRequest lastInBatch = null;
		for (Iterator i = req.getRPCManager().getRequests().iterator(); i.hasNext(); ) {
			Object requestObj = i.next();
			if (!(requestObj instanceof DSRequest)) continue;
			DSRequest request = (DSRequest)requestObj;
			if (thisKey.equals(getBatchKey(request))) {
				lastInBatch = request;
			}
		}
		return lastInBatch == req;
	}

	private Object getAPIObject(DSRequest req) throws Exception {
		DataSource ds = req.getDataSource();
		String className = (String)ds.getConfig().get("archServerObject");
		if (className == null) {
			throw new Exception ("DataSource " + ds.getName() + " does not specify 'archServerObject'");
		}
		return Reflection.newInstance(className);
	}

	private String getAPIMethod(DSRequest req) throws Exception {
		DataSource ds = req.getDataSource();
		String opType = req.getOperationType();
		String opId = req.getOperationId();
		Map opBinding = getOperationBinding(opType, opId);
		if (opBinding == null) {
			throw new Exception ("DataSource " + ds.getName() + ", operation type " + opType + 
								 ", operation Id " + opId + " does not exist");
		}
		if (opBinding.get("archServerMethod") == null) {
			throw new Exception ("DataSource " + ds.getName() + ", operation type " + opType + 
					 ", operation Id " + opId + " does not specify 'archServerMethod'");
		}
		return (String)opBinding.get("archServerMethod");
	}
	
	private List executeMethod(Object object, String methodName, DSRequest req) 
	throws Exception 
	{
		// Call the method derived from the Archway-specific DataSource properties, passing in
		// the batch of requests we've been accruing for this DataSource/opType/opId.
		List batch = (List)req.context.request.getSession().getAttribute("_batch_" + getBatchKey(req));
		if (batch == null) batch = new ArrayList();
		batch.add(req);
		Object[] params = new Object[1];
		params[0] = batch;
		Class[] paramTemplate = new Class[1];
		paramTemplate[0] = batch.getClass();
		Method method = null;
		try {
			method = Reflection.findMethod(object.getClass(), methodName, paramTemplate);
		} catch(NoSuchMethodException nsme) {}
		if (method == null) {
			throw new NoSuchMethodException(object.getClass().getName() + "::" + methodName);
		}
		Object result = method.invoke(object, params);
		
		// Just let this fail with a CCE if the target method doesn't return a List - there's 
		// nothing more sensible we could do with it anyway
		return (List)result;
	}
	
	private DSResponse executeFetchMethod(Object object, String methodName, DSRequest req) 
	throws Exception 
	{
		Object[] params = new Object[1];
		params[0] = req;
		Class[] paramTemplate = new Class[1];
		paramTemplate[0] = req.getClass();
		Method method = null;
		try {
			method = Reflection.findMethod(object.getClass(), methodName, paramTemplate);
		} catch(NoSuchMethodException nsme) {}
		if (method == null) {
			throw new NoSuchMethodException(object.getClass().getName() + "::" + methodName);
		}
		Object result = method.invoke(object, params);
		
		// Just let this fail with a CCE if the target method doesn't return a DSResponse
		return (DSResponse)result;
	}
	
	private void addToBatch(DSRequest req) throws Exception {
		String key = getBatchKey(req);
		List batch = (List)req.context.request.getSession().getAttribute("_batch_" + key);
		if (batch == null) batch = new ArrayList();
		batch.add(req);
		req.context.request.getSession().setAttribute("_batch_" + key, batch);
	}
	
	private void clearBatchCache(DSRequest req) throws Exception {
		req.context.request.getSession().removeAttribute("_batch_" + getBatchKey(req));
	}
	
	private String getBatchKey(DSRequest request) throws Exception {
		DataSource ds = request.getDataSource();
		String opType = request.getOperationType();
		String opId = request.getOperationId();
		return ds.getName() + "_" + opType + "_" + opId;
	}

	public void onFailure(RPCManager rpc, boolean arg1) throws Exception {
		processResponses(rpc);
	}

	public void onSuccess(RPCManager rpc) throws Exception {
		processResponses(rpc);
	}
	
	private void processResponses(RPCManager rpc) throws Exception {
		// Overwrite the returned responses (which are dummies) with the real responses returned
		// wrapped inside the last response of a given signature

		Map batches = new HashMap();
		List requests = rpc.getRequests();
		for (int i = 0; i < requests.size(); i++ ) {
			Object reqObj = requests.get(i);
			if (!(reqObj instanceof DSRequest)) continue;
			DSRequest req = (DSRequest)reqObj;
			String key = getBatchKey(req);
			List batch = (List)batches.get(key);
			if (batch == null) {
				batch = new ArrayList();
				batches.put(key, batch);
			}
			batch.add(req);
		}
		
		for (Iterator i = batches.keySet().iterator(); i.hasNext(); ) {
			List batch = (List)batches.get(i.next());
			DSResponse lastResponse = rpc.getResponse((DSRequest)batch.get(batch.size() - 1));
			List realResponses = lastResponse.getDataList();
			
			// Bail out now if there are no "real" responses - presumably the very first request 
			// crashed
			if (realResponses == null || realResponses.size() == 0) return;
			List wrappedResponses = lastResponse.getDataList();
			for (int j = 0; j < batch.size(); j++) {
				DSRequest req = (DSRequest)batch.get(j);
				DSResponse dummy = rpc.getResponse(req);
				if (dummy == null) {
					log.error("Found null dummy response processing batches - ignoring that request");
					continue;
				}
				
				// Stop if we have run out of responses - presumably something failed, so we 
				// haven't got as many responses as there were requests
				if (realResponses.size() <= j) return;
				
				Object realObj = realResponses.get(j);
				
				// If the object is not a DSResponse, this implies that we have already processed
				// the responses.  We are probably being called multiple times, because we are 
				// instantiating a new BatchDataSource to handle each DSRequest.  We should maybe 
				// create a factory that just returns the same object each time, but that would 
				// create potential thread-safety issues - look into it...
				// For now, just bail if we find a non-DSResponse object
				if (!(realObj instanceof DSResponse)) return;
				
				DSResponse real = (DSResponse)realObj;
				
				// The existing DSResponse can't be replaced, so we'll have to just change 
				// everything about it (well, everything important) to reflect what's on the 
				// real response
				dummy.setData(real.getData());
				if (real.getErrors() != null) dummy.setErrors(real.getErrors());
				dummy.setStartRow(real.getStartRow());
				dummy.setEndRow(real.getEndRow());
				dummy.setTotalRows(real.getTotalRows());
				dummy.setAffectedRows(real.getAffectedRows());
				dummy.setStatus(real.getStatus());
			}
		}
	}

}
