#!/usr/bin/env AFNI_Batch_R

first.in.path <- function(file) {
   ff <- paste(strsplit(Sys.getenv('PATH'),':')[[1]],'/', file, sep='')
   ff<-ff[lapply(ff,file.exists)==TRUE];
  #cat('Using ', ff[1],'\n');
   return(gsub('//','/',ff[1], fixed=TRUE))
}
source(first.in.path('AFNIio.R'))
ExecName <- '3dLMEr'
# Global variables
tolL <- 1e-16 # bottom tolerance for avoiding division by 0 and for avoiding analyzing data with most 0's

#################################################################################
##################### Begin 3dLMEr Input functions ################################
#################################################################################

#The help function for 3dLMEr batch (AFNI-style script mode)
help.LME.opts <- function (params, alpha = TRUE, itspace='   ', adieu=FALSE) {

   intro <-
'
             ================== Welcome to 3dLMEr ==================
       Program for Voxelwise Linear Mixed-Effects (LME) Analysis
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Version 1.2.0, March 25, 2026
Author: Gang Chen (gangchen@mail.nih.gov)
SSCC/NIMH, National Institutes of Health, Bethesda MD 20892, USA
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Introduction
------

 ### Overview of 3dLMEr  

 Linear Mixed-Effects (LME) analysis follows a traditional framework that
 distinguishes between two types of effects:  
 - Fixed effects capture population-level components.  
 - Random effects account for lower-level variability, such as subjects, families,
   or scanning sites.  
 
 3dLMEr is an advanced and more flexible successor to 3dLME. It enhances model
 specification, particularly in handling random-effects components. While 3dLME was
 built on the nlme R package, 3dLMEr leverages lme4, allowing for greater flexibility.
 Additionally, statistical values for main effects and interactions are approximated
 using Satterthwaite’s method.  
 
 ### Key Differences Between 3dLMEr and 3dLME  
 
 1. Random-effects specification:  
    - In 3dLMEr, random effects are fully integrated into the model formula (via `-model ...`).  
    - The `-ranEff` option from 3dLME is no longer needed.  
    - Users must explicitly specify the model structure. See this blogpost for details:  
      How to Specify Individual-Level Random Effects in Hierarchical Modeling
      https://discuss.afni.nimh.nih.gov/t/how-to-specify-individual-level-random-effects-in-hierarchical-modeling/6462  
 
 2. Simplified effect specification:  
    - Labels for simple and composite effects are now part of `-gltCode` and `-glfCode`,
 eliminating the need for `-gltLabel`.  
 
 3. Output format for statistical values:  
    - Main effects, interactions, and composite effects (generated automatically by 3dLMEr)
 are stored as chi-square statistics (with 2 degrees of freedom).  
    - Simple effects (specified by the user) are stored as Z-statistics.  
    - The fixed 2 degrees of freedom for chi-square statistics simplifies interpretation,
 as the Satterthwaite method produces varying degrees of freedom.  
 
 ### Citing 3dLMEr  
 
 If you use 3dLMEr in your analysis, cite:  
 
 - General LME approach:  
    Chen, G., Saad, Z.S., Britton, J.C., Pine, D.S., Cox, R.W. (2013).  
    Linear Mixed-Effects Modeling Approach to FMRI Group Analysis. *NeuroImage, 73*, 176-190.  
    [DOI: 10.1016/j.neuroimage.2013.01.047](http://dx.doi.org/10.1016/j.neuroimage.2013.01.047)  
 
 - Test-retest reliability using trial-level effect estimates (`-TRR` option):  
    Chen, G., Pine, D.S., Brotman, M.A., Smith, A.R., Cox, R.W., Haller, S.P. (2021).  
    Trial and error: A hierarchical modeling approach to test-retest reliability.  
    *NeuroImage, 245*, 118647.  
    [DOI: 10.1016/j.neuroimage.2021.118647](https://doi.org/10.1016/j.neuroimage.2021.118647)  
 
 ### Input & Output Formats  
 
 Supported input formats:  
 - AFNI  
 - NIfTI  
 - Surface (`niml.dset`)  
 - 1D text files  
 
 To match the output format to the input, append an appropriate suffix to `-prefix`
 (e.g., `.nii`, `.niml.dset`, or `.1D`).  
 
 ### Model Specification & Considerations  
 
 Explanatory variables:  
 3dLMEr supports:  
 - Categorical variables (e.g., between- and within-subject factors)  
 - Quantitative variables (e.g., age, behavioral measures)  
 
 User responsibility:  
 - The burden of specifying lower-level effects is on the user.  
 - For clarifications, refer to this FAQ: [Mixed Models FAQ]
 (https://bbolker.github.io/mixedmodels-misc/glmmFAQ.html).  
 
 Handling quantitative variables:  
 - Declare them explicitly using `-qVars`.  
 - Consider centering options:  
   - Global centering (across all subjects)  
   - Within-condition/group centering (depends on research context)  
   - More details on centering: https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/statistics/center.html  
 
 ### Example Scripts  
 
 Check out example scripts below that demonstrate different data structures. If one
 matches your study, use it as a template. More examples will be added over time—
 contributions are welcome!  
 
 ### Installation Requirements  
 
 Before running 3dLMEr, install the following R packages:  
 ```
 install.packages("lmerTest")
 install.packages("phia")
 install.packages("snow")
 ```
 Alternatively, use AFNI’s installer:  
 ```
 rPkgsInstall -pkgs "lmerTest,phia,snow"
 ```
 
 ### Running 3dLMEr  
 
 Once your script is ready, run it in the terminal:  
 
 ```
 nohup tcsh -x LME.txt &
 ```
 or, to save the output log:  
 
 ```
 nohup tcsh -x LME.txt > diary.txt &
 ```
 or, to display output live while saving it:  
 
 ```
 nohup tcsh -x LME.txt |& tee diary.txt &
 ```
 Saving logs allows you to review output later if issues arise.\n'

   ex1 <-
"Example 1 --- Simplest case: LME analysis for one group of subjects each of
  which has three effects associated with three emotions (pos, neg and neu),
  and the effects of interest are the comparisons among the three emotions
  at the population level (missing data allowed). This data structure is usually
  considered as one-way repeated-measures (or within-subject) ANOVA if no
  missing data occurred. The LME model is typically formulated with a random
  intercept in this case. With the option -bounds, values beyond [-2, 2] will
  be treated as outliers and considered as missing. If you want to set a range, 
  choose the bounds that make sense with your input data.

  **NOTE**: Using the -bounds option to remove outliers should be approached
    with caution due to its arbitrariness. A more principled alternative is 
    to use 3dGLMM with a Student's t-distribution.

-------------------------------------------------------------------------
    3dLMEr -prefix LME -jobs 12                                          \\
         -mask myMask+tlrc                                               \\
          -model  'emotion+(1|Subj)'                                     \\
          -SS_type 3                                                     \\
          -bounds  -2 2                                                  \\
          -gltCode mean     'emotion : 0.333*pos +0.333*neg + 0.333*neu' \\
          -gltCode pos      'emotion : 1*pos'                            \\
          -gltCode neg      'emotion : 1*neg'                            \\
          -gltCode neu      'emotion : 1*neu'                            \\
          -gltCode pos-neg  'emotion : 1*pos -1*neg'                     \\
          -gltCode pos-neu  'emotion : 1*pos -1*neu'                     \\
          -gltCode neg-neu  'emotion : 1*neg -1*neu'                     \\
          -gltCode em-eff1  'emotion : 0.5*pos +0.5*neg -1*neu'          \\
          -glfCode em-eff2  'emotion : 1*pos -1*neg & 1*pos -1*neu'      \\
          -dataTable                              \\
          Subj emotion  InputFile                 \\
          s1    pos     s1_pos+tlrc               \\
          s1    neg     s1_neg+tlrc               \\
          s1    neu     s1_neu+tlrc               \\
          s2    pos     s2_pos+tlrc               \\
          s2    neg     s2_neg+tlrc               \\
          s2    pos     s2_neu+tlrc               \\
          ...
          s20   pos     s20_pos+tlrc              \\
          s20   neg     s20_neg+tlrc              \\
          s20   neu     s20_neu+tlrc              \\
          ...

  **Note:** `3dLMEr` does not explicitly output the model intercept (overall mean).
  However, you can extract it using the `-gltCode` option, as shown in the script above:
  -gltCode mean     'emotion : 0.333*pos +0.333*neg +0.333*neu'
  \n"

   ex2 <-
"Example 2 --- LME analysis for one group of subjects each of  which has
  three effects associated with three emotions (pos, neg and neu), and the
  effects of interest are the comparisons among the three emotions  at the
  population level. In addition, reaction time (RT) is available per emotion
  from each subject. An LME model can be formulated to include both random
  intercept and random slope. Be careful about the centering issue about any 
  quantitative variable: you have to decide which makes more sense - global
  centering or within-condition (or within-group) centering?

  **NOTE**: Using the -bounds option to remove outliers should be approached 
    with caution due to its arbitrariness. A more principled alternative is
    to use 3dGLMM with a Student's t-distribution.

-------------------------------------------------------------------------
    3dLMEr -prefix LME -jobs 12                   \\
          -mask myMask+tlrc                       \\
          -model  'emotion*RT+(RT|Subj)'          \\
          -SS_type 3                              \\
          -bounds -2 2                            \\
          -qVars  'RT'                            \\
          -qVarCenters 0                          \\
          -gltCode pos      'emotion : 1*pos'                       \\
          -gltCode neg      'emotion : 1*neg'                       \\
          -gltCode neu      'emotion : 1*neu'                       \\
          -gltCode pos-neg  'emotion : 1*pos -1*neg'                \\
          -gltCode pos-neu  'emotion : 1*pos -1*neu'                \\
          -gltCode neg-neu  'emotion : 1*neg -1*neu'                \\
          -gltCode em-eff1  'emotion : 0.5*pos +0.5*neg -1*neu'     \\
          -glfCode em-eff2  'emotion : 1*pos -1*neg & 1*pos -1*neu' \\
          -dataTable                              \\
          Subj emotion  RT  InputFile             \\
          s1    pos     23   s1_pos+tlrc          \\
          s1    neg     34   s1_neg+tlrc          \\
          s1    neu     28   s1_neu+tlrc          \\
          s2    pos     31   s2_pos+tlrc          \\
          s2    neg     22   s2_neg+tlrc          \\
          s2    pos     29   s2_neu+tlrc          \\
          ... 		
          s20   pos     12   s20_pos+tlrc         \\
          s20   neg     20   s20_neg+tlrc         \\
          s20   neu     30   s20_neu+tlrc         \\
          ...
   \n"

   ex3 <-
"Example 3 --- LME analysis for one group of subjects each of which has three
  effects associated with three emotions (pos, neg and neu), and the effects
  of interest are the comparisons among the three emotions at the population
  level. As the data were acquired across 12 scanning sites, we set up an LME
  model with a crossed random-effects structure, one for cross-subjects and one
  for cross-sites variability.

  **NOTE**: Using the -bounds option to remove outliers should be approached 
    with caution due to its arbitrariness. A more principled alternative is
    to use 3dGLMM with a Student's t-distribution.

-------------------------------------------------------------------------
    3dLMEr -prefix LME -jobs 12                       \\
          -mask myMask+tlrc                           \\
          -model  'emotion+(1|Subj)+(1|site)'         \\
          -SS_type 3                                  \\
          -bounds -2 2                                \\
          -gltCode pos      'emotion : 1*pos'                       \\
          -gltCode neg      'emotion : 1*neg'                       \\
          -gltCode neu      'emotion : 1*neu'                       \\
          -gltCode pos-neg  'emotion : 1*pos -1*neg'                \\
          -gltCode pos-neu  'emotion : 1*pos -1*neu'                \\
          -gltCode neg-neu  'emotion : 1*neg -1*neu'                \\
          -gltCode em-eff1  'emotion : 0.5*pos +0.5*neg -1*neu'     \\
          -glfCode em-eff2  'emotion : 1*pos -1*neg & 1*pos -1*neu' \\
          -dataTable                                  \\
          Subj emotion  site  InputFile               \\
          s1    pos     site1   s1_pos+tlrc           \\
          s1    neg     site1   s1_neg+tlrc           \\
          s1    neu     site2   s1_neu+tlrc           \\
          s2    pos     site1   s2_pos+tlrc           \\
          s2    neg     site2   s2_neg+tlrc           \\
          s2    pos     site3   s2_neu+tlrc           \\
          ...
          s80   pos     site12  s80_pos+tlrc          \\
          s80   neg     site12  s80_neg+tlrc          \\
          s80   neu     site10  s80_neu+tlrc          \\
          ...
   \n"

   ex4 <-
"Example 4 --- LME analysis with a between-subject factor (group: two groups of
  subjects -- control, patient), two within-subject factros (emotion: 3 levels 
  -- pos, neg, neu; type: 2 levels -- face, word), one quantitative variable (age).

  **NOTE**: Using the -bounds option to remove outliers should be approached 
    with caution due to its arbitrariness. A more principled alternative is to
    use 3dGLMM with a Student's t-distribution.

-------------------------------------------------------------------------
    3dLMEr -prefix LME -jobs 12                                                           \\
          -mask myMask+tlrc                                                               \\
          -model  'group*emotion*type+age+(1|Subj)+(1|Subj:emotion)+(1|Subj:type)'        \\
          -SS_type 3                                                                      \\
          -bounds -2 2                                                                    \\
          -gltCode pat.pos      'group : 1*patient emotion : 1*pos'                       \\
          -gltCode pat.neg      'group : 1*patient emotion : 1*neg'                       \\
          -gltCode ctr.pos.age  'group : 1*control emotion : 1*pos age :'                 \\
          -dataTable                                              \\
          Subj  group    emotion  type  age  InputFile            \\
          s1    control   pos     face  35  s1_pos+tlrc           \\
          s1    control   neg     face  35  s1_neg+tlrc           \\
          s1    control   neu     face  35  s1_neu+tlrc           \\
          s2    control   pos     face  23  s2_pos+tlrc           \\
          s2    control   neg     face  23  s2_neg+tlrc           \\
          s2    control   pos     face  23  s2_neu+tlrc           \\
          ...         		
          s80   patient   pos     word  28  s80_pos+tlrc          \\
          s80   patient   neg     word  28  s80_neg+tlrc          \\
          s80   patient   neu     word  28  s80_neu+tlrc          \\
          ...
   \n"

   ex5 <-
"Example 5 --- Test-retest reliability. LME model can be adopted for test-
  retest reliability analysis if trial-level effect estimates (e.g., using
  option -stim_times_IM in 3dDeconvolve/3dREMLfit) are available from each
  subjects. The following script demonstrates a situation where each subject
  performed same two tasks across two sessions. The goal is to obtain the
  test-retest reliability at the whole-brain voxel level for the contrast
  between the two tasks with the test-retest reliability for the average 
  effect between the two tasks as a byproduct. 

  WARNING: numerical failures may occur, especially for a contrast between 
  two conditions. The failures manifest with a large portion of 0, 1 and -1
  values in the output. In that case, use the program TRR to conduct 
  region-level test-retest reliability analysis.

  **NOTE**: Using the -bounds option to remove outliers should be approached 
    with caution due to its arbitrariness. A more principled alternative is
    to use 3dGLMM with a Student's t-distribution.

-------------------------------------------------------------------------
     3dLMEr -prefix output -TRR -jobs 16 \
             -qVars 'cond'        \
             -bounds -2 2         \
             -model '0+sess+cond:sess+(0+sess|Subj)+(0+cond:sess|Subj)' \
             -dataTable @data.tbl

  With many trials per condition, it is recommended that the data table
  is saved as a separate file in pure text of long format with condition 
  (variable 'cond' in the script above) through dummy coding of -0.5 and
  0.5 with the option -qVars 'cond'. Code subject and session as factor
  labels with labels. Below is an example of the data table. There is no 
  need to add backslash at the end of each line. If sub-brick selector
  is used, do NOT use gzipped files (otherwise the file reading time would
  be too long) and do NOT add quotes around the square brackets [] for the
  sub-brick selector.
 
  Subj   sess cond      InputFile
  Subj1   s1  -0.5  Subj1s1c1_trial1.nii
  Subj1   s1  -0.5  Subj1s1c1_trial2.nii
  ...
  Subj1   s1  -0.5  Subj1s1c1_trial40.nii  
  Subj1   s1   0.5  Subj1s1c2_trial1.nii
  Subj1   s1   0.5  Subj1s1c2_trial2.nii
  ...
  Subj1   s1   0.5  Subj1s1c2_trial40.nii
  Subj1   s2  -0.5  Subj1s2c1_trial1.nii
  Subj1   s2  -0.5  Subj1s2c1_trial2.nii
  ...
  Subj1   s2  -0.5  Subj1s2c1_trial40.nii
  Subj1   s2   0.5  Subj1s2c2_trial1.nii
  Subj1   s2   0.5  Subj1s2c2_trial2.nii
  ...
  Subj1   s2   0.5  Subj1s2c2_trial40.nii
  ...
\n"

   parnames <- names(params)
   ss <- vector('character')
   if(alpha) {
       parnames <- sort(parnames)
       ss <- paste('Options in alphabetical order:\n',
                  '------------------------------\n', sep='')
   } else ss <- paste('Options:\n', '--------\n', sep='')
   for(ii in 1:length(parnames)) {
      op <- params[parnames[ii]][[1]]
      if(!is.null(op$help)) ss <- c(ss , paste(itspace, op$help, sep='')) else
         ss <- c(ss, paste(itspace, parnames[ii], '(no help available)\n', sep=''))
   }
   ss <- paste(ss, sep='\n')
   cat(intro, ex1, ex2, ex3, ex4, ex5, ss, sep='\n')

   if (adieu) exit.AFNI();
}
   
#Change command line arguments into an options list
read.LME.opts.batch <- function (args=NULL, verb = 0) {
   params <- list (
      '-prefix' = apl(n = 1, d = NA,  h = paste(
   "-prefix PREFIX: Output file name. For AFNI format, provide prefix only,",
   "         with no view+suffix needed. Filename for NIfTI format should have",
   "         .nii attached (otherwise the output would be saved in AFNI format).\n", sep = '\n'
      ) ),

      '-resid' = apl(n = 1, d = NA,  h = paste(
   "-resid PREFIX: Output file name for the residuals. For AFNI format, provide",
   "         prefix only without view+suffix. Filename for NIfTI format should",
   "         have .nii attached, while file name for surface data is expected",
   "         to end with .niml.dset. The sub-brick labeled with the '(Intercept)',",
   "         if present, should be interpreted as the effect with each factor",
   "         at the reference level (alphabetically the lowest level) for each",
   "         factor and with each quantitative covariate at the center value.\n", sep = '\n'
                     ) ),

      '-mask' = apl(n=1,  d = NA, h = paste(
   "-mask MASK: Process voxels inside this mask only.\n",
   "         Default is no masking.\n"
                     ) ),

      '-jobs' = apl(n = 1, d = 1, h = paste(
   "-jobs NJOBS: On a multi-processor machine, parallel computing will speed ",
   "         up the program significantly.",
   "         Choose 1 for a single-processor computer.\n", sep = '\n'
                     ) ),

      '-IF' = apl(n = 1, d = NA,  h = paste(
   "-IF var_name: var_name is used to specify the column name that is designated for",
   "        input files of effect estimate. The default (when this option is not invoked",
   "        is 'InputFile', in which case the column header has to be exactly as 'InputFile'",
   "        This input file for effect estimates has to be the last column.\n", sep = '\n'
                     ) ),

      '-model' = apl(n = 1, d = 1, h = paste(
   "-model FORMULA: Specify the model structure for all the variables. The",
   "         expression FORMULA with more than one variable has to be surrounded",
   "         within (single or double) quotes. Variable names in the formula",
   "         should be consistent with the ones used in the header of -dataTable.",
   "         In the LME context the simplest model is \"1+(1|Subj)\" in",
   "         which the random effect from each of the two subjects in a pair is",
   "         symmetrically incorporated in the model. Each random-effects factor is",
   "         specified within parentheses per formula convention in R. Any",
   "         effects of interest and confounding variables (quantitative or",
   "         categorical variables) can be added as fixed effects without parentheses.\n", sep = '\n'
             ) ),

       '-dbgArgs' = apl(n=0, h = paste(
   "-dbgArgs: This option will enable R to save the parameters in a",
   "         file called .3dLMEr.dbg.AFNI.args in the current directory",
   "          so that debugging can be performed.\n", sep='\n')),

       '-R2' = apl(n=0, h = paste(
   "-R2: Enabling this option will prompt the program to provide both",
   "         conditional and marginal coefficient of determination (R^2)",
   "         values associated with the adopted model. Marginal R^2 indicates",
   "         the proportion of variance explained by the fixed effects in the",
   "         model, while conditional R^2 represents the proportion of variance",
   "         explained by the entire model, encompassing both fixed and random",
   "         effects. Two sub-bricks labeled 'R2m' and 'R2c' will be provided",
   "         in the output.\n", sep='\n')),

       '-TRR' = apl(n=0, h = paste(
   "-TRR: This option will allow the analyst to perform test-retest reliability analysis",
   "         at the whole-brain voxel level. To be able to adopt this modeling approach,",
   "         trial-level effect estimates have to be provided from each subject (e.g.,",
   "         using option -stim_times_IM in 3dDeconvolve/3dREMLfit). Currently it works",
   "         with the situation with two conditions for a group of subjects that went",
   "         two sessions. The analytical goal to assess test-retest reliability across",
   "         the two sessions for the contrast between the two conditions. Check out",
   "         Example 4 for model specification. It is possible that numerical failures",
   "         may occur for a contrast between two conditions with values of 0, 1 or -1 in",
   "         the output. Use program TRR for ROI-level test-retest reliability analysis.\n", sep='\n')),

       '-bounds' = apl(n=2, h = paste(
   "-bounds lb ub: This option is for outlier removal. Two numbers are expected from",
   "         the user: the lower bound (lb) and the upper bound (ub). The input data will",
   "         be confined within [lb, ub]: any values in the input data that are beyond",
   "         the bounds will be removed and treated as missing. Make sure the first number",
   "         is less than the second. The default (the absence of this option) is no",
   "         outlier removal.",
   "         **NOTE**: Using the -bounds option to remove outliers should be approached",
   "           with caution due to its arbitrariness. A more principled alternative is",
   "           to use 3dGLMM with a Student's t-distribution.\n", sep='\n')),

      '-qVars' = apl(n=c(1,100), d=NA, h = paste(
   "-qVars variable_list: Identify quantitative variables (or covariates) with",
   "         this option. The list with more than one variable has to be",
   "         separated with comma (,) without any other characters such as",
   "         spaces and should be surrounded within (single or double) quotes.",
   "         For example, -qVars \"Age,IQ\"",
   "         WARNINGS:",
   "         1) Centering a quantitative variable through -qVarsCenters is",
   "         very critical when other fixed effects are of interest.",
   "         2) Between-subjects covariates are generally acceptable.",
   "         However EXTREME caution should be taken when the groups",
   "         differ substantially in the average value of the covariate.",
   "         \n",
             sep = '\n'
             ) ),

      '-vVars' = apl(n=c(1,100), d=NA, h = paste(
   "-vVars variable_list: Identify voxel-wise covariates with this option.",
   "         Currently one voxel-wise covariate is allowed only. By default",
   "         mean centering is performed voxel-wise across all subjects.",
   "         Alternatively centering can be specified through a global value",
   "         under -vVarsCenters. If the voxel-wise covariates have already",
   "         been centered, set the centers at 0 with -vVarsCenters.\n",
             sep = '\n'
             ) ),

     '-vVarCenters' = apl(n=1, d=NA, h = paste(
   "-vVarCenters VALUES: Specify centering values for voxel-wise covariates",
   "         identified under -vVars. Multiple centers are separated by ",
   "         commas (,) within (single or double) quotes. The order of the",
   "         values should match that of the quantitative variables in -qVars.",
   "         Default (absence of option -vVarsCenters) means centering on the",
   "         average of the variable across ALL subjects regardless their",
   "         grouping. If within-group centering is desirable, center the",
   "         variable yourself first before the files are fed under -dataTable.\n",
             sep = '\n'
                     ) ),

     '-gltCode' = apl(n=c(1,1000), d=NA, h = paste(
   "-gltCode label weights: Specify the label and weights of interest in a general",
   "       linear t-style (GLT) formulation in which only one null relationship is",
   "       involved (cf. -glfCode). The weights should be surrounded with quotes. For",
   "       example, the specification '-gltCode AvB 'Condition : 1*A -1*B' compares A",
   "       and B with a label 'AvB' for the output sub-bricks.  \n", sep = '\n'
                     ) ),

     '-glfCode' = apl(n=c(1,1000), d=NA, h = paste(
   "-glfCode label CODING: Specify a general linear F-style (GLF) formulation",
   "         with the weights among factor levels in which two or more null",
   "         relationships (e.g., A-B=0 and B-C=0) are involved. The symbolic",
   "         coding has to be within (single or double) quotes. For example, the",
   "         coding '-glfCode AvBvc 'Condition : 1*A -1*B & 1*A -1*C Emotion : 1*pos''",
   "         examines the main effect of Condition at the positive Emotion with",
   "         the output labeled as AvBvC. Similarly the coding '-glfCode CondByEmo'",
   "         'Condition : 1*A -1*B & 1*A -1*C Emotion : 1*pos -1*neg' looks",
   "         for the interaction between the three levels of Condition and the",
   "         two levels of Emotion and the resulting sub-brick is labeled as",
   "         'CondByEmo'.\n",
   "         NOTE:\n",
   "         1) The weights for a variable do not have to add up to 0.\n",
   "         2) When a quantitative variable is present, other effects are",
   "         tested at the center value of the covariate unless the covariate",
   "         value is specified as, for example, 'Group : 1*Old Age : 2', where",
   "         the Old Group is tested at the Age of 2 above the center.\n",
   "         3)  The absence of a categorical variable in a coding means the",
   "         levels of that factor are averaged (or collapsed) for the GLF.\n",
   "         4) The appearance of a categorical variable has to be followed",
   "         by the linear combination of its levels.\n",
             sep = '\n'
             ) ),

#      '-cVars' = apl(n=c(1,100), d=NA, h = paste(
#   "-cVars variable_list: Identify categorical (qualitive) variables (or",
#   "         factors) with this option. The list with more than one variable",
#   "         has to be separated with comma (,) without any other characters such",
#   "         as spaces and should be surrounded within (single or double) quotes.",
#   "         For example, -cVars \"sex,site\"\n",
#             sep = '\n'
#             ) ),
#

     '-qVarCenters' = apl(n=c(1,100), d=NA, h = paste(
   "-qVarCenters VALUES: Specify centering values for quantitative variables",
   "         identified under -qVars. Multiple centers are separated by ",
   "         commas (,) without any other characters such as spaces and should",
   "         be surrounded within (single or double) quotes. The order of the",
   "         values should match that of the quantitative variables in -qVars.",
   "         Default (absence of option -qVarCenters) means centering on the",
   "         average of the variable across ALL subjects regardless their",
   "         grouping. If within-group centering is desirable, center the",
   "         variable YOURSELF first before the values are fed into -dataTable.\n",
             sep = '\n'
                     ) ),

     '-dataTable' = apl(n=c(1, 1000000), d=NA, h = paste(
   "-dataTable TABLE: List the data structure with a header as the first line.\n",
   "         NOTE:\n",
   "         1) This option has to occur last in the script; that is, no other",
   "         options are allowed thereafter. Each line should end with a backslash",
   "         except for the last line.\n",
   "         2) The order of the columns should not matter except that the last",
   "         column has to be the one for input files, 'InputFile'. Unlike 3dLME, the",
   "         subject column (Subj in 3dLME) does not have to be the first column;",
   "         and it does not have to include a subject ID column under some situations",
   "         Each row should contain only one input file in the table of long format",
   "         (cf. wide format) as defined in R. Input files can be in AFNI, NIfTI or",
   "         surface format. AFNI files can be specified with sub-brick selector (square",
   "         brackets [] within quotes) specified with a number or label.\n",
   "         3) It is fine to have variables (or columns) in the table that are",
   "         not modeled in the analysis.\n",
   "         4) When the table is part of the script, a backslash is needed at the end",
   "         of each line (except for the last line) to indicate the continuation to the",
   "         next line. Alternatively, one can save the context of the table as a separate",
   "         file, e.g., calling it table.txt, and then in the script specify the data",
   "         with '-dataTable @table.txt'. However, when the table is provided as a",
   "         separate file, do NOT put any quotes around the square brackets for each",
   "         sub-brick, otherwise the program would not properly read the files, unlike the",
   "         situation when quotes are required if the table is included as part of the",
   "         script. Backslash is also not needed at the end of each line, but it would",
   "         not cause any problem if present. This option of separating the table from",
   "         the script is useful: (a) when there are many input files so that the program",
   "         complains with an 'Arg list too long' error; (b) when you want to try",
   "         different models with the same dataset.\n",
             sep = '\n'
                     ) ),

     '-SS_type' = apl(n=1, d=3, h = paste(
   "-SS_type NUMBER: Specify the type for sums of squares in the F-statistics.",
   "         Three options are: sequential (1), hierarchical (2), and marginal (3).",
   "         When this option is absent (default), marginal (3) is automatically set.",
   "         Some discussion regarding their differences can be found here:",
   "         https://sscc.nimh.nih.gov/sscc/gangc/SS.html\n ",
             sep = '\n'
                     ) ),		     

      '-help' = apl(n=0, h = '-help: this help message\n'),
      '-show_allowed_options' = apl(n=0, h=
   "-show_allowed_options: list of allowed options\n" ),

       '-cio' = apl(n=0, h = paste(
   "-cio: Use AFNI's C io functions, which is the default. Alternatively, -Rio",
   "         can be used.\n", sep='\n')),
       '-Rio' = apl(n=0, h = "-Rio: Use R's io functions. The alternative is -cio.\n")

         )
   #browser()
   ops <- parse.AFNI.args(args, params, other_ok=FALSE)
   if (verb) show.AFNI.args(ops, verb=0, hstr='')
   if (is.null(ops))
      errex.AFNI('Error parsing arguments. See 3dLMEr -help for details.')

   #Parse dems options
   #initialize with defaults
      com_history<-AFNI.command.history(ExecName, args,NULL)
      lop <- list (com_history = com_history)
      lop$nNodes <- 1
      lop$cutoff <- 0
      #lop$fixEff  <- 1
      lop$maskFN <- NA
      lop$IF     <- 'InputFile' #default

      #lop$ranEff <- NA
      lop$model  <- NA
      lop$qVars  <- NA
      lop$bounds <- NULL
      lop$vVars  <- NA
      lop$vQV    <- NA
      lop$qVarCenters <- NA
      lop$vVarCenters <- NA
      lop$gltCode    <- NULL
      lop$glfCode  <- NULL
      lop$dataTable <- NULL
      lop$SS_type <- 3
      lop$R2      <- FALSE 

      lop$iometh  <- 'clib'
      lop$dbgArgs <- FALSE # for debugging purpose
      lop$TRR     <- FALSE # for test-retest reliability
      lop$verb <- 0

   #Get user's input
   for (i in 1:length(ops)) {
      opname <- strsplit(names(ops)[i],'^-')[[1]];
      opname <- opname[length(opname)];
      switch(opname,
             prefix = lop$outFN  <- pprefix.AFNI.name(ops[[i]]),
             resid  = lop$resid  <- pprefix.AFNI.name(ops[[i]]),
             mask   = lop$maskFN <- ops[[i]],
             jobs   = lop$nNodes <- ops[[i]],
	     model  = lop$model  <- ops[[i]],
	     IF     = lop$IF     <- ops[[i]],
	     #num_glt   = lop$num_glt   <- ops[[i]],
	     gltCode   = lop$gltCode   <- ops[[i]],
	     glfCode  = lop$glfCode <- ops[[i]],
             qVars  = lop$qVars  <- ops[[i]],
             bounds = lop$bounds <- ops[[i]],
             vVars  = lop$vVars  <- ops[[i]],
             qVarCenters = lop$qVarCenters <- ops[[i]],
             vVarCenters = lop$vVarCenters <- ops[[i]],
             dataTable  = lop$dataTable <- dataTable.AFNI.parse(ops[[i]]),
	     SS_type = lop$SS_type <- ops[[i]],
	     
	     R2      = lop$R2 <- TRUE,
             help = help.LME.opts(params, adieu=TRUE),
             dbgArgs = lop$dbgArgs <- TRUE,
             TRR     = lop$TRR     <- TRUE,

             cio = lop$iometh<-'clib',
             Rio = lop$iometh<-'Rlib'
             )
   }

   return(lop)
}# end of read.LME.opts.batch

glfConstr <- function(cStr, dataStr) {
   pos <- which(cStr==":")
   vars  <- cStr[pos-1]  # factors to be specified
   nvar <- length(vars)
   pos <- c(pos, length(cStr)+2) # add an artificial one for convenient usage below
   varsOK <- vars %in% colnames(dataStr)
   if(all(varsOK)) {
      glfList <- vector('list', nvar)
      names(glfList) <- vars
      for(ii in 1:nvar) {
	 lvl  <- levels(dataStr[,vars[ii]])   # all the levels for each involved factor
         fpos <- which(cStr[(pos[ii]+1):(pos[ii+1]-2)]=="&")  # positions of &'s
         nc   <- length(fpos) # number of columns for this factor minus 1
            glfList[[ii]] <- matrix(0, nrow=length(lvl), ncol=nc+1)
            fpos <- c(0, fpos, length(cStr[(pos[ii]+1):(pos[ii+1]-2)])+1)
            for(jj in 1:(length(fpos)-1)) {
               sepTerms <- unlist(lapply(cStr[(pos[ii]+1):(pos[ii+1]-2)][(fpos[jj]+1):(fpos[jj+1]-1)], strsplit, '\\*'))
	       lvlInv <- sepTerms[seq(2,length(sepTerms),2)]   # levels involved
	       lvlOK <- lvlInv %in% lvl
	       if(all(lvlOK)) {
	          sq <- match(lvlInv, lvl)
                  glfList[[ii]][sq,jj] <- as.numeric(sepTerms[seq(1,length(sepTerms),2)])
	       } else errex.AFNI(paste("Incorrect level coding in variable", vars[ii],
	         ": ", lvlInv[which(!lvlOK)], " \n   "))
            }
         #}
      }
      return(glfList)
   } else errex.AFNI(paste("Incorrect variable name in GLF coding: ", vars[which(!varsOK)], " \n   "))
}
# glfConstr(lop$glfCode[[1]], lop$dataStr)


#Change options list to 3dLMEr variable list
process.LME.opts <- function (lop, verb = 0) {
   if(is.null(lop$outFN)) errex.AFNI(c("Output filename not specified! Add filename with -prefix.\n"))
   an <- parse.AFNI.name(lop$outFN)
   if(an$type == "NIML") {
      if(file.exists(lop$outFN)) errex.AFNI(c("File ", lop$outFN, " exists! Try a different name.\n"))
   } else if(file.exists(paste(lop$outFN,"+tlrc.HEAD", sep="")) ||
     file.exists(paste(lop$outFN,"+tlrc.BRIK", sep="")) ||
     file.exists(paste(lop$outFN,"+orig.HEAD", sep="")) ||
     file.exists(paste(lop$outFN,"+orig.BRIK", sep=""))) {
     errex.AFNI(c("File ", lop$outFN, " exists! Try a different name.\n"))
     return(NULL)
   }
   #browser()
   if(an$type != 'BRIK' && lop$iometh != 'clib')
      errex.AFNI(c('Must of use -cio option with any input/output ',
                   'format other than BRIK'))

   if(!is.null(lop$resid)) {
      an2 <- parse.AFNI.name(lop$resid)
      if(an2$type == "NIML") {
         if(file.exists(lop$resid)) errex.AFNI(c("File ", lop$resid, " exists! Try a different name.\n"))
      } else if(file.exists(paste(lop$resid,"+tlrc.HEAD", sep="")) ||
        file.exists(paste(lop$resid,"+tlrc.BRIK", sep="")) ||
        file.exists(paste(lop$resid,"+orig.HEAD", sep="")) ||
        file.exists(paste(lop$resid,"+orig.BRIK", sep=""))) {
        errex.AFNI(c("File ", lop$resid, " exists! Try a different name.\n"))
        return(NULL)
      }
      if(an2$type != 'BRIK' && lop$iometh != 'clib')
          errex.AFNI(c('Must of use -cio option with any input/output ',
                       'format other than BRIK'))
   }
   # assume the quantitative variables are separated by + here
   #if(!is.na(lop$qVars)) lop$QV <- strsplit(lop$qVars, '\\,')[[1]]
   if(!identical(lop$qVars, NA)) lop$QV <- strsplit(lop$qVars, '\\,')[[1]]
   if(!is.na(lop$vVars[1])) lop$vQV <- strsplit(lop$vVars, '\\,')[[1]]

   if(!(is.null(lop$bounds))) {
      if(lop$bounds[1] > lop$bounds[2]) 
         errex.AFNI(paste0('Incorrect setting with option -bounds! The lower bound ', lop$bounds[1], 
            ' should be smaller than the upper bound ', lop$bounds[2], '!'))
   }

   len <- length(lop$dataTable)
   wd <- which(lop$dataTable == lop$IF)  # assuming the input file is the last column here!
   hi <- len / wd - 1

   if(len %% wd != 0)
      errex.AFNI(paste('The content under -dataTable is not rectangular!', len, wd)) else {
      lop$dataStr <- NULL
      for(ii in 1:wd) lop$dataStr <- data.frame(cbind(lop$dataStr, lop$dataTable[seq(wd+ii, len, wd)]))
      names(lop$dataStr) <- lop$dataTable[1:wd]
      #if(!is.na(lop$qVars)) for(jj in lop$QV) lop$dataStr[,jj] <- as.numeric(as.character(lop$dataStr[,jj]))
      if(!identical(lop$qVars, NA)) for(jj in lop$QV) lop$dataStr[,jj] <- as.numeric(as.character(lop$dataStr[,jj]))
      #if(!is.na(lop$vVars[1])) for(jj in lop$vQV) lop$dataStr[,jj] <- as.character(lop$dataStr[,jj])
      for(ii in 1:(wd-1)) if(sapply(lop$dataStr, class)[ii] == "character")
         lop$dataStr[,ii] <- as.factor(lop$dataStr[,ii])
      if(!is.na(lop$vVars[1])) for(jj in lop$vQV) lop$dataStr[,jj] <- as.character(lop$dataStr[,jj])
   }

   if(!is.null(lop$gltCode)) {
      lop$gltLabel <- unlist(lapply(lop$gltCode, `[`, 1))
      lop$num_glt     <- length(lop$gltLabel)
      glt             <- gl_Constr(lop$num_glt, lapply(lop$gltCode, '[',-1), lop)
      lop$gltList     <- glt[[1]]
      lop$slpList     <- glt[[2]]
      lop$covValList  <- glt[[3]]
   } else lop$num_glt <- 0

   if(!is.null(lop$glfCode)) {
      lop$glfLabel <- unlist(lapply(lop$glfCode, `[`, 1))
      lop$num_glf     <- length(lop$glfLabel)
      glf             <- gl_Constr(lop$num_glf, lapply(lop$glfCode, '[',-1), lop)
      lop$glfList     <- glf[[1]]
      lop$slpListF     <- glf[[2]]
      lop$covValListF  <- glf[[3]]
   } else lop$num_glf <- 0

   if(lop$iometh == 'Rlib') {
      lop$outFN <- paste(lop$outFN, "+tlrc", sep="")
      if(!is.null(lop$resid)) lop$resid <- paste(lop$resid, "+tlrc", sep="")
   } else {
      #an <- parse.AFNI.name(lop$outFN)
      if(an$type == "BRIK" && an$ext == "" && is.na(an$view))
         lop$outFN <- paste(lop$outFN, "+tlrc", sep="")
      if (exists.AFNI.name(lop$outFN) ||
          exists.AFNI.name(modify.AFNI.name(lop$outFN,"view","+tlrc")))
         errex.AFNI(c("File ", lop$outFN, " exists! Try a different name.\n"))
      if(!is.null(lop$resid)) {
         #an2 <- paste(lop$resid, "+tlrc", sep="")
         if(an2$type == "BRIK" && an2$ext == "" && is.na(an2$view))
            lop$resid <- paste(lop$resid, "+tlrc", sep="")
         if (exists.AFNI.name(lop$resid) ||
             exists.AFNI.name(modify.AFNI.name(lop$resid,"view","+tlrc")))
             errex.AFNI(c("File ", lop$resid, " exists! Try a different name.\n"))
      }
   }

   if(lop$nNodes < 1) lop$nNodes <- 1

   if(!is.na(lop$maskFN)) {
      if(verb) cat("Will read ", lop$maskFN,'\n')
      if(is.null(mm <- read.AFNI(lop$maskFN, verb=lop$verb, meth=lop$iometh, forcedset = TRUE))) {
         warning("Failed to read mask", immediate.=TRUE)
         return(NULL)
      }
      #lop$maskData <- mm$brk[,,,1]
      #lop$maskData <- mm$brk
      lop$maskData <- ifelse(abs(mm$brk) > tolL, 1, 0) # 01/17/2023: sometimes mask is defined as 0s and nonzeros
      if(verb) cat("Done read ", lop$maskFN,'\n')
      if(dim(mm$brk)[4] > 1) stop("More than 1 sub-brick in the mask file!") 
   }
   #if(!is.na(lop$maskFN))
   #   if(!all(dim(lop$maskData)==lop$myDim[1:3]))
   #      stop("Mask dimensions don't match the input files!")
   return(lop)
}
# process.LME.opts(lop, 0)

#################################################################################
################# LME Computation functions ##############################
#################################################################################

# LME: bare-bone approach with LME for LME: some voxels may have 0 LME values: effect estimates as input only
runLME <- function(myData, DM, tag) {
   #browser()
   Stat <- rep(0, lop$NoBrick)
   if(!is.null(lop$resid)) {
      resid <- rep(0, nrow(lop$dataStr))
      res.na <- which(is.na(myData)) # indices for missing data
   }
   if(any(!is.na(lop$vQV))) {  # voxel-wise centering for voxel-wise covariate
      DM <- assVV2(DM, lop$vQV, myData[(length(myData)/2+1):length(myData)], all(is.na(lop$vVarCenters)))
   }

   if(!all(na.omit(myData) == 0)) {
      DM$yy <- myData[1:nrow(DM)]
      fm <- NULL
      options(warn=-1)
      try(fm <- lmer(lop$model, data=DM), silent=TRUE)

      if(!is.null(fm)) {
         #Stat[1:lop$nF] <- anova(fm, type=lop$SS_type)$`F value` # F-stat
	 Stat[1:lop$nF] <- qchisq(anova(fm, type=lop$SS_type)$`Pr(>F)`, 2, lower.tail = F) # convert to chisq
	 #qnorm(anova(fm, type=lop$SS_type)$`Pr(>F)`/2, lower.tail = F) # Z-stat: should use one-tailed!
	 if(lop$num_glt > 0) for(ii in 1:lop$num_glt) {
            tt <- NULL
            #if(is.na(lop$gltList[[ii]])[1]) tt <- tryCatch(testInteractions(fm, pairwise=NULL, slope=lop$slpList[[ii]],
	    if(identical(lop$gltList[[ii]], NA)) tt <- tryCatch(testInteractions(fm, pairwise=NULL, slope=lop$slpList[[ii]], # 01/19/2023: fix the problem w/ length of lop$gltList > 1
               covariates=lop$covValList[[ii]], adjustment="none"), error=function(e) NULL) else
            tt <- tryCatch(testInteractions(fm, custom=lop$gltList[[ii]], slope=lop$slpList[[ii]],
               covariates=lop$covValList[[ii]], adjustment="none"), error=function(e) NULL)
            if(!is.null(tt)[1]) {
               Stat[lop$nF[1]+2*ii-1] <- tt[1,'Value']
	       Stat[lop$nF[1]+2*ii]   <- sign(tt[1,'Value'])*qnorm(tt[1,'Pr(>Chisq)']/2, lower.tail = F)  # convert chisq to Z
            }
         }
	 if(lop$num_glf > 0) for(ii in 1:lop$num_glf) {
            ff <- NULL
            if(any(is.na(lop$glfList[[ii]]))) ff <- tryCatch(testFactors(fm, pairwise=NULL,
	       slope=lop$slpListF[[ii]], covariates=lop$covValListF[[ii]],
	       adjustment="none")$terms$`(Intercept)`$test, error=function(e) NULL) else
	    ff <- tryCatch(testFactors(fm, levels=lop$glfList[[ii]],
	       slope=lop$slpListF[[ii]], covariates=lop$covValListF[[ii]],
	       adjustment="none")$terms$`(Intercept)`$test, error=function(e) NULL)
            if(!is.null(ff)[1]) {
               Stat[lop$nF[1]+2*lop$num_glt+ii] <- qchisq(ff[2,'Pr(>Chisq)'], 2, lower.tail = F) # glf[2,2]  # convert chisq to Z
            }
         }
         if(lop$R2) Stat[(lop$nF[1]+2*lop$num_glt+lop$num_glf+1):(lop$nF[1]+2*lop$num_glt+lop$num_glf+2)] <- r.squaredGLMM(fm)
         if(!is.null(lop$resid)) {
            resid <- unname(residuals(fm))
            if(!is.null(res.na)) for(aa in res.na) resid <- append(resid, 0, after=aa-1) # fill in missing data with 0s 
         }
      }
   }
   if(!is.null(lop$resid)) Stat <- c(Stat, resid)
   return(Stat)
}
# runLME(inData[30,30,30,], lop$dataStr, 0)

# for LME only, not TRR yet
assVV2 <- function(DF, vQV, value, c) {
      # centering - c: center; value: voxel-wise value; vQV: voxel-wise variable name; DF: dataframe
   #browser()
   if(is.na(c)) DF[, vQV] <- scale(value, center=TRUE, scale=F) else
       DF[, vQV] <- scale(value, center=c, scale=F)
   DF[, vQV] <- as.numeric(DF[, vQV])
   return(DF)
}

runTRR <- function(myData, DM, tag) { # assuming 2 conditions and 2 sessions
   #browser()
   Stat <- rep(0, lop$NoBrick)
   if(!all(myData[!is.na(myData)] == 0)) {
      DM$yy <- myData
      fm <- NULL
      options(warn=-1)
      try(fm <- lmer(lop$model, data=DM), silent=TRUE)

      if(!is.null(fm)) {
         mm <- summary(fm)
         es <- mm$coefficients[,'Estimate']
         zz <- sign(es)*abs(qnorm(mm$coefficients[,'Pr(>|t|)']/2))
         Stat <- c(c(rbind(es, zz)), attr(mm$varcor[[1]], "correlation")[1,2], attr(mm$varcor[[2]], "correlation")[1,2])
      }
   }
   return(Stat)
}
# runTRR(inData[30,30,30,], lop$dataStr, 0)

#################################################################################
########################## Begin LME main ######################################
#################################################################################

   if(!exists('.DBG_args')) {
      args = (commandArgs(TRUE))
      rfile <- first.in.path(sprintf('%s.R',ExecName))
      # save only on -dbg_args          28 Apr 2016 [rickr]
      if ( '-dbgArgs' %in% args ) {
         try(save(args, rfile, file=".3dLMEr.dbg.AFNI.args", ascii = TRUE), silent=TRUE)
      }
   } else {
      note.AFNI("Using .DBG_args resident in workspace")
      args <- .DBG_args
   }
   if(!length(args)) {
      BATCH_MODE <<- 0
      cat(greeting.LME(),
      "Use CNTL-C on Unix or ESC on GUI version of R to stop at any moment.\n",
      sep='\n')
      #browser()
      if(length(args)<6) modFile <- "model.txt" else modFile <- args[6]
      if (is.null(lop <- read.LME.opts.from.file(modFile, verb=0))) {
         stop('Error parsing input from file!');
      }

      if(0) str(lop)

   } else {
      if(!exists('.DBG_args')) {
         BATCH_MODE <<- 1
      } else {
         BATCH_MODE <<- 0
      }
      if(is.null(lop <- read.LME.opts.batch(args, verb = 0)))
         stop('Error parsing input')

      #str(lop);
      if(is.null(lop <- process.LME.opts(lop, verb = lop$verb)))
         stop('Error processing input')

   }
   #if(lop$verb > 1) {
      #Too much output, big dump of header structs of input dsets..
   #   str(lop)
   #}


########################################################################

# in case the user didn't put space around each colon (:), this
#lop$gltCode <- lapply(lop$gltCode, function(ss) unlist(strsplit(ss, split="(?=:)", perl=TRUE)))

if(!is.na(lop$qVarCenters)) lop$qVarCenters <- as.numeric(strsplit(as.character(lop$qVarCenters), '\\,')[[1]])

# effect coding leads to the same type III as SAS
options(contrasts = c("contr.sum", "contr.poly"))

# standardize the names for Y, ROI and subject
#names(lop$dataStr)[names(lop$dataStr)==lop$Subj] <- 'Subj'
names(lop$dataStr)[names(lop$dataStr)==lop$IF] <- 'InputFile'

# Maybe not list for these two, or yes?
#lop$dataStr$Subj <-  as.factor(lop$dataStr$Subj)
lop$dataStr$InputFile <-  as.character(lop$dataStr$InputFile)
#if(is.null(lop$Tstat)) lop$dataStr$Tstat <-  as.character(lop$dataStr$Tstat)

# center on user-speficied value or mean
if(!lop$TRR)
if(any(!is.na(lop$qVars))) if(all(is.na(lop$qVarCenters)))
   lop$dataStr[,lop$QV] <- scale(lop$dataStr[,lop$QV], center=TRUE, scale=F)[,,drop=T] else
   lop$dataStr[,lop$QV] <- scale(lop$dataStr[,lop$QV], center=lop$qVarCenters, scale=F)[,,drop=T]

cat('\n++++++++++++++++++++++++++++++++++++++++++++++++++++\n')
cat('***** Summary information of data structure *****\n')

#nS <- levels(lop$dataStr$Subj) # total number of subjects
nF <- dim(lop$dataStr[1])[1] # number of input files

#cat(nS, 'subjects in total:', nS, '\n')
cat(nF, 'response values\n')

if(dim(lop$dataStr)[2] > 3) for(ii in 3:(dim(lop$dataStr)[2]-1)) if(class(lop$dataStr[,ii]) == 'factor')
   cat(nlevels(lop$dataStr[,ii]), 'levels for factor', names(lop$dataStr)[ii], ':',
   levels(lop$dataStr[,ii]), '\n') else if(class(lop$dataStr[,ii]) == 'numeric' | class(lop$dataStr[,ii]) == 'matrix')  # numeric doesn't work
   cat(length(lop$dataStr[,ii]), 'centered values for numeric variable', names(lop$dataStr)[ii], ':', lop$dataStr[,ii], '\n')
#cat(lop$num_glt, 'post hoc tests\n')

#cat('\nContingency tables of subject distributions among the categorical variables:\n\n')

cat('***** End of data structure information *****\n')
cat('++++++++++++++++++++++++++++++++++++++++++++++++++++\n\n')

cat('Reading input files now...\n\n')

# Read in the 1st input file so that we have the dimension information
inData <- read.AFNI(lop$dataStr[1, 'InputFile'], verb=lop$verb, meth=lop$iometh, forcedset = TRUE)
dimx <- inData$dim[1]
dimy <- inData$dim[2]
dimz <- inData$dim[3]
# for writing output purpose
head <- inData

# Read in all input files
inData <- unlist(lapply(lapply(lop$dataStr[, 'InputFile'], read.AFNI, verb=lop$verb, meth=lop$iometh, forcedset = TRUE), '[[', 1))
tryCatch(dim(inData) <- c(dimx, dimy, dimz, nF), error=function(e)
   errex.AFNI(c("Dimension mismatch!\n",
   "Check files in the InputFile column all have\n",
   " (1) a single timepoint/value per voxel\n",
   "     Use sub-brick selectors if \"3dinfo -nt\" is >1 volume",
   " (2) the same number of voxels along X, Y, and Z axes\n",
   "Run \"3dinfo -header_line -prefix -same_grid -n4 *.HEAD\" in the directory where\n",
   "the files are stored, and pinpoint out which file(s) is the trouble maker.\n",
   "Replace *.HEAD with *.nii or something similar for other file formats.\n")))
cat('Reading input files for effect estimates: Done!\n\n')

# masking
if(!is.na(lop$maskFN)) {
   #Mask <- read.AFNI(lop$maskFN, verb=lop$verb, meth=lop$iometh, forcedset = TRUE)$brk[,,,1]
   if(!all(c(dimx, dimy, dimz)==dim(lop$maskData)[1:3])) stop("Mask dimensions don't match the input files!")
   lop$maskData <- array(lop$maskData, dim=c(dimx, dimy, dimz))
   inData <- array(apply(inData, 4, function(x) x*(abs(lop$maskData)>tolL)), dim=c(dimx,dimy,dimz,nF))
   #if(!is.na(lop$dataStr$tStat)) inDataV <- array(apply(inDataV, 4, function(x) x*(abs(Mask)>tolL)), dim=c(dimx,dimy,dimz,nF))
}

# voxel-wise covariate files: for LME only; not TRR yet
if(!is.na(lop$vQV)) {
   tmpDat <- read.AFNI(as.character(unique(lop$dataStr[,lop$vQV[1]])[1]), verb=lop$verb, meth=lop$iometh, forcedset = TRUE)
   dimx <- tmpDat$dim[1]
   dimy <- tmpDat$dim[2]
   dimz <- tmpDat$dim[3]
   head <- tmpDat
   #for(ii in lop$vQV)
   #if(length(unique(lop$dataStr[,ii])) != nlevels(lop$dataStr$Subj))
   #   errex.AFNI(c("Error with voxel-wise covariate ", ii, ": Each subject is only\n",
   #             "allowed to have one volume; that is, the covariate has to be at the\n",
   #             "subject level.")) else {  # currently consider one voxel-wise covariate only: may generalize later?
      #vQV <- unlist(lapply(lapply(unique(lop$dataStr[,lop$vQV[1]]), read.AFNI, verb=lop$verb, meth=lop$iometh, forcedset = TRUE), '[[', 1))
      vQV <- unlist(lapply(lapply(as.character(lop$dataStr[,lop$vQV[1]]), read.AFNI, verb=lop$verb, meth=lop$iometh, forcedset = TRUE), '[[', 1))
      #dim(vQV) <- c(dimx, dimy, dimz, length(unique(lop$dataStr[,lop$vQV[1]])))
      dim(vQV) <- c(dimx, dimy, dimz, length(lop$dataStr[,lop$vQV[1]]))
      inData <- c(inData, vQV)
      #dim(inData) <- c(dimx, dimy, dimz, lop$nVVars+lop$nSubj)
      dim(inData) <- c(dimx, dimy, dimz, 2*length(lop$dataStr[,lop$vQV[1]]))
   #}
} else vQV <- NULL

# try out a few voxels and see if the model is OK, and find out the number of F tests and DF's
# for t tests (and catch potential problems as well)
#ii<-dimx%/%3; jj<-dimy%/%3; kk<-dimz%/%3

###############################

# add a new column to store the voxel-wise filenames
if(any(!is.na(lop$vVars))) {
   lop$dataStr <- cbind(lop$dataStr, lop$dataStr[, lop$vQV[1]])
   names(lop$dataStr)[length(names(lop$dataStr))] <- paste(lop$vQV[1], '_fn', sep='')
   lop$nVVars <- nlevels(lop$dataStr[,paste(lop$vQV[1], '_fn', sep='')])
}

# show the range of input data
rg <- range(inData)
cat(paste0('\nRange of input data: [', sprintf(rg[1], fmt = '%#.3f'), ', ', sprintf(rg[2], fmt = '%#.3f'), ']\n\n'))

# outlier removal
if(!is.null(lop$bounds)) {
   inData[inData > lop$bounds[2]] <- NA
   inData[inData < lop$bounds[1]] <- NA
   cat(paste0('\nInput data confined within [', lop$bounds[1], ', ', lop$bounds[2], ']\n\n'))
}

cat('If the program hangs here for more than, for example, half an hour,\n')
cat('kill the process because the model specification or something else\n')
cat('is likely inappropriate.\n\n')

# pick up a test voxel
if(!is.na(lop$maskFN)) {
   idx <- which(lop$maskData == 1, arr.ind = T)

  # make sure there was something in the mask
  #if(length(idx)==0L) errex.AFNI(
  # c("Input mask has no voxels == 1!\n",
  #   "If your mask has many non-zero values (e.g an atlas),\n",
  #   "Run: 3dcalc -m ",lop$maskFN," -expr 'step(m)'"))

  idx <- idx[floor(dim(idx)[1]/2),1:3]
  xinit <- idx[1]; yinit <- idx[2]; zinit <- idx[3]
  ii <- xinit; jj <- yinit; kk <- zinit
} else {
  xinit <- dimx%/%3
  if(dimy==1) {xinit <-1; yinit <- 1} else yinit <- dimy%/%3
  if(dimz==1) {xinit <-1; zinit <- 1} else zinit <- dimz%/%3
  ii <- xinit; jj <- yinit; kk <- zinit
}

#if(unlist(strsplit(lop$model, split="[+]"))[1]==1) {
#   intercept <- 1
#   lop$NoBrick <- 2
#} else {
#   intercept <- 0
#   lop$NoBrick <- 2*nrow(lop$gltM) # LME plus its F-stat
#}

lop$model <- as.formula(paste('yy ~ ', lop$model))
pkgLoad('lme4'); 
pkgLoad('lmerTest')
pkgLoad('phia')
if(lop$R2)  pkgLoad('MuMIn')
#if(!lop$TRR) pkgLoad('phia')
fm<-NULL
if(any(!is.na(lop$vQV))) {
     lop$dataStr <- assVV2(lop$dataStr, lop$vQV, inData[ii,jj,kk,(nrow(lop$dataStr)+1):(2*nrow(lop$dataStr))], all(is.na(lop$vVarCenters)))
}

if(lop$num_glt > 0) gltRes <- vector('list', lop$num_glt)
if(lop$num_glf > 0) glfRes <- vector('list', lop$num_glf)
#chi_DF <- NULL
while(is.null(fm)) {
   #lop$dataStr$yy <- inData[ii, jj, kk,]
   lop$dataStr$yy <- inData[ii, jj, kk,1:nrow(lop$dataStr)]
   options(warn=-1)
   try(fm <- lmer(lop$model, data=lop$dataStr), silent=TRUE)
   if(!lop$TRR) {
   if(!is.null(fm)) if (lop$num_glt > 0) {
      n <- 1
      while(!is.null(fm) & (n <= lop$num_glt)) {
         #if(is.na(lop$gltList[[n]])[1]) gltRes[[n]] <- tryCatch(testInteractions(fm, pairwise=NULL,
	 if(identical(lop$gltList[[n]], NA)) gltRes[[n]] <- tryCatch(testInteractions(fm, pairwise=NULL, # 01/19/2023: fix the problem w/ length of lop$gltList > 1
            covariates=lop$covValList[[n]], slope=lop$slpList[[n]], adjustment="none"), error=function(e) NA) else
         gltRes[[n]] <- tryCatch(testInteractions(fm, custom=lop$gltList[[n]],
            covariates=lop$covValList[[n]], slope=lop$slpList[[n]], adjustment="none"), error=function(e) NA)
         if(is.null(gltRes[[n]])) fm <- NULL
         n <- n+1
      }
   }
   if(!is.null(fm)) if (lop$num_glf > 0) {
      n <- 1
      while(!is.null(fm) & (n <= lop$num_glf)) {
         if(any(is.na(lop$glfList[[n]]))) glfRes[[n]] <- tryCatch(testFactors(fm, pairwise=NULL,
            covariates=lop$covValList[[n]], slope=lop$slpList[[n]], adjustment="none"), error=function(e) NA) else
         glfRes[[n]] <- tryCatch(testFactors(fm, levels=lop$glfList[[n]],
            covariates=lop$covValList[[n]], slope=lop$slpList[[n]], adjustment="none"), error=function(e) NA)
         if(is.null(glfRes[[n]])) fm <- NULL #else chi_DF <- c(chi_DF, glfRes[[n]]$terms$`(Intercept)`$test[2,'Df'])
         n <- n+1
      }
   }
   } # if(!lop$TRR)
   if(!is.null(fm))  {
      print(sprintf("Great, test run passed at voxel (%i, %i, %i)!", ii, jj, kk))
      if(lop$TRR) {
         nT          <- 2*nrow(summary(fm)$coefficients)
         lop$NoBrick <- nT + 2 # 2 ICC values: one for avefage and one for contrast
      } else {
         lop$nF      <- nrow(anova(fm, type=lop$SS_type))    # total number of F-stat
         nT          <- 2*lop$num_glt
         lop$NoBrick <- lop$nF + nT + lop$num_glf + 2*lop$R2
      }
   } else if(ii<dimx) ii<-ii+1 else if(jj<dimy) {ii<-xinit; jj <- jj+1} else if(kk<dimz) {
      ii<-xinit; jj <- yinit; kk <- kk+1 } else {
      cat('~~~~~~~~~~~~~~~~~~~ Model test failed  ~~~~~~~~~~~~~~~~~~~\n')
      cat('Possible reasons:\n\n')
      cat('0) Missing R packages lme4, lmerTest, and phia. \n')
      cat('1) Inappropriate model specification with options -model, or -qVars.\n\n')
      cat('2) Incorrect specifications with -gltCode.\n\n')
      cat('3) inconsistencies in the data table.\n')
      cat('4) Inconsistent variable names (e.g., typos, case sensitive characters).\n')
      errex.AFNI("Quitting due to model test failure...")
   }
}

print(sprintf("Start to compute %s slices along Z axis. You can monitor the progress", dimz))
print("and estimate the total run time as shown below.")
print(format(Sys.time(), "%D %H:%M:%OS3"))

###############################
#options(warn = -1) # suppress warnings!
#getOption('warn')

options(contrasts = c("contr.sum", "contr.poly"))

if(lop$TRR) { # test-retest analysis
   # no single CPU yet
   if(dimy==1 & dimz==1) { # TRR with 1D data
      nSeg <- 20
      # drop the dimensions with a length of 1
      inData <- inData[, , ,]
      # break into 20 segments, leading to 5% incremental in parallel computing
      dimx_n <- dimx%/%nSeg + 1
      # number of datasets need to be filled
      fill <- nSeg-dimx%%nSeg
      # pad with extra 0s
      inData <- rbind(inData, array(0, dim=c(fill, nF)))
      # break input multiple segments for parrel computation
      dim(inData) <- c(dimx_n, nSeg, nF)
      Stat <- array(0, dim=c(dimx_n, nSeg, lop$NoBrick))
      if (lop$nNodes==1) for(kk in 1:nSeg) {
         for(kk in 1:nSeg) {
            Stat[,kk,] <- aperm(apply(inData[,,kk,], 1, runTRR, DM=lop$dataStr, tag=0), c(2,1))
            cat("Computation done ", 100*kk/nSeg, "%: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n", sep='')
         } #  for(kk in 1:nSeg)
      } else { # parallelization
         pkgLoad('snow')
         cl <- makeCluster(lop$nNodes, type = "SOCK")
         clusterExport(cl, "z2r", envir=environment())
         clusterEvalQ(cl, library(lme4))
         clusterEvalQ(cl, options(contrasts = c("contr.sum", "contr.poly")))
         for(kk in 1:nSeg) {
            Stat[,kk,] <- aperm(parApply(cl, inData[,,kk,], 1, runTRR, DM=lop$dataStr, tag=0), c(2,1))
            cat("Computation done ", 100*kk/nSeg, "%: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n", sep='')   
         } # runLME(inData[30,1,], lop$model, lop$dataStr, lop$gltM, intercept, nF, nS, 0)
         stopCluster(cl)
      } # if (lop$nNodes>1)
      # convert to 4D
      dim(Stat) <- c(dimx_n*nSeg, 1, 1, lop$NoBrick)
      # remove the trailers (padded 0s)
      Stat <- Stat[-c((dimx_n*nSeg-fill+1):(dimx_n*nSeg)), 1, 1,,drop=F]
   } else { # TRR with volumetric data
      Stat <- array(0, dim=c(dimx, dimy, dimz, lop$NoBrick))
      if (lop$nNodes==1) {
         for (kk in 1:dimz) {
            Stat[,,kk,] <- aperm(apply(inData[,,kk,], c(1,2), runTRR,
                  DM=lop$dataStr, tag=0), c(2,3,1))
            cat("Z slice #", kk, "done: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n")
         } 
      } else {
         pkgLoad('snow')
         cl <- makeCluster(lop$nNodes, type = "SOCK")
         clusterExport(cl, "lop", envir=environment())
         clusterEvalQ(cl, library(lmerTest)); clusterEvalQ(cl, library(phia))
         if(lop$R2) clusterEvalQ(cl, library(MuMIn))
         clusterEvalQ(cl, options(contrasts = c("contr.sum", "contr.poly")))
         for (kk in 1:dimz) {
            Stat[,,kk,] <- aperm(parApply(cl, inData[,,kk,], c(1,2), runTRR,
                  DM=lop$dataStr, tag=0), c(2,3,1))
            cat("Z slice #", kk, "done: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n")
         }
         stopCluster(cl)
      }
      # runTRR(inData[30,30,30,], lop$model, lop$dataStr, lop$glt, nF, 0)
   } # TRR with volumetric data
} else {  # typical LME modeling
   if(dimy==1 & dimz==1) { # 1D data
      nSeg <- 20
      # drop the dimensions with a length of 1
      inData <- inData[, , ,]
      # break into 20 segments, leading to 5% incremental in parallel computing
      dimx_n <- dimx%/%nSeg + 1
      # number of datasets need to be filled
      fill <- nSeg-dimx%%nSeg
      # pad with extra 0s
      nF <- dim(inData)[2]
      inData <- rbind(inData, array(0, dim=c(fill, nF)))
      # break input multiple segments for parrel computation
      dim(inData) <- c(dimx_n, nSeg, nF)
      Stat <- array(0, dim=c(dimx_n, nSeg, lop$NoBrick+(!is.null(lop$resid))*nrow(lop$dataStr)))
      if (lop$nNodes==1) for(kk in 1:nSeg) {
         for(kk in 1:nSeg) {
            Stat[,kk,] <- aperm(apply(inData[,kk,], 1, runLME, DM=lop$dataStr, tag=0), c(2,1))
            cat("Computation done ", 100*kk/nSeg, "%: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n", sep='')
         }
      } # runLME(inData[30,1,], lop$model, lop$dataStr, lop$gltM, intercept, nF, nS, 0)
      
      if (lop$nNodes>1) {
         pkgLoad('snow')
         cl <- makeCluster(lop$nNodes, type = "SOCK")
         clusterExport(cl, c("lop", "assVV2"), envir=environment())
         clusterEvalQ(cl, library(lmerTest)); clusterEvalQ(cl, library(phia))
         if(lop$R2) clusterEvalQ(cl, library(MuMIn))
         clusterEvalQ(cl, options(contrasts = c("contr.sum", "contr.poly")))
         for(kk in 1:nSeg) {
            Stat[,kk,] <- aperm(parApply(cl, inData[,kk,], 1, runLME, DM=lop$dataStr, tag=0), c(2,1))
            cat("Computation done ", 100*kk/nSeg, "%: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n", sep='')   
         } # runLME(inData[30,1,], lop$model, lop$dataStr, lop$gltM, intercept, nF, nS, 0)
         stopCluster(cl)
      }
      # convert to 4D
      dim(Stat) <- c(dimx_n*nSeg, 1, 1, lop$NoBrick+(!is.null(lop$resid))*nrow(lop$dataStr))
      # remove the trailers (padded 0s)
      Stat <- Stat[-c((dimx_n*nSeg-fill+1):(dimx_n*nSeg)), 1, 1,,drop=F]
   } else { # volumetric data
      Stat <- array(0, dim=c(dimx, dimy, dimz, lop$NoBrick+(!is.null(lop$resid))*nrow(lop$dataStr)))
      if (lop$nNodes==1) {
         for (kk in 1:dimz) {
            if((lop$NoBrick > 1) | (!is.null(lop$resid))) Stat[,,kk,] <- aperm(apply(inData[,,kk,], c(1,2), runLME,
               DM=lop$dataStr, tag=0), c(2,3,1)) else
            Stat[,,kk,1] <- apply(inData[,,kk,], c(1,2), runLME, DM=lop$dataStr, tag=0)
            cat("Z slice #", kk, "done: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n")
         }
      } else {
         pkgLoad('snow')
         cl <- makeCluster(lop$nNodes, type = "SOCK")
         clusterExport(cl, c("lop", "assVV2"), envir=environment())
         clusterEvalQ(cl, library(lmerTest)); clusterEvalQ(cl, library(phia))
         if(lop$R2) clusterEvalQ(cl, library(MuMIn))
         clusterEvalQ(cl, options(contrasts = c("contr.sum", "contr.poly")))
         for (kk in 1:dimz) {
            if((lop$NoBrick > 1) | (!is.null(lop$resid))) Stat[,,kk,] <- aperm(parApply(cl, inData[,,kk,], c(1,2), runLME,
               DM=lop$dataStr, tag=0), c(2,3,1)) else
            Stat[,,kk,1] <- parApply(cl, inData[,,kk,], c(1,2), runLME, DM=lop$dataStr, tag=0)
            cat("Z slice #", kk, "done: ", format(Sys.time(), "%D %H:%M:%OS3"), "\n")
         }
         stopCluster(cl)
      } # volumetric data
      # runLME(inData[30,30,30,], lop$dataStr, 0)
   } # if (lop$nNodes>1)
} # LME modeling

#Top <- 100
Stat[is.nan(Stat)] <- 0
#Stat[Stat > Top] <- Top
#Stat[Stat < (-Top)] <- -Top

if(lop$TRR) {
   brickNames <- c(c(rbind(rownames(summary(fm)$coefficients), 
      paste0(rownames(summary(fm)$coefficients), '-Z'))), 'TRR-ave', 'TRR-con')

   statsym <- NULL
   for(n in 1:nrow(summary(fm)$coefficients)) 
      statsym <- c(statsym, list(list(sb=2*n-1, typ="fizt", par=NULL)))
} else {
   brickNames <- paste(rownames(anova(fm, type=lop$SS_type)), 'Chi-sq')
   if(lop$num_glt > 0) for (n in 1:lop$num_glt) {
      brickNames <- append(brickNames, lop$gltLabel[n])
      brickNames <- append(brickNames, paste(lop$gltLabel[n], "Z"))
   }
   if(lop$num_glf > 0) for (n in 1:lop$num_glf) {
      brickNames <- append(brickNames, paste(lop$glfLabel[n], "Chi-sq"))
   }
   if(lop$R2) brickNames <- append(brickNames, c('R2m', 'R2c'))
   
   statsym <- NULL
   if(lop$nF > 0) for (n in 1:lop$nF)
      statsym <- c(statsym, list(list(sb=n-1, typ="fict", par=2)))
   #   statsym <- c(statsym, list(list(sb=n-1, typ="fizt", par=NULL)))
   if(lop$num_glt > 0) for (n in 1:lop$num_glt)
      statsym <- c(statsym, list(list(sb=lop$nF+2*n-1, typ="fizt", par=NULL)))
   if(lop$num_glf > 0) for (n in 1:lop$num_glf)
      statsym <- c(statsym, list(list(sb=lop$nF+lop$num_glt+n-1, typ="fict", par=2)))
#      statsym <- c(statsym, list(list(sb=lop$nF+2*lop$num_glt+n, typ="fizt", par=NULL)))
}

write.AFNI(lop$outFN, Stat[,,,1:lop$NoBrick,drop=FALSE], brickNames, defhead=head, idcode=newid.AFNI(),
   com_hist=lop$com_history, statsym=statsym, addFDR=1, type='MRI_float', scale=FALSE)

if(!is.null(lop$resid))
   write.AFNI(lop$resid, Stat[,,,(lop$NoBrick+1):(lop$NoBrick+(!is.null(lop$resid))*nrow(lop$dataStr)), drop=FALSE],
      label=0:(dim(lop$dataStr)[1]-1), defhead=head, idcode=newid.AFNI(), com_hist=lop$com_history, type='MRI_float', scale=FALSE)

print(sprintf("Congratulations! You've got an output %s", lop$outFN))

##############  END  ############
