package ca.ucalgary.services;

import ca.ucalgary.services.util.CGIUtils;
import ca.ucalgary.services.util.XHTMLForm;

import org.biomoby.service.MobyServlet;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.methods.multipart.*;
import org.apache.commons.httpclient.params.*;

import java.io.*;
import java.net.URL;
import java.util.*;

/**
 * In this class, we reshape an XHTML form into a Moby description of a service,
 * which then uses the standard MobyServlet mechanism for service signature creation, meta-data
 * publishing, etc.  The service description fetching, and the command invocation
 * are overridden.
 */
public class CGIService extends WrapperService<XHTMLForm>{
    protected URL remoteFormURL;
    // params that override annoation and servlet context/config params
    protected XHTMLForm formConfig;
    // mobyParamName -> form fields that used a transformed version of it
    protected Map<String,String[]> mobyPrimary2FormFields;
    protected Map<String,String[]> mobySecondary2FormFields;

    protected HttpClient httpClient;
    protected MultiThreadedHttpConnectionManager connectionManager; 

    public static final String HTML_FORM_URL_PARAM = "htmlFormURL";
    public static final String CGISERVICE_USERAGENT_NAME = "BioMoby CGIService Servlet";

    public void init(){
	super.init();

	mobyPrimary2FormFields = new HashMap<String,String[]>();
	mobySecondary2FormFields = new HashMap<String,String[]>();

	HttpClientParams params = new HttpClientParams();
	params.setParameter("http.useragent", CGISERVICE_USERAGENT_NAME);
	// Make sure we use a thread-safe client, because there can be
	// concurrent calls to processRequest() below.
	connectionManager = new MultiThreadedHttpConnectionManager();
	httpClient = new HttpClient(params, connectionManager);
    }

    public void processRequest(MobyDataJob request, MobyDataJob result) throws Exception{
	MobyService service = getService();
	// name -> text or binary data
	Map<String,byte[]> formDataInstanceMap = new HashMap<String,byte[]>();

	// Reformat data into format needed by http client
	for(Map.Entry<String,String> fixedParam: formConfig.getFixedParams().entrySet()){
	    formDataInstanceMap.put(fixedParam.getKey(), fixedParam.getValue().getBytes());
	}

	Map<String,String> textFormats = formConfig.getPrimaryInputFormats();
	for(MobyPrimaryData mobyInputTemplate: service.getPrimaryInputs()){
	    // Retrieve the input with the same name as the service template specifies
	    String paramName = mobyInputTemplate.getName();
	    MobyDataInstance mobyData = request.get(paramName);
	    if(!(mobyData instanceof MobyDataObject) &&
	       !(mobyData instanceof MobyDataObjectSet)){
		throw new MobyException("The Moby parameter '" + paramName + 
					"' is not a primary input as expected (" +
					"found " + mobyData.getClass().getName() + ")");
	    }
	    
	    // Transform the moby data as required and put it in the form
	    for(String formFieldName: mobyPrimary2FormFields.get(paramName)){
		formDataInstanceMap.put(formFieldName,
				        getLegacyData(mobyData,
						      textFormats.get(formFieldName)));
	    }
	}
	for(MobySecondaryData mobySecondaryTemplate: service.getSecondaryInputs()){
	    // Retrieve the input with the same name as the service template specifies
	    String paramName = mobySecondaryTemplate.getName();
	    MobyDataInstance mobyData = request.get(paramName);
	    if(!(mobyData instanceof MobyDataSecondaryInstance)){
		throw new MobyException("The Moby parameter '" + paramName + 
					"' is not a secondary as expected (" +
					"found " + mobyData.getClass().getName() + ")");
	    }

	    for(String formFieldName: mobySecondary2FormFields.get(paramName)){
		// TODO: check that the value passed in was acceptable?
		formDataInstanceMap.put(formFieldName, 
					((MobyDataSecondaryInstance) mobyData).getValue().getBytes());
	    }
	}

	HttpMethod method;
	if("POST".equals(formConfig.getFormMethod())){
	    method = new PostMethod(formConfig.getFormAction());
	    if(XHTMLForm.MULTIPART.toLowerCase().equals(
			 formConfig.getFormEncodingType().toLowerCase())){
		((PostMethod) method).setRequestEntity(CGIUtils.getMultipartRequest(formDataInstanceMap, 
										    method.getParams(),
										    formConfig.getFormFiles()));
	    }
	    else{
		((PostMethod) method).setRequestBody(CGIUtils.getNameValuePairs(formDataInstanceMap));
	    }
	}
	// If not POST, assume GET
	else{
	    method = new GetMethod(formConfig.getFormAction()+"?"+CGIUtils.getURLQuery(formDataInstanceMap));
	}

	int statusCode;
	byte[] responseBody;
	// Send the request via HTTP
	try {
	    // Execute the method
	    statusCode = httpClient.executeMethod(method);
	    
	    // Read the response body
	    responseBody = method.getResponseBody();

	} catch (HttpException he) {
	    System.err.println("Fatal protocol violation: " + he.getMessage());
	    throw he;
	} catch (IOException ioe) {
	    System.err.println("Fatal transport error: " + ioe.getMessage());
	    throw ioe;
	} finally {
	    // Release the connection.
	    method.releaseConnection();
	}

	if (statusCode != HttpStatus.SC_OK) {
	    throw new Exception("HTTP CGI call failed: " + method.getStatusLine());
	}
	    
	// parse the results
	Map<String,byte[]> responseData = new HashMap<String,byte[]>();
	responseData.put("response", responseBody);
	for(MobyPrimaryData mobyOutputTemplate: service.getPrimaryOutputs()){
	    MobyDataInstance mdi = getMobyData(responseData, mobyOutputTemplate);
	    if(mdi == null){
		throw new Exception("The output parameter '" + mobyOutputTemplate.getName() + 
				    "' of data type '" + mobyOutputTemplate.getDataType().getName() + 
				    "' could not be created from the form submission response (" +
				    "TextClient returned null transforming the legacy data).");
	    }
	    result.put(mobyOutputTemplate.getName(), mdi);
	}
    }

    public MobyService createServiceFromConfig(javax.servlet.http.HttpServletRequest request)
	throws Exception{
	
        remoteFormURL = getSpecURL(HTML_FORM_URL_PARAM);

	try{
	    formConfig = new XHTMLForm(remoteFormURL);
	} catch(Exception e){
            e.printStackTrace();
	    throw new Exception("Could not determine Moby service configuration from the HTML form (" + 
				remoteFormURL.toString() + "): " + e);
	}

        //Call to parent, which handles spec-wrapper-to-MobyServlet-config conversion
	return createServiceFromConfig(request, formConfig);
    }

    public String createInputSpecString(XHTMLForm form){
	Map<String,String> ins = form.getPrimaryInputs();
	// Create a reverse map so we can look up where a moby param 
	// slots into in the form.
	for(Map.Entry<String,String> entry: ins.entrySet()){
	    String[] mobyParamSpec = entry.getValue().split(":");  // "name:datatype"
	    if(!mobyPrimary2FormFields.containsKey(mobyParamSpec[0])){
		mobyPrimary2FormFields.put(mobyParamSpec[0], 
					   new String[]{entry.getKey()});
	    }
	    // We've assume a moby object is used only once in a form most
	    // of the time, hence the somewhat inefficient realloc here for every
	    // additional form field a moby param, is associated with.
	    else{
		String[] oldFormFields = mobyPrimary2FormFields.get(mobyParamSpec[0]);
		String[] newFormFields = new String[oldFormFields.length+1];
		newFormFields[newFormFields.length-1] = entry.getKey(); //the new data
		// the existing data
		System.arraycopy(oldFormFields, 0, newFormFields, 0, 
				 oldFormFields.length);
		mobyPrimary2FormFields.put(mobyParamSpec[0], newFormFields);
	    }
	}
	// We need to eliminate any duplicate moby param definitions, which
	// occur when the same param is used to defined more than one form field
	Collection<String> values = ins.values();
	for(String mobyParamName: mobyPrimary2FormFields.keySet()){
	    for(int i = 1; i < mobyPrimary2FormFields.get(mobyParamName).length; i++){
		values.remove(mobyParamName);
	    }
	}

	// Create the formatted info required by MobyServlet annotations
	return XHTMLForm.join(",", values.toArray(new String[values.size()]));
    }

    public String createOutputSpecString(XHTMLForm form){
	Map<String,String> outs = form.getPrimaryOutputs();
	String[] outSpec = new String[outs.size()];
	int i = 0;
	for(String key: outs.keySet()){
	    outSpec[i++] = key;
	}
	return XHTMLForm.join(",", outSpec);
    }

    public String createSecondarySpecString(XHTMLForm form){
	Map<String,String> secondaries = form.getSecondaryInputs();
	// Create a reverse map so we can look up where a moby param 
	// slots into in the form.
	for(Map.Entry<String,String> entry: secondaries.entrySet()){
	    // "name:datatype:defaultValue:[range]"
	    String[] mobyParamSpec = entry.getValue().split(":");  
	    if(!mobySecondary2FormFields.containsKey(mobyParamSpec[0])){
		mobySecondary2FormFields.put(mobyParamSpec[0], 
					     new String[]{entry.getKey()});
	    }
	    // We've assume a moby secondary param is used only once in a form most
	    // of the time, hence the somewhat inefficient realloc here for every
	    // additional form field a moby param, is associated with.
	    else{
		String[] oldFormFields = mobySecondary2FormFields.get(mobyParamSpec[0]);
		String[] newFormFields = new String[oldFormFields.length+1];
		newFormFields[newFormFields.length-1] = entry.getKey(); //the new data
		// the existing data
		System.arraycopy(oldFormFields, 0, newFormFields, 0, 
				 oldFormFields.length);
		mobySecondary2FormFields.put(mobyParamSpec[0], newFormFields);
	    }
	}
	// Make sure that if a secondary param is reused that the specs don't conflict
	// Keep only the one with the full spec (which must be the longest one) 
	Collection<String> values = secondaries.values();
	for(String mobyParamName: mobySecondary2FormFields.keySet()){
	    String[] fields = mobySecondary2FormFields.get(mobyParamName);
	    if(fields.length > 1){
		String longestSpec = "";
		for(int i = 0; i < fields.length; i++){
		    if(fields[i].length() > longestSpec.length()){
			values.remove(longestSpec);
			longestSpec = fields[i];
		    }
		}
	    }
	}

	return XHTMLForm.join("\t", values.toArray(new String[values.size()]));
    }

}
