package hirondelle.web4j.database;

import java.util.*;
import java.util.logging.*;
import java.sql.*;
import java.lang.reflect.Constructor;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.model.ConvertParam;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelCtorUtil;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Args;

/**
 Construct a Model Object from a <tt>ResultSet</tt>.
 
 <P>Uses all columns. See package level comment for more details.
 <P>Can construct either simple Model Objects, passing only building block objects to the constructor, 
 or may construct limited forms of compound objects as well. See {@link Db} for more details.
*/
class ModelFromRow<E> extends ModelBuilder<E> {

  /**
   Simple case of a typical Model Object, whose constructor takes only building block values.
    
   @param aClass class literal for the target Model Object.
  */
  ModelFromRow(Class<E> aClass){
    fClass = aClass;  
  }
  
  /**
   Extended case of a compound Model Object, which takes a single <tt>List</tt> of child objects at the 
   end of its constructor.
     
   @param aParentClass class literal for the target parent Model Object.
   @param aChildClass class literal for the target child Model Object. The child object appears as a <tt>List</tt>
   at the end of the parent's constructor.
   @param aNumTrailingColsForChildList number of columns at the end of the <tt>ResultSet</tt> that are 
   used to construct a child object.
  */
  ModelFromRow(Class<E> aParentClass, Class<?> aChildClass, int aNumTrailingColsForChildList){
    fClass = aParentClass;
    fChildClass = aChildClass;
    Args.checkForPositive(aNumTrailingColsForChildList);
    fNumColsForChildList = aNumTrailingColsForChildList;
  }
  
  E build(ResultSet aRow) throws SQLException, ModelCtorException {
    E result = null;
    if( isSimple() ){
      result = buildSimple(aRow);
    }
    else {
      result = buildWithChildList(aRow);
    }
    return result;
  }

  // PRIVATE //
  
  private final Class<E> fClass;
  private Class<?> fChildClass;
  private int fNumColsForChildList;
  
  private ConvertColumn fColumnToObject = BuildImpl.forConvertColumn();
  private static final boolean INCLUDE_LAST_CTOR_ARG = true;
  private static final boolean EXCLUDE_LAST_CTOR_ARG = false;
  private static final int FIRST_COLUMN = 1;
  private static final Logger fLogger = Util.getLogger(ModelFromRow.class);
  
  private boolean isSimple(){
    return fChildClass == null;
  }
  
  private E buildSimple(ResultSet aRow) throws SQLException, ModelCtorException {
    Constructor<E> modelCtor = ModelCtorUtil.getConstructor(fClass, getNumColsInResultSet(aRow)); 
    List<Object> ctorArgValues = getCtorArgValues(modelCtor, aRow, INCLUDE_LAST_CTOR_ARG);
    return ModelCtorUtil.buildModelObject(modelCtor, ctorArgValues); 
  }
  
  private int getNumColsInResultSet(ResultSet aRow) throws SQLException {
    return aRow.getMetaData().getColumnCount();
  }
  
  private List<Object> getCtorArgValues(Constructor<E> aConstructor, ResultSet aRow, boolean aIncludeLastArg) throws SQLException {
    List<Object> result = new ArrayList<Object>();
    int columnIdx = 1;
    Class<?>[] targetTypes = aConstructor.getParameterTypes();
    for(Class argType: targetTypes){
      if( isNotLastArg(columnIdx, targetTypes) ){
        result.add(fColumnToObject.convert(aRow, columnIdx, argType));
      }
      else {
        if(aIncludeLastArg){
          result.add(fColumnToObject.convert(aRow, columnIdx, argType));
        }
      }
      ++columnIdx;
    }
    return result;
  }
  
  private boolean isNotLastArg(int aColIndex, Class[] aTargetTypes){
    return aColIndex != aTargetTypes.length;
  }
  
  /*
    Remaining methods all deal with children.
  */
  
  private E buildWithChildList(ResultSet aRow) throws SQLException, ModelCtorException {
    fLogger.fine("Building with Child List at end of constructor.");
    //first gather parent args (but do not construct yet)
    Constructor<E> parentCtor = ModelCtorUtil.getConstructor(fClass, getNumColsForParent(aRow) + 1); 
    List<Object> ctorArgsMinusChildren = getCtorArgValues(parentCtor, aRow, EXCLUDE_LAST_CTOR_ARG);
    fLogger.finest("Parent constructor arg values, minus children : " + ctorArgsMinusChildren);
    
    //build a child for each collection of rows that belongs to the given parent
    //identify new parents using changes in FIRST col only
    Constructor<?> childCtor = ModelCtorUtil.getConstructor(fChildClass, fNumColsForChildList);
    Id parentId = new Id(aRow.getString(FIRST_COLUMN));
    fLogger.finest("Building Child List for Parent Id: " + parentId);
    Id currentId = null;
    List<Object> childList = new ArrayList<Object>();
    do {
      addChildToList(childCtor, childList, aRow);
      aRow.next();
      if ( ! aRow.isAfterLast() ){
        currentId = new Id(aRow.getString(FIRST_COLUMN));
        fLogger.finest("Updated current id: " + currentId);
      }
    }
    while( !aRow.isAfterLast() && currentId.equals(parentId) );
    //back up to PREVIOUS row, in preparation for next section/full model object, if any.
    aRow.previous();
    return buildFinalModelObjectWithChildList(parentCtor, ctorArgsMinusChildren, childList);
  }
  
  private int getNumColsForParent(ResultSet aRow) throws SQLException {
    int numCols = aRow.getMetaData().getColumnCount();
    int result = numCols - fNumColsForChildList;
    if ( result < 1){
      throw new RuntimeException("Number of columns available for Parent constructor < 1 : " + Util.quote(result));
    }
    return result;
  }

  private void addChildToList(Constructor<?> aChildCtor, List<Object> aChildren, ResultSet aRow) throws SQLException, ModelCtorException {
    ConvertParam convertParam = BuildImpl.forConvertParam();
    if( convertParam.isSupported(aChildCtor.getDeclaringClass())) {
      addBaseChildToList(aChildCtor.getDeclaringClass(), aChildren, aRow);
    }
    else {
      //regular model object, not a base object
      addRegularChildToList(aChildCtor, aChildren, aRow);
    }
  }
  
  /** Child object is a regular Model Object, whose ctor takes Base Objects supported by ConvertColumn. */
  private void addRegularChildToList(Constructor<?> aChildCtor, List<Object> aChildren, ResultSet aRow) throws SQLException, ModelCtorException {
    fLogger.fine("Building a regular child, and adding to the child list.");
    Class<?>[] targetTypes = aChildCtor.getParameterTypes();
    List<Object> argValues = new ArrayList<Object>();
    int columnIdx = getNumColsForParent(aRow) + 1; //ignore the leading columns belonging to parent
    //some outer joins will have all child columns null
    boolean atLeastOneHasContent = false;
    for (Class argType: targetTypes){
      Object value = fColumnToObject.convert(aRow, columnIdx, argType);
      if ( value != null ){
        atLeastOneHasContent = true;
      }
      argValues.add(value);
      ++columnIdx;
    }
    if( atLeastOneHasContent ) {
      fLogger.finest("At least one child column has content."); 
      Object child = ModelCtorUtil.buildModelObject(aChildCtor, argValues);
      aChildren.add(child);
    }
    else {
      //do not add anthing to the list of children
      fLogger.finest("ALL columns for Child object are empty/null - no child object to construct.");
    }
  }
  
  /** Child object is simply a Base Object, supported by ConvertColumn. */
  private <T> void addBaseChildToList(Class<T> aBaseClass, List<Object> aChildren, ResultSet aRow) throws SQLException {
    //Note : this extra branch was added when String was removed from supported Base Objects.
    fLogger.fine("Building a base object child, and adding to the child list.");
    int lastColumn = getNumColsInResultSet(aRow); 
    T baseObject = fColumnToObject.convert(aRow, lastColumn, aBaseClass); //possibly-null
    aChildren.add(baseObject);
  }
  
  private E buildFinalModelObjectWithChildList(Constructor<E> aParentCtor, List<Object> aArgValuesMinusChildren, List<Object> aChildren) throws ModelCtorException {
    fLogger.fine("Building complete Parent with Child List.");
    List<Object> allArgs = new ArrayList<Object>();
    allArgs.addAll(aArgValuesMinusChildren);
    allArgs.add(aChildren);
    fLogger.finest("Building complete Parent Model Object.");
    return ModelCtorUtil.buildModelObject(aParentCtor, allArgs);
  }
}
