#!/usr/bin/env Rscript

#
# generate-options.R
#
# Copyright (C) 2022 by Posit Software, PBC
#
# Unless you have received this program directly from Posit Software pursuant
# to the terms of a commercial license agreement with Posit Software, then
# this program is licensed to you under the terms of version 3 of the
# GNU Affero General Public License. This program is distributed WITHOUT
# ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
# MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
# AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.


# This script generates binary options for various RStudio Server binaries,
# creating the necessary boiler plate options code as well as creating
# documentation for the options in the form of appendices to the admin
# guide. 
#
# To add options, do the following:
#
# 1. Add your option to the JSON options file corresponding to the binary
# that the option belongs to
# 2. In a terminal, navigate to this folder (src/cpp/)
# 3. Run this script: /generate-options.R
# 4. Commit the .gen.hpp files and the admin guide documentation files

require(here)
require(jsonlite)
require(stringi)
require(stringr)
require(tibble)

root <- here::here()
setwd(root)

defaultOptionsFiles <- c(
   "src/cpp/server/server-options.json",
   "src/cpp/session/session-options.json"
)

generateCopyright <- function (filename) {
   sprintf("/*
 * %s
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */", filename)
}

generateHandEditWarning <- function () {
   "// THIS FILE WAS AUTOMATICALLY GENERATED BY A TOOL (generate-options.R) - DO NOT HAND EDIT IT"
}

generateQmdHandEditWarning <- function (sourceFile, overlayFile) {
   overlayLine <- if (!is.null(overlayFile) && file.exists(overlayFile)) {
      sprintf("\n(and %s in rstudio/rstudio-pro for Pro-only options)", overlayFile)
   } else {
      ""
   }
   sprintf("<!--
THIS FILE WAS AUTOMATICALLY GENERATED BY A TOOL (generate-options.R) - DO NOT HAND EDIT IT.
To modify these options, edit %s in rstudio/rstudio%s,
then re-run: Rscript scripts/generate-options.R
-->", sourceFile, overlayLine)
}

generateIncludeGuard <- function (includeGuardName) {
   sprintf("#ifndef %s
#define %s", includeGuardName, includeGuardName)
}

generateEndIncludeGuard <- function (includeGuardName) {
   sprintf("#endif // %s", includeGuardName)
}

generateIncludes <- function (includes) {
   includeStr <- ""
   for (include in includes) {
      if (!startsWith(include, "<")) {
         include <- sprintf("\"%s\"", include)
      }
      includeStr <- paste0(includeStr, sprintf("#include %s\n", include))
   }
   includeStr
}

generateNamespace <- function (namespaceName) {
   namespaces <- strsplit(namespaceName, "::")
   namespaceStr <- ""
   for (namespace in namespaces[[1]]) {
      namespaceStr <- paste0(namespaceStr, sprintf("namespace %s {\n", namespace))
   }
   namespaceStr
}

generateEndNamespace <- function (namespaceName) {
   namespaces <- strsplit(namespaceName, "::")
   namespaceStr <- ""
   for (namespace in rev(namespaces[[1]])) {
      namespaceStr <- paste0(namespaceStr, sprintf("} // namespace %s\n", namespace))
   }
   namespaceStr
}

# converts a type specified within an options json file to the corresponding cpp type
cppTypeFromJsonType <- function (type) {
   if (!is.null(type)) {
      if (identical(type, "string")) {
         type <- "std::string"
      } else if (identical(type, "stringList")) {
         type <- "std::vector<std::string>"
      }
   }
   type
}

# generates qmd documentation for the given options
generateQmd <- function (optionsJson, overlayOptionsJson, sourceFile, overlayFile) {
   metadata <- optionsJson$metadata
   
   configFile <- metadata$configFile
   outputDocFile <- metadata$outputDocFile
   if (is.null(outputDocFile)) {
      stop(sprintf("No outputDocFile specified for %s config", configFile))
   }
   
   # header for Quarto file
   qmd <- paste("---",
                paste0("title: \"", configFile, "\""),
                "aliases:",
                paste0("  - /rstudio-configuration-1.html#", configFile),
                "---",
                "",
                generateQmdHandEditWarning(sourceFile, overlayFile),
                "",
                sep = "\n")

   docDescription <- metadata$docDescription
   if (is.null(docDescription)) {
      docDescription <- 
         sprintf("The following is a list of available options that can be specified in the `%s` configuration file",
                 configFile)
   }
   qmd <- paste0(qmd, "\n", docDescription, "\n\n")
   
   # combine options and overlay options
   options <- optionsJson$options
   overlayOptions <- overlayOptionsJson$options
   for (category in names(overlayOptions)) {
      for (option in overlayOptions[[category]]) {
         option[["isOverlay"]] <- TRUE
         options[[category]][[length(options[[category]]) + 1]] <- option
      }
   }
   
   for (category in names(options)) {
      categoryOptions <- ""
      for (option in options[[category]]) {
         optionName <- option$name
         if (is.null(optionName)) {
            # no name means this is a hidden option that should not be documented
            next
         }
         
         isHidden <- option$isHidden
         if (isTRUE(isHidden)) {
            # if marked hidden, do not document this option
            next
         }
         
         if (identical(typeof(optionName), "list")) {
            optionConstant <- optionName[["constant"]]
            optionName <- optionName[["value"]]
            if (is.null(optionName)) {
               stop(sprintf("No user constant value specified for %s - required for documentation", optionConstant))
            }
         }
         
         description <- option$description
         type <- option$tempType
         if (is.null(type)) {
            type <- option$type
         }
         
         # fix up type for commonly used non-boost types
         if (identical(type, "core::FilePath")) {
            type <- "string"
         } else if (identical(type, "stringList")) {
            type <- "string"
         }
         
         default <- option$defaultValue
         if (identical(type, "string") && (is.null(default) || identical(default, "") || identical(default, "std::string()"))) {
            default <- "<empty string>"
         } else if (identical(type, "bool") && identical(typeof(default), "logical")) {
            default <- if (default) "1" else "0"
         } else if (identical(typeof(default), "list")) {
            default <- default[["description"]]
            if (is.null(default)) {
               stop(sprintf("No code description given for default value for %s - required for documentation", optionName))
            }
         }

         isDeprecated <- option$isDeprecated
         if (isTRUE(isDeprecated)) {
            description <- sprintf("%s  \n**This option is deprecated and should not be used.**", description)
         }
         
         proHeader <- ""
         isOverlay <- option$isOverlay
         if (isTRUE(isOverlay)) {
            proHeader <- "{.pro-header}"
         }
         
         categoryOptions <- paste0(categoryOptions,
                                   sprintf("#### **%s** %s  \n", optionName, proHeader),
                                   description, "  \n  \n",
                                   sprintf("Type: %s  \n", type),
                                   sprintf("Default: `%s  `\n  \n", default))
      }
      
      if (str_length(categoryOptions) > 0) {
         qmd <- paste(qmd, sprintf("### *%s* Settings", category), sep="\n")
         qmd <- paste(qmd, categoryOptions, sep="\n\n")
      }
   }
   
   writeLines(qmd, con = outputDocFile)
}

# generates the include guard name
getIncludeGuard <- function (metadata) {
   # generate include guard
   includeGuard <- metadata$includeGuard
   if (is.null(includeGuard)) {
      # generate include guard from output file name when not specified
      includeGuard <- str_replace_all(toupper(basename(metadata$outputSourceFile)), fixed("."), "_")
   }
   includeGuard
}

# generates options CPP code in boost::program_options format
generateProgramOptions <- function (optionsJson, overlayOptionsJson) {
   metadata <- optionsJson$metadata
   
   outputSourceFile <- metadata$outputSourceFile
   if (is.null(outputSourceFile)) {
      stop("Required metadata parameter outputSourceFile not specified")
   }
   
   # generate copyright notice
   sourceFilename <- basename(outputSourceFile)
   generatedContents <- generateCopyright(sourceFilename)
   
   # generate hand edit warning
   generatedContents <- paste(generatedContents, generateHandEditWarning(), sep="\n\n")
   
   # generate include guard
   includeGuard <- getIncludeGuard(metadata)
   generatedContents <- paste(generatedContents, generateIncludeGuard(includeGuard), sep="\n\n")
   
   # generate include directives
   includes <- c("<string>", "<map>", "<shared_core/FilePath.hpp>", "<core/ProgramOptions.hpp>")
   additionalIncludes <- metadata$additionalIncludes
   includes <- c(includes, additionalIncludes)
   
   generatedContents <- paste(generatedContents, generateIncludes(includes), sep="\n\n")
   
   # generate namespace
   namespace <- metadata$namespace
   generatedContents <- paste(generatedContents, generateNamespace(namespace), sep="\n\n")
   
   # generate class which holds the options
   classContents <- paste0("class GeneratedOptions\n",
                           "{\n",
                           "public:\n",
                           "   virtual ~GeneratedOptions() {}\n",
                           "   virtual core::ProgramStatus read(int argc,\n",
                           "                                    char * const argv[],\n",
                           "                                    std::ostream& osWarnings) = 0;")
   
   # generate accessors and members
   accessors <- "public:\n"
   members <- "protected:\n"
   options <- optionsJson$options
   for (category in names(options)) {
      for (option in options[[category]]) {
         # build property for this option
         name <- option$name
         type <- cppTypeFromJsonType(option$type)
         if (is.null(type)) {
            stop(sprintf("No type specified for option %s", name))
         }
         
         # fix up member type for commonly used non-boost types
         memberType <- type
         if (identical(memberType, "core::FilePath")) {
            memberType <- "std::string"
         }
         
         memberName <- option$memberName
         if (!is.null(memberName)) {
            members <- paste0(members, sprintf("   %s %s;\n", memberType, memberName))
         }
         
         # build accessor for this option
         skipAccessor <- option$skipAccessorGeneration
         if (is.null(skipAccessor)) {
            skipAccessor <- FALSE
         }
         if (skipAccessor) {
            next
         }
         
         accessorName <- option$accessorName
         if (is.null(accessorName)) {
            if (!is.null(memberName)) {
               # default accessor to the member name without any trailing underscores
               accessorName <- str_replace(memberName, fixed("_"), "")
            } else {
               stop(sprintf("One of accessorName or memberName must be specified for option %s", name))
            }
         }
         
         accessorCode <- ""
         accessorValue <- option$accessorValue
         if (is.null(accessorValue)) {
            # handle any implicit conversions that we should support
            if (identical(type, "core::FilePath")) {
               accessorCode <- sprintf("return core::FilePath(%s);", memberName)
            } else if (identical(category, "allow")) {
               accessorCode <- sprintf("return %s || allowOverlay();", memberName)
            } else {
               accessorCode <- sprintf("return %s;", memberName)
            }
         } else {
            accessorCode <- accessorValue[["code"]]
            if (is.null(accessorCode)) {
               stop(sprintf("Invalid accessorValue specified - must be a code specification: %s", accessorValue))
            }
         }
         
         accessors <- paste0(accessors, sprintf("   %s %s() const { %s }\n", type, accessorName, accessorCode))
      }
   }
   
   # generate the buildOptions function which handles the boost options interface
   buildOptions <- paste0("protected:\n",
                          "   rstudio::core::program_options::OptionsDescription\n",
                          "   buildOptions(")
   buildOptionsArgs <- c()
   categoryVars <- c()
   for (category in names(options)) {
      # transform category names into output parameters
      categoryParam <- category
      substr(categoryParam, 1, 1) <- toupper(substr(categoryParam, 1, 1))
      categoryParam <- paste0("p", categoryParam)
      categoryVars[category] <- categoryParam
      buildOptionsArgs <- append(buildOptionsArgs, sprintf("boost::program_options::options_description* %s", categoryParam))
   }
   for (category in names(options)) {
      for (option in options[[category]]) {
         tempName <- option$tempName
         tempType <- cppTypeFromJsonType(option$tempType)
         if (!is.null(tempName) && !is.null(tempType)) {
            buildOptionsArgs <- append(buildOptionsArgs, sprintf("%s* %s", tempType, tempName))
         }
      }
   }
   buildOptionsArgsStr <- paste(buildOptionsArgs, collapse=",\n                ")
   buildOptions <- paste0(buildOptions, 
                          buildOptionsArgsStr, 
                          ")\n{\n",
                          "   using namespace rstudio::core;\n",
                          "   using namespace boost::program_options;\n")
   
   # now generate the meat of the function, the boost program options integration
   boostOptions <- c()
   for (category in names(options)) {
      if (length(boostOptions) > 0) {
         # stamp the ending semicolon for the last option in the previous category
         boostOptions <- paste0(boostOptions, ";")
      }
      boostOptions <- paste(boostOptions, sprintf("%s->add_options()", categoryVars[[category]]), sep="\n\n   ")
      
      for (option in options[[category]]) {
         optionName <- option$name
         if (is.null(optionName)) {
            # no member name means this is likely just an accessor option and should not be a true option
            # simply continue to the next
            next
         }
         
         # fixup constants for names
         isOptionNameConstant <- FALSE
         if (identical(typeof(optionName), "list")) {
            optionName <- optionName[["constant"]]
            isOptionNameConstant <- TRUE
         }
         
         isShortNameConstant <- FALSE
         shortName <- option$shortName
         if (identical(typeof(shortName), "list")) {
            shortName <- shortName[["constant"]]
            isShortNameConstant <- TRUE
         }
         
         storageType <- cppTypeFromJsonType(option$tempType)
         if (is.null(storageType)) {
            storageType <- cppTypeFromJsonType(option$type)
            if (is.null(storageType)) {
               stop(sprintf("No variable type defined for option %s", optionName))
            }
         }
         
         # last minute type fixes for commonly used non-boost types
         if (identical(storageType, "core::FilePath")) {
            storageType <- "std::string"
         }
         
         storageMember <- option$tempName
         if (is.null(storageMember)) {
            # if no temp variable was defined, then use the member variable
            storageMember <- paste0("&", option$memberName)
            if (is.null(storageMember)) {
               stop(sprintf("No memberName or tempName defined for option %s", optionName))
            }
         }
         
         # fix up the default value based on the variable type
         defaultValue <- option$defaultValue
         if (is.null(defaultValue)) {
            # resolve the default value based on the variable type
            if (identical(storageType, "std::string")) {
               defaultValue <- "std::string()"
            } else if (identical(storageType, "bool")) {
               defaultValue <- "true"
            } else if (identical(storageType, "std::vector<std::string>")) {
               defaultValue <- "std::vector<std::string>()"
            } else {
               stop(sprintf("No defaultValue specified for option %s, and could not be inferred", optionName))
            }
         } else {
            defaultValueCode <- if (identical(typeof(defaultValue), "list")) defaultValue[["code"]] else NULL
            if (is.null(defaultValueCode)) {
               if (identical(storageType, "std::string")) {
                  if (defaultValue != "std::string()") {
                     # strings need to be quoted
                     defaultValue <- sprintf("\"%s\"", defaultValue)
                  }
               } else if (identical(storageType, "bool")) {
                  defaultValue <- if (defaultValue) "true" else "false"   
               }
            } else {
               defaultValue <- defaultValueCode
            }
         }
         
         implicitValueStr <- ""
         implicitValue <- option$implicitValue
         if (is.null(implicitValue)) {
            implicitValueStr <- ""
         } else {
            implicitValueCode <- if (identical(typeof(implicitValue), "list")) implicitValue[["code"]] else NULL
            if (is.null(implicitValueCode)) {
               if (identical(storageType, "std::string")) {
                  if (implicitValue != "std::string()") {
                     # strings need to be quoted
                     implicitValue <- sprintf("\"%s\"", implicitValue)
                  }
               } else if (identical(storageType, "bool")) {
                  implicitValue <- if (implicitValue) "true" else "false"   
               }
            } else {
               implicitValue <- defaultValueCode
            }
            
            implicitValueStr <- sprintf("->implicit_value(%s)", implicitValue)
         }
         
         isMultitoken <- option$isMultitoken
         if (is.null(isMultitoken)) {
            isMultitoken <- FALSE
         }
         multitoken <- ""
         if (isMultitoken) {
            multitoken <- "->multitoken()"
         }
         
         description <- option$description
         if (is.null(description)) {
            stop(sprintf("No description specified for option %s", optionName))
         }
         description <- str_replace_all(description, fixed("\""), "\\\"")
         description <- sprintf("\"%s\"", description)
         
         optionName <- if (isOptionNameConstant) optionName else sprintf("\"%s\"", optionName)
         if (!is.null(shortName)) {
            shortName <- if (isShortNameConstant) shortName else sprintf("\"%s\"", shortName)
            shortName <- sprintf("%s", shortName)
            
            optionName <- sprintf("%s \",\" %s", optionName, shortName)
         }
         
         optionSet <- paste0(sprintf("      (%s,\n", optionName),
                             sprintf("      value<%s>(%s)->default_value(%s)%s%s,\n", storageType, storageMember, defaultValue, implicitValueStr, multitoken),
                             sprintf("      %s)", description))
         boostOptions <- paste(boostOptions, optionSet, sep="\n")                               
      }
   }
   
   boostOptions <- trimws(boostOptions, "left")
   boostOptions <- paste0(boostOptions, ";")
   buildOptions <- paste(buildOptions, boostOptions, sep="\n   ")
   
   # generate config file information
   configFile <- metadata$configFile
   binary <- str_replace(configFile, fixed(".conf"), "")
   buildOptions <- paste0(buildOptions,
                          "\n\n",
                          sprintf("   FilePath defaultConfigPath = core::system::xdg::findSystemConfigFile(\"%s configuration\", \"%s\");\n", binary, configFile),
                          "   std::string configFile = defaultConfigPath.exists() ?\n",
                          "      defaultConfigPath.getAbsolutePath() : \"\";\n",
                          sprintf("   return program_options::OptionsDescription(\"%s\", configFile);", binary))
   
   # close out the buildOptions function
   buildOptions <- paste0(buildOptions, "\n}")
   
   classContents <- paste(classContents, buildOptions, sep="\n\n")
   classContents <- paste(classContents, accessors, sep="\n\n")
   classContents <- paste(classContents, members, sep="\n\n")
   classContents <- paste(classContents, "  virtual bool allowOverlay() const { return false; };\n")
   
   # finally, close out the class
   classContents <- paste0(classContents, "};")
   classContents <- paste(classContents, generateEndNamespace(namespace), sep="\n\n")
   classContents <- paste(classContents, generateEndIncludeGuard(includeGuard), sep="\n\n")
   
   generatedContents <- paste(generatedContents, classContents, sep="\n")
   writeLines(generatedContents, con = outputSourceFile)
   
   # generate overlay options if applicable
   if (!is.null(overlayOptionsJson)) {
      generateOverlayOptions(overlayOptionsJson)
   }
}

generateOverlayOptions <- function (overlayOptionsJson) {
   metadata <- overlayOptionsJson$metadata
   if (is.null(metadata)) {
      stop("Overlay options missing required metadata parameter")
   }
   
   outputSourceFile <- metadata$outputSourceFile
   if (is.null(outputSourceFile)) {
      stop("Overlay options missing required metadata parameter outputSourceFile")
   }
   
   outputHeaderFile <- metadata$outputHeaderFile
   if (is.null(outputHeaderFile)) {
      stop("Overlay options missing required metadata parameter outputHeaderFile")
   }
   
   # generate copyright notice
   sourceFilename <- basename(outputSourceFile)
   headerFilename <- basename(outputHeaderFile)
   generatedHeaderContents <- generateCopyright(headerFilename)
   generatedSourceContents <- generateCopyright(sourceFilename)
   
   # generate hand edit warning
   warning <- generateHandEditWarning()
   generatedSourceContents <- paste(generatedSourceContents, warning, sep="\n\n")
   generatedHeaderContents <- paste(generatedHeaderContents, warning, sep="\n\n")
   
   # generate include of header file in cpp file
   pos <- str_locate(outputHeaderFile, "include/")[2]
   if (!is.na(pos)) {
      includeDirective <- sprintf("#include <%s>", substr(outputHeaderFile, pos + 1, str_length(outputHeaderFile)))
   }
   generatedSourceContents <- paste(generatedSourceContents, includeDirective, sep="\n\n")
   
   # generate include guard
   includeGuard <- getIncludeGuard(metadata)
   generatedHeaderContents <- paste(generatedHeaderContents, generateIncludeGuard(includeGuard), sep="\n\n")
   
   # generate include directives
   includes <- c("<string>", "<map>", "<shared_core/FilePath.hpp>", "<core/ProgramOptions.hpp>")
   additionalIncludes <- metadata$additionalIncludes
   if (!is.null(additionalIncludes)) {
      includes <- c(includes, additionalIncludes)
   }
   generatedHeaderContents <- paste(generatedHeaderContents, generateIncludes(includes), sep="\n\n")
   
   # generate #define directives for all of the specified overlay options constants
   options <- overlayOptionsJson$options
   constants <- data.frame(Constant=character(), Value=character())
   for (category in names(options)) {
      for (option in options[[category]]) {
         optionName <- option$name
         if (is.null(optionName)) {
            stop("Option with no name specified in overlay options. All overlay options must specify a name.")
         }
         
         optionConstant <- optionName[["constant"]]
         if (is.null(optionConstant)) {
            stop(sprintf("Overlay option %s specified without a constant. All overlay option names must be constants.", optionName))
         }
         
         optionConstantValue <- optionName[["value"]]
         if (is.null(optionConstantValue)) {
            stop(sprintf("Overlay option %s constant specified without a value. All constants must have values for documentation.", optionName))
         }
         
         skipDefine <- optionName[["skipDefine"]]
         if (!is.null(skipDefine) && skipDefine) {
            # this definition should be skipped - it is most likely an external definition
            next
         }
         
         constants <- constants %>% add_row(Constant = optionConstant, Value = optionConstantValue)
      }
   }
   
   # add additional constants
   additionalConstants <- metadata$additionalConstants
   if (!is.null(additionalConstants)) {
      for (constant in additionalConstants) {
         constants <- constants %>% add_row(Constant = constant[["constant"]], Value = constant[["value"]])
      }
   }
   
   defines <- ""
   nameLengths <- c()
   for (i in seq_len(nrow(constants))) {
      constant <- constants[i,]
      
      # ensure the constant values line up properly by tracking how many spaces they should have after the constant
      optionConstant <- constant[["Constant"]]
      optionConstantValue <- constant[["Value"]]
      
      constantLength <- str_length(optionConstant)
      nameLengths <- append(nameLengths, constantLength)
      
      defines <- paste0(defines, "#define ", optionConstant, sprintf("<SPACER%s>", i), sprintf("\"%s\"", optionConstantValue), "\n")
   }
  
   # fill in spacers to line up the constants to make them more readable
   iter <- 1
   maxNameLengths <- max(unlist(nameLengths))
   for (i in nameLengths) {
      spaces <- strrep(" ", maxNameLengths - i + 1)
      defines <- str_replace_all(defines, fixed(sprintf("<SPACER%s>", iter)), spaces)
      iter <- iter+1
   }
   
   generatedHeaderContents <- paste(generatedHeaderContents, defines, sep="\n\n")
   
   # generate namespace
   namespace <- metadata$namespace
   generatedSourceContents <- paste(generatedSourceContents, generateNamespace(namespace), sep="\n\n")
   
   # generate addOverlayOptions function
   addOverlayOptionsFunc <- paste0("void Options::addOverlayOptions(\n      ")
   
   categoryVars <- c()
   overlayOptionsArgs <- c()
   for (category in names(options)) {
      # transform category names into output parameters
      categoryParam <- category
      substr(categoryParam, 1, 1) <- toupper(substr(categoryParam, 1, 1))
      categoryParam <- paste0("p", categoryParam)
      categoryVars[category] <- categoryParam
      overlayOptionsArgs <- append(overlayOptionsArgs, sprintf("boost::program_options::options_description* %s", categoryParam))
   }
   overlayOptionsArgsStr <- paste(overlayOptionsArgs, collapse = ",\n      ")
   addOverlayOptionsFunc <- paste0(addOverlayOptionsFunc, overlayOptionsArgsStr, ")\n{\n",
                                   "   using namespace boost::program_options;\n")
   
   # now generate the meat of the function, the boost program options integration
   boostOptions <- c()
   for (category in names(options)) {
      if (length(boostOptions) > 0) {
         # stamp the ending semicolon for the last option in the previous category
         boostOptions <- paste0(boostOptions, ";")
      }
      boostOptions <- paste(boostOptions, sprintf("%s->add_options()", categoryVars[[category]]), sep="\n\n   ")
      
      for (option in options[[category]]) {
         optionName <- option$name
         optionName <- optionName[["constant"]]
                             
         type <- option$type
         defaultValue <- option$defaultValue
         
         if (is.null(defaultValue)) {
            defaultValue <- "std::string()"
         } else {
            defaultValueCode <- if (identical(typeof(defaultValue), "list")) defaultValue[["code"]] else NULL
            if (is.null(defaultValueCode)) {
               if (defaultValue != "std::string()") {
                  if (identical(type, "bool")) {
                     # convert TRUE and FALSE to "1" and "0"
                     if (identical(typeof(defaultValue), "logical")) {
                        defaultValue <- if (defaultValue) "1" else "0"
                     }
                  }
                  
                  # strings need to be quoted
                  defaultValue <- sprintf("\"%s\"", defaultValue)
               }
            } else {
               defaultValue <- defaultValueCode
            }
         }
         
         description <- option$description
         if (is.null(description)) {
            stop(sprintf("No description specified for option %s", optionName))
         }
         
         description <- str_replace_all(description, fixed("\""), "\\\"")
         description <- sprintf("\"%s\"", description)
         
         optionSet <- paste0(sprintf("      (%s,\n", optionName),
                             sprintf("      value<std::string>(&overlayOptions_[%s])->default_value(%s),\n", optionName, defaultValue),
                             sprintf("      %s)", description))
         boostOptions <- paste(boostOptions, optionSet, sep="\n")      
      }
   }
   
   boostOptions <- trimws(boostOptions, "left")
   boostOptions <- paste0(boostOptions, ";")
   addOverlayOptionsFunc <- paste(addOverlayOptionsFunc, boostOptions, sep="\n   ")
   
   generatedSourceContents <- paste(generatedSourceContents, addOverlayOptionsFunc, sep="\n")
   
   # close out the function
   generatedSourceContents <- paste(generatedSourceContents, "}", sep="\n")
   generatedSourceContents <- paste(generatedSourceContents, generateEndNamespace(namespace), sep="\n\n")
   
   generatedHeaderContents <- paste(generatedHeaderContents, generateEndIncludeGuard(includeGuard), sep="\n\n")
   
   writeLines(generatedSourceContents, con = outputSourceFile)
   writeLines(generatedHeaderContents, con = outputHeaderFile)
}

# generates options CPP code and admin guide documentation for the given options JSON file
generate <- function (optionsFile) {
   optionsJson <- jsonlite::read_json(optionsFile)

   metadata <- optionsJson$metadata
   if (is.null(metadata)) {
      stop(sprintf("Options file %s missing required metadata section", optionsFile))
   }
   
   generatorType <- metadata$generatorType
   if (is.null(generatorType)) {
      generatorType <- "ProgramOptions"
   }
   
   if (identical(generatorType, "ProgramOptions")) {
      directory <- dirname(optionsFile)
      filename <- tools::file_path_sans_ext(basename(optionsFile))
      ext <- tools::file_ext(optionsFile)
      overlayFile = file.path(directory, sprintf("%s-overlay.%s", filename, ext))
      
      overlayOptionsJson <- NULL
      if (file.exists(overlayFile)) {
         overlayOptionsJson <- jsonlite::read_json(overlayFile)
      }
      
      cat(sprintf("Generating program options from %s\n", optionsFile))
      if (!is.null(overlayOptionsJson)) {
         cat(sprintf("Using overlay options file %s\n", overlayFile))
      }
      generateProgramOptions(optionsJson, overlayOptionsJson)
      
      cat("Generating documentation\n")
      generateQmd(optionsJson, overlayOptionsJson, optionsFile, overlayFile)
   } else {
      stop(sprintf("Requested generator type %s is not supported", generatorType))
   }
}

main <- function () {

   args <- commandArgs(trailingOnly=TRUE)
   if (length(args) == 0) {
      args <- defaultOptionsFiles
   }

   for (optionsFile in args) {
      if (!file.exists(optionsFile)) {
         stop(sprintf("Options file %s does not exist - ensure your working directory is set to this script's location", 
                      optionsFile))
      }
      
      generate(optionsFile)
   }

}

main()
