package hirondelle.web4j.model;

import java.util.*;
import java.lang.reflect.*;
import hirondelle.web4j.util.Args;

/**
 <b>UNPUBLISHED</b> - Utilities for constructing Model Objects.

 <P>Instead of using this class directly, the application programmer will
 use {@link hirondelle.web4j.model.ModelFromRequest} instead. 
*/
public final class ModelCtorUtil {
  
  /**
   Return the {@link java.lang.reflect.Constructor} having the given number of arguments.
  
   <P>In WEB4J, Model Objects have <tt>public</tt> constructors. 
   Any no-argument constructors are ignored by WEB4J. Constructors which take arguments
   are used by WEB4J to build Model Objects out of both user input and database
   {@link java.sql.ResultSet}s.
    
   <P>In WEB4J, the recommended style is to define Model Objects as immutable,
   with a single constructor taking all necessary data.
  
  @param aMOClass has a public constructor which takes <tt>aNumArguments</tt>
  @param aNumArguments number of arguments taken by a constructor, must be greater 
   than <tt>0</tt>
  */
  public static <T> Constructor<T> getConstructor(Class<T> aMOClass, int aNumArguments) {
    Args.checkForPositive(aNumArguments);
    Constructor<T> result = null;
    //Original line did not compile under JDK 6    Constructor<T>[] allCtors = aMOClass.getConstructors(); 
    Constructor<T>[] allCtors = (Constructor<T>[]) aMOClass.getConstructors();
    if (allCtors.length == 0) {
      throw new IllegalArgumentException(
        "Model Object class has no public constructor : " + aMOClass.getName()
      );   
    }
    //cycle thru, and take the first constructor with the matching number of arguments
    for ( Constructor<T> ctor : allCtors) {
      if (ctor.getParameterTypes().length == aNumArguments) {
        result = ctor;
        break;
      }
    }
    if (result == null){
      throw new IllegalArgumentException(
        "Model Object class " + aMOClass.getName() + " does not have a public " +
        "constructor having " + aNumArguments + " arguments."
      );
    }
    return result;
  }
  
  /**
   Return the <tt>public</tt>  {@link java.lang.reflect.Constructor} whose 
   <em>exact</em> argument types are listed, in order, in <tt>aArgClasses</tt>. 
   
   <P><em>This method is intended only for cases in which specifying the number of 
   arguments would not uniquely identify the desired <tt>public</tt> constructor.</em>
  
   <P>Example values for <tt>aArgClasses</tt> : <P>
   <tt>
   <table cellpadding="3" cellspacing="0" border="1">
   <tr><th>public Constructor</th><th>aArgClasses array</th></tr>
   <tr><td>Account(String, int)</td><td>{String.class, int.class}</td></tr>
   <tr><td>Account(String, BigDecimal)</td><td>{String.class, BigDecimal.class}</td></tr>
   </table>
   </tt>
   @param aArgClasses has the same restrictions as the arguments to 
   {@link Class#getConstructor(Class[])}, and must have at least one element.
  */
  public static <T> Constructor<T> getExactConstructor(Class<T> aMOClass, Class<?>[] aArgClasses) {
    Args.checkForPositive(aArgClasses.length);
    Constructor<T> result = null;
    try {
      result = aMOClass.getConstructor(aArgClasses);
    }
    catch (NoSuchMethodException ex){
      throw new IllegalArgumentException(
        "Model Object class " + aMOClass.getName() + " does not have a public " +
        "constructor having as arguments " + getClassNames(aArgClasses)
      );
    }
    return result;
  }
  
  /**
   Build and return a Model Object, by passing the given argument values
   to the given constructor.
   
   @param aMOCtor Model Object constructor having at least one argument
   @param aArgValues contains possibly-null Objects, to be passed to <tt>aMOCtor</tt> ; 
   if the Model Object constructor takes primitives, and not objects, then the primitives 
   are transparently "unwrapped" from corresponding wrapper objects. 
   See {@link Constructor#newInstance}.
  */
  public static <T> T buildModelObject(Constructor<T> aMOCtor, List<Object> aArgValues) throws ModelCtorException {
    T result = null;
    try { 
      result =  aMOCtor.newInstance( aArgValues.toArray() );
    }
    catch (InstantiationException ex){
      vomit(aMOCtor, aArgValues, ex);
    }
    catch (IllegalAccessException ex){
      vomit(aMOCtor, aArgValues, ex);
    }
    catch(IllegalArgumentException ex){
      vomit(aMOCtor, aArgValues, ex);
    }
    catch (InvocationTargetException ex){
      //If the underlying MO ctor throws an exception, then this branch is exercised.
      //Curiously, it is appropriate here to unwrap the underlying ModelCtorException, 
      //just to re-throw it!
      Throwable cause = ex.getCause();
      if ( cause instanceof ModelCtorException ) {
        //the MO cannot accept the given arguments
        throw (ModelCtorException)cause;
      }
      //a bug has occurred
      throw (RuntimeException)cause;
    }
    return result;
  }
  
  // PRIVATE //
  
  private ModelCtorUtil(){
    //empty - prevent construction by the caller
  }
  
  /**
   This will not be called during normal operation of the program, even 
   when the user has input invalid values. That is, a call to this method 
   means a bug in the program or its environment, and not a user input error.
  */
  private static void vomit(Constructor<?> aMOCtor, List<Object> aArgValues, Exception aEx) {
    throw new RuntimeException(
      "Cannot reflectively construct Model Object of class " 
      + aMOCtor.getDeclaringClass().getName() + 
      ", using arguments " + aArgValues + ". " + aEx.toString(),
      aEx
    );
  }
  
  private static String getClassNames(Class<?>[] aClasses){
    List names = Arrays.asList(aClasses);
    return names.toString(); 
  }
}
