/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */
/*
 *                  BioJava development code
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */

package org.biojava.nbio.structure.align.util;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * Utilities for autoconfiguring javabeans based on command line arguments.
 *
 * @author Thomas Down
 */

public class CliTools {
	private CliTools() {
	}


	/**
	 * Configure a JavaBean based on a set of command line arguments.
	 * For a command line construct such as "-foo 42", this method will use
	 * available <code>BeanInfo</code> (usually obtained by introspection)
	 * to find a property named "foo".  The argument will be interpreted
	 * according to the type of the "foo" property, then the appropriate
	 * mutator method (generally named setFoo) will be called to configure
	 * the property on the bean.
	 *
	 * <p>
	 * Currently supported property types are <code>int, double,
	 * boolean, String, File, Reader, Writer, InputStream, OutputStream, Enum</code>,
	 * plus arrays of all the above types.  In the case of arrays, the option
	 * may appear multiple times on the command line, otherwise recurrance of
	 * the same option is an error.
	 * </p>
	 *
	 * <p>
	 * For stream types, the parameter is interpreted as a filename unless it
	 * is equal to "-" in which case standard input or standard output are
	 * used as appropriate.  Each of the standard streams may only be used
	 * one.
	 * </p>
	 *
	 * <p>
	 * In the future, this method will probably be extended to handle multiple
	 * parameter occurances, and use Annotations to generate more useful help
	 * messages when something goes wrong.
	 * </p>
	 *
	 * @param bean
	 * @param args
	 * @return A string array which contains any 'anonymous' arguments (may be empty)
	 * @throws ConfigurationException
	 */

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static String[] configureBean(Object bean, String[] args)
	throws ConfigurationException
	{
		BeanInfo bi;
		try {
			bi = Introspector.getBeanInfo(bean.getClass());
		} catch (Exception ex) {
			throw new ConfigurationException("Couldn't get information for target bean " + ex.getMessage());
		}

		Map<String,PropertyDescriptor> propertiesByName = new HashMap<String, PropertyDescriptor>();
		for (PropertyDescriptor pd : bi.getPropertyDescriptors() ) {
			propertiesByName.put(pd.getName(), pd);
		}

		List<String> anonArgs = new ArrayList<String>();
		Map<PropertyDescriptor,List<String>> arrayProps = new HashMap<PropertyDescriptor, List<String>>();
		Set<PropertyDescriptor> usedProps = new HashSet<PropertyDescriptor>();

		boolean stdInUsed = false;
		boolean stdOutUsed = false;

		for (int i = 0; i < args.length; ++i) {
			String arg = args[i];

			//System.out.println("checking argument: " + arg);

			if ( arg == null)
				continue;

			if ((arg.length() > 0 ) && arg.charAt(0) == '-') {
				PropertyDescriptor pd = propertiesByName.get(arg.substring(1));

				boolean arrayMode = false;
				Object propVal = null;
				Class propType = null;

				if (pd == null) {
					if (arg.startsWith("-no")) {
						String altPropName = Introspector.decapitalize(arg.substring(3));
						pd = propertiesByName.get(altPropName);
						if (pd == null) {
							throw new ConfigurationException("No property named " + arg.substring(1) + " or " + altPropName);
						}
						propType = pd.getPropertyType();
						if (propType == Boolean.TYPE) {
							propVal = Boolean.FALSE;
						} else {
							throw new ConfigurationException("Negatory option " + arg + " does not refer to a boolean property");
						}
					} else {
						throw new ConfigurationException("No property named " + arg.substring(1));
					}
				} else {
					propType = pd.getPropertyType();

					if (propType.isArray()) {
						arrayMode = true;
						propType = propType.getComponentType();
					}

					if (propType == Integer.TYPE) {
						try {
							propVal = new Integer(args[++i]);
						} catch (Exception ex) {
							throw new ConfigurationException("Option " + arg + " requires an integer parameter");
						}
					} else if (propType == Double.TYPE || propType == Double.class ) {
						try {
							propVal = new Double(args[++i]);
						} catch (Exception ex) {
							throw new ConfigurationException("Option " + arg + " requires a numerical parameter");
						}
					} else if (propType == String.class) {
						propVal = args[++i];
					} else if (propType == Boolean.TYPE) {

						String val = args[++i];
						if ( val == null )
							propVal = Boolean.TRUE;
						else {
							if ( val.equalsIgnoreCase("true") || val.equalsIgnoreCase("t"))
								propVal = Boolean.TRUE;
							else if( val.equalsIgnoreCase("false") || val.equalsIgnoreCase("f"))
								propVal = Boolean.FALSE;
							else
								throw new ConfigurationException("Option "+arg+" requires a boolean parameter");
						}
					} else if (File.class.isAssignableFrom(propType)) {
						// can't distinguish if the file is for reading or writing
						// at the moment, so accept it without validation.
						propVal = new File(args[++i]);
					} else if (Reader.class.isAssignableFrom(propType)) {
						String name = args[++i];
						if ("-".equals(name)) {
							if (stdInUsed) {
								throw new ConfigurationException("Can't use standard input more than once");
							}
							propVal = new InputStreamReader(System.in);
							stdInUsed = true;
						} else {
							try {
								propVal = new FileReader(new File(name));
							} catch (Exception ex) {
								throw new ConfigurationException("Can't open " + name + " for input");
							}
						}
					} else if (InputStream.class.isAssignableFrom(propType)) {
						String name = args[++i];
						if ("-".equals(name)) {
							if (stdInUsed) {
								throw new ConfigurationException("Can't use standard input more than once");
							}
							propVal = System.in;
							stdInUsed = true;
						} else {
							try {
								propVal = new FileInputStream(new File(name));
							} catch (Exception ex) {
								throw new ConfigurationException("Can't open " + name + " for input");
							}
						}
					} else if (Writer.class.isAssignableFrom(propType)) {
						String name = args[++i];
						if ("-".equals(name)) {
							if (stdOutUsed) {
								throw new ConfigurationException("Can't use standard output more than once");
							}
							propVal = new OutputStreamWriter(System.out);
							stdOutUsed = true;
						} else {
							try {
								propVal = new FileWriter(new File(name));
							} catch (Exception ex) {
								throw new ConfigurationException("Can't open " + name + " for output");
							}
						}
					} else if (OutputStream.class.isAssignableFrom(propType)) {
						String name = args[++i];
						if ("-".equals(name)) {
							if (stdOutUsed) {
								throw new ConfigurationException("Can't use standard output more than once");
							}
							propVal = System.out;
							stdOutUsed = true;
						} else {
							try {
								propVal = new FileOutputStream(new File(name));
							} catch (Exception ex) {
								throw new ConfigurationException("Can't open " + name + " for output");
							}
						}
					} else if( propType.isEnum()) {
						String name = args[++i];
						try {
							propVal = Enum.valueOf(propType, name);
						} catch (Exception ex) {
							try {
								// Try with uppercase version, as common for enums
								propVal = Enum.valueOf(propType, name.toUpperCase());
							}  catch (Exception ex2) {
								//give up
								StringBuilder errMsg = new StringBuilder();
								errMsg.append("Option ").append(arg);
								errMsg.append(" requires a ").append(propType.getSimpleName());
								errMsg.append(" parameter. One of: ");
								for(Object val: propType.getEnumConstants() ) {
									Enum enumVal = (Enum) val;
									errMsg.append(enumVal.name());
									errMsg.append(" ");
								}
								throw new ConfigurationException(errMsg.toString());
							}
						}

					} else {
						System.err.println("Unsupported optionType for " + arg + " propType:" + propType);
						System.exit(1);
					}
				}

				//System.out.println("setting to: " + propVal + " " + propVal.getClass().getName());

				if (arrayMode) {
					List valList = arrayProps.get(pd);
					if (valList == null) {
						valList = new ArrayList();
						arrayProps.put(pd, valList);
					}
					valList.add(propVal);
				} else {
					if (usedProps.contains(pd)) {
						throw new ConfigurationException("Multiple values supplied for " + pd.getName());
					}
					try {
						pd.getWriteMethod().invoke(bean, new Object[] {propVal});
					} catch (InvocationTargetException ex) {
						throw new ConfigurationException("Error configuring '" + pd.getName() + "'");
					} catch (Exception ex) {
						throw new ConfigurationException("Error configuring '" + pd.getName() + "'");
					}
					usedProps.add(pd);
				}
			} else {
				anonArgs.add(arg);
			}
		}

		for (Iterator api = arrayProps.entrySet().iterator(); api.hasNext(); ) {
			Map.Entry me = (Map.Entry) api.next();
			PropertyDescriptor pd = (PropertyDescriptor) me.getKey();
			List vals = (List) me.getValue();

			Class compType = pd.getPropertyType().getComponentType();
			Object valArray;
			if (compType.isPrimitive()) {
				if (compType == Integer.TYPE) {
					valArray = CollectionTools.toIntArray(vals);
				} else if (compType == Double.TYPE) {
					valArray = CollectionTools.toDoubleArray(vals);
				} else {
					throw new ConfigurationException("Arrays of type " + compType.getName() + " are currently unsupported");
				}
			} else {
				valArray = vals.toArray((Object[]) Array.newInstance(compType, vals.size()));
			}
			try {
				pd.getWriteMethod().invoke(bean, new Object[] {valArray});
			} catch (InvocationTargetException ex) {
				throw new ConfigurationException("Error configuring '" + pd.getName() + "'");
			} catch (Exception ex) {
				throw new ConfigurationException("Error configuring '" + pd.getName() + "'");
			}
		}

		return anonArgs.toArray(new String[anonArgs.size()]);
	}

	/**
	 * Constructs a comma-separated list of values for an enum.
	 *
	 * Example:
	 * > getEnumValues(ScoringStrategy.class)
	 * "CA_SCORING, SIDE_CHAIN_SCORING, SIDE_CHAIN_ANGLE_SCORING, CA_AND_SIDE_CHAIN_ANGLE_SCORING, or SEQUENCE_CONSERVATION"
	 * @param enumClass
	 * @return
	 */
	public static <T extends Enum<?>> String getEnumValuesAsString(Class<T> enumClass) {
		//ScoringStrategy[] vals = ScoringStrategy.values();
		T[] vals = enumClass.getEnumConstants();

		StringBuilder str = new StringBuilder();
		if(vals.length == 1) {
			str.append(vals[0].name());
		} else if(vals.length > 1 ) {
			for(int i=0;i<vals.length-1;i++) {
				str.append(vals[i].name());
				str.append(", ");
			}
			str.append("or ");
			str.append(vals[vals.length-1].name());
		}

		return str.toString();
	}

}
