/* Copyright (c) 2009 Peter Troshin
 *  
 *  JAva Bioinformatics Analysis Web Services (JABAWS) @version: 1.0     
 * 
 *  This library is free software; you can redistribute it and/or modify it under the terms of the
 *  Apache License version 2 as published by the Apache Software Foundation
 * 
 *  This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Apache 
 *  License for more details.
 * 
 *  A copy of the license is in apache_license.txt. It is also available here:
 * @see: http://www.apache.org/licenses/LICENSE-2.0.txt
 * 
 * Any republication or derived work distributed in source code form
 * must include this copyright and license notice.
 */

package compbio.ws.client;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;

import compbio.data.msa.MsaWS;
import compbio.data.sequence.Alignment;
import compbio.data.sequence.ClustalAlignmentUtil;
import compbio.data.sequence.FastaSequence;
import compbio.data.sequence.SequenceUtil;
import compbio.metadata.JobSubmissionException;
import compbio.metadata.Limit;
import compbio.metadata.LimitsManager;
import compbio.metadata.Option;
import compbio.metadata.Preset;
import compbio.metadata.PresetManager;
import compbio.metadata.ResultNotAvailableException;
import compbio.metadata.RunnerConfig;
import compbio.metadata.WrongParameterException;

/**
 * A command line client for JAva Bioinformatics Analysis Web Services
 * 
 * @author pvtroshin
 * @version 1.0
 */
public class Jws2Client {

    /*
     * Use java.util.Logger instead of log4j logger to reduce the size of the
     * client package
     */
    private static final Logger log = Logger.getLogger(Jws2Client.class
	    .getCanonicalName());

    final static String pseparator = "=";

    // Parameters for required command line options
    final static String hostkey = "-h";
    final static String servicekey = "-s";

    // Actions
    final static String inputkey = "-i";

    final static String paramList = "-parameters";
    final static String presetList = "-presets";
    final static String limitList = "-limits";

    // Options
    final static String paramFile = "-f";
    final static String outputkey = "-o";
    final static String parameterkey = "-p";
    final static String presetkey = "-r";

    //JABAWS version 1.0 service name
    static final String qualifiedServiceName = "http://msa.data.compbio/01/01/2010/";

    /**
     * Searches the command line keys in the array of parameters
     * 
     * @param cmd
     *            command line options
     * @return true is the list of Parameters is requested, false otherwise
     */
    private boolean listParameters(String[] cmd) {
	return keyFound(cmd, paramList);
    }

    /**
     * Check whether presetList is set in the command line
     * 
     * @param cmd
     *            command line options
     * @return true if presetList is found, false otherwise
     */
    boolean listPresets(String[] cmd) {
	return keyFound(cmd, presetList);
    }

    /**
     * Returns {@code Preset} by its name
     * 
     * @see Preset
     * @param <T>
     * @param msaws
     * @param presetName
     * @return Return a Preset by its optionName
     */
    <T> Preset<T> getPreset(MsaWS<T> msaws, String presetName) {
	assert presetName != null;
	PresetManager<T> presets = getPresetList(msaws);
	if (presets == null) {
	    System.out
		    .println("No presets are supported by the service! Ignoring -r directive!");
	    return null;
	}
	Preset<T> pre = presets.getPresetByName(presetName);
	if (pre == null) {
	    System.out.println("Cannot find preset: " + presetName
		    + " WARN: ignoring -r directive!");
	}
	return pre;
    }

    /**
     * Extracts preset name from the command line is any
     * 
     * @param cmd
     *            command line options
     * @return presetName or null if no presets is defined
     */
    String getPresetName(String[] cmd) {
	String preset = null;
	for (int i = 0; i < cmd.length; i++) {
	    String presetPrm = cmd[i];
	    if (presetPrm.trim().toLowerCase().startsWith(
		    presetkey + pseparator)) {
		preset = presetPrm.substring(presetPrm.indexOf(pseparator) + 1);
		break;
	    }
	}
	return preset;
    }

    /**
     * Checks whether limitList parameter is in the command line
     * 
     * @param cmd
     *            - command line options
     * @return true if it is, false otherwise
     */
    boolean listLimits(String[] cmd) {
	return keyFound(cmd, limitList);
    }

    /**
     * Checks whether the key is in the command line
     * 
     * @param cmd
     * @param key
     * @return true if it is, false otherwise
     */
    boolean keyFound(String[] cmd, String key) {
	assert cmd != null && cmd.length > 0;
	assert key != null;
	for (int i = 0; i < cmd.length; i++) {
	    String listPresets = cmd[i];
	    if (listPresets.trim().equalsIgnoreCase(key)) {
		return true;
	    }
	}
	return false;
    }

    /**
     * Attempt to construct the URL object from the string
     * 
     * @param urlstr
     * @return true if it succeed false otherwise
     */
    public static boolean validURL(String urlstr) {
	try {
	    if (urlstr == null || urlstr.trim().length() == 0) {
		return false;
	    }
	    new URL(urlstr);
	} catch (MalformedURLException e) {
	    return false;
	}
	return true;
    }

    /**
     * Extracts service name from the command line
     * 
     * @param cmd
     *            command line options
     * @return service name or null if it is not defined
     */
    public static String getServiceName(String[] cmd) {
	for (int i = 0; i < cmd.length; i++) {
	    String serv = cmd[i];
	    if (serv.trim().toLowerCase().startsWith(servicekey + pseparator)) {
		return serv.substring(serv.indexOf(pseparator) + 1);
	    }
	}
	return null;
    }

    /**
     * Extracts host name from the command line
     * 
     * @param cmd
     *            command line options
     * @return host name or null if it is not defined
     */
    public static String getHost(String[] cmd) {
	for (int i = 0; i < cmd.length; i++) {
	    String host = cmd[i];
	    if (host.trim().toLowerCase().startsWith(hostkey + pseparator)) {
		return host.substring(host.indexOf(pseparator) + 1);
	    }
	}
	return null;
    }

    /**
     * Checks -i options and return the File if one was provided, null otherwise
     * 
     * @param cmd
     * @param key
     * @param mustExist
     * @return
     * @throws IOException
     */
    File getFile(String[] cmd, String key, boolean mustExist)
	    throws IOException {
	assert key != null && key.trim().length() != 0;
	for (int i = 0; i < cmd.length; i++) {
	    String filename = cmd[i];
	    filename = filename.trim();
	    if (filename.toLowerCase().startsWith(key + pseparator)) {
		filename = filename.substring((key + pseparator).length());
		File file = new File(filename);
		if (mustExist && !file.exists()) {
		    System.out.println(key + " file " + file.getAbsolutePath()
			    + " does not exist");
		    return null;
		}
		if (!mustExist && !file.exists()) {
		    file.createNewFile();
		}
		if (!file.canRead()) {
		    System.out.println("Cannot read " + key + " file "
			    + file.getAbsolutePath());
		    return null;
		}
		return file;
	    }
	}
	return null;
    }

    /**
     * Connects to the service and do the job as requested, if something goes
     * wrong reports or/and prints usage help.
     * 
     * @param <T>
     *            web service type
     * @param cmd
     *            command line options
     * @throws IOException
     */
    <T> Jws2Client(String[] cmd) throws IOException {

	String hostname = getHost(cmd);
	if (hostname == null) {
	    System.out.println("Host name is not provided!");
	    printUsage(1);
	}

	if (!validURL(hostname)) {
	    System.out.println("Host name is not valid!");
	    printUsage(1);
	}
	String serviceName = getServiceName(cmd);
	if (serviceName == null) {
	    System.out.println("Service name is no provided!");
	    printUsage(1);
	}
	Services service = Services.getService(serviceName);
	if (service == null) {
	    System.out.println("Service " + serviceName
		    + " is no supported! Valid values are: "
		    + Arrays.toString(Services.values()));
	    printUsage(1);
	}
	File inputFile = getFile(cmd, inputkey, true);
	File outFile = getFile(cmd, outputkey, false);
	File parametersFile = getFile(cmd, paramFile, true);
	String presetName = getPresetName(cmd);

	MsaWS<T> msaws = connect(hostname, service);
	Preset<T> preset = null;
	if (presetName != null) {
	    preset = getPreset(msaws, presetName);
	}
	List<Option<T>> customOptions = null;
	if (parametersFile != null) {
	    List<String> prms = loadParameters(parametersFile);
	    customOptions = processParameters(prms, msaws.getRunnerOptions());
	}
	Alignment alignment = null;
	if (inputFile != null) {
	    alignment = align(inputFile, msaws, preset, customOptions);
	    OutputStream outStream = null;
	    if (outFile != null) {
		outStream = getOutStream(outFile);
	    } else {
		// this stream is going to be closed later which is fine as
		// std.out will not be
		outStream = System.out;
	    }
	    writeOut(outStream, alignment);
	    // stream is closed in the method no need to close it here
	}

	boolean listParameters = listParameters(cmd);
	if (listParameters) {
	    System.out.println(getParametersList(msaws));
	}
	boolean listPreset = listPresets(cmd);
	if (listPreset) {
	    System.out.println(getPresetList(msaws));
	}
	boolean listLimits = listLimits(cmd);
	if (listLimits) {
	    System.out.println(getLimits(msaws));
	}
	log.fine("Disconnecting...");
	((Closeable) msaws).close();
	log.fine("Disconnected successfully!");
    }

    /**
     * Load parameters from file
     * 
     * @throws IOException
     */
    List<String> loadParameters(File paramsfile) throws IOException {
	assert paramsfile != null && paramsfile.exists();
	BufferedReader reader = new BufferedReader(new FileReader(paramsfile));
	String line = null;
	ArrayList<String> params = new ArrayList<String>();
	while ((line = reader.readLine()) != null) {
	    line = line.trim();
	    if (line.length() == 0)
		continue;
	    params.add(line);
	}
	return params;
    }

    /**
     * Converts options supplied via parameters file into {@code Option} objects
     * 
     * @param <T>
     *            web service type
     * @param params
     * @param options
     * @return List of Options of type T
     */
    <T> List<Option<T>> processParameters(List<String> params,
	    RunnerConfig<T> options) {
	List<Option<T>> chosenOptions = new ArrayList<Option<T>>();
	for (String param : params) {
	    String oname = null;
	    if (isParameter(param)) {
		oname = this.getParamName(param);
	    } else {
		oname = param;
	    }
	    Option<T> o = options.getArgumentByOptionName(oname);
	    if (o == null) {
		System.out.println("WARN ignoring unsuppoted parameter: "
			+ oname);
		continue;
	    }
	    if (isParameter(param)) {
		try {
		    o.setValue(getParamValue(param));
		} catch (WrongParameterException e) {
		    System.out
			    .println("Problem setting value for the parameter: "
				    + param);
		    e.printStackTrace();
		}
	    }
	    chosenOptions.add(o);
	}
	return chosenOptions;
    }

    String getParamName(String fullName) {
	assert isParameter(fullName);
	return fullName.substring(0, fullName.indexOf(pseparator));
    }

    String getParamValue(String fullName) {
	assert isParameter(fullName);
	return fullName.substring(fullName.indexOf(pseparator) + 1);
    }

    boolean isParameter(String param) {
	return param.contains(pseparator);
    }

    OutputStream getOutStream(File file) {
	assert file != null && file.exists();
	try {
	    return new FileOutputStream(file);
	} catch (FileNotFoundException e) {
	    e.printStackTrace();
	}
	return null;
    }

    /**
     * Outputs clustal formatted alignment into the file represented by the
     * outStream
     * 
     * @param outStream
     * @param align
     *            the alignment to output
     */
    void writeOut(OutputStream outStream, Alignment align) {
	try {
	    ClustalAlignmentUtil.writeClustalAlignment(outStream, align);
	} catch (IOException e) {
	    System.err
		    .println("Problems writing output file! Stack trace is below: ");
	    e.printStackTrace();
	} finally {
	    if (outStream != null) {
		try {
		    outStream.close();
		} catch (IOException ignored) {
		    // e.printStackTrace();
		}
	    }
	}
    }

    /**
     * Connects to a web service by the host and the service name
     * 
     * @param <T>
     *            web service type
     * @param host
     * @param service
     * @return MsaWS<T>
     * @throws WebServiceException
     */
    public static <T> MsaWS<T> connect(String host, Services service)
	    throws WebServiceException {
	URL url = null;
	log.log(Level.FINE, "Attempt to connect...");
	try {
	    url = new URL(host + "/" + service.toString() + "?wsdl");
	} catch (MalformedURLException e) {
	    e.printStackTrace();
	    // ignore as the host name is already verified
	}
	QName qname = new QName(qualifiedServiceName, service.toString());
	Service serv = Service.create(url, qname);
	MsaWS<T> msaws = serv.getPort(new QName(qualifiedServiceName, service
		+ "Port"), MsaWS.class);
	log.log(Level.FINE, "Connected successfully!");
	return msaws;
    }

    /**
     * Align sequences from the file using MsaWS
     * 
     * @param <T>
     *            web service type e.g. Clustal
     * @param file
     *            to write the resulting alignment to
     * @param msaws
     *            MsaWS required
     * @param preset
     *            Preset to use optional
     * @param customOptions
     *            file which contains new line separated list of options
     * @return Alignment
     */
    static <T> Alignment align(File file, MsaWS<T> msaws, Preset<T> preset,
	    List<Option<T>> customOptions) {
	FileInputStream instream = null;
	List<FastaSequence> fastalist = null;
	Alignment alignment = null;
	try {
	    instream = new FileInputStream(file);
	    fastalist = SequenceUtil.readFasta(instream);
	    instream.close();
	    String jobId = null;
	    if (customOptions != null && preset != null) {
		System.out
			.println("WARN: Parameters (-f) are defined together with a preset (-r) ignoring preset!");
	    }
	    if (customOptions != null) {
		jobId = msaws.customAlign(fastalist, customOptions);
	    } else if (preset != null) {
		jobId = msaws.presetAlign(fastalist, preset);
	    } else {
		jobId = msaws.align(fastalist);
	    }
	    Thread.sleep(1000);
	    alignment = msaws.getResult(jobId);

	} catch (IOException e) {
	    System.err
		    .println("Exception while reading the input file. "
			    + "Check that the input file contains a list of fasta formatted sequences! "
			    + "Exception details are below:");
	    e.printStackTrace();
	} catch (JobSubmissionException e) {
	    System.err
		    .println("Exception while submitting job to a web server. "
			    + "Exception details are below:");
	    e.printStackTrace();
	} catch (ResultNotAvailableException e) {
	    System.err.println("Exception while waiting for results. "
		    + "Exception details are below:");
	    e.printStackTrace();
	} catch (InterruptedException ignored) {
	    // ignore and propagate an interruption
	    Thread.currentThread().interrupt();
	} catch (WrongParameterException e) {
	    e.printStackTrace();
	} finally {
	    if (instream != null) {
		try {
		    instream.close();
		} catch (IOException ignored) {
		    // ignore
		}
	    }
	}
	return alignment;
    }

    /**
     * Returns a list of options supported by web service
     * 
     * @param <T>
     *            web service type
     * @param msaws
     *            web service proxy
     * @return List of options supported by a web service
     */
    <T> List<Option<T>> getParametersList(MsaWS<T> msaws) {
	assert msaws != null;
	return msaws.getRunnerOptions().getArguments();
    }

    /**
     * Returns an objects from which the list of presets supported by web
     * service <T> can be obtained
     * 
     * @param <T>
     *            web service type
     * @param msaws
     *            web service proxy
     * @return PresetManager, object which operates on presets
     */
    <T> PresetManager<T> getPresetList(MsaWS<T> msaws) {
	assert msaws != null;
	PresetManager<T> presetman = msaws.getPresets();
	return presetman;
    }

    /**
     * Returns a list of limits supported by web service Each limit correspond
     * to a particular preset.
     * 
     * @param <T>
     *            web service type
     * @param msaws
     *            web service proxy
     * @return List of limits supported by a web service
     */
    <T> List<Limit<T>> getLimits(MsaWS<T> msaws) {
	assert msaws != null;
	LimitsManager<T> lmanger = msaws.getLimits();

	return lmanger != null ? lmanger.getLimits() : null;
    }

    /**
     * Prints Jws2Client usage information to standard out
     * 
     * @param exitStatus
     */
    static void printUsage(int exitStatus) {
	System.out.println();
	System.out.println("Usage: <Class or Jar file name> " + hostkey
		+ pseparator + "host_and_context " + servicekey + pseparator
		+ "serviceName ACTION [OPTIONS] ");
	System.out.println();
	System.out
		.println(hostkey
			+ pseparator
			+ "<host_and_context> - a full URL to the JWS2 web server including context path e.g. http://10.31.1.159:8080/ws");
	System.out.println(servicekey + pseparator + "<ServiceName> - one of "
		+ Arrays.toString(Services.values()));
	System.out.println();
	System.out.println("ACTIONS: ");
	System.out
		.println(inputkey
			+ pseparator
			+ "<inputFile> - full path to fasta formatted sequence file, from which to align sequences");
	System.out.println(paramList
		+ " - lists parameters supported by web service");
	System.out.println(presetList
		+ " - lists presets supported by web service");
	System.out.println(limitList + " - lists web services limits");
	System.out
		.println("Please note that if input file is specified other actions are ignored");

	System.out.println();
	System.out.println("OPTIONS (only for use with -i action):");

	System.out.println(presetkey + pseparator
		+ "<presetName> - name of the preset to use");
	System.out
		.println(outputkey
			+ pseparator
			+ "<outputFile> - full path to the file where to write an alignment");
	System.out
		.println("-f=<parameterInputFile> - the name of the file with the list of parameters to use.");
	System.out
		.println("Please note that -r and -f options cannot be used together. "
			+ "Alignment is done with either preset or a parameters from the file, but not both!");

	System.exit(exitStatus);
    }

    /**
     * Starts command line client, if no parameter are supported print help. Two
     * parameters are required for successfull call the JWS2 host name and a
     * service name.
     * 
     * @param args
     *            Usage: <Class or Jar file name> -h=host_and_context
     *            -s=serviceName ACTION [OPTIONS]
     * 
     *            -h=<host_and_context> - a full URL to the JWS2 web server
     *            including context path e.g. http://10.31.1.159:8080/ws
     * 
     *            -s=<ServiceName> - one of [MafftWS, MuscleWS, ClustalWS,
     *            TcoffeeWS, ProbconsWS] ACTIONS:
     * 
     *            -i=<inputFile> - full path to fasta formatted sequence file,
     *            from which to align sequences
     * 
     *            -parameters - lists parameters supported by web service
     * 
     *            -presets - lists presets supported by web service
     * 
     *            -limits - lists web services limits Please note that if input
     *            file is specified other actions are ignored
     * 
     *            OPTIONS: (only for use with -i action):
     * 
     *            -r=<presetName> - name of the preset to use
     * 
     *            -o=<outputFile> - full path to the file where to write an
     *            alignment -f=<parameterInputFile> - the name of the file with
     *            the list of parameters to use. Please note that -r and -f
     *            options cannot be used together. Alignment is done with either
     *            preset or a parameters from the file, but not both!
     * 
     */
    public static void main(String[] args) {

	if (args == null) {
	    printUsage(1);
	}
	if (args.length < 2) {
	    System.out.println("Host and service names are required!");
	    printUsage(1);
	}

	try {
	    new Jws2Client(args);
	} catch (IOException e) {
	    log.log(Level.SEVERE, "IOException in client! " + e.getMessage(), e
		    .getCause());
	    System.err.println("Cannot write output file! Stack trace: ");
	    e.printStackTrace();
	}
    }
}
