/* Copyright (c) 2011 Peter Troshin
 *  
 *  JAva Bioinformatics Analysis Web Services (JABAWS) @version: 2.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 static compbio.ws.client.Constraints.inputkey;
import static compbio.ws.client.Constraints.outputkey;
import static compbio.ws.client.Constraints.paramFile;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
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.JABAService;
import compbio.data.msa.Metadata;
import compbio.data.msa.MsaWS;
import compbio.data.msa.RegistryWS;
import compbio.data.msa.SequenceAnnotation;
import compbio.data.sequence.Alignment;
import compbio.data.sequence.AlignmentMetadata;
//import compbio.data.sequence.JpredAlignment;
import compbio.data.sequence.FastaSequence;
import compbio.data.sequence.ScoreManager;
import compbio.data.sequence.SequenceUtil;
import compbio.data.sequence.UnknownFileFormatException;
import compbio.metadata.JobSubmissionException;
import compbio.metadata.JobStatus;
import compbio.metadata.Option;
import compbio.metadata.Limit;
import compbio.metadata.Preset;
import compbio.metadata.PresetManager;
import compbio.metadata.ResultNotAvailableException;
import compbio.metadata.WrongParameterException;
import compbio.util.FileUtil;

/**
 * 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());

	/**
	 * 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;
	}

	/**
	 * 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
	 */
	@SuppressWarnings("unchecked")
	<T> Jws2Client(String[] cmd) throws IOException {
		

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

		if (!validURL(hostname)) {
			System.err.println("Host name is not valid!");
			System.out.println(Constraints.help_text);
			System.exit(1);
		}

		boolean listServices = CmdHelper.listServices(cmd);
		if (listServices) {
			listAllServices(hostname);
			System.exit(0);
		}

		String serviceName = CmdHelper.getServiceName(cmd);
		if (serviceName == null) {
			System.err.println("Service name is no provided!");
			System.out.println(Constraints.help_text);
			System.exit(1);
		}

		Services service = Services.getService(serviceName);
		if (service == null) {
			String mess = "Service " + serviceName + " is no available! Valid values are: ";
			System.err.println(mess	+ Arrays.toString(Services.values()));
			System.out.println(Constraints.help_text);
			System.exit(1);
		}

		if (CmdHelper.testService(cmd)) {
			testService(hostname, service, new PrintWriter(System.out, true));
			System.exit(0);
		}

		Metadata<T> thews = (Metadata<T>) connect(hostname, service);
		Preset<T> preset = null;
		if (null != CmdHelper.getPresetName(cmd)) {
			preset = MetadataHelper.getPreset(thews, CmdHelper.getPresetName(cmd));
		}

		List<Option<T>> customOptions = null;
		if (null != IOHelper.getFile(cmd, paramFile, true)) {
			List<String> prms = IOHelper.loadParameters(IOHelper.getFile(cmd, paramFile, true));
			customOptions = MetadataHelper.processParameters(prms, thews.getRunnerOptions());
		}

		if (null != IOHelper.getFile(cmd, inputkey, true)) {
			File infile = IOHelper.getFile(cmd, inputkey, true);
			File outfile = IOHelper.getFile(cmd, outputkey, false);
			Writer writer = null;
			if (outfile != null) {
				writer = IOHelper.getWriter(outfile);
			} else {
				writer = new PrintWriter(System.out, true);
			}
			if (service.getServiceType() == SequenceAnnotation.class) {
				ScoreManager result = analize(infile, ((SequenceAnnotation<T>) thews), preset, customOptions);
				IOHelper.writeOut(writer, result);
				writer.close();
			} else if (service.getServiceType() == MsaWS.class) {
				Alignment alignment = align(infile, (MsaWS<T>) thews, preset, customOptions);
//				if (serviceName.equalsIgnoreCase("JpredWS")) {
//					writer.close();
//					JpredAlignment jpred = (JpredAlignment)alignment;
//					if (outfile != null) {
//						FileOutputStream fout = new FileOutputStream(outfile);
//						SequenceUtil.writeFastaKeepTheStream(fout, jpred.getJpredSequences(), 60);
//						fout.close();
//					} else {
//						SequenceUtil.writeFasta(System.out, jpred.getJpredSequences());
//					}
//				} else {
//					IOHelper.writeOut(writer, alignment);
//					writer.close();
//				}

				IOHelper.writeOut(writer, alignment);
				writer.close();
				AlignmentMetadata md = alignment.getMetadata();
				System.out.println("Output has been prepared with " + md.getProgram());
			}
		}

		if (CmdHelper.listParameters(cmd)) {
			List<Option<T>> opts = MetadataHelper.getParametersList(thews, hostname);
			for (Option<T> o : opts) {
				System.out.println("##############################################################################\n" + o.toString());
			}
		}
		if (CmdHelper.listPresets(cmd)) {
			PresetManager<T> psm = MetadataHelper.getPresetList(thews);
			if (null != psm) {
				System.out.print(psm);
			} else {
				System.out.println("No presets are defined for the Web service");
			}
		}
		if (CmdHelper.listLimits(cmd)) {
			List<Limit<T>> lims = MetadataHelper.getLimits(thews);
			for (Limit<T> l : lims) {
				System.out.println("##############################################################################\n" + l.toString());
			}
		}
		log.fine("Disconnecting...");
		((Closeable) thews).close();
		log.fine("Disconnected successfully!");
	}

	/**
	 * Connects to a web service by the host and the service name web service type
	 * 
	 * @param host
	 *            the fully qualified name of JABAWS server including JABAWS
	 *            context name e.g
	 *            http://nanna.cluster.lifesci.dundee.ac.uk:8080/jaba
	 * @param service
	 *            the name of the JABAWS service to connect to
	 * @return JABAService<T>
	 * @throws WebServiceException
	 * @throws ConnectException
	 *             if fails to connect to the service on the host
	 */
	public static JABAService connect(String host, Services service)
			throws WebServiceException, ConnectException {
		URL url = null;
		log.log(Level.FINE, "Attempting to connect with " + service.toString() + "...");
		System.out.println ("Attempting to connect with " + service.toString() + "...");
		try {
			url = new URL(host + "/" + service.toString() + "?wsdl");
		} catch (MalformedURLException e) {
			e.printStackTrace();
			// ignore as the host name is already verified
		}
		Service serv = null;
		try {
			serv = service.getService(url, service.getServiceNamespace());
		} catch (WebServiceException wse) {
			wse.printStackTrace();
		}
		if (serv == null) {
			throw new ConnectException("Could not connect to " + url + ". Is the server down?");
		}
		JABAService serviceIF = service.getInterface(serv);
		log.log(Level.INFO, "Connected successfully!");

		return serviceIF;
	}

	/**
	 * Get a connection of JABAWS registry
	 * 
	 * @param host
	 *            the fully qualified name of JABAWS server including JABAWS
	 *            context name e.g
	 *            http://nanna.cluster.lifesci.dundee.ac.uk:8080/jaba
	 * @return compbio.data.msa.RegistryWS - instance of a RegistryWS web
	 *         service
	 * @throws WebServiceException
	 * @throws ConnectException
	 */
	public static compbio.data.msa.RegistryWS connectToRegistry(String host)
			throws WebServiceException, ConnectException {
		URL url = null;
		String service = "RegistryWS";
		log.log(Level.FINE, "Attempting to connect...");

		try {
			url = new URL(host + "/" + service + "?wsdl");
		} catch (MalformedURLException e) {
			e.printStackTrace();
			// ignore as the host name is already verified
		}
		QName qname = new QName(JABAService.V2_SERVICE_NAMESPACE, service);
		Service serv = Service.create(url, qname);

		if (serv == null) {
			throw new ConnectException("Could not connect to " + url + ". Is the server down?");
		}

		QName portName = new QName(serv.getServiceName().getNamespaceURI(), service + "Port");
		compbio.data.msa.RegistryWS serviceIF = serv.getPort(portName, compbio.data.msa.RegistryWS.class);
		log.log(Level.INFO, "Connected to " + service + " successfully!");

		return serviceIF;
	}

	/**
	 * Asks registry to test the service on the host hostname
	 * 
	 * @param hostname
	 * @param service
	 * @param writer
	 * @throws ConnectException
	 * @throws WebServiceException
	 */
	public static void testService(String hostname, Services service, PrintWriter writer)
			throws ConnectException, WebServiceException {
		RegistryWS registry = connectToRegistry(hostname);
		if (registry != null) {
			String message = registry.testService(service);
			writer.println("Service " + service + " testing results: ");
			writer.println(message);
			FileUtil.closeSilently(((Closeable) registry));
		}
		writer.flush();
	}

	private static void listAllServices(String hostname) throws WebServiceException, IOException {
		RegistryWS registry = connectToRegistry(hostname);
		Set<Services> func_services = Collections.emptySet();
		//Set<Services> nonfunc_services = Collections.emptySet();
		if (registry != null) {
			func_services = registry.getSupportedServices();
			//nonfunc_services = registry.getNonoperatedServices();
			FileUtil.closeSilently(((Closeable) registry));
		} else {
			System.out.println("Failed to connect to the registry! ");
			return;
		}
		if (!func_services.isEmpty()) {
			System.out.println("There are " + func_services.size() + " services at " + hostname + ":");
			String mess = "\n\rThe list:\n";
			System.out.println(mess + Services.toString(func_services));
		}
		/*
		if (!nonfunc_services.isEmpty()) {
			System.out.println("There are " + nonfunc_services.size() + " non-available services at " + hostname + ":");
			String mess = "The list (internal tests failed): ";
			System.out.println(mess + Services.toString(nonfunc_services));
		}
		*/
	}

	/**
	 * Calculate conservation for sequences loaded from a FASTA record list structure 
	 * 
	 * @param fastalist
	 *            the list of FASTA records
	 * @param wsproxy
	 *            a web service proxy
	 * @param preset
	 *            Preset to use optional
	 * @param customOptions
	 *            the list of options
	 * @return Set<Score> the conservation scores
	 * @throws UnknownFileFormatException
	 */
	static <T> ScoreManager analize(List<FastaSequence> fastalist, SequenceAnnotation<T> wsproxy, Preset<T> preset, List<Option<T>> customOptions) {
		ScoreManager scores = null;
		try {
			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 = wsproxy.customAnalize(fastalist, customOptions);
			} else if (preset != null) {
				jobId = wsproxy.presetAnalize(fastalist, preset);
			} else {
				jobId = wsproxy.analize(fastalist);
			}
			System.out.println("\n\rcalling predictor.........");
			Thread.sleep(1000);
			/*
			JobStatus status = wsproxy.getJobStatus(jobId);
			System.out.println("\njob " + jobId + " status: " + status);
			*/
			scores = wsproxy.getAnnotation(jobId);
		} 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 e) {
			Thread.currentThread().interrupt();
			System.err.println("Exception while waiting for results. Exception details are below:");
			e.printStackTrace();
		} catch (WrongParameterException e) {
			String mess = "Parsing the web method input parameters failed Exception details are below:";
			System.err.println(mess);
			e.printStackTrace();
		}
		return scores;

	}

	/**
	 * Calculate conservation for sequences loaded from the file
	 * 
	 * @param wsproxy
	 *            a web service proxy
	 * @param file
	 *            the file to read the results from
	 * @param preset
	 *            Preset to use optional
	 * @param customOptions
	 *            the list of options
	 * @return Set<Score> the conservation scores
	 * @throws IOException
	 * @throws UnknownFileFormatException
	 */
	static <T> ScoreManager analize(File file, SequenceAnnotation<T> wsproxy, Preset<T> preset, List<Option<T>> customOptions) {
		List<FastaSequence> fastalist = null;
		try {
			fastalist = SequenceUtil.openInputStream(file.getAbsolutePath());
			assert !fastalist.isEmpty() : "Input is empty!";
		} catch (IOException e) {
			String mess = "Reading the input file failed. Check that the file contains a list of FASTA records!\n";
			System.err.println(mess + "Exception details are below:");
			e.printStackTrace();
		} catch (UnknownFileFormatException e) {
			String mess = "Reading the input file failed. Exception details are below:";
			System.err.println(mess);
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
		return analize(fastalist, wsproxy, preset, customOptions);
	}

	/**
	 * 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;
		Alignment alignment = null;
		try {
			instream = new FileInputStream(file);
			List<FastaSequence> 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);
			}
			System.out.println("\ncalling program.........");
			/*
			long startTime = System.nanoTime();
			while (JobStatus.RUNNING == msaws.getJobStatus(jobId)) {
				Thread.sleep(1000);
				long endTime = System.nanoTime();
				System.out.println("job " + jobId + " time executing: "+ (endTime - startTime) / 1000000 +" msec, status: " + msaws.getJobStatus(jobId));
			}
			*/
			Thread.sleep(1000);
			alignment = msaws.getResult(jobId);
		} catch (IOException e) {
			System.err.println("Exception while reading the input file. Check that the input file is a FASTA file! "
						+ "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;
	}
	
	/**
	 * Starts command line client, if no parameter are supported print help. Two
	 * parameters are required for successful 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) {
			System.out.println(Constraints.help_text);
			System.exit(1);
		}
		if (args.length < 2) {
			System.err.println("Host (-h=<host>, e.g. -h=http://www.compbio.dundee.ac.uk/jabaws) and service (-s=<ServiceName>, e.g. -s=MafftWS) are required!");
			System.out.println(Constraints.help_text);
			System.exit(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();
		}
	}
}
