#Copyright 2012 The Board of Regents of the University of Wisconsin System.
#Contributors: Jason Shao, James McCurdy, Enhai Xie, Adam G.W. Halstead, 
#Michael H. Whitney, Nathan DiPiazza, Minh Bui, Trey K. Sato and Yury V. Bukhman
#
#This file is part of GCAT.
#
#GCAT is free software: you can redistribute it and/or modify
#it under the terms of the GNU Lesser General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#GCAT 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
#GNU Lesser General Public License for more details.
#
#You should have received a copy of the GNU Lesser General Public License  
#along with GCAT.  If not, see <http://www.gnu.org/licenses/>.

########################################################################
#                                                                      #
#               Package-level documentation and imports                #
#                                                                      #
########################################################################
#'  GCAT: Growth Curve Analysis Tool
#'  
#'  Mathematical modeling and parameter estimation of high volume microbial growth data.
#'  
#'  @details
#'  GCAT input is in .csv format.  GCAT analysis is accessed using \code{\link{gcat.analysis.main}}
#'  
#'  GCAT utilizes the \code{\link[stats]{nls}} function in the R stats package to fit logistic, Gompertz and Richards models to growth curve 
#'  data. Best model is selected automatically.  Alternatively, the user may choose LOESS local regression fits, implemented using
#'  \code{\link[stats]{loess}} function in the R stats package  
#'  
#'  Internally, the data are stored in an array of \linkS4class{well} objects
#'  
#'  @import pheatmap gplots methods
#'  @docType package
#'  @name GCAT
NULL

# Initialization
PLATE.LETTERS = paste(rep(c("", LETTERS), each = 26), rep(LETTERS, 26), sep="")
global.version.number = packageDescription(pkg="GCAT")$Version 

########################################################################
#                                                                      #
# Top-level functions for analysis of screening data from .csv files.  #
#                                                                      #
########################################################################
# This functions is called directly by the user interface.
# They in turn call the main function <gcat.fit.main> (below) multiple times for each data file provided in <file.list>.  

# Arguments:
# file.list - a list of full paths to .csv files. all files must be in the same format (see <single.plate>)
# single.plate - are the file in the single plate (wide) format vs. the multi-plate (long) format?
# layout.file - (optional) provide full path to a layout file with strain and media definitions (applies to all files in list)

# out.dir - name a directory to output the table of curve parameters to (defaults to working directory) 
# graphic.dir - name a directory to output the images of the fitted curves to (defaults to subdirectory "pics" of <out.dir> above)


# add.constant- should be a numeric constant that will be added to each curve before the log transform (defaults to 1) 
# blank.value - user can enter a blank OD measurement for uninoculated wells. if NULL, defaults to the value of the first OD measurement of each well. 
# start.index - which timepoint should be used as the first one after inoculation (defaults to the 2th one)
# growth.cutoff - minimum threshold for curve growth. 
# points.to.remove - a list of numbers referring to troublesome points that should be removed across all wells.
# remove.jumps - should the slope checking function be on the lookout for large jumps in OD?
  
# silent - should messages be returned to the console?
# verbose - should sub-functions return messages to console? (when I say verbose, I mean it!)

# Returns:
# if <return.fit> = F (default), avector of full paths to all the files generated by the function.
# otherwise, the fitted array of well objects. 

# Use this function to analyze any set of .csv files using the same plate layout info. 

#' Analyze screening growth data from the given .csv files.
#' 
#' Top-level GCAT function 
#' 
#' @param file.list A list of full paths to .csv files. all files must be in the same format (see <single.plate>)
#' @param single.plate The file in the single plate (wide) format vs. the multi-plate (long) format?
#' @param layout.file Full path to a layout file with strain and media definitions (applies to all files in list)
#' @param out.dir A directory to output the table of curve parameters to (defaults to working directory) 
#' @param graphic.dir A directory to output the images of the fitted curves to (defaults to subdirectory "pics" of <out.dir> above)
#' @param use.linear.param Whether to use linear parameters or not?
#' @param use.loess Whether to use LOESS model or not?
#' @param smooth.param Smoothing parameter for LOESS model.
#' @param add.constant A numeric constant that will be added to each curve before the log transform (defaults to 1) 
#' @param blank.value User can enter a blank OD measurement for uninoculated wells. if NULL, defaults to the value of the first OD measurement of each well. 
#' @param start.index Which timepoint should be used as the first one after inoculation (defaults to the 2th one)
#' @param growth.cutoff Minimum threshold for curve growth. 
#' @param points.to.remove A list of numbers referring to troublesome points that should be removed across all wells.
#' @param remove.jumps Should the slope checking function be on the lookout for large jumps in OD?
#' @param time.input The time setting in which the current system is running?
#' @param plate.nrow The number of rows in a plate.
#' @param plate.ncol The number of columns in a plate.
#' @param input.skip.lines If specified, this number of lines shall be skipped from the top when reading the input file with read.csv 
#' @param multi.column.headers The headers of the result tabular data when analyzing multiple plates at once.
#' @param single.column.headers The headers of the result tebaular data when analyzaing a single plate.
#' @param layout.sheet.headers The headers of the layout file?
#' @param silent Shoulde messages be returned to the console?
#' @param verbose Should sub-functions return messages to console? (when I say verbose, I mean it!)
#' @param overview.jpgs Should GCAT enable an overview image?
#' @param return.fit Whether should a fit well object is returned or not.
#' @param lagRange The heatmap specific range for lag time.
#' @param totalRange The heatmap specific range for the achieved growth.
#' @param specRange The heatmap specific range for spec growth rate.
#' 
#' @return Depending on return.fit setting, an array of fitted well objects or a list of output files
#' 
#' @export
gcat.analysis.main = function(file.list, single.plate, layout.file = NULL,   
  out.dir = getwd(), graphic.dir = paste(out.dir, "/pics", sep = ""), 
  add.constant = 0, blank.value = NULL, start.index = 2, growth.cutoff = 0.05,
  use.linear.param = F, use.loess = F, smooth.param=0.1,
  lagRange = NA, totalRange = NA, specRange = NA,
  points.to.remove = 0, remove.jumps = F, time.input = NA,
  plate.nrow = 8, plate.ncol = 12, input.skip.lines = 0,
  multi.column.headers = c("Plate.ID", "Well", "OD", "Time"), single.column.headers = c("","A1"), 
  layout.sheet.headers = c("Strain", "Media Definition"),
  silent = T, verbose = F, return.fit = F, overview.jpgs = T){
  
  #  Capture the starting environment for debugging
  main.envir = c(as.list(environment()))

    # MB: Not the best solution.
    if (is.na(time.input)) {
      if (single.plate)
        time.input = 1/3600
      else
        exception("", "time.input is NA.")
    }
  
    # MB: Now add.constant will always be 0.
    # No need to check.
    #if (add.constant < 0)
    #  exception("", "The constant r should not be negative.")
    # End prototyping temporary solution.
    
    #  YB: seem to need this to avoid spurious discarding of some wells in example multiplate dataset: Trac ticket 1780.
    #  however, this causes an error in Rails
    if(length(points.to.remove)==0) points.to.remove = 0
    
    upload.timestamp = strftime(Sys.time(), format="%Y-%m-%d %H:%M:%S") # Get a timestamp for the time of upload.  
    fitted.well.array.master = list()
    source.file.list = c()
  
    dim(fitted.well.array.master) = c(plate.nrow,plate.ncol,0)
    dimnames(fitted.well.array.master) = list(PLATE.LETTERS[1:plate.nrow], 1:plate.ncol, c())
    
    for(file.name in file.list){
      
      # Call <gcat.fit.main> on the file with single plate options
    	fitted.well.array = try(gcat.fit.main(file.name = file.name, load.type = "csv",   
          single.plate = single.plate, layout.file = layout.file, start.index = start.index, 
          time.input = time.input, add.constant = add.constant, blank.value = blank.value, 
          growth.cutoff = growth.cutoff, points.to.remove = points.to.remove, remove.jumps = remove.jumps,
    	    use.linear.param=use.linear.param, use.loess=use.loess, smooth.param=smooth.param,
          plate.nrow = plate.nrow, plate.ncol = plate.ncol, multi.column.headers = multi.column.headers, 
          single.column.headers = single.column.headers, layout.sheet.headers = layout.sheet.headers,
          input.skip.lines = input.skip.lines, silent = silent, verbose = verbose), silent = T)
      
      # Return error message if the function fails.
      if(class(fitted.well.array) == "try-error")
        return(as.character(fitted.well.array))  
    }
      
    # Add fitted well array onto existing fitted wells
    fitted.well.array.master = gcat.append.arrays(fitted.well.array.master, fitted.well.array, plate.ncol, plate.nrow)
    
    # Remove the "processed_" tag from file names and add to the list of source files.
    source.file.list = c(source.file.list, basename(paste(strsplit(file.name, "processed_")[[1]],collapse="/")))        

    out.files = try(gcat.output.main(fitted.well.array.master, out.prefix = "output", 
          source.file.list = source.file.list, upload.timestamp = upload.timestamp,   
          growth.cutoff = growth.cutoff, add.constant = add.constant, blank.value = blank.value, start.index = start.index, 
          points.to.remove = points.to.remove, remove.jumps = remove.jumps, 
          lagRange = lagRange, specRange = specRange, totalRange = totalRange,
          out.dir = out.dir, graphic.dir = graphic.dir, overview.jpgs=overview.jpgs,
          use.linear.param=use.linear.param, use.loess=use.loess, plate.ncol = plate.ncol, plate.nrow = plate.nrow,
          silent = silent, main.envir = main.envir), silent = T)
    
    # Return file list or error message otherwise return "successful analysis" message (?)

    # file.list = c("Data was successfully analyzed.", file.list) # <--- yet to be implemented. causes errors downstream right now   
    if(class(out.files) == "try-error") return(as.character(out.files)) 
    
    if(return.fit) return(fitted.well.array.master)
      else return(out.files)  
}

########################################################################
#                                                                      #
#   Main function for analysis of screening data from input tables.    #
#                                                                      #
########################################################################
# This is the main function that handles all the analyses for files in both single and multiple plate formats. 
# It is called by the top level function <analysis.main> 
#
# It then calls the following functions on each member of the array: 
#    - curve normalization and standardization: <gcat.start.times>, <remove.points>, <normalize.ODs>, <transform.ODs>, 
#    - curve shape analysis before model fitting: <fill.slopes>, <check.curve>, <classify.curve>
#    - to fit a nonlinear model to the growth data: <fit.model>
# Finally, it returns the fitted array of well objects.

#' Main analysis function for GCAT 
#' 
#' This is the main function that handles all the analyses for data files in both single and multiple plate formats. 
#' It is called by the top level function \code{gcat.analysis.main} along with \code{gcat.output.main}.
#' 
#' @param file.name Complete path and file name of a comma-separated values (.csv) file containing growth curve data 
#' in the multiple-plate (long) format.       
#' @param input.data A list of tables representing input files read with \code{read.table}. Used to save time in cases
#' of running multiple analyses on the same dataset. If used, the function will ignore \code{file.name} entirely.
#' @param load.type .csv by default. 
#' @param layout.file Specifies the location of a layout file containing identifying information.
#' @param single.plate Whether the GCAT is analyzing a single plate or not.
#' @param blank.value Blank OD measurement for uninoculated wells. By default(NULL), the value of the first OD
#'measurement in each well is used.
#' @param start.index Which timepoint should be used as the first one after inoculation?
#' @param time.input Either a character describing the format used to convert timestamps in the input to numbers
#' representing number of seconds (see \code{strptime}), or a factor to divide entries in the Time column by to get the 
#' numbers of hours.
#' @param normalize.method Describes the method used by \code{normalize.ODs} to normalize cell density values using blank reads.
#' @param add.constant A value for r in the log(OD + r) transformation.
#' @param use.log Should the analysis use log on all values.
#' @param points.to.remove A vector of integers specifying which timepoints should be removed across all wells. 
#' By default(0) none are marked for removal.
#' @param use.linear.param Should the linear parameter be used or not.
#' @param use.loess Should the loess model be used or not.
#' @param smooth.param If loess model is used, this parameter define the smoothing parameter for the loess model.
#' @param fall.cutoff A cutoff used by \code{check.slopes} to decide on thresholds for jumps and tanking.
#' @param growth.cutoff A threshold used by check.growth to decide whether a well displays growth.
#' @param remove.jumps Should jumps in OD detected by the subfunction \code{check.slopes}?
#' @param plate.nrow The number of rows in the input files.
#' @param plate.ncol The number of columns in the input files.
#' @param input.skip.lines If specified, this number of lines shall be skipped from the top when reading the input file with read.csv 
#' @param multi.column.headers The headers of the column when analyzing multiple plates.
#' @param single.column.headers The headers of the column when analyzing a single plate.
#' @param layout.sheet.headers The headers of the layout file.
#' @param growth.model What growth model should be used?
#' @param backup.growth.model If the main growth model fails, the back up model will be used.
#' @param silent Surpress all messages.
#' @param verbose Display all messages when analyzing each well.
#' 
#' @return An array of well objects 
gcat.fit.main = function(file.name, input.data = NULL, load.type = "csv", layout.file = NULL, 
  single.plate = F, blank.value = NULL, start.index = 2, time.input = NA,
  normalize.method = "default", add.constant = 1, use.log = T, points.to.remove = 0,
  use.linear.param=F, use.loess=F, smooth.param=0.1,
  fall.cutoff = -0.0025, growth.cutoff = 0.05, remove.jumps = F,  
  plate.nrow = 8, plate.ncol = 12, input.skip.lines = 0,
  multi.column.headers = c("Plate.ID", "Well", "OD", "Time"), single.column.headers = c("","A1"), 
  layout.sheet.headers = c("Strain", "Media Definition"),
  growth.model = NA, backup.growth.model = NA, 
  silent = F, verbose = F){
  
  # Explanation of arguments:
  
  # ---File Handling---
  #   file.name - full path to an excel spreadsheet, .csv or tab-delimited text file, in either the single or multiple-plate format
  #   input.data - use pre-loaded data set (output from <gcat.load.data> function only). will override <file.name> if not NULL 
  #   load.type - supports "csv." 
  #   layout.file - full path to a file containing the plate layout in the same format as <file.name>. will not be used if <load.type> is "xlsx"

  # ---Input file format---
  #   single.plate - true denotes data in single-plate format, i.e. simple OD output. false denotes multiple-plate robotic screening output.  
  #      note: reading directly from excel to R results in timestamps being converted to days.

  # ---Normalization and Transforms---
  #  blank.value - user can enter a blank OD measurement for uninoculated wells. if NULL, defaults to the value of the first OD measurement of each well. 
  #  start.index - which timepoint should be used as the first one after inoculation (defaults to the 2th one)
  #  normalize.method - how should each growth curve be normalized? allowed values are:
  #     "first": subtracts the first OD, assumed to be the blank, from all ODs
  #     "none": does nothing, assumes no blank. highly recommend log(OD+1) transform in this case.
  #     "average.first": forces all filled wells on each plate to match the average value at <start.index> (after subtracting the first OD) 
  #  add.constant - a numeric constant that will be added to each curve before the log transform (default is 0)
  #  use.log - should a log transform be applied to the data after normalization? 
  #  points.to.remove - a list of numbers referring to troublesome points that should be removed across all wells.
  
  # ---Pre-fitting processing---
  #  fall.cutoff - a cutoff value for determining whether OD falls significantly between two timepoints. see <check.slopes> in prefit.processing.R for details. 
  #  growth.cutoff - a cutoff value for determining whether a well contains a successfully growing culture or not.
  #  remove.jumps - should the slope checking function be on the lookout for large jumps in OD?
  
  # ---Model fitting---
  #  model - which parametrized growth model to use? can be richards, gompertz, or logistic. models are defined as objects of class model, see "model.class.R"
  #  backup.model - which model should be used if fitting using <model> fails? should ideally be simpler than the main model (less parameters)
  
  # ---Miscellanous input/output preferences---
  #  silent - should messages be returned to the console?
  #  verbose - should sub-functions return messages to console? (when I say verbose, I mean it!) 
  #  unlog - should exported graphics be transformed back to the OD scale? 
  #  return.fit - should the function return an array of wells? if not, it will return a list of generated files.  
  
  	
  ########################################################################
  #    Read from .csv file                                               #
  ########################################################################
  #
  # The functions used here are found in table2well.R
  
  if(!silent) cat("\nReading input files...")
    # Read from .csv or tab-delimited text file using <gcat.load.data> (in load.R)
    #   if <layout.file> is provided, it will be used here.  
    
    plate.layout = NULL          
    # Read layout file if it is specified. 
    if(!is.null(layout.file)){
      if(load.type=="csv") plate.layout = read.csv(layout.file,header=T,stringsAsFactors=F)
      else plate.layout = read.table(layout.file,header=T,sep="\t",stringsAsFactors=F)                                      
      if(!silent) cat("\n\tAdded plate layout information from", layout.file, "\n")
    }
      
    # Load the data    
		well.array = try(gcat.load.data(file.name = file.name, input.data = input.data, 
                      plate.layout = plate.layout, plate.nrow = plate.nrow, plate.ncol = plate.ncol, 
                      input.skip.lines = input.skip.lines, multi.column.headers = multi.column.headers, 
                      single.column.headers = single.column.headers, layout.sheet.headers = layout.sheet.headers,
                      blank.value = blank.value, start.index = start.index, single.plate = single.plate, 
                      load.type = load.type, silent=silent),silent=silent)

   # Return an error if there is a problem with file loading. 
    if (class(well.array) == "try-error")
    	stop("Error in <gcat.load.data>: ", well.array)
		

  # !---At this point, <well.array> is an array of well objects, each containing raw data and media/strain information if provided--- 
  
  # Attempt to apply time formatting to all wells in array 
  well.array = try(aapply(well.array, gcat.start.times, start.index = start.index, time.input = time.input),silent=silent)
  
      # Return an error if there is a problem with time formatting
      if (class(well.array) == "try-error")
      	stop("Error in <gcat.start.times>: ", well.array)
  	
  ########################################################################
  #    Perform  normalization and transformation of raw data             #
  ########################################################################                                           
  #
  # The functions used here are found in normalize.and.transform.R 
  
  if(!silent) cat("\nProcessing raw data...")
  
  # Set all timepoints to active for now using "points.to.remove=0" argument with <remove.points> 
  #   adds an extra column to the "well.array" slot of each well specifying which points to remove when data is retrieved from the well
  well.array = aapply(well.array, remove.points, points = 0)
  
  # Normalize ODs using specified method and adding a constant if desired.  
  #   sets the "norm" slot of each well to a value to be subtracted from OD values whenever data is retrieved from the well
  well.array = try(normalize.ODs(well.array, normalize.method = normalize.method, 
    start.index = start.index, blank.value = blank.value, add.constant = add.constant),silent=silent)
  
      # Return an error if there is a problem with normalization
      if (class(well.array) == "try-error")
      	stop("Error in <normalize.ODs>: ", well.array)
  #well.array = try(subtract.blank(well.array, blank.value), silent = silent)
  
  # Transform ODs on the logarithmic scale, regardless of whether <use.log> is true 
  #   an extra column of log-transformed values is added to the "well.array" slot of each well 
  #   the "use.log" slot of each well is set instead to determine whether the transformed values will be returned when data is retrieved from the well.
  well.array = try(aapply(well.array, transform.ODs, start.index = start.index, blank.value = blank.value, use.log = use.log, constant.added = add.constant),silent=silent)
  
      # Return an error if there is a problem with transformation
      if (class(well.array) == "try-error")
      	stop("Error in <transform.ODs>: ", well.array)
  
  # Remove specified timepoints across wells (use "points.to.remove=NULL" if no points to remove) 
  well.array = try(aapply(well.array, remove.points, points = points.to.remove),silent=silent)
  
  # Return an error if there is a problem with point removal
  if (class(well.array) == "try-error")
    stop("Error in <remove.points>: ", well.array)   
  
  
    
  ########################################################################
  #    Pre-fitting data processing (analysis of curve shapes)            #
  ########################################################################                                           
  #
  # The functions used here are found in slope.analysis.R
                               
  # Estimate slope at each timepoint 
  #  add a column to the "well.array" slot of each well with the local slope at each timepoint 
  
  well.array = try(aapply(well.array, calculate.slopes, silent=!verbose),silent=silent)
  
      # Return an error if there is a problem with slope calculation
      if (class(well.array) == "try-error")
      	stop("Error in <calculate.slopes>: ", well.array)
  
  # Check slopes for tanking and/or jumping behavior
  #  fills the "curve.par" slot of each well with <tanking.start>, denoting the timepoint at which tanking starts (if none, value is NA) 
  #  uses <remove.points> to remove all points after <tanking.start>
  #  It will also fill the "jump.error" slot with a status message, and try to use an automated process to remove the 
  #     erroneous points if <remove.jumps> is true (default false). 
    
  well.array = try(aapply(well.array, check.slopes, fall.cutoff = fall.cutoff, remove.jumps = remove.jumps, silent=!verbose, draw = F),silent=silent)
 
      # Return an error if there is a problem with slope analysis
      if (class(well.array) == "try-error")
      	stop("Error in <check.slopes>: ", well.array)
      	
      	
  # Check curves for growth above cutoff
  #   fills the "curve.par" slot of each well with <no.growth>, denoting whether the well has no detectable growth. 
  well.array = try(aapply(well.array, check.growth, growth.cutoff = growth.cutoff, start.index = start.index),silent=silent)
  
      # Return an error if there is a problem with growth.check
      if (class(well.array) == "try-error")
      	stop("Error in <check.growth>: ", well.array)
      	
  ########################################################################
  #           Fit parameterized models to data                           #
  ########################################################################                                           
  #
  # The functions used here are found in fit.model.R    
                                 
  # Fit each well with the selected model and attempt to catch failed fittings with the backup model 
  #   skips wells designated as <no.growth> above
  #   fills the "fit.info" slot of each well with "success," "failed," or "skipped"
  #   if fit was successful:
  #     fills the "equation" and "model.name" slots with the relevant info for the successful model 
  #     fills the "fit.par" slot with fitted parameters if fit is successful   
  
  if(!silent) cat("\nFitting models to data...")
  well.array = aapply(well.array, fit.model, growth.model=growth.model,
                      backup.growth.model = backup.growth.model, use.linear.param=use.linear.param,
                      use.loess=use.loess, smooth.param=smooth.param, silent=!verbose)
  
      # Return an error if there is a problem with model fitting
      if (class(well.array) == "try-error")
      	stop("Error in <fit.model>: ", well.array)
        
  if(!silent) cat("\ndone!\n")     
  return(well.array)     
  }    
  
  
########################################################################
#                                                                      #
#   Output function for generating files from fitted data.             #
#                                                                      #
########################################################################  
#' Output function for generating files from fitted data.
#' 
#' Handles files and directories, calls \code{table.out}, \code{plate.overview} and \code{view.fit} 
#' to generate output tables and graphics.
#' 
#' @param fitted.well.array A list of fitted well objects.
#' @param out.prefix Prefix that is in the name of output files.
#' @param blank.value User can enter a blank OD measurement for uninoculated wells. 
#' If NULL, defaults to the value of the first OD measurement of each well.
#' @param start.index Which timepoint should be used as the first one after inoculation (defaults to the 2th one)
#' @param growth.cutoff Minimum threshold for curve growth.
#' @param points.to.remove A list of numbers referring to troublesome points that should be removed across all wells.
#' @param remove.jumps Should the slope checking function be on the lookout for large jumps in OD?
#' @param out.dir name a directory to output the table of curve parameters to (defaults to working directory) 
#' @param graphic.dir name a directory to output the images of the fitted curves to 
#' (defaults to subdirectory "pics" of <out.dir> above)
#' @param overview.jpgs should jpgs be generated for each plate with the overview graphic? 
#' This is for backwards compatibility with the old web server.  
#' @param silent should messages be returned to the console?
#' @param unlog should exported graphics be transformed back to the OD scale? 
#' @param source.file.list A list of the source files' names.
#' @param upload.timestamp The time format indicated by the user.
#' @param add.constant used to readjust for the constant added during the log transform when plotting ODs.
#' @param use.linear.param linear parameter is used or not?
#' @param use.loess Is LOESS model going to be used?
#' @param plate.nrow The number of rows for a plate.
#' @param plate.ncol The number of columns for a plate
#' @param lagRange The heatmap specific range for lag time.
#' @param totalRange The heatmap specific range for the achieved growth.
#' @param specRange The heatmap specific range for spec growth rate.
#' @param main.envir starting environment of gcat.analysis.main(), captured as a list, printed out for debugging
#' 
#' @return A list of output files if success.
gcat.output.main = function(fitted.well.array, out.prefix = "", source.file.list, upload.timestamp = NULL,   
  add.constant, blank.value, start.index, growth.cutoff, points.to.remove, remove.jumps, 
  out.dir = getwd(), graphic.dir = paste(out.dir,"/pics",sep = ""), overview.jpgs = T,
  use.linear.param=F, use.loess=F, lagRange = NA, totalRange = NA, specRange = NA,
  plate.nrow = 8, plate.ncol = 12, unlog = F, silent = T, main.envir){     

  # Prepare timestamp for addition to output file names. 
  filename.timestamp = strftime(upload.timestamp, format="_%Y-%m-%d_%H.%M.%S")
    	
  ########################################################################
  #           Prepare to write to output files                           #
  ########################################################################                                            	
	 if(is.null(blank.value)) blank.value = "First timepoint in well"
	 
  if(!silent) cat("\nFinding/creating new output directories...")
   
  old.wd = getwd()  
  # Create output directory if it doesn't exist
  if(class(try(setwd(out.dir), silent = T)) == "try-error"){
  	if(!silent) cat("\ncreating new output directory")
  	if (class(try(dir.create(out.dir))) == "try-error")
  		stop("Error creating new output directory!")
  	}
  
  # Create graphics directory if it doesn't exist
  if(class(try(setwd(graphic.dir), silent = T)) == "try-error"){
  	if(!silent) cat("\ncreating new graphics directory")
  	if (class(try(dir.create(graphic.dir))) == "try-error")
  		stop("Error creating new graphics directory!")
  	}
  	
  ########################################################################
  #    Populate a data table with fit results and write to file          #
  ######################################################################## 
  #                                         
  # The functions used here are found in table.output.R 
                                 
  # Creates a table with a row for each well and a column for each of various identifiers and fitted and calculated parameters. 
  
  if(!silent) cat("\nPopulating data table...")
  table.fit = try(table.out(fitted.well.array, filename.timestamp=filename.timestamp,use.linear.param=use.linear.param, use.loess=use.loess, constant.added=add.constant))
  
      # Return an error if there is a problem with returning the table
      if (class(fitted.well.array) == "try-error")
      	stop("Error in <table.out>: ", fitted.well.array)
      	
 	
  # Set working directory to <out.dir>
	if (class(try(setwd(out.dir))) == "try-error")
		stop("Error setting directory for table output")
		
	# Write output table to file in <out.dir> 
  table.filename = paste(out.dir, "/", out.prefix, "_gcat.fit", filename.timestamp, ".txt", sep = "")
  if (class(try(write.table(table.fit, table.filename, sep = "\t", row.names = F))) == "try-error")
		stop("Error writing tabular output")
		
 	# ---If successfully written, add postscript and start a list of generated files.	
  generated.files = table.filename

  ########################################################################
  #    Write individual fit and overview graphics to file                #
  ######################################################################## 
  #                         
  # The functions used here are found in graphic.output.R
  
  if(!silent) cat("\nDrawing graphics...")
  
  # Set working directory to <graphic.dir>
	if (class(try(setwd(graphic.dir))) == "try-error")
		stop("Error setting directory for graphic output")
	# Use function <pdf.by.plate> to write fit graphics to file. 
  
	graphic.files = try(pdf.by.plate(fitted.well.array, out.prefix=out.prefix, upload.timestamp = upload.timestamp, 
    unlog=unlog,constant.added=add.constant,overview.jpgs=overview.jpgs, lagRange = lagRange, specRange = specRange, totalRange = totalRange,
    plate.ncol = plate.ncol, plate.nrow = plate.nrow),silent=silent)
  
  if (class(graphic.files) == "try-error")
		stop("Error in <pdf.by.plate>: ", graphic.files)
 
  # If successfully written, add to the list of generated files.	
  generated.files = c(generated.files, graphic.files)
  
  ########################################################################
  #    Add a postscript to the output table with legend and file info.   #
  ######################################################################## 
  #                           	
  sink(table.filename, append = T)
  analysis.timestamp = strftime(Sys.time(), format="%Y-%m-%d %H:%M:%S")
  cat("\n# Raw OD values are adjusted and log-transformed before fitting a growth curve as follows: log.OD = log(OD - blank + const) where blank is OD of blank medium and const is specified by the user (1 by default)",
      "\n# Values are reported on the above 'log.OD' scale unless otherwise specified.",
      "\n# .SE columns report standard errors of those values that are estimated directly as parameters of global sigmoid models.",
      "\n# .OD columns report values back-transformed to the linear 'OD - blank' scale.",
      "\n")
  
  cat("\n# -- Explanation of columns --",
      "\n#    - model: Name of the model the well was successfully fit with (if any)",
      "\n#    - lag.time: Lag time estimate inferred from the fitted model",
      "\n#    - inflection.time: inflection time point of the growth curve when drawn on the log scale",
      "\n#    - max.spec.growth.rate: maximum specific growth rate estimate inferred from the fitted model.  Estimated as the first derivative of the growth curve at inflection time point", 
      "\n#    - baseline: growth curve baseline.  Global sigmoid model: baseline is parameter 'b' of the model.  LOESS: baseline is the same as the lowest predicted log.OD value",
      "\n#    - amplitude: difference between upper plateau and baseline values.  Global sigmoid model: amplitude is parameter 'A' of the model.  LOESS: amplitude = max.log.OD - min.log.OD",
      "\n#    - plateau: upper asymptote value of the fitted model. Global sigmoid model: plateau = b + A.  LOESS: plateau = max.log.OD",
      "\n#    - inoc.log.OD: log.OD value at inoculation.  Estimated value from the fitted model is used, rather than the actual measurement", 
      "\n#    - max.log.OD: maximal log.OD value reached during the experiment.  Estimated value from the fitted model is used rather than the actual measurement",
      "\n#    - projected.growth: maximal projected growth over inoculation value.  Global sigmoid model: projected.growth = plateau - inoc.log.OD. LOESS: not reported",
      "\n#    - achieved.growth: maximal growth over inoculation value actually achieved during the experiment.  achieved.growth = max.log.OD - inoc.log.OD",
      "\n#    - shape.par: shape parameter of the Richard equation",
      "\n#    - R.squared: goodness of fit metric.  Also known as coefficient of determination.  R.squared is usually between 0 and 1.  A value close to 1 indicates good fit.",
      "\n#    - RSS: residual sum of squares.  Another goodness of fit metric.  Smaller values indicate better fits.",
      "\n#    - empty: (Well indicator)", 
      "\n#              - an 'E' indicates that the well was empty and no growth was detected. ",
      "\n#              - an 'I' indicates that the well was inoculated and growth was detected above the threshold. ",
      "\n#              - an 'E*' indicates that the well was empty and growth was detected (possible contamination). ",
      "\n#              - an '!' indicates that the well was inoculated and no growth was detected. ",
      "\n#    - asymp.not.reached: shows \"L\" if the bottom asymptote (baseline) was not reached and \"U\" if the upper asymptote (plateau) was not reached.",
      "\n#    - tank: (Tanking indicator) If a number is present then the growth trend was determined to tank at that timepoint index.", 
      "\n#    - other: Additional flag column.  Displays information about whether jumps in OD were detected and what was done about them.",
      "\n#    - pdf.file and page.no: location of the figure for this well in the output .pdf files."
      )
  
	 # Analysis information 
   
   cat("\n#\n# -- Source file information--",
      "\n# ",  paste(source.file.list, collapse = "\n# "), 
      "\n#  analyzed using GCAT v", global.version.number,  
      "\n#  request sent: ", upload.timestamp, 
      "\n#  completed: ", analysis.timestamp, 
      "\n#\n# -- Parameters used in current analysis --",
      "\n#    - Constant added to log(OD + n) transformation:", add.constant,
      "\n#    - Blank OD value: ", blank.value,
      "\n#    - Index of inoculation timepoint", start.index,
      "\n#    - Minimum growth threshold:", growth.cutoff, 
      "\n#    - Removed points:", paste(points.to.remove, collapse = " "),
      "\n#    - Jump detection:", remove.jumps
   )
  
  # gcat.analysis.main() starting environment
  cat("\n#\n# -- gcat.analysis.main() starting environment --\n#")
  print(main.envir)
  
  #  Done with text file output
  sink()
  
  ########################################################################
  #                 Return values to R                                   #
  ######################################################################## 
  #  

  if(!silent) cat("\ndone!")                    
  setwd(old.wd)
  # Return list of generated files
	return(generated.files)
  }
  
