// star_align_reads v0.2.0
// 
// This wrapper script is auto-generated by viash 0.9.0 and is thus a derivative
// work thereof. This software comes with ABSOLUTELY NO WARRANTY from Data
// Intuitive.
// 
// The component may contain files which fall under a different license. The
// authors of this component should specify the license in the header of such
// files, or include a separate license file detailing the licenses of all included
// files.
// 
// Component authors:
//  * Angela Oliveira Pisco (author)
//  * Robrecht Cannoodt (author, maintainer)

////////////////////////////
// VDSL3 helper functions //
////////////////////////////

// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_checkArgumentType.nf'
class UnexpectedArgumentTypeException extends Exception {
  String errorIdentifier
  String stage
  String plainName
  String expectedClass
  String foundClass
  
  // ${key ? " in module '$key'" : ""}${id ? " id '$id'" : ""}
  UnexpectedArgumentTypeException(String errorIdentifier, String stage, String plainName, String expectedClass, String foundClass) {
    super("Error${errorIdentifier ? " $errorIdentifier" : ""}:${stage ? " $stage" : "" } argument '${plainName}' has the wrong type. " +
      "Expected type: ${expectedClass}. Found type: ${foundClass}")
    this.errorIdentifier = errorIdentifier
    this.stage = stage
    this.plainName = plainName
    this.expectedClass = expectedClass
    this.foundClass = foundClass
  }
}

/**
  * Checks if the given value is of the expected type. If not, an exception is thrown.
  *
  * @param stage The stage of the argument (input or output)
  * @param par The parameter definition
  * @param value The value to check
  * @param errorIdentifier The identifier to use in the error message
  * @return The value, if it is of the expected type
  * @throws UnexpectedArgumentTypeException If the value is not of the expected type
*/
def _checkArgumentType(String stage, Map par, Object value, String errorIdentifier) {
  // expectedClass will only be != null if value is not of the expected type
  def expectedClass = null
  def foundClass = null
  
  // todo: split if need be
  
  if (!par.required && value == null) {
    expectedClass = null
  } else if (par.multiple) {
    if (value !instanceof Collection) {
      value = [value]
    }
    
    // split strings
    value = value.collectMany{ val ->
      if (val instanceof String) {
        // collect() to ensure that the result is a List and not simply an array
        val.split(par.multiple_sep).collect()
      } else {
        [val]
      }
    }

    // process globs
    if (par.type == "file" && par.direction == "input") {
      value = value.collect{ it instanceof String ? file(it, hidden: true) : it }.flatten()
    }

    // check types of elements in list
    try {
      value = value.collect { listVal ->
        _checkArgumentType(stage, par + [multiple: false], listVal, errorIdentifier)
      }
    } catch (UnexpectedArgumentTypeException e) {
      expectedClass = "List[${e.expectedClass}]"
      foundClass = "List[${e.foundClass}]"
    }
  } else if (par.type == "string") {
    // cast to string if need be
    if (value instanceof GString) {
      value = value.toString()
    }
    expectedClass = value instanceof String ? null : "String"
  } else if (par.type == "integer") {
    // cast to integer if need be
    if (value instanceof String) {
      try {
        value = value.toInteger()
      } catch (NumberFormatException e) {
        // do nothing
      }
    }
    if (value instanceof java.math.BigInteger) {
      value = value.intValue()
    }
    expectedClass = value instanceof Integer ? null : "Integer"
  } else if (par.type == "long") {
    // cast to long if need be
    if (value instanceof String) {
      try {
        value = value.toLong()
      } catch (NumberFormatException e) {
        // do nothing
      }
    }
    if (value instanceof Integer) {
      value = value.toLong()
    }
    expectedClass = value instanceof Long ? null : "Long"
  } else if (par.type == "double") {
    // cast to double if need be
    if (value instanceof String) {
      try {
        value = value.toDouble()
      } catch (NumberFormatException e) {
        // do nothing
      }
    }
    if (value instanceof java.math.BigDecimal) {
      value = value.doubleValue()
    }
    if (value instanceof Float) {
      value = value.toDouble()
    }
    expectedClass = value instanceof Double ? null : "Double"
  } else if (par.type == "boolean" | par.type == "boolean_true" | par.type == "boolean_false") {
    // cast to boolean if need be
    if (value instanceof String) {
      def valueLower = value.toLowerCase()
      if (valueLower == "true") {
        value = true
      } else if (valueLower == "false") {
        value = false
      }
    }
    expectedClass = value instanceof Boolean ? null : "Boolean"
  } else if (par.type == "file" && (par.direction == "input" || stage == "output")) {
    // cast to path if need be
    if (value instanceof String) {
      value = file(value, hidden: true)
    }
    if (value instanceof File) {
      value = value.toPath()
    }
    expectedClass = value instanceof Path ? null : "Path"
  } else if (par.type == "file" && stage == "input" && par.direction == "output") {
    // cast to string if need be
    if (value instanceof GString) {
      value = value.toString()
    }
    expectedClass = value instanceof String ? null : "String"
  } else {
    // didn't find a match for par.type
    expectedClass = par.type
  }

  if (expectedClass != null) {
    if (foundClass == null) {
      foundClass = value.getClass().getName()
    }
    throw new UnexpectedArgumentTypeException(errorIdentifier, stage, par.plainName, expectedClass, foundClass)
  }
  
  return value
}
// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processInputValues.nf'
Map _processInputValues(Map inputs, Map config, String id, String key) {
  if (!workflow.stubRun) {
    config.allArguments.each { arg ->
      if (arg.required) {
        assert inputs.containsKey(arg.plainName) && inputs.get(arg.plainName) != null : 
          "Error in module '${key}' id '${id}': required input argument '${arg.plainName}' is missing"
      }
    }

    inputs = inputs.collectEntries { name, value ->
      def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") }
      assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid input argument"

      value = _checkArgumentType("input", par, value, "in module '$key' id '$id'")

      [ name, value ]
    }
  }
  return inputs
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/arguments/_processOutputValues.nf'
Map _processOutputValues(Map outputs, Map config, String id, String key) {
  if (!workflow.stubRun) {
    config.allArguments.each { arg ->
      if (arg.direction == "output" && arg.required) {
        assert outputs.containsKey(arg.plainName) && outputs.get(arg.plainName) != null : 
          "Error in module '${key}' id '${id}': required output argument '${arg.plainName}' is missing"
      }
    }

    outputs = outputs.collectEntries { name, value ->
      def par = config.allArguments.find { it.plainName == name && it.direction == "output" }
      assert par != null : "Error in module '${key}' id '${id}': '${name}' is not a valid output argument"
      
      value = _checkArgumentType("output", par, value, "in module '$key' id '$id'")
      
      [ name, value ]
    }
  }
  return outputs
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/IDChecker.nf'
class IDChecker {
  final def items = [] as Set

  @groovy.transform.WithWriteLock
  boolean observe(String item) {
    if (items.contains(item)) {
      return false
    } else {
      items << item
      return true
    }
  }

  @groovy.transform.WithReadLock
  boolean contains(String item) {
    return items.contains(item)
  }

  @groovy.transform.WithReadLock
  Set getItems() {
    return items.clone()
  }
}
// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_checkUniqueIds.nf'

/**
 * Check if the ids are unique across parameter sets
 *
 * @param parameterSets a list of parameter sets.
 */
private void _checkUniqueIds(List<Tuple2<String, Map<String, Object>>> parameterSets) {
  def ppIds = parameterSets.collect{it[0]}
  assert ppIds.size() == ppIds.unique().size() : "All argument sets should have unique ids. Detected ids: $ppIds"
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_getChild.nf'

// helper functions for reading params from file //
def _getChild(parent, child) {
  if (child.contains("://") || java.nio.file.Paths.get(child).isAbsolute()) {
    child
  } else {
    def parentAbsolute = java.nio.file.Paths.get(parent).toAbsolutePath().toString()
    parentAbsolute.replaceAll('/[^/]*$', "/") + child
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_parseParamList.nf'
/**
  * Figure out the param list format based on the file extension
  *
  * @param param_list A String containing the path to the parameter list file.
  *
  * @return A String containing the format of the parameter list file.
  */
def _paramListGuessFormat(param_list) {
  if (param_list !instanceof String) {
    "asis"
  } else if (param_list.endsWith(".csv")) {
    "csv"
  } else if (param_list.endsWith(".json") || param_list.endsWith(".jsn")) {
    "json"
  } else if (param_list.endsWith(".yaml") || param_list.endsWith(".yml")) {
    "yaml"
  } else {
    "yaml_blob"
  }
}


/**
  * Read the param list
  * 
  * @param param_list One of the following:
  *   - A String containing the path to the parameter list file (csv, json or yaml),
  *   - A yaml blob of a list of maps (yaml_blob),
  *   - Or a groovy list of maps (asis).
  * @param config A Map of the Viash configuration.
  * 
  * @return A List of Maps containing the parameters.
  */
def _parseParamList(param_list, Map config) {
  // first determine format by extension
  def paramListFormat = _paramListGuessFormat(param_list)

  def paramListPath = (paramListFormat != "asis" && paramListFormat != "yaml_blob") ?
    file(param_list, hidden: true) :
    null

  // get the correct parser function for the detected params_list format
  def paramSets = []
  if (paramListFormat == "asis") {
    paramSets = param_list
  } else if (paramListFormat == "yaml_blob") {
    paramSets = readYamlBlob(param_list)
  } else if (paramListFormat == "yaml") {
    paramSets = readYaml(paramListPath)
  } else if (paramListFormat == "json") {
    paramSets = readJson(paramListPath)
  } else if (paramListFormat == "csv") {
    paramSets = readCsv(paramListPath)
  } else {
    error "Format of provided --param_list not recognised.\n" +
    "Found: '$paramListFormat'.\n" +
    "Expected: a csv file, a json file, a yaml file,\n" +
    "a yaml blob or a groovy list of maps."
  }

  // data checks
  assert paramSets instanceof List: "--param_list should contain a list of maps"
  for (value in paramSets) {
    assert value instanceof Map: "--param_list should contain a list of maps"
  }

  // id is argument
  def idIsArgument = config.allArguments.any{it.plainName == "id"}

  // Reformat from List<Map> to List<Tuple2<String, Map>> by adding the ID as first element of a Tuple2
  paramSets = paramSets.collect({ data ->
    def id = data.id
    if (!idIsArgument) {
      data = data.findAll{k, v -> k != "id"}
    }
    [id, data]
  })

  // Split parameters with 'multiple: true'
  paramSets = paramSets.collect({ id, data ->
    data = _splitParams(data, config)
    [id, data]
  })
  
  // The paths of input files inside a param_list file may have been specified relatively to the
  // location of the param_list file. These paths must be made absolute.
  if (paramListPath) {
    paramSets = paramSets.collect({ id, data ->
      def new_data = data.collectEntries{ parName, parValue ->
        def par = config.allArguments.find{it.plainName == parName}
        if (par && par.type == "file" && par.direction == "input") {
          if (parValue instanceof Collection) {
            parValue = parValue.collectMany{path -> 
              def x = _resolveSiblingIfNotAbsolute(path, paramListPath)
              x instanceof Collection ? x : [x]
            }
          } else {
            parValue = _resolveSiblingIfNotAbsolute(parValue, paramListPath) 
          }
        }
        [parName, parValue]
      }
      [id, new_data]
    })
  }

  return paramSets
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/_splitParams.nf'
/**
 * Split parameters for arguments that accept multiple values using their separator
 *
 * @param paramList A Map containing parameters to split.
 * @param config A Map of the Viash configuration. This Map can be generated from the config file
 *               using the readConfig() function.
 *
 * @return A Map of parameters where the parameter values have been split into a list using
 *         their seperator.
 */
Map<String, Object> _splitParams(Map<String, Object> parValues, Map config){
  def parsedParamValues = parValues.collectEntries { parName, parValue ->
    def parameterSettings = config.allArguments.find({it.plainName == parName})

    if (!parameterSettings) {
      // if argument is not found, do not alter 
      return [parName, parValue]
    }
    if (parameterSettings.multiple) { // Check if parameter can accept multiple values
      if (parValue instanceof Collection) {
        parValue = parValue.collect{it instanceof String ? it.split(parameterSettings.multiple_sep) : it }
      } else if (parValue instanceof String) {
        parValue = parValue.split(parameterSettings.multiple_sep)
      } else if (parValue == null) {
        parValue = []
      } else {
        parValue = [ parValue ]
      }
      parValue = parValue.flatten()
    }
    // For all parameters check if multiple values are only passed for
    // arguments that allow it. Quietly simplify lists of length 1.
    if (!parameterSettings.multiple && parValue instanceof Collection) {
      assert parValue.size() == 1 : 
      "Error: argument ${parName} has too many values.\n" +
      "  Expected amount: 1. Found: ${parValue.size()}"
      parValue = parValue[0]
    }
    [parName, parValue]
  }
  return parsedParamValues
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/channelFromParams.nf'
/**
 * Parse nextflow parameters based on settings defined in a viash config.
 * Return a list of parameter sets, each parameter set corresponding to 
 * an event in a nextflow channel. The output from this function can be used
 * with Channel.fromList to create a nextflow channel with Vdsl3 formatted 
 * events.
 *
 * This function performs:
 *   - A filtering of the params which can be found in the config file.
 *   - Process the params_list argument which allows a user to to initialise 
 *     a Vsdl3 channel with multiple parameter sets. Possible formats are 
 *     csv, json, yaml, or simply a yaml_blob. A csv should have column names 
 *     which correspond to the different arguments of this pipeline. A json or a yaml
 *     file should be a list of maps, each of which has keys corresponding to the
 *     arguments of the pipeline. A yaml blob can also be passed directly as a parameter.
 *     When passing a csv, json or yaml, relative path names are relativized to the
 *     location of the parameter file.
 *   - Combine the parameter sets into a vdsl3 Channel.
 *
 * @param params Input parameters. Can optionaly contain a 'param_list' key that
 *               provides a list of arguments that can be split up into multiple events
 *               in the output channel possible formats of param_lists are: a csv file, 
 *               json file, a yaml file or a yaml blob. Each parameters set (event) must
 *               have a unique ID.
 * @param config A Map of the Viash configuration. This Map can be generated from the config file
 *               using the readConfig() function.
 * 
 * @return A list of parameters with the first element of the event being
 *         the event ID and the second element containing a map of the parsed parameters.
 */
 
private List<Tuple2<String, Map<String, Object>>> _paramsToParamSets(Map params, Map config){
  // todo: fetch key from run args
  def key_ = config.name
  
  /* parse regular parameters (not in param_list)  */
  /*************************************************/
  def globalParams = config.allArguments
    .findAll { params.containsKey(it.plainName) }
    .collectEntries { [ it.plainName, params[it.plainName] ] }
  def globalID = params.get("id", null)

  /* process params_list arguments */
  /*********************************/
  def paramList = params.containsKey("param_list") && params.param_list != null ?
    params.param_list : []
  // if (paramList instanceof String) {
  //   paramList = [paramList]
  // }
  // def paramSets = paramList.collectMany{ _parseParamList(it, config) }
  // TODO: be able to process param_list when it is a list of strings
  def paramSets = _parseParamList(paramList, config)
  if (paramSets.isEmpty()) {
    paramSets = [[null, [:]]]
  }

  /* combine arguments into channel */
  /**********************************/
  def processedParams = paramSets.indexed().collect{ index, tup ->
    // Process ID
    def id = tup[0] ?: globalID
  
    if (workflow.stubRun && !id) {
      // if stub run, explicitly add an id if missing
      id = "stub${index}"
    }
    assert id != null: "Each parameter set should have at least an 'id'"

    // Process params
    def parValues = globalParams + tup[1]
    // // Remove parameters which are null, if the default is also null
    // parValues = parValues.collectEntries{paramName, paramValue ->
    //   parameterSettings = config.functionality.allArguments.find({it.plainName == paramName})
    //   if ( paramValue != null || parameterSettings.get("default", null) != null ) {
    //     [paramName, paramValue]
    //   }
    // }
    parValues = parValues.collectEntries { name, value ->
      def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") }
      assert par != null : "Error in module '${key_}' id '${id}': '${name}' is not a valid input argument"

      if (par == null) {
        return [:]
      }
      value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'")

      [ name, value ]
    }

    [id, parValues]
  }

  // Check if ids (first element of each list) is unique
  _checkUniqueIds(processedParams)
  return processedParams
}

/**
 * Parse nextflow parameters based on settings defined in a viash config 
 * and return a nextflow channel.
 * 
 * @param params Input parameters. Can optionaly contain a 'param_list' key that
 *               provides a list of arguments that can be split up into multiple events
 *               in the output channel possible formats of param_lists are: a csv file, 
 *               json file, a yaml file or a yaml blob. Each parameters set (event) must
 *               have a unique ID.
 * @param config A Map of the Viash configuration. This Map can be generated from the config file
 *               using the readConfig() function.
 * 
 * @return A nextflow Channel with events. Events are formatted as a tuple that contains 
 *         first contains the ID of the event and as second element holds a parameter map.
 *       
 *
 */
def channelFromParams(Map params, Map config) {
  def processedParams = _paramsToParamSets(params, config)
  return Channel.fromList(processedParams)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/checkUniqueIds.nf'
def checkUniqueIds(Map args) {
  def stopOnError = args.stopOnError == null ? args.stopOnError : true

  def idChecker = new IDChecker()

  return filter { tup ->
    if (!idChecker.observe(tup[0])) {
      if (stopOnError) {
        error "Duplicate id: ${tup[0]}"
      } else {
        log.warn "Duplicate id: ${tup[0]}, removing duplicate entry"
        return false
      }
    }
    return true
  }
}
// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/preprocessInputs.nf'
// This helper file will be deprecated soon
preprocessInputsDeprecationWarningPrinted = false

def preprocessInputsDeprecationWarning() {
  if (!preprocessInputsDeprecationWarningPrinted) {
    preprocessInputsDeprecationWarningPrinted = true
    System.err.println("Warning: preprocessInputs() is deprecated and will be removed in Viash 0.9.0.")
  }
}

/**
 * Generate a nextflow Workflow that allows processing a channel of 
 * Vdsl3 formatted events and apply a Viash config to them:
 *    - Gather default parameters from the Viash config and make 
 *      sure that they are correctly formatted (see applyConfig method).
 *    - Format the input parameters (also using the applyConfig method).
 *    - Apply the default parameter to the input parameters.
 *    - Do some assertions:
 *        ~ Check if the event IDs in the channel are unique.
 * 
 * The events in the channel are formatted as tuples, with the 
 * first element of the tuples being a unique id of the parameter set, 
 * and the second element containg the the parameters themselves.
 * Optional extra elements of the tuples will be passed to the output as is.
 *
 * @param args A map that must contain a 'config' key that points
 *              to a parsed config (see readConfig()). Optionally, a
 *              'key' key can be provided which can be used to create a unique
 *              name for the workflow process.
 *
 * @return A workflow that allows processing a channel of Vdsl3 formatted events
 * and apply a Viash config to them.
 */
def preprocessInputs(Map args) {
  preprocessInputsDeprecationWarning()

  def config = args.config
  assert config instanceof Map : 
    "Error in preprocessInputs: config must be a map. " +
    "Expected class: Map. Found: config.getClass() is ${config.getClass()}"
  def key_ = args.key ?: config.name

  // Get different parameter types (used throughout this function)
  def defaultArgs = config.allArguments
    .findAll { it.containsKey("default") }
    .collectEntries { [ it.plainName, it.default ] }

  map { tup ->
    def id = tup[0]
    def data = tup[1]
    def passthrough = tup.drop(2)

    def new_data = (defaultArgs + data).collectEntries { name, value ->
      def par = config.allArguments.find { it.plainName == name && (it.direction == "input" || it.type == "file") }
      
      if (par != null) {
        value = _checkArgumentType("input", par, value, "in module '$key_' id '$id'")
      }

      [ name, value ]
    }

    [ id, new_data ] + passthrough
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runComponents.nf'
/**
 * Run a list of components on a stream of data.
 * 
 * @param components: list of Viash VDSL3 modules to run
 * @param fromState: a closure, a map or a list of keys to extract from the input data.
 *   If a closure, it will be called with the id, the data and the component config.
 * @param toState: a closure, a map or a list of keys to extract from the output data
 *   If a closure, it will be called with the id, the output data, the old state and the component config.
 * @param filter: filter function to apply to the input.
 *   It will be called with the id, the data and the component config.
 * @param id: id to use for the output data
 *   If a closure, it will be called with the id, the data and the component config.
 * @param auto: auto options to pass to the components
 *
 * @return: a workflow that runs the components
 **/
def runComponents(Map args) {
  log.warn("runComponents is deprecated, use runEach instead")
  assert args.components: "runComponents should be passed a list of components to run"

  def components_ = args.components
  if (components_ !instanceof List) {
    components_ = [ components_ ]
  }
  assert components_.size() > 0: "pass at least one component to runComponents"

  def fromState_ = args.fromState
  def toState_ = args.toState
  def filter_ = args.filter
  def id_ = args.id

  workflow runComponentsWf {
    take: input_ch
    main:

    // generate one channel per method
    out_chs = components_.collect{ comp_ ->
      def comp_config = comp_.config

      def filter_ch = filter_
        ? input_ch | filter{tup ->
          filter_(tup[0], tup[1], comp_config)
        }
        : input_ch
      def id_ch = id_
        ? filter_ch | map{tup ->
          // def new_id = id_(tup[0], tup[1], comp_config)
          def new_id = tup[0]
          if (id_ instanceof String) {
            new_id = id_
          } else if (id_ instanceof Closure) {
            new_id = id_(new_id, tup[1], comp_config)
          }
          [new_id] + tup.drop(1)
        }
        : filter_ch
      def data_ch = id_ch | map{tup ->
          def new_data = tup[1]
          if (fromState_ instanceof Map) {
            new_data = fromState_.collectEntries{ key0, key1 ->
              [key0, new_data[key1]]
            }
          } else if (fromState_ instanceof List) {
            new_data = fromState_.collectEntries{ key ->
              [key, new_data[key]]
            }
          } else if (fromState_ instanceof Closure) {
            new_data = fromState_(tup[0], new_data, comp_config)
          }
          tup.take(1) + [new_data] + tup.drop(1)
        }
      def out_ch = data_ch
        | comp_.run(
          auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false]
        )
      def post_ch = toState_
        ? out_ch | map{tup ->
          def output = tup[1]
          def old_state = tup[2]
          def new_state = null
          if (toState_ instanceof Map) {
            new_state = old_state + toState_.collectEntries{ key0, key1 ->
              [key0, output[key1]]
            }
          } else if (toState_ instanceof List) {
            new_state = old_state + toState_.collectEntries{ key ->
              [key, output[key]]
            }
          } else if (toState_ instanceof Closure) {
            new_state = toState_(tup[0], output, old_state, comp_config)
          }
          [tup[0], new_state] + tup.drop(3)
        }
        : out_ch
      
      post_ch
    }

    // mix all results
    output_ch =
      (out_chs.size == 1)
        ? out_chs[0]
        : out_chs[0].mix(*out_chs.drop(1))

    emit: output_ch
  }

  return runComponentsWf
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/runEach.nf'
/**
 * Run a list of components on a stream of data.
 * 
 * @param components: list of Viash VDSL3 modules to run
 * @param fromState: a closure, a map or a list of keys to extract from the input data.
 *   If a closure, it will be called with the id, the data and the component itself.
 * @param toState: a closure, a map or a list of keys to extract from the output data
 *   If a closure, it will be called with the id, the output data, the old state and the component itself.
 * @param filter: filter function to apply to the input.
 *   It will be called with the id, the data and the component itself.
 * @param id: id to use for the output data
 *   If a closure, it will be called with the id, the data and the component itself.
 * @param auto: auto options to pass to the components
 *
 * @return: a workflow that runs the components
 **/
def runEach(Map args) {
  assert args.components: "runEach should be passed a list of components to run"

  def components_ = args.components
  if (components_ !instanceof List) {
    components_ = [ components_ ]
  }
  assert components_.size() > 0: "pass at least one component to runEach"

  def fromState_ = args.fromState
  def toState_ = args.toState
  def filter_ = args.filter
  def runIf_ = args.runIf
  def id_ = args.id

  assert !runIf_ || runIf_ instanceof Closure: "runEach: must pass a Closure to runIf."

  workflow runEachWf {
    take: input_ch
    main:

    // generate one channel per method
    out_chs = components_.collect{ comp_ ->
      def filter_ch = filter_
        ? input_ch | filter{tup ->
          filter_(tup[0], tup[1], comp_)
        }
        : input_ch
      def id_ch = id_
        ? filter_ch | map{tup ->
          def new_id = id_
          if (new_id instanceof Closure) {
            new_id = new_id(tup[0], tup[1], comp_)
          }
          assert new_id instanceof String : "Error in runEach: id should be a String or a Closure that returns a String. Expected: id instanceof String. Found: ${new_id.getClass()}"
          [new_id] + tup.drop(1)
        }
        : filter_ch
      def chPassthrough = null
      def chRun = null
      if (runIf_) {
        def idRunIfBranch = id_ch.branch{ tup ->
          run: runIf_(tup[0], tup[1], comp_)
          passthrough: true
        }
        chPassthrough = idRunIfBranch.passthrough
        chRun = idRunIfBranch.run
      } else {
        chRun = id_ch
        chPassthrough = Channel.empty()
      }
      def data_ch = chRun | map{tup ->
          def new_data = tup[1]
          if (fromState_ instanceof Map) {
            new_data = fromState_.collectEntries{ key0, key1 ->
              [key0, new_data[key1]]
            }
          } else if (fromState_ instanceof List) {
            new_data = fromState_.collectEntries{ key ->
              [key, new_data[key]]
            }
          } else if (fromState_ instanceof Closure) {
            new_data = fromState_(tup[0], new_data, comp_)
          }
          tup.take(1) + [new_data] + tup.drop(1)
        }
      def out_ch = data_ch
        | comp_.run(
          auto: (args.auto ?: [:]) + [simplifyInput: false, simplifyOutput: false]
        )
      def post_ch = toState_
        ? out_ch | map{tup ->
          def output = tup[1]
          def old_state = tup[2]
          def new_state = null
          if (toState_ instanceof Map) {
            new_state = old_state + toState_.collectEntries{ key0, key1 ->
              [key0, output[key1]]
            }
          } else if (toState_ instanceof List) {
            new_state = old_state + toState_.collectEntries{ key ->
              [key, output[key]]
            }
          } else if (toState_ instanceof Closure) {
            new_state = toState_(tup[0], output, old_state, comp_)
          }
          [tup[0], new_state] + tup.drop(3)
        }
        : out_ch

      def return_ch = post_ch
        | concat(chPassthrough)
      
      return_ch
    }

    // mix all results
    output_ch =
      (out_chs.size == 1)
        ? out_chs[0]
        : out_chs[0].mix(*out_chs.drop(1))

    emit: output_ch
  }

  return runEachWf
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/channel/safeJoin.nf'
/**
 * Join sourceChannel to targetChannel
 * 
 * This function joins the sourceChannel to the targetChannel. 
 * However, each id in the targetChannel must be present in the
 * sourceChannel. If _meta.join_id exists in the targetChannel, that is 
 * used as an id instead. If the id doesn't match any id in the sourceChannel,
 * an error is thrown.
 */

def safeJoin(targetChannel, sourceChannel, key) {
  def sourceIDs = new IDChecker()

  def sourceCheck = sourceChannel
    | map { tup ->
      sourceIDs.observe(tup[0])
      tup
    }
  def targetCheck = targetChannel
    | map { tup ->
      def id = tup[0]
      
      if (!sourceIDs.contains(id)) {
        error (
          "Error in module '${key}' when merging output with original state.\n" +
          "  Reason: output with id '${id}' could not be joined with source channel.\n" +
          "    If the IDs in the output channel differ from the input channel,\n" + 
          "    please set `tup[1]._meta.join_id to the original ID.\n" +
          "  Original IDs in input channel: ['${sourceIDs.getItems().join("', '")}'].\n" + 
          "  Unexpected ID in the output channel: '${id}'.\n" +
          "  Example input event: [\"id\", [input: file(...)]],\n" +
          "  Example output event: [\"newid\", [output: file(...), _meta: [join_id: \"id\"]]]"
        )
      }
      // TODO: add link to our documentation on how to fix this

      tup
    }
  
  sourceCheck.cross(targetChannel)
    | map{ left, right ->
      right + left.drop(1)
    }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/config/_processArgument.nf'
def _processArgument(arg) {
  arg.multiple = arg.multiple != null ? arg.multiple : false
  arg.required = arg.required != null ? arg.required : false
  arg.direction = arg.direction != null ? arg.direction : "input"
  arg.multiple_sep = arg.multiple_sep != null ? arg.multiple_sep : ";"
  arg.plainName = arg.name.replaceAll("^-*", "")

  if (arg.type == "file") {
    arg.must_exist = arg.must_exist != null ? arg.must_exist : true
    arg.create_parent = arg.create_parent != null ? arg.create_parent : true
  }

  // add default values to output files which haven't already got a default
  if (arg.type == "file" && arg.direction == "output" && arg.default == null) {
    def mult = arg.multiple ? "_*" : ""
    def extSearch = ""
    if (arg.default != null) {
      extSearch = arg.default
    } else if (arg.example != null) {
      extSearch = arg.example
    }
    if (extSearch instanceof List) {
      extSearch = extSearch[0]
    }
    def extSearchResult = extSearch.find("\\.[^\\.]+\$")
    def ext = extSearchResult != null ? extSearchResult : ""
    arg.default = "\$id.\$key.${arg.plainName}${mult}${ext}"
    if (arg.multiple) {
      arg.default = [arg.default]
    }
  }

  if (!arg.multiple) {
    if (arg.default != null && arg.default instanceof List) {
      arg.default = arg.default[0]
    }
    if (arg.example != null && arg.example instanceof List) {
      arg.example = arg.example[0]
    }
  }

  if (arg.type == "boolean_true") {
    arg.default = false
  }
  if (arg.type == "boolean_false") {
    arg.default = true
  }

  arg
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/config/addGlobalParams.nf'
def addGlobalArguments(config) {
  def localConfig = [
    "argument_groups": [
      [
        "name": "Nextflow input-output arguments",
        "description": "Input/output parameters for Nextflow itself. Please note that both publishDir and publish_dir are supported but at least one has to be configured.",
        "arguments" : [
          [
            'name': '--publish_dir',
            'required': true,
            'type': 'string',
            'description': 'Path to an output directory.',
            'example': 'output/',
            'multiple': false
          ],
          [
            'name': '--param_list',
            'required': false,
            'type': 'string',
            'description': '''Allows inputting multiple parameter sets to initialise a Nextflow channel. A `param_list` can either be a list of maps, a csv file, a json file, a yaml file, or simply a yaml blob.
            |
            |* A list of maps (as-is) where the keys of each map corresponds to the arguments of the pipeline. Example: in a `nextflow.config` file: `param_list: [ ['id': 'foo', 'input': 'foo.txt'], ['id': 'bar', 'input': 'bar.txt'] ]`.
            |* A csv file should have column names which correspond to the different arguments of this pipeline. Example: `--param_list data.csv` with columns `id,input`.
            |* A json or a yaml file should be a list of maps, each of which has keys corresponding to the arguments of the pipeline. Example: `--param_list data.json` with contents `[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]`.
            |* A yaml blob can also be passed directly as a string. Example: `--param_list "[ {'id': 'foo', 'input': 'foo.txt'}, {'id': 'bar', 'input': 'bar.txt'} ]"`.
            |
            |When passing a csv, json or yaml file, relative path names are relativized to the location of the parameter file. No relativation is performed when `param_list` is a list of maps (as-is) or a yaml blob.'''.stripMargin(),
            'example': 'my_params.yaml',
            'multiple': false,
            'hidden': true
          ]
          // TODO: allow multiple: true in param_list?
          // TODO: allow to specify a --param_list_regex to filter the param_list?
          // TODO: allow to specify a --param_list_from_state to remap entries in the param_list?
        ]
      ]
    ]
  ]

  return processConfig(_mergeMap(config, localConfig))
}

def _mergeMap(Map lhs, Map rhs) {
  return rhs.inject(lhs.clone()) { map, entry ->
    if (map[entry.key] instanceof Map && entry.value instanceof Map) {
      map[entry.key] = _mergeMap(map[entry.key], entry.value)
    } else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) {
      map[entry.key] += entry.value
    } else {
      map[entry.key] = entry.value
    }
    return map
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/config/generateHelp.nf'
def _generateArgumentHelp(param) {
  // alternatives are not supported
  // def names = param.alternatives ::: List(param.name)

  def unnamedProps = [
    ["required parameter", param.required],
    ["multiple values allowed", param.multiple],
    ["output", param.direction.toLowerCase() == "output"],
    ["file must exist", param.type == "file" && param.must_exist]
  ].findAll{it[1]}.collect{it[0]}
  
  def dflt = null
  if (param.default != null) {
    if (param.default instanceof List) {
      dflt = param.default.join(param.multiple_sep != null ? param.multiple_sep : ", ")
    } else {
      dflt = param.default.toString()
    }
  }
  def example = null
  if (param.example != null) {
    if (param.example instanceof List) {
      example = param.example.join(param.multiple_sep != null ? param.multiple_sep : ", ")
    } else {
      example = param.example.toString()
    }
  }
  def min = param.min?.toString()
  def max = param.max?.toString()

  def escapeChoice = { choice ->
    def s1 = choice.replaceAll("\\n", "\\\\n")
    def s2 = s1.replaceAll("\"", """\\\"""")
    s2.contains(",") || s2 != choice ? "\"" + s2 + "\"" : s2
  }
  def choices = param.choices == null ? 
    null : 
    "[ " + param.choices.collect{escapeChoice(it.toString())}.join(", ") + " ]"

  def namedPropsStr = [
    ["type", ([param.type] + unnamedProps).join(", ")],
    ["default", dflt],
    ["example", example],
    ["choices", choices],
    ["min", min],
    ["max", max]
  ]
    .findAll{it[1]}
    .collect{"\n        " + it[0] + ": " + it[1].replaceAll("\n", "\\n")}
    .join("")
  
  def descStr = param.description == null ?
    "" :
    _paragraphWrap("\n" + param.description.trim(), 80 - 8).join("\n        ")
  
  "\n    --" + param.plainName +
    namedPropsStr +
    descStr
}

// Based on Helper.generateHelp() in Helper.scala
def _generateHelp(config) {
  def fun = config

  // PART 1: NAME AND VERSION
  def nameStr = fun.name + 
    (fun.version == null ? "" : " " + fun.version)

  // PART 2: DESCRIPTION
  def descrStr = fun.description == null ? 
    "" :
    "\n\n" + _paragraphWrap(fun.description.trim(), 80).join("\n")

  // PART 3: Usage
  def usageStr = fun.usage == null ? 
    "" :
    "\n\nUsage:\n" + fun.usage.trim()

  // PART 4: Options
  def argGroupStrs = fun.allArgumentGroups.collect{argGroup ->
    def name = argGroup.name
    def descriptionStr = argGroup.description == null ?
      "" :
      "\n    " + _paragraphWrap(argGroup.description.trim(), 80-4).join("\n    ") + "\n"
    def arguments = argGroup.arguments.collect{arg -> 
      arg instanceof String ? fun.allArguments.find{it.plainName == arg} : arg
    }.findAll{it != null}
    def argumentStrs = arguments.collect{param -> _generateArgumentHelp(param)}
    
    "\n\n$name:" +
      descriptionStr +
      argumentStrs.join("\n")
  }

  // FINAL: combine
  def out = nameStr + 
    descrStr +
    usageStr + 
    argGroupStrs.join("")

  return out
}

// based on Format._paragraphWrap
def _paragraphWrap(str, maxLength) {
  def outLines = []
  str.split("\n").each{par ->
    def words = par.split("\\s").toList()

    def word = null
    def line = words.pop()
    while(!words.isEmpty()) {
      word = words.pop()
      if (line.length() + word.length() + 1 <= maxLength) {
        line = line + " " + word
      } else {
        outLines.add(line)
        line = word
      }
    }
    if (words.isEmpty()) {
      outLines.add(line)
    }
  }
  return outLines
}

def helpMessage(config) {
  if (params.containsKey("help") && params.help) {
    def mergedConfig = addGlobalArguments(config)
    def helpStr = _generateHelp(mergedConfig)
    println(helpStr)
    exit 0
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/config/processConfig.nf'
def processConfig(config) {
  // set defaults for arguments
  config.arguments = 
    (config.arguments ?: []).collect{_processArgument(it)}

  // set defaults for argument_group arguments
  config.argument_groups =
    (config.argument_groups ?: []).collect{grp ->
      grp.arguments = (grp.arguments ?: []).collect{_processArgument(it)}
      grp
    }

  // create combined arguments list
  config.allArguments = 
    config.arguments +
    config.argument_groups.collectMany{it.arguments}

  // add missing argument groups (based on Functionality::allArgumentGroups())
  def argGroups = config.argument_groups
  if (argGroups.any{it.name.toLowerCase() == "arguments"}) {
    argGroups = argGroups.collect{ grp ->
      if (grp.name.toLowerCase() == "arguments") {
        grp = grp + [
          arguments: grp.arguments + config.arguments
        ]
      }
      grp
    }
  } else {
    argGroups = argGroups + [
      name: "Arguments",
      arguments: config.arguments
    ]
  }
  config.allArgumentGroups = argGroups

  config
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/config/readConfig.nf'

def readConfig(file) {
  def config = readYaml(file ?: moduleDir.resolve("config.vsh.yaml"))
  processConfig(config)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_resolveSiblingIfNotAbsolute.nf'
/**
  * Resolve a path relative to the current file.
  * 
  * @param str The path to resolve, as a String.
  * @param parentPath The path to resolve relative to, as a Path.
  *
  * @return The path that may have been resovled, as a Path.
  */
def _resolveSiblingIfNotAbsolute(str, parentPath) {
  if (str !instanceof String) {
    return str
  }
  if (!_stringIsAbsolutePath(str)) {
    return parentPath.resolveSibling(str)
  } else {
    return file(str, hidden: true)
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/_stringIsAbsolutePath.nf'
/**
  * Check whether a path as a string is absolute.
  *
  * In the past, we tried using `file(., relative: true).isAbsolute()`,
  * but the 'relative' option was added in 22.10.0.
  *
  * @param path The path to check, as a String.
  *
  * @return Whether the path is absolute, as a boolean.
  */
def _stringIsAbsolutePath(path) {
  def _resolve_URL_PROTOCOL = ~/^([a-zA-Z][a-zA-Z0-9]*:)?\\/.+/

  assert path instanceof String
  return _resolve_URL_PROTOCOL.matcher(path).matches()
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/collectTraces.nf'
class CustomTraceObserver implements nextflow.trace.TraceObserver {
  List traces

  CustomTraceObserver(List traces) {
    this.traces = traces
  }

  @Override
  void onProcessComplete(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) {
    def trace2 = trace.store.clone()
    trace2.script = null
    traces.add(trace2)
  }

  @Override
  void onProcessCached(nextflow.processor.TaskHandler handler, nextflow.trace.TraceRecord trace) {
    def trace2 = trace.store.clone()
    trace2.script = null
    traces.add(trace2)
  }
}

def collectTraces() {
  def traces = Collections.synchronizedList([])

  // add custom trace observer which stores traces in the traces object
  session.observers.add(new CustomTraceObserver(traces))

  traces
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/deepClone.nf'
/**
  * Performs a deep clone of the given object.
  * @param x an object
  */
def deepClone(x) {
  iterateMap(x, {it instanceof Cloneable ? it.clone() : it})
}
// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getPublishDir.nf'
def getPublishDir() {
  return params.containsKey("publish_dir") ? params.publish_dir : 
    params.containsKey("publishDir") ? params.publishDir : 
    null
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/getRootDir.nf'

// Recurse upwards until we find a '.build.yaml' file
def _findBuildYamlFile(pathPossiblySymlink) {
  def path = pathPossiblySymlink.toRealPath()
  def child = path.resolve(".build.yaml")
  if (java.nio.file.Files.isDirectory(path) && java.nio.file.Files.exists(child)) {
    return child
  } else {
    def parent = path.getParent()
    if (parent == null) {
      return null
    } else {
      return _findBuildYamlFile(parent)
    }
  }
}

// get the root of the target folder
def getRootDir() {
  def dir = _findBuildYamlFile(meta.resources_dir)
  assert dir != null: "Could not find .build.yaml in the folder structure"
  dir.getParent()
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/iterateMap.nf'
/**
  * Recursively apply a function over the leaves of an object.
  * @param obj The object to iterate over.
  * @param fun The function to apply to each value.
  * @return The object with the function applied to each value.
  */
def iterateMap(obj, fun) {
  if (obj instanceof List && obj !instanceof String) {
    return obj.collect{item ->
      iterateMap(item, fun)
    }
  } else if (obj instanceof Map) {
    return obj.collectEntries{key, item ->
      [key.toString(), iterateMap(item, fun)]
    }
  } else {
    return fun(obj)
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/functions/niceView.nf'
/**
  * A view for printing the event of each channel as a YAML blob.
  * This is useful for debugging.
  */
def niceView() {
  workflow niceViewWf {
    take: input
    main:
      output = input
        | view{toYamlBlob(it)}
    emit: output
  }
  return niceViewWf
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readCsv.nf'

def readCsv(file_path) {
  def output = []
  def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path

  // todo: allow escaped quotes in string
  // todo: allow single quotes?
  def splitRegex = java.util.regex.Pattern.compile(''',(?=(?:[^"]*"[^"]*")*[^"]*$)''')
  def removeQuote = java.util.regex.Pattern.compile('''"(.*)"''')

  def br = java.nio.file.Files.newBufferedReader(inputFile)

  def row = -1
  def header = null
  while (br.ready() && header == null) {
    def line = br.readLine()
    row++
    if (!line.startsWith("#")) {
      header = splitRegex.split(line, -1).collect{field ->
        m = removeQuote.matcher(field)
        m.find() ? m.replaceFirst('$1') : field
      }
    }
  }
  assert header != null: "CSV file should contain a header"

  while (br.ready()) {
    def line = br.readLine()
    row++
    if (line == null) {
      br.close()
      break
    }

    if (!line.startsWith("#")) {
      def predata = splitRegex.split(line, -1)
      def data = predata.collect{field ->
        if (field == "") {
          return null
        }
        def m = removeQuote.matcher(field)
        if (m.find()) {
          return m.replaceFirst('$1')
        } else {
          return field
        }
      }
      assert header.size() == data.size(): "Row $row should contain the same number as fields as the header"
      
      def dataMap = [header, data].transpose().collectEntries().findAll{it.value != null}
      output.add(dataMap)
    }
  }

  output
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJson.nf'
def readJson(file_path) {
  def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path
  def jsonSlurper = new groovy.json.JsonSlurper()
  jsonSlurper.parse(inputFile)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readJsonBlob.nf'
def readJsonBlob(str) {
  def jsonSlurper = new groovy.json.JsonSlurper()
  jsonSlurper.parseText(str)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readTaggedYaml.nf'
// Custom constructor to modify how certain objects are parsed from YAML
class CustomConstructor extends org.yaml.snakeyaml.constructor.Constructor {
  Path root

  class ConstructPath extends org.yaml.snakeyaml.constructor.AbstractConstruct {
    public Object construct(org.yaml.snakeyaml.nodes.Node node) {
      String filename = (String) constructScalar(node);
      if (root != null) {
        return root.resolve(filename);
      }
      return java.nio.file.Paths.get(filename);
    }
  }

  CustomConstructor(org.yaml.snakeyaml.LoaderOptions options, Path root) {
    super(options)
    this.root = root
    // Handling !file tag and parse it back to a File type
    this.yamlConstructors.put(new org.yaml.snakeyaml.nodes.Tag("!file"), new ConstructPath())
  }
}

def readTaggedYaml(Path path) {
  def options = new org.yaml.snakeyaml.LoaderOptions()
  def constructor = new CustomConstructor(options, path.getParent())
  def yaml = new org.yaml.snakeyaml.Yaml(constructor)
  return yaml.load(path.text)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYaml.nf'
def readYaml(file_path) {
  def inputFile = file_path !instanceof Path ? file(file_path, hidden: true) : file_path
  def yamlSlurper = new org.yaml.snakeyaml.Yaml()
  yamlSlurper.load(inputFile)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/readYamlBlob.nf'
def readYamlBlob(str) {
  def yamlSlurper = new org.yaml.snakeyaml.Yaml()
  yamlSlurper.load(str)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toJsonBlob.nf'
String toJsonBlob(data) {
  return groovy.json.JsonOutput.toJson(data)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toTaggedYamlBlob.nf'
// Custom representer to modify how certain objects are represented in YAML
class CustomRepresenter extends org.yaml.snakeyaml.representer.Representer {
  Path relativizer

  class RepresentPath implements org.yaml.snakeyaml.representer.Represent {
    public String getFileName(Object obj) {
      if (obj instanceof File) {
        obj = ((File) obj).toPath();
      }
      if (obj !instanceof Path) {
        throw new IllegalArgumentException("Object: " + obj + " is not a Path or File");
      }
      def path = (Path) obj;

      if (relativizer != null) {
        return relativizer.relativize(path).toString()
      } else {
        return path.toString()
      }
    }

    public org.yaml.snakeyaml.nodes.Node representData(Object data) {
      String filename = getFileName(data);
      def tag = new org.yaml.snakeyaml.nodes.Tag("!file");
      return representScalar(tag, filename);
    }
  }
  CustomRepresenter(org.yaml.snakeyaml.DumperOptions options, Path relativizer) {
    super(options)
    this.relativizer = relativizer
    this.representers.put(sun.nio.fs.UnixPath, new RepresentPath())
    this.representers.put(Path, new RepresentPath())
    this.representers.put(File, new RepresentPath())
  }
}

String toTaggedYamlBlob(data) {
  return toRelativeTaggedYamlBlob(data, null)
}
String toRelativeTaggedYamlBlob(data, Path relativizer) {
  def options = new org.yaml.snakeyaml.DumperOptions()
  options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK)
  def representer = new CustomRepresenter(options, relativizer)
  def yaml = new org.yaml.snakeyaml.Yaml(representer, options)
  return yaml.dump(data)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/toYamlBlob.nf'
String toYamlBlob(data) {
  def options = new org.yaml.snakeyaml.DumperOptions()
  options.setDefaultFlowStyle(org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK)
  options.setPrettyFlow(true)
  def yaml = new org.yaml.snakeyaml.Yaml(options)
  def cleanData = iterateMap(data, { it instanceof Path ? it.toString() : it })
  return yaml.dump(cleanData)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeJson.nf'
void writeJson(data, file) {
  assert data: "writeJson: data should not be null"
  assert file: "writeJson: file should not be null"
  file.write(toJsonBlob(data))
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/readwrite/writeYaml.nf'
void writeYaml(data, file) {
  assert data: "writeYaml: data should not be null"
  assert file: "writeYaml: file should not be null"
  file.write(toYamlBlob(data))
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/states/findStates.nf'
def findStates(Map params, Map config) {
  def auto_config = deepClone(config)
  def auto_params = deepClone(params)

  auto_config = auto_config.clone()
  // override arguments
  auto_config.argument_groups = []
  auto_config.arguments = [
    [
      type: "string",
      name: "--id",
      description: "A dummy identifier",
      required: false
    ],
    [
      type: "file",
      name: "--input_states",
      example: "/path/to/input/directory/**/state.yaml",
      description: "Path to input directory containing the datasets to be integrated.",
      required: true,
      multiple: true,
      multiple_sep: ";"
    ],
    [
      type: "string",
      name: "--filter",
      example: "foo/.*/state.yaml",
      description: "Regex to filter state files by path.",
      required: false
    ],
    // to do: make this a yaml blob?
    [
      type: "string",
      name: "--rename_keys",
      example: ["newKey1:oldKey1", "newKey2:oldKey2"],
      description: "Rename keys in the detected input files. This is useful if the input files do not match the set of input arguments of the workflow.",
      required: false,
      multiple: true,
      multiple_sep: ";"
    ],
    [
      type: "string",
      name: "--settings",
      example: '{"output_dataset": "dataset.h5ad", "k": 10}',
      description: "Global arguments as a JSON glob to be passed to all components.",
      required: false
    ]
  ]
  if (!(auto_params.containsKey("id"))) {
    auto_params["id"] = "auto"
  }

  // run auto config through processConfig once more
  auto_config = processConfig(auto_config)

  workflow findStatesWf {
    helpMessage(auto_config)

    output_ch = 
      channelFromParams(auto_params, auto_config)
        | flatMap { autoId, args ->

          def globalSettings = args.settings ? readYamlBlob(args.settings) : [:]

          // look for state files in input dir
          def stateFiles = args.input_states

          // filter state files by regex
          if (args.filter) {
            stateFiles = stateFiles.findAll{ stateFile ->
              def stateFileStr = stateFile.toString()
              def matcher = stateFileStr =~ args.filter
              matcher.matches()}
          }

          // read in states
          def states = stateFiles.collect { stateFile ->
            def state_ = readTaggedYaml(stateFile)
            [state_.id, state_]
          }

          // construct renameMap
          if (args.rename_keys) {
            def renameMap = args.rename_keys.collectEntries{renameString ->
              def split = renameString.split(":")
              assert split.size() == 2: "Argument 'rename_keys' should be of the form 'newKey:oldKey', or 'newKey:oldKey;newKey:oldKey' in case of multiple values"
              split
            }

            // rename keys in state, only let states through which have all keys
            // also add global settings
            states = states.collectMany{id, state ->
              def newState = [:]

              for (key in renameMap.keySet()) {
                def origKey = renameMap[key]
                if (!(state.containsKey(origKey))) {
                  return []
                }
                newState[key] = state[origKey]
              }

              [[id, globalSettings + newState]]
            }
          }

          states
        }
    emit:
    output_ch
  }

  return findStatesWf
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/states/joinStates.nf'
def joinStates(Closure apply_) {
  workflow joinStatesWf {
    take: input_ch
    main:
    output_ch = input_ch
      | toSortedList
      | filter{ it.size() > 0 }
      | map{ tups ->
        def ids = tups.collect{it[0]}
        def states = tups.collect{it[1]}
        apply_(ids, states)
      }

    emit: output_ch
  }
  return joinStatesWf
}
// helper file: 'src/main/resources/io/viash/runners/nextflow/states/publishStates.nf'
def collectFiles(obj) {
  if (obj instanceof java.io.File || obj instanceof Path)  {
    return [obj]
  } else if (obj instanceof List && obj !instanceof String) {
    return obj.collectMany{item ->
      collectFiles(item)
    }
  } else if (obj instanceof Map) {
    return obj.collectMany{key, item ->
      collectFiles(item)
    }
  } else {
    return []
  }
}

/**
 * Recurse through a state and collect all input files and their target output filenames.
 * @param obj The state to recurse through.
 * @param prefix The prefix to prepend to the output filenames.
 */
def collectInputOutputPaths(obj, prefix) {
  if (obj instanceof File || obj instanceof Path)  {
    def path = obj instanceof Path ? obj : obj.toPath()
    def ext = path.getFileName().toString().find("\\.[^\\.]+\$") ?: ""
    def newFilename = prefix + ext
    return [[obj, newFilename]]
  } else if (obj instanceof List && obj !instanceof String) {
    return obj.withIndex().collectMany{item, ix ->
      collectInputOutputPaths(item, prefix + "_" + ix)
    }
  } else if (obj instanceof Map) {
    return obj.collectMany{key, item ->
      collectInputOutputPaths(item, prefix + "." + key)
    }
  } else {
    return []
  }
}

def publishStates(Map args) {
  def key_ = args.get("key")
  def yamlTemplate_ = args.get("output_state", args.get("outputState", '$id.$key.state.yaml'))

  assert key_ != null : "publishStates: key must be specified"
  
  workflow publishStatesWf {
    take: input_ch
    main:
      input_ch
        | map { tup ->
          def id_ = tup[0]
          def state_ = tup[1]

          // the input files and the target output filenames
          def inputoutputFilenames_ = collectInputOutputPaths(state_, id_ + "." + key_).transpose()
          def inputFiles_ = inputoutputFilenames_[0]
          def outputFilenames_ = inputoutputFilenames_[1]

          def yamlFilename = yamlTemplate_
            .replaceAll('\\$id', id_)
            .replaceAll('\\$\\{id\\}', id_)
            .replaceAll('\\$key', key_)
            .replaceAll('\\$\\{key\\}', key_)

            // TODO: do the pathnames in state_ match up with the outputFilenames_?

          // convert state to yaml blob
          def yamlBlob_ = toRelativeTaggedYamlBlob([id: id_] + state_, java.nio.file.Paths.get(yamlFilename))

          [id_, yamlBlob_, yamlFilename, inputFiles_, outputFilenames_]
        }
        | publishStatesProc
    emit: input_ch
  }
  return publishStatesWf
}
process publishStatesProc {
  // todo: check publishpath?
  publishDir path: "${getPublishDir()}/", mode: "copy"
  tag "$id"
  input:
    tuple val(id), val(yamlBlob), val(yamlFile), path(inputFiles, stageAs: "_inputfile?/*"), val(outputFiles)
  output:
    tuple val(id), path{[yamlFile] + outputFiles}
  script:
  def copyCommands = [
    inputFiles instanceof List ? inputFiles : [inputFiles],
    outputFiles instanceof List ? outputFiles : [outputFiles]
  ]
    .transpose()
    .collectMany{infile, outfile ->
      if (infile.toString() != outfile.toString()) {
        [
          "[ -d \"\$(dirname '${outfile.toString()}')\" ] || mkdir -p \"\$(dirname '${outfile.toString()}')\"",
          "cp -r '${infile.toString()}' '${outfile.toString()}'"
        ]
      } else {
        // no need to copy if infile is the same as outfile
        []
      }
    }
  """
mkdir -p "\$(dirname '${yamlFile}')"
echo "Storing state as yaml"
echo '${yamlBlob}' > '${yamlFile}'
echo "Copying output files to destination folder"
${copyCommands.join("\n  ")}
"""
}


// this assumes that the state contains no other values other than those specified in the config
def publishStatesByConfig(Map args) {
  def config = args.get("config")
  assert config != null : "publishStatesByConfig: config must be specified"

  def key_ = args.get("key", config.name)
  assert key_ != null : "publishStatesByConfig: key must be specified"
  
  workflow publishStatesSimpleWf {
    take: input_ch
    main:
      input_ch
        | map { tup ->
          def id_ = tup[0]
          def state_ = tup[1] // e.g. [output: new File("myoutput.h5ad"), k: 10]
          def origState_ = tup[2] // e.g. [output: '$id.$key.foo.h5ad']

          // TODO: allow overriding the state.yaml template
          // TODO TODO: if auto.publish == "state", add output_state as an argument
          def yamlTemplate = params.containsKey("output_state") ? params.output_state : '$id.$key.state.yaml'
          def yamlFilename = yamlTemplate
            .replaceAll('\\$id', id_)
            .replaceAll('\\$\\{id\\}', id_)
            .replaceAll('\\$key', key_)
            .replaceAll('\\$\\{key\\}', key_)
          def yamlDir = java.nio.file.Paths.get(yamlFilename).getParent()

          // the processed state is a list of [key, value, inputPath, outputFilename] tuples, where
          //   - key is a String
          //   - value is any object that can be serialized to a Yaml (so a String/Integer/Long/Double/Boolean, a List, a Map, or a Path)
          //   - inputPath is a List[Path]
          //   - outputFilename is a List[String]
          //   - (key, value) are the tuples that will be saved to the state.yaml file
          //   - (inputPath, outputFilename) are the files that will be copied from src to dest (relative to the state.yaml)
          def processedState =
            config.allArguments
              .findAll { it.direction == "output" }
              .collectMany { par ->
                def plainName_ = par.plainName
                // if the state does not contain the key, it's an
                // optional argument for which the component did 
                // not generate any output
                if (!state_.containsKey(plainName_)) {
                  return []
                }
                def value = state_[plainName_]
                // if the parameter is not a file, it should be stored
                // in the state as-is, but is not something that needs 
                // to be copied from the source path to the dest path
                if (par.type != "file") {
                  return [[key: plainName_, value: value, inputPath: [], outputFilename: []]]
                }
                // if the orig state does not contain this filename,
                // it's an optional argument for which the user specified
                // that it should not be returned as a state
                if (!origState_.containsKey(plainName_)) {
                  return []
                }
                def filenameTemplate = origState_[plainName_]
                // if the pararameter is multiple: true, fetch the template
                if (par.multiple && filenameTemplate instanceof List) {
                  filenameTemplate = filenameTemplate[0]
                }
                // instantiate the template
                def filename = filenameTemplate
                  .replaceAll('\\$id', id_)
                  .replaceAll('\\$\\{id\\}', id_)
                  .replaceAll('\\$key', key_)
                  .replaceAll('\\$\\{key\\}', key_)
                if (par.multiple) {
                  // if the parameter is multiple: true, the filename
                  // should contain a wildcard '*' that is replaced with
                  // the index of the file
                  assert filename.contains("*") : "Module '${key_}' id '${id_}': Multiple output files specified, but no wildcard '*' in the filename: ${filename}"
                  def outputPerFile = value.withIndex().collect{ val, ix ->
                    def filename_ix = filename.replace("*", ix.toString())
                    def value_ = java.nio.file.Paths.get(filename_ix)
                    // if id contains a slash
                    if (yamlDir != null) {
                      value_ = yamlDir.relativize(value_)
                    }
                    def inputPath = val instanceof File ? val.toPath() : val
                    [value: value_, inputPath: inputPath, outputFilename: filename_ix]
                  }
                  def transposedOutputs = ["value", "inputPath", "outputFilename"].collectEntries{ key -> 
                    [key, outputPerFile.collect{dic -> dic[key]}]
                  }
                  return [[key: plainName_] + transposedOutputs]
                } else {
                  def value_ = java.nio.file.Paths.get(filename)
                  // if id contains a slash
                  if (yamlDir != null) {
                    value_ = yamlDir.relativize(value_)
                  }
                  def inputPath = value instanceof File ? value.toPath() : value
                  return [[key: plainName_, value: value_, inputPath: [inputPath], outputFilename: [filename]]]
                }
              }
          
          def updatedState_ = processedState.collectEntries{[it.key, it.value]}
          def inputPaths = processedState.collectMany{it.inputPath}
          def outputFilenames = processedState.collectMany{it.outputFilename}
          
          // convert state to yaml blob
          def yamlBlob_ = toTaggedYamlBlob([id: id_] + updatedState_)

          [id_, yamlBlob_, yamlFilename, inputPaths, outputFilenames]
        }
        | publishStatesProc
    emit: input_ch
  }
  return publishStatesSimpleWf
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/states/setState.nf'
def setState(fun) {
  assert fun instanceof Closure || fun instanceof Map || fun instanceof List :
    "Error in setState: Expected process argument to be a Closure, a Map, or a List. Found: class ${fun.getClass()}"

  // if fun is a List, convert to map
  if (fun instanceof List) {
    // check whether fun is a list[string]
    assert fun.every{it instanceof CharSequence} : "Error in setState: argument is a List, but not all elements are Strings"
    fun = fun.collectEntries{[it, it]}
  }

  // if fun is a map, convert to closure
  if (fun instanceof Map) {
    // check whether fun is a map[string, string]
    assert fun.values().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all values are Strings"
    assert fun.keySet().every{it instanceof CharSequence} : "Error in setState: argument is a Map, but not all keys are Strings"
    def funMap = fun.clone()
    // turn the map into a closure to be used later on
    fun = { id_, state_ ->
      assert state_ instanceof Map : "Error in setState: the state is not a Map"
      funMap.collectMany{newkey, origkey ->
        if (state_.containsKey(origkey)) {
          [[newkey, state_[origkey]]]
        } else {
          []
        }
      }.collectEntries()
    }
  }

  map { tup ->
    def id = tup[0]
    def state = tup[1]
    def unfilteredState = fun(id, state)
    def newState = unfilteredState.findAll{key, val -> val != null}
    [id, newState] + tup.drop(2)
  }
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processAuto.nf'
// TODO: unit test processAuto
def processAuto(Map auto) {
  // remove null values
  auto = auto.findAll{k, v -> v != null}

  // check for unexpected keys
  def expectedKeys = ["simplifyInput", "simplifyOutput", "transcript", "publish"]
  def unexpectedKeys = auto.keySet() - expectedKeys
  assert unexpectedKeys.isEmpty(), "unexpected keys in auto: '${unexpectedKeys.join("', '")}'"

  // check auto.simplifyInput
  assert auto.simplifyInput instanceof Boolean, "auto.simplifyInput must be a boolean"

  // check auto.simplifyOutput
  assert auto.simplifyOutput instanceof Boolean, "auto.simplifyOutput must be a boolean"

  // check auto.transcript
  assert auto.transcript instanceof Boolean, "auto.transcript must be a boolean"

  // check auto.publish
  assert auto.publish instanceof Boolean || auto.publish == "state", "auto.publish must be a boolean or 'state'"

  return auto.subMap(expectedKeys)
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processDirectives.nf'
def assertMapKeys(map, expectedKeys, requiredKeys, mapName) {
  assert map instanceof Map : "Expected argument '$mapName' to be a Map. Found: class ${map.getClass()}"
  map.forEach { key, val -> 
    assert key in expectedKeys : "Unexpected key '$key' in ${mapName ? mapName + " " : ""}map"
  }
  requiredKeys.forEach { requiredKey -> 
    assert map.containsKey(requiredKey) : "Missing required key '$key' in ${mapName ? mapName + " " : ""}map"
  }
}

// TODO: unit test processDirectives
def processDirectives(Map drctv) {
  // remove null values
  drctv = drctv.findAll{k, v -> v != null}

  // check for unexpected keys
  def expectedKeys = [
    "accelerator", "afterScript", "beforeScript", "cache", "conda", "container", "containerOptions", "cpus", "disk", "echo", "errorStrategy", "executor", "machineType", "maxErrors", "maxForks", "maxRetries", "memory", "module", "penv", "pod", "publishDir", "queue", "label", "scratch", "storeDir", "stageInMode", "stageOutMode", "tag", "time"
  ]
  def unexpectedKeys = drctv.keySet() - expectedKeys
  assert unexpectedKeys.isEmpty() : "Unexpected keys in process directive: '${unexpectedKeys.join("', '")}'"

  /* DIRECTIVE accelerator
    accepted examples:
    - [ limit: 4, type: "nvidia-tesla-k80" ]
  */
  if (drctv.containsKey("accelerator")) {
    assertMapKeys(drctv["accelerator"], ["type", "limit", "request", "runtime"], [], "accelerator")
  }

  /* DIRECTIVE afterScript
    accepted examples:
    - "source /cluster/bin/cleanup"
  */
  if (drctv.containsKey("afterScript")) {
    assert drctv["afterScript"] instanceof CharSequence
  }

  /* DIRECTIVE beforeScript
    accepted examples:
    - "source /cluster/bin/setup"
  */
  if (drctv.containsKey("beforeScript")) {
    assert drctv["beforeScript"] instanceof CharSequence
  }

  /* DIRECTIVE cache
    accepted examples:
    - true
    - false
    - "deep"
    - "lenient"
  */
  if (drctv.containsKey("cache")) {
    assert drctv["cache"] instanceof CharSequence || drctv["cache"] instanceof Boolean
    if (drctv["cache"] instanceof CharSequence) {
      assert drctv["cache"] in ["deep", "lenient"] : "Unexpected value for cache"
    }
  }

  /* DIRECTIVE conda
    accepted examples:
    - "bwa=0.7.15"
    - "bwa=0.7.15 fastqc=0.11.5"
    - ["bwa=0.7.15", "fastqc=0.11.5"]
  */
  if (drctv.containsKey("conda")) {
    if (drctv["conda"] instanceof List) {
      drctv["conda"] = drctv["conda"].join(" ")
    }
    assert drctv["conda"] instanceof CharSequence
  }

  /* DIRECTIVE container
    accepted examples:
    - "foo/bar:tag"
    - [ registry: "reg", image: "im", tag: "ta" ]
      is transformed to "reg/im:ta"
    - [ image: "im" ] 
      is transformed to "im:latest"
  */
  if (drctv.containsKey("container")) {
    assert drctv["container"] instanceof Map || drctv["container"] instanceof CharSequence
    if (drctv["container"] instanceof Map) {
      def m = drctv["container"]
      assertMapKeys(m, [ "registry", "image", "tag" ], ["image"], "container")
      def part1 = 
        System.getenv('OVERRIDE_CONTAINER_REGISTRY') ? System.getenv('OVERRIDE_CONTAINER_REGISTRY') + "/" : 
        params.containsKey("override_container_registry") ? params["override_container_registry"] + "/" : // todo: remove?
        m.registry ? m.registry + "/" : 
        ""
      def part2 = m.image
      def part3 = m.tag ? ":" + m.tag : ":latest"
      drctv["container"] = part1 + part2 + part3
    }
  }

  /* DIRECTIVE containerOptions
    accepted examples:
    - "--foo bar"
    - ["--foo bar", "-f b"]
  */
  if (drctv.containsKey("containerOptions")) {
    if (drctv["containerOptions"] instanceof List) {
      drctv["containerOptions"] = drctv["containerOptions"].join(" ")
    }
    assert drctv["containerOptions"] instanceof CharSequence
  }

  /* DIRECTIVE cpus
    accepted examples:
    - 1
    - 10
  */
  if (drctv.containsKey("cpus")) {
    assert drctv["cpus"] instanceof Integer
  }

  /* DIRECTIVE disk
    accepted examples:
    - "1 GB"
    - "2TB"
    - "3.2KB"
    - "10.B"
  */
  if (drctv.containsKey("disk")) {
    assert drctv["disk"] instanceof CharSequence
    // assert drctv["disk"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B")
    // ^ does not allow closures
  }

  /* DIRECTIVE echo
    accepted examples:
    - true
    - false
  */
  if (drctv.containsKey("echo")) {
    assert drctv["echo"] instanceof Boolean
  }

  /* DIRECTIVE errorStrategy
    accepted examples:
    - "terminate"
    - "finish"
  */
  if (drctv.containsKey("errorStrategy")) {
    assert drctv["errorStrategy"] instanceof CharSequence
    assert drctv["errorStrategy"] in ["terminate", "finish", "ignore", "retry"] : "Unexpected value for errorStrategy"
  }

  /* DIRECTIVE executor
    accepted examples:
    - "local"
    - "sge"
  */
  if (drctv.containsKey("executor")) {
    assert drctv["executor"] instanceof CharSequence
    assert drctv["executor"] in ["local", "sge", "uge", "lsf", "slurm", "pbs", "pbspro", "moab", "condor", "nqsii", "ignite", "k8s", "awsbatch", "google-pipelines"] : "Unexpected value for executor"
  }

  /* DIRECTIVE machineType
    accepted examples:
    - "n1-highmem-8"
  */
  if (drctv.containsKey("machineType")) {
    assert drctv["machineType"] instanceof CharSequence
  }

  /* DIRECTIVE maxErrors
    accepted examples:
    - 1
    - 3
  */
  if (drctv.containsKey("maxErrors")) {
    assert drctv["maxErrors"] instanceof Integer
  }

  /* DIRECTIVE maxForks
    accepted examples:
    - 1
    - 3
  */
  if (drctv.containsKey("maxForks")) {
    assert drctv["maxForks"] instanceof Integer
  }

  /* DIRECTIVE maxRetries
    accepted examples:
    - 1
    - 3
  */
  if (drctv.containsKey("maxRetries")) {
    assert drctv["maxRetries"] instanceof Integer
  }

  /* DIRECTIVE memory
    accepted examples:
    - "1 GB"
    - "2TB"
    - "3.2KB"
    - "10.B"
  */
  if (drctv.containsKey("memory")) {
    assert drctv["memory"] instanceof CharSequence
    // assert drctv["memory"].matches("[0-9]+(\\.[0-9]*)? *[KMGTPEZY]?B")
    // ^ does not allow closures
  }

  /* DIRECTIVE module
    accepted examples:
    - "ncbi-blast/2.2.27"
    - "ncbi-blast/2.2.27:t_coffee/10.0"
    - ["ncbi-blast/2.2.27", "t_coffee/10.0"]
  */
  if (drctv.containsKey("module")) {
    if (drctv["module"] instanceof List) {
      drctv["module"] = drctv["module"].join(":")
    }
    assert drctv["module"] instanceof CharSequence
  }

  /* DIRECTIVE penv
    accepted examples:
    - "smp"
  */
  if (drctv.containsKey("penv")) {
    assert drctv["penv"] instanceof CharSequence
  }

  /* DIRECTIVE pod
    accepted examples:
    - [ label: "key", value: "val" ]
    - [ annotation: "key", value: "val" ]
    - [ env: "key", value: "val" ]
    - [ [label: "l", value: "v"], [env: "e", value: "v"]]
  */
  if (drctv.containsKey("pod")) {
    if (drctv["pod"] instanceof Map) {
      drctv["pod"] = [ drctv["pod"] ]
    }
    assert drctv["pod"] instanceof List
    drctv["pod"].forEach { pod ->
      assert pod instanceof Map
      // TODO: should more checks be added?
      // See https://www.nextflow.io/docs/latest/process.html?highlight=directives#pod
      // e.g. does it contain 'label' and 'value', or 'annotation' and 'value', or ...?
    }
  }

  /* DIRECTIVE publishDir
    accepted examples:
    - []
    - [ [ path: "foo", enabled: true ], [ path: "bar", enabled: false ] ]
    - "/path/to/dir" 
      is transformed to [[ path: "/path/to/dir" ]]
    - [ path: "/path/to/dir", mode: "cache" ]
      is transformed to [[ path: "/path/to/dir", mode: "cache" ]]
  */
  // TODO: should we also look at params["publishDir"]?
  if (drctv.containsKey("publishDir")) {
    def pblsh = drctv["publishDir"]
    
    // check different options
    assert pblsh instanceof List || pblsh instanceof Map || pblsh instanceof CharSequence
    
    // turn into list if not already so
    // for some reason, 'if (!pblsh instanceof List) pblsh = [ pblsh ]' doesn't work.
    pblsh = pblsh instanceof List ? pblsh : [ pblsh ]

    // check elements of publishDir
    pblsh = pblsh.collect{ elem ->
      // turn into map if not already so
      elem = elem instanceof CharSequence ? [ path: elem ] : elem

      // check types and keys
      assert elem instanceof Map : "Expected publish argument '$elem' to be a String or a Map. Found: class ${elem.getClass()}"
      assertMapKeys(elem, [ "path", "mode", "overwrite", "pattern", "saveAs", "enabled" ], ["path"], "publishDir")

      // check elements in map
      assert elem.containsKey("path")
      assert elem["path"] instanceof CharSequence
      if (elem.containsKey("mode")) {
        assert elem["mode"] instanceof CharSequence
        assert elem["mode"] in [ "symlink", "rellink", "link", "copy", "copyNoFollow", "move" ]
      }
      if (elem.containsKey("overwrite")) {
        assert elem["overwrite"] instanceof Boolean
      }
      if (elem.containsKey("pattern")) {
        assert elem["pattern"] instanceof CharSequence
      }
      if (elem.containsKey("saveAs")) {
        assert elem["saveAs"] instanceof CharSequence //: "saveAs as a Closure is currently not supported. Surround your closure with single quotes to get the desired effect. Example: '\{ foo \}'"
      }
      if (elem.containsKey("enabled")) {
        assert elem["enabled"] instanceof Boolean
      }

      // return final result
      elem
    }
    // store final directive
    drctv["publishDir"] = pblsh
  }

  /* DIRECTIVE queue
    accepted examples:
    - "long"
    - "short,long"
    - ["short", "long"]
  */
  if (drctv.containsKey("queue")) {
    if (drctv["queue"] instanceof List) {
      drctv["queue"] = drctv["queue"].join(",")
    }
    assert drctv["queue"] instanceof CharSequence
  }

  /* DIRECTIVE label
    accepted examples:
    - "big_mem"
    - "big_cpu"
    - ["big_mem", "big_cpu"]
  */
  if (drctv.containsKey("label")) {
    if (drctv["label"] instanceof CharSequence) {
      drctv["label"] = [ drctv["label"] ]
    }
    assert drctv["label"] instanceof List
    drctv["label"].forEach { label ->
      assert label instanceof CharSequence
      // assert label.matches("[a-zA-Z0-9]([a-zA-Z0-9_]*[a-zA-Z0-9])?")
      // ^ does not allow closures
    }
  }

  /* DIRECTIVE scratch
    accepted examples:
    - true
    - "/path/to/scratch"
    - '$MY_PATH_TO_SCRATCH'
    - "ram-disk"
  */
  if (drctv.containsKey("scratch")) {
    assert drctv["scratch"] == true || drctv["scratch"] instanceof CharSequence
  }

  /* DIRECTIVE storeDir
    accepted examples:
    - "/path/to/storeDir"
  */
  if (drctv.containsKey("storeDir")) {
    assert drctv["storeDir"] instanceof CharSequence
  }

  /* DIRECTIVE stageInMode
    accepted examples:
    - "copy"
    - "link"
  */
  if (drctv.containsKey("stageInMode")) {
    assert drctv["stageInMode"] instanceof CharSequence
    assert drctv["stageInMode"] in ["copy", "link", "symlink", "rellink"]
  }

  /* DIRECTIVE stageOutMode
    accepted examples:
    - "copy"
    - "link"
  */
  if (drctv.containsKey("stageOutMode")) {
    assert drctv["stageOutMode"] instanceof CharSequence
    assert drctv["stageOutMode"] in ["copy", "move", "rsync"]
  }

  /* DIRECTIVE tag
    accepted examples:
    - "foo"
    - '$id'
  */
  if (drctv.containsKey("tag")) {
    assert drctv["tag"] instanceof CharSequence
  }

  /* DIRECTIVE time
    accepted examples:
    - "1h"
    - "2days"
    - "1day 6hours 3minutes 30seconds"
  */
  if (drctv.containsKey("time")) {
    assert drctv["time"] instanceof CharSequence
    // todo: validation regex?
  }

  return drctv
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/processWorkflowArgs.nf'
def processWorkflowArgs(Map args, Map defaultWfArgs, Map meta) {
  // override defaults with args
  def workflowArgs = defaultWfArgs + args

  // check whether 'key' exists
  assert workflowArgs.containsKey("key") : "Error in module '${meta.config.name}': key is a required argument"

  // if 'key' is a closure, apply it to the original key
  if (workflowArgs["key"] instanceof Closure) {
    workflowArgs["key"] = workflowArgs["key"](meta.config.name)
  }
  def key = workflowArgs["key"]
  assert key instanceof CharSequence : "Expected process argument 'key' to be a String. Found: class ${key.getClass()}"
  assert key ==~ /^[a-zA-Z_]\w*$/ : "Error in module '$key': Expected process argument 'key' to consist of only letters, digits or underscores. Found: ${key}"

  // check for any unexpected keys
  def expectedKeys = ["key", "directives", "auto", "map", "mapId", "mapData", "mapPassthrough", "filter", "runIf", "fromState", "toState", "args", "renameKeys", "debug"]
  def unexpectedKeys = workflowArgs.keySet() - expectedKeys
  assert unexpectedKeys.isEmpty() : "Error in module '$key': unexpected arguments to the '.run()' function: '${unexpectedKeys.join("', '")}'"

  // check whether directives exists and apply defaults
  assert workflowArgs.containsKey("directives") : "Error in module '$key': directives is a required argument"
  assert workflowArgs["directives"] instanceof Map : "Error in module '$key': Expected process argument 'directives' to be a Map. Found: class ${workflowArgs['directives'].getClass()}"
  workflowArgs["directives"] = processDirectives(defaultWfArgs.directives + workflowArgs["directives"])

  // check whether directives exists and apply defaults
  assert workflowArgs.containsKey("auto") : "Error in module '$key': auto is a required argument"
  assert workflowArgs["auto"] instanceof Map : "Error in module '$key': Expected process argument 'auto' to be a Map. Found: class ${workflowArgs['auto'].getClass()}"
  workflowArgs["auto"] = processAuto(defaultWfArgs.auto + workflowArgs["auto"])

  // auto define publish, if so desired
  if (workflowArgs.auto.publish == true && (workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : [:]).isEmpty()) {
    // can't assert at this level thanks to the no_publish profile
    // assert params.containsKey("publishDir") || params.containsKey("publish_dir") : 
    //   "Error in module '${workflowArgs['key']}': if auto.publish is true, params.publish_dir needs to be defined.\n" +
    //   "  Example: params.publish_dir = \"./output/\""
    def publishDir = getPublishDir()
    
    if (publishDir != null) {
      workflowArgs.directives.publishDir = [[ 
        path: publishDir, 
        saveAs: "{ it.startsWith('.') ? null : it }", // don't publish hidden files, by default
        mode: "copy"
      ]]
    }
  }

  // auto define transcript, if so desired
  if (workflowArgs.auto.transcript == true) {
    // can't assert at this level thanks to the no_publish profile
    // assert params.containsKey("transcriptsDir") || params.containsKey("transcripts_dir") || params.containsKey("publishDir") || params.containsKey("publish_dir") : 
    //   "Error in module '${workflowArgs['key']}': if auto.transcript is true, either params.transcripts_dir or params.publish_dir needs to be defined.\n" +
    //   "  Example: params.transcripts_dir = \"./transcripts/\""
    def transcriptsDir = 
      params.containsKey("transcripts_dir") ? params.transcripts_dir : 
      params.containsKey("transcriptsDir") ? params.transcriptsDir : 
      params.containsKey("publish_dir") ? params.publish_dir + "/_transcripts" :
      params.containsKey("publishDir") ? params.publishDir + "/_transcripts" : 
      null
    if (transcriptsDir != null) {
      def timestamp = nextflow.Nextflow.getSession().getWorkflowMetadata().start.format('yyyy-MM-dd_HH-mm-ss')
      def transcriptsPublishDir = [ 
        path: "$transcriptsDir/$timestamp/\${task.process.replaceAll(':', '-')}/\${id}/",
        saveAs: "{ it.startsWith('.') ? it.replaceAll('^.', '') : null }", 
        mode: "copy"
      ]
      def publishDirs = workflowArgs.directives.publishDir != null ? workflowArgs.directives.publishDir : null ? workflowArgs.directives.publishDir : []
      workflowArgs.directives.publishDir = publishDirs + transcriptsPublishDir
    }
  }

  // if this is a stubrun, remove certain directives?
  if (workflow.stubRun) {
    workflowArgs.directives.keySet().removeAll(["publishDir", "cpus", "memory", "label"])
  }

  for (nam in ["map", "mapId", "mapData", "mapPassthrough", "filter", "runIf"]) {
    if (workflowArgs.containsKey(nam) && workflowArgs[nam]) {
      assert workflowArgs[nam] instanceof Closure : "Error in module '$key': Expected process argument '$nam' to be null or a Closure. Found: class ${workflowArgs[nam].getClass()}"
    }
  }

  // TODO: should functions like 'map', 'mapId', 'mapData', 'mapPassthrough' be deprecated as well?
  for (nam in ["map", "mapData", "mapPassthrough", "renameKeys"]) {
    if (workflowArgs.containsKey(nam) && workflowArgs[nam] != null) {
      log.warn "module '$key': workflow argument '$nam' is deprecated and will be removed in Viash 0.9.0. Please use 'fromState' and 'toState' instead."
    }
  }

  // check fromState
  workflowArgs["fromState"] = _processFromState(workflowArgs.get("fromState"), key, meta.config)

  // check toState
  workflowArgs["toState"] = _processToState(workflowArgs.get("toState"), key, meta.config)

  // return output
  return workflowArgs
}

def _processFromState(fromState, key_, config_) {
  assert fromState == null || fromState instanceof Closure || fromState instanceof Map || fromState instanceof List :
    "Error in module '$key_': Expected process argument 'fromState' to be null, a Closure, a Map, or a List. Found: class ${fromState.getClass()}"
  if (fromState == null) {
    return null
  }
  
  // if fromState is a List, convert to map
  if (fromState instanceof List) {
    // check whether fromstate is a list[string]
    assert fromState.every{it instanceof CharSequence} : "Error in module '$key_': fromState is a List, but not all elements are Strings"
    fromState = fromState.collectEntries{[it, it]}
  }

  // if fromState is a map, convert to closure
  if (fromState instanceof Map) {
    // check whether fromstate is a map[string, string]
    assert fromState.values().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all values are Strings"
    assert fromState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': fromState is a Map, but not all keys are Strings"
    def fromStateMap = fromState.clone()
    def requiredInputNames = meta.config.allArguments.findAll{it.required && it.direction == "Input"}.collect{it.plainName}
    // turn the map into a closure to be used later on
    fromState = { it ->
      def state = it[1]
      assert state instanceof Map : "Error in module '$key_': the state is not a Map"
      def data = fromStateMap.collectMany{newkey, origkey ->
        // check whether newkey corresponds to a required argument
        if (state.containsKey(origkey)) {
          [[newkey, state[origkey]]]
        } else if (!requiredInputNames.contains(origkey)) {
          []
        } else {
          throw new Exception("Error in module '$key_': fromState key '$origkey' not found in current state")
        }
      }.collectEntries()
      data
    }
  }
  
  return fromState
}

def _processToState(toState, key_, config_) {
  if (toState == null) {
    toState = { tup -> tup[1] }
  }

  // toState should be a closure, map[string, string], or list[string]
  assert toState instanceof Closure || toState instanceof Map || toState instanceof List :
    "Error in module '$key_': Expected process argument 'toState' to be a Closure, a Map, or a List. Found: class ${toState.getClass()}"

  // if toState is a List, convert to map
  if (toState instanceof List) {
    // check whether toState is a list[string]
    assert toState.every{it instanceof CharSequence} : "Error in module '$key_': toState is a List, but not all elements are Strings"
    toState = toState.collectEntries{[it, it]}
  }

  // if toState is a map, convert to closure
  if (toState instanceof Map) {
    // check whether toState is a map[string, string]
    assert toState.values().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all values are Strings"
    assert toState.keySet().every{it instanceof CharSequence} : "Error in module '$key_': toState is a Map, but not all keys are Strings"
    def toStateMap = toState.clone()
    def requiredOutputNames = config_.allArguments.findAll{it.required && it.direction == "Output"}.collect{it.plainName}
    // turn the map into a closure to be used later on
    toState = { it ->
      def output = it[1]
      def state = it[2]
      assert output instanceof Map : "Error in module '$key_': the output is not a Map"
      assert state instanceof Map : "Error in module '$key_': the state is not a Map"
      def extraEntries = toStateMap.collectMany{newkey, origkey ->
        // check whether newkey corresponds to a required argument
        if (output.containsKey(origkey)) {
          [[newkey, output[origkey]]]
        } else if (!requiredOutputNames.contains(origkey)) {
          []
        } else {
          throw new Exception("Error in module '$key_': toState key '$origkey' not found in current output")
        }
      }.collectEntries()
      state + extraEntries
    }
  }

  return toState
}

// helper file: 'src/main/resources/io/viash/runners/nextflow/workflowFactory/workflowFactory.nf'
def _debug(workflowArgs, debugKey) {
  if (workflowArgs.debug) {
    view { "process '${workflowArgs.key}' $debugKey tuple: $it"  }
  } else {
    map { it }
  }
}

// depends on: innerWorkflowFactory
def workflowFactory(Map args, Map defaultWfArgs, Map meta) {
  def workflowArgs = processWorkflowArgs(args, defaultWfArgs, meta)
  def key_ = workflowArgs["key"]
  
  workflow workflowInstance {
    take: input_

    main:
    def chModified = input_
      | checkUniqueIds([:])
      | _debug(workflowArgs, "input")
      | map { tuple ->
        tuple = deepClone(tuple)
        
        if (workflowArgs.map) {
          tuple = workflowArgs.map(tuple)
        }
        if (workflowArgs.mapId) {
          tuple[0] = workflowArgs.mapId(tuple[0])
        }
        if (workflowArgs.mapData) {
          tuple[1] = workflowArgs.mapData(tuple[1])
        }
        if (workflowArgs.mapPassthrough) {
          tuple = tuple.take(2) + workflowArgs.mapPassthrough(tuple.drop(2))
        }

        // check tuple
        assert tuple instanceof List : 
          "Error in module '${key_}': element in channel should be a tuple [id, data, ...otherargs...]\n" +
          "  Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" +
          "  Expected class: List. Found: tuple.getClass() is ${tuple.getClass()}"
        assert tuple.size() >= 2 : 
          "Error in module '${key_}': expected length of tuple in input channel to be two or greater.\n" +
          "  Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" +
          "  Found: tuple.size() == ${tuple.size()}"
        
        // check id field
        if (tuple[0] instanceof GString) {
          tuple[0] = tuple[0].toString()
        }
        assert tuple[0] instanceof CharSequence : 
          "Error in module '${key_}': first element of tuple in channel should be a String\n" +
          "  Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" +
          "  Found: ${tuple[0]}"
        
        // match file to input file
        if (workflowArgs.auto.simplifyInput && (tuple[1] instanceof Path || tuple[1] instanceof List)) {
          def inputFiles = meta.config.allArguments
            .findAll { it.type == "file" && it.direction == "input" }
          
          assert inputFiles.size() == 1 : 
              "Error in module '${key_}' id '${tuple[0]}'.\n" +
              "  Anonymous file inputs are only allowed when the process has exactly one file input.\n" +
              "  Expected: inputFiles.size() == 1. Found: inputFiles.size() is ${inputFiles.size()}"

          tuple[1] = [[ inputFiles[0].plainName, tuple[1] ]].collectEntries()
        }

        // check data field
        assert tuple[1] instanceof Map : 
          "Error in module '${key_}' id '${tuple[0]}': second element of tuple in channel should be a Map\n" +
          "  Example: [\"id\", [input: file('foo.txt'), arg: 10]].\n" +
          "  Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}"

        // rename keys of data field in tuple
        if (workflowArgs.renameKeys) {
          assert workflowArgs.renameKeys instanceof Map : 
              "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" +
              "  Example: renameKeys: ['new_key': 'old_key'].\n" +
              "  Expected class: Map. Found: renameKeys.getClass() is ${workflowArgs.renameKeys.getClass()}"
          assert tuple[1] instanceof Map : 
              "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" +
              "  Expected class: Map. Found: tuple[1].getClass() is ${tuple[1].getClass()}"

          // TODO: allow renameKeys to be a function?
          workflowArgs.renameKeys.each { newKey, oldKey ->
            assert newKey instanceof CharSequence : 
              "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" +
              "  Example: renameKeys: ['new_key': 'old_key'].\n" +
              "  Expected class of newKey: String. Found: newKey.getClass() is ${newKey.getClass()}"
            assert oldKey instanceof CharSequence : 
              "Error renaming data keys in module '${key_}' id '${tuple[0]}'.\n" +
              "  Example: renameKeys: ['new_key': 'old_key'].\n" +
              "  Expected class of oldKey: String. Found: oldKey.getClass() is ${oldKey.getClass()}"
            assert tuple[1].containsKey(oldKey) : 
              "Error renaming data keys in module '${key}' id '${tuple[0]}'.\n" +
              "  Key '$oldKey' is missing in the data map. tuple[1].keySet() is '${tuple[1].keySet()}'"
            tuple[1].put(newKey, tuple[1][oldKey])
          }
          tuple[1].keySet().removeAll(workflowArgs.renameKeys.collect{ newKey, oldKey -> oldKey })
        }
        tuple
      }


    def chRun = null
    def chPassthrough = null
    if (workflowArgs.runIf) {
      def runIfBranch = chModified.branch{ tup ->
        run: workflowArgs.runIf(tup[0], tup[1])
        passthrough: true
      }
      chRun = runIfBranch.run
      chPassthrough = runIfBranch.passthrough
    } else {
      chRun = chModified
      chPassthrough = Channel.empty()
    }

    def chRunFiltered = workflowArgs.filter ?
      chRun | filter{workflowArgs.filter(it)} :
      chRun

    def chArgs = workflowArgs.fromState ? 
      chRunFiltered | map{
        def new_data = workflowArgs.fromState(it.take(2))
        [it[0], new_data]
      } :
      chRunFiltered | map {tup -> tup.take(2)}

    // fill in defaults
    def chArgsWithDefaults = chArgs
      | map { tuple ->
        def id_ = tuple[0]
        def data_ = tuple[1]

        // TODO: could move fromState to here

        // fetch default params from functionality
        def defaultArgs = meta.config.allArguments
          .findAll { it.containsKey("default") }
          .collectEntries { [ it.plainName, it.default ] }

        // fetch overrides in params
        def paramArgs = meta.config.allArguments
          .findAll { par ->
            def argKey = key_ + "__" + par.plainName
            params.containsKey(argKey)
          }
          .collectEntries { [ it.plainName, params[key_ + "__" + it.plainName] ] }
        
        // fetch overrides in data
        def dataArgs = meta.config.allArguments
          .findAll { data_.containsKey(it.plainName) }
          .collectEntries { [ it.plainName, data_[it.plainName] ] }
        
        // combine params
        def combinedArgs = defaultArgs + paramArgs + workflowArgs.args + dataArgs

        // remove arguments with explicit null values
        combinedArgs
          .removeAll{_, val -> val == null || val == "viash_no_value" || val == "force_null"}

        combinedArgs = _processInputValues(combinedArgs, meta.config, id_, key_)

        [id_, combinedArgs] + tuple.drop(2)
      }

    // TODO: move some of the _meta.join_id wrangling to the safeJoin() function.
    def chInitialOutput = chArgsWithDefaults
      | _debug(workflowArgs, "processed")
      // run workflow
      | innerWorkflowFactory(workflowArgs)
      // check output tuple
      | map { id_, output_ ->

        // see if output map contains metadata
        def meta_ =
          output_ instanceof Map && output_.containsKey("_meta") ? 
          output_["_meta"] :
          [:]
        def join_id = meta_.join_id ?: id_
        
        // remove metadata
        output_ = output_.findAll{k, v -> k != "_meta"}

        // check value types
        output_ = _processOutputValues(output_, meta.config, id_, key_)

        // simplify output if need be
        if (workflowArgs.auto.simplifyOutput && output_.size() == 1) {
          output_ = output_.values()[0]
        }

        [join_id, id_, output_]
      }
      // | view{"chInitialOutput: ${it.take(3)}"}

    // join the output [prev_id, new_id, output] with the previous state [prev_id, state, ...]
    def chNewState = safeJoin(chInitialOutput, chRunFiltered, key_)
      // input tuple format: [join_id, id, output, prev_state, ...]
      // output tuple format: [join_id, id, new_state, ...]
      | map{ tup ->
        def new_state = workflowArgs.toState(tup.drop(1).take(3))
        tup.take(2) + [new_state] + tup.drop(4)
      }

    if (workflowArgs.auto.publish == "state") {
      def chPublish = chNewState
        // input tuple format: [join_id, id, new_state, ...]
        // output tuple format: [join_id, id, new_state]
        | map{ tup ->
          tup.take(3)
        }

      safeJoin(chPublish, chArgsWithDefaults, key_)
        // input tuple format: [join_id, id, new_state, orig_state, ...]
        // output tuple format: [id, new_state, orig_state]
        | map { tup ->
          tup.drop(1).take(3)
      }
        | publishStatesByConfig(key: key_, config: meta.config)
    }

    // remove join_id and meta
    chReturn = chNewState
      | map { tup ->
        // input tuple format: [join_id, id, new_state, ...]
        // output tuple format: [id, new_state, ...]
        tup.drop(1)
      }
      | _debug(workflowArgs, "output")
      | concat(chPassthrough)

    emit: chReturn
  }

  def wf = workflowInstance.cloneWithName(key_)

  // add factory function
  wf.metaClass.run = { runArgs ->
    workflowFactory(runArgs, workflowArgs, meta)
  }
  // add config to module for later introspection
  wf.metaClass.config = meta.config

  return wf
}

nextflow.enable.dsl=2

// START COMPONENT-SPECIFIC CODE

// create meta object
meta = [
  "resources_dir": moduleDir.toRealPath().normalize(),
  "config": processConfig(readJsonBlob('''{
  "name" : "star_align_reads",
  "namespace" : "star",
  "version" : "v0.2.0",
  "authors" : [
    {
      "name" : "Angela Oliveira Pisco",
      "roles" : [
        "author"
      ],
      "info" : {
        "role" : "Contributor",
        "links" : {
          "github" : "aopisco",
          "orcid" : "0000-0003-0142-2355",
          "linkedin" : "aopisco"
        },
        "organizations" : [
          {
            "name" : "Insitro",
            "href" : "https://insitro.com",
            "role" : "Director of Computational Biology"
          },
          {
            "name" : "Open Problems",
            "href" : "https://openproblems.bio",
            "role" : "Core Member"
          }
        ]
      }
    },
    {
      "name" : "Robrecht Cannoodt",
      "roles" : [
        "author",
        "maintainer"
      ],
      "info" : {
        "links" : {
          "email" : "robrecht@data-intuitive.com",
          "github" : "rcannood",
          "orcid" : "0000-0003-3641-729X",
          "linkedin" : "robrechtcannoodt"
        },
        "organizations" : [
          {
            "name" : "Data Intuitive",
            "href" : "https://www.data-intuitive.com",
            "role" : "Data Science Engineer"
          },
          {
            "name" : "Open Problems",
            "href" : "https://openproblems.bio",
            "role" : "Core Member"
          }
        ]
      }
    }
  ],
  "argument_groups" : [
    {
      "name" : "Run Parameters",
      "arguments" : [
        {
          "type" : "integer",
          "name" : "--run_rng_seed",
          "description" : "random number generator seed.",
          "info" : {
            "orig_name" : "--runRNGseed"
          },
          "example" : [
            777
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Genome Parameters",
      "arguments" : [
        {
          "type" : "file",
          "name" : "--genome_dir",
          "description" : "path to the directory where genome files are stored (for --runMode alignReads) or will be generated (for --runMode generateGenome)",
          "info" : {
            "orig_name" : "--genomeDir"
          },
          "example" : [
            "./GenomeDir"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : true,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--genome_load",
          "description" : "mode of shared memory usage for the genome files. Only used with --runMode alignReads.\n\n- LoadAndKeep     ... load genome into shared and keep it in memory after run\n- LoadAndRemove   ... load genome into shared but remove it after run\n- LoadAndExit     ... load genome into shared memory and exit, keeping the genome in memory for future runs\n- Remove          ... do not map anything, just remove loaded genome from memory\n- NoSharedMemory  ... do not use shared memory, each job will have its own private copy of the genome",
          "info" : {
            "orig_name" : "--genomeLoad"
          },
          "example" : [
            "NoSharedMemory"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--genome_fasta_files",
          "description" : "path(s) to the fasta files with the genome sequences, separated by spaces. These files should be plain text FASTA files, they *cannot* be zipped.\n\nRequired for the genome generation (--runMode genomeGenerate). Can also be used in the mapping (--runMode alignReads) to add extra (new) sequences to the genome (e.g. spike-ins).",
          "info" : {
            "orig_name" : "--genomeFastaFiles"
          },
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--genome_file_sizes",
          "description" : "genome files exact sizes in bytes. Typically, this should not be defined by the user.",
          "info" : {
            "orig_name" : "--genomeFileSizes"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--genome_transform_output",
          "description" : "which output to transform back to original genome\n\n- SAM     ... SAM/BAM alignments\n- SJ      ... splice junctions (SJ.out.tab)\n- Quant   ... quantifications (from --quant_mode option)\n- None    ... no transformation of the output",
          "info" : {
            "orig_name" : "--genomeTransformOutput"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--genome_chr_set_mitochondrial",
          "description" : "names of the mitochondrial chromosomes. Presently only used for STARsolo statistics output/",
          "info" : {
            "orig_name" : "--genomeChrSetMitochondrial"
          },
          "example" : [
            "chrM",
            "M",
            "MT"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Splice Junctions Database",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--sjdb_file_chr_start_end",
          "description" : "path to the files with genomic coordinates (chr <tab> start <tab> end <tab> strand) for the splice junction introns. Multiple files can be supplied and will be concatenated.",
          "info" : {
            "orig_name" : "--sjdbFileChrStartEnd"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--sjdb_gtf_file",
          "description" : "path to the GTF file with annotations",
          "info" : {
            "orig_name" : "--sjdbGTFfile"
          },
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_chr_prefix",
          "description" : "prefix for chromosome names in a GTF file (e.g. 'chr' for using ENSMEBL annotations with UCSC genomes)",
          "info" : {
            "orig_name" : "--sjdbGTFchrPrefix"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_feature_exon",
          "description" : "feature type in GTF file to be used as exons for building transcripts",
          "info" : {
            "orig_name" : "--sjdbGTFfeatureExon"
          },
          "example" : [
            "exon"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_tag_exon_parent_transcript",
          "description" : "GTF attribute name for parent transcript ID (default \\"transcript_id\\" works for GTF files)",
          "info" : {
            "orig_name" : "--sjdbGTFtagExonParentTranscript"
          },
          "example" : [
            "transcript_id"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_tag_exon_parent_gene",
          "description" : "GTF attribute name for parent gene ID (default \\"gene_id\\" works for GTF files)",
          "info" : {
            "orig_name" : "--sjdbGTFtagExonParentGene"
          },
          "example" : [
            "gene_id"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_tag_exon_parent_gene_name",
          "description" : "GTF attribute name for parent gene name",
          "info" : {
            "orig_name" : "--sjdbGTFtagExonParentGeneName"
          },
          "example" : [
            "gene_name"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_gtf_tag_exon_parent_gene_type",
          "description" : "GTF attribute name for parent gene type",
          "info" : {
            "orig_name" : "--sjdbGTFtagExonParentGeneType"
          },
          "example" : [
            "gene_type",
            "gene_biotype"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--sjdb_overhang",
          "description" : "length of the donor/acceptor sequence on each side of the junctions, ideally = (mate_length - 1)",
          "info" : {
            "orig_name" : "--sjdbOverhang"
          },
          "example" : [
            100
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--sjdb_score",
          "description" : "extra alignment score for alignments that cross database junctions",
          "info" : {
            "orig_name" : "--sjdbScore"
          },
          "example" : [
            2
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--sjdb_insert_save",
          "description" : "which files to save when sjdb junctions are inserted on the fly at the mapping step\n\n- Basic ... only small junction / transcript files\n- All   ... all files including big Genome, SA and SAindex - this will create a complete genome directory",
          "info" : {
            "orig_name" : "--sjdbInsertSave"
          },
          "example" : [
            "Basic"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Variation parameters",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--var_vcf_file",
          "description" : "path to the VCF file that contains variation data. The 10th column should contain the genotype information, e.g. 0/1",
          "info" : {
            "orig_name" : "--varVCFfile"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Read Parameters",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--read_files_type",
          "description" : "format of input read files\n\n- Fastx       ... FASTA or FASTQ\n- SAM SE      ... SAM or BAM single-end reads; for BAM use --read_files_command samtools view\n- SAM PE      ... SAM or BAM paired-end reads; for BAM use --read_files_command samtools view",
          "info" : {
            "orig_name" : "--readFilesType"
          },
          "example" : [
            "Fastx"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--read_files_sam_attr_keep",
          "description" : "for --read_files_type SAM SE/PE, which SAM tags to keep in the output BAM, e.g.: --readFilesSAMtagsKeep RG PL\n\n- All     ... keep all tags\n- None    ... do not keep any tags",
          "info" : {
            "orig_name" : "--readFilesSAMattrKeep"
          },
          "example" : [
            "All"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--read_files_manifest",
          "description" : "path to the \\"manifest\\" file with the names of read files. The manifest file should contain 3 tab-separated columns:\n\npaired-end reads: read1_file_name $tab$ read2_file_name $tab$ read_group_line.\nsingle-end reads: read1_file_name $tab$ -               $tab$ read_group_line.\nSpaces, but not tabs are allowed in file names.\nIf read_group_line does not start with ID:, it can only contain one ID field, and ID: will be added to it.\nIf read_group_line starts with ID:, it can contain several fields separated by $tab$, and all fields will be be copied verbatim into SAM @RG header line.",
          "info" : {
            "orig_name" : "--readFilesManifest"
          },
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--read_files_prefix",
          "description" : "prefix for the read files names, i.e. it will be added in front of the strings in --readFilesIn",
          "info" : {
            "orig_name" : "--readFilesPrefix"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--read_files_command",
          "description" : "command line to execute for each of the input file. This command should generate FASTA or FASTQ text and send it to stdout\n\nFor example: zcat - to uncompress .gz files, bzcat - to uncompress .bz2 files, etc.",
          "info" : {
            "orig_name" : "--readFilesCommand"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--read_map_number",
          "description" : "number of reads to map from the beginning of the file\n\n-1: map all reads",
          "info" : {
            "orig_name" : "--readMapNumber"
          },
          "example" : [
            -1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--read_mates_lengths_in",
          "description" : "Equal/NotEqual - lengths of names,sequences,qualities for both mates are the same  / not the same. NotEqual is safe in all situations.",
          "info" : {
            "orig_name" : "--readMatesLengthsIn"
          },
          "example" : [
            "NotEqual"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--read_name_separator",
          "description" : "character(s) separating the part of the read names that will be trimmed in output (read name after space is always trimmed)",
          "info" : {
            "orig_name" : "--readNameSeparator"
          },
          "example" : [
            "/"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--read_quality_score_base",
          "description" : "number to be subtracted from the ASCII code to get Phred quality score",
          "info" : {
            "orig_name" : "--readQualityScoreBase"
          },
          "example" : [
            33
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Read Clipping",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--clip_adapter_type",
          "description" : "adapter clipping type\n\n- Hamming ... adapter clipping based on Hamming distance, with the number of mismatches controlled by --clip5pAdapterMMp\n- CellRanger4 ... 5p and 3p adapter clipping similar to CellRanger4. Utilizes Opal package by Martin Sosic: https://github.com/Martinsos/opal\n- None ... no adapter clipping, all other clip* parameters are disregarded",
          "info" : {
            "orig_name" : "--clipAdapterType"
          },
          "example" : [
            "Hamming"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--clip3p_nbases",
          "description" : "number(s) of bases to clip from 3p of each mate. If one value is given, it will be assumed the same for both mates.",
          "info" : {
            "orig_name" : "--clip3pNbases"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--clip3p_adapter_seq",
          "description" : "adapter sequences to clip from 3p of each mate.  If one value is given, it will be assumed the same for both mates.\n\n- polyA ... polyA sequence with the length equal to read length",
          "info" : {
            "orig_name" : "--clip3pAdapterSeq"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--clip3p_adapter_mm_p",
          "description" : "max proportion of mismatches for 3p adapter clipping for each mate.  If one value is given, it will be assumed the same for both mates.",
          "info" : {
            "orig_name" : "--clip3pAdapterMMp"
          },
          "example" : [
            0.1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--clip3p_after_adapter_nbases",
          "description" : "number of bases to clip from 3p of each mate after the adapter clipping. If one value is given, it will be assumed the same for both mates.",
          "info" : {
            "orig_name" : "--clip3pAfterAdapterNbases"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--clip5p_nbases",
          "description" : "number(s) of bases to clip from 5p of each mate. If one value is given, it will be assumed the same for both mates.",
          "info" : {
            "orig_name" : "--clip5pNbases"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Limits",
      "arguments" : [
        {
          "type" : "long",
          "name" : "--limit_genome_generate_ram",
          "description" : "maximum available RAM (bytes) for genome generation",
          "info" : {
            "orig_name" : "--limitGenomeGenerateRAM"
          },
          "example" : [
            31000000000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "long",
          "name" : "--limit_io_buffer_size",
          "description" : "max available buffers size (bytes) for input/output, per thread",
          "info" : {
            "orig_name" : "--limitIObufferSize"
          },
          "example" : [
            30000000,
            50000000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "long",
          "name" : "--limit_out_sam_one_read_bytes",
          "description" : "max size of the SAM record (bytes) for one read. Recommended value: >(2*(LengthMate1+LengthMate2+100)*outFilterMultimapNmax",
          "info" : {
            "orig_name" : "--limitOutSAMoneReadBytes"
          },
          "example" : [
            100000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--limit_out_sj_one_read",
          "description" : "max number of junctions for one read (including all multi-mappers)",
          "info" : {
            "orig_name" : "--limitOutSJoneRead"
          },
          "example" : [
            1000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--limit_out_sj_collapsed",
          "description" : "max number of collapsed junctions",
          "info" : {
            "orig_name" : "--limitOutSJcollapsed"
          },
          "example" : [
            1000000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "long",
          "name" : "--limit_bam_sort_ram",
          "description" : "maximum available RAM (bytes) for sorting BAM. If =0, it will be set to the genome index size. 0 value can only be used with --genome_load NoSharedMemory option.",
          "info" : {
            "orig_name" : "--limitBAMsortRAM"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--limit_sjdb_insert_nsj",
          "description" : "maximum number of junctions to be inserted to the genome on the fly at the mapping stage, including those from annotations and those detected in the 1st step of the 2-pass run",
          "info" : {
            "orig_name" : "--limitSjdbInsertNsj"
          },
          "example" : [
            1000000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--limit_nreads_soft",
          "description" : "soft limit on the number of reads",
          "info" : {
            "orig_name" : "--limitNreadsSoft"
          },
          "example" : [
            -1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output: general",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_tmp_keep",
          "description" : "whether to keep the temporary files after STAR runs is finished\n\n- None ... remove all temporary files\n- All ... keep all files",
          "info" : {
            "orig_name" : "--outTmpKeep"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_std",
          "description" : "which output will be directed to stdout (standard out)\n\n- Log                    ... log messages\n- SAM                    ... alignments in SAM format (which normally are output to Aligned.out.sam file), normal standard output will go into Log.std.out\n- BAM_Unsorted           ... alignments in BAM format, unsorted. Requires --out_sam_type BAM Unsorted\n- BAM_SortedByCoordinate ... alignments in BAM format, sorted by coordinate. Requires --out_sam_type BAM SortedByCoordinate\n- BAM_Quant              ... alignments to transcriptome in BAM format, unsorted. Requires --quant_mode TranscriptomeSAM",
          "info" : {
            "orig_name" : "--outStd"
          },
          "example" : [
            "Log"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_reads_unmapped",
          "description" : "output of unmapped and partially mapped (i.e. mapped only one mate of a paired end read) reads in separate file(s).\n\n- None    ... no output\n- Fastx   ... output in separate fasta/fastq files, Unmapped.out.mate1/2",
          "info" : {
            "orig_name" : "--outReadsUnmapped"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_qs_conversion_add",
          "description" : "add this number to the quality score (e.g. to convert from Illumina to Sanger, use -31)",
          "info" : {
            "orig_name" : "--outQSconversionAdd"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_multimapper_order",
          "description" : "order of multimapping alignments in the output files\n\n- Old_2.4             ... quasi-random order used before 2.5.0\n- Random              ... random order of alignments for each multi-mapper. Read mates (pairs) are always adjacent, all alignment for each read stay together. This option will become default in the future releases.",
          "info" : {
            "orig_name" : "--outMultimapperOrder"
          },
          "example" : [
            "Old_2.4"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output: SAM and BAM",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_sam_type",
          "description" : "type of SAM/BAM output\n\n1st word:\n- BAM  ... output BAM without sorting\n- SAM  ... output SAM without sorting\n- None ... no SAM/BAM output\n2nd, 3rd:\n- Unsorted           ... standard unsorted\n- SortedByCoordinate ... sorted by coordinate. This option will allocate extra memory for sorting which can be specified by --limit_bam_sort_ram.",
          "info" : {
            "orig_name" : "--outSAMtype"
          },
          "example" : [
            "SAM"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_mode",
          "description" : "mode of SAM output\n\n- None ... no SAM output\n- Full ... full SAM output\n- NoQS ... full SAM but without quality scores",
          "info" : {
            "orig_name" : "--outSAMmode"
          },
          "example" : [
            "Full"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_strand_field",
          "description" : "Cufflinks-like strand field flag\n\n- None        ... not used\n- intronMotif ... strand derived from the intron motif. This option changes the output alignments: reads with inconsistent and/or non-canonical introns are filtered out.",
          "info" : {
            "orig_name" : "--outSAMstrandField"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_attributes",
          "description" : "a string of desired SAM attributes, in the order desired for the output SAM. Tags can be listed in any combination/order.\n\n***Presets:\n- None        ... no attributes\n- Standard    ... NH HI AS nM\n- All         ... NH HI AS nM NM MD jM jI MC ch\n***Alignment:\n- NH          ... number of loci the reads maps to: =1 for unique mappers, >1 for multimappers. Standard SAM tag.\n- HI          ... multiple alignment index, starts with --out_sam_attr_ih_start (=1 by default). Standard SAM tag.\n- AS          ... local alignment score, +1/-1 for matches/mismateches, score* penalties for indels and gaps. For PE reads, total score for two mates. Stadnard SAM tag.\n- nM          ... number of mismatches. For PE reads, sum over two mates.\n- NM          ... edit distance to the reference (number of mismatched + inserted + deleted bases) for each mate. Standard SAM tag.\n- MD          ... string encoding mismatched and deleted reference bases (see standard SAM specifications). Standard SAM tag.\n- jM          ... intron motifs for all junctions (i.e. N in CIGAR): 0: non-canonical; 1: GT/AG, 2: CT/AC, 3: GC/AG, 4: CT/GC, 5: AT/AC, 6: GT/AT. If splice junctions database is used, and a junction is annotated, 20 is added to its motif value.\n- jI          ... start and end of introns for all junctions (1-based).\n- XS          ... alignment strand according to --out_sam_strand_field.\n- MC          ... mate's CIGAR string. Standard SAM tag.\n- ch          ... marks all segment of all chimeric alingments for --chim_out_type WithinBAM output.\n- cN          ... number of bases clipped from the read ends: 5' and 3'\n***Variation:\n- vA          ... variant allele\n- vG          ... genomic coordinate of the variant overlapped by the read.\n- vW          ... 1 - alignment passes WASP filtering; 2,3,4,5,6,7 - alignment does not pass WASP filtering. Requires --wasp_output_mode SAMtag.\n- ha          ... haplotype (1/2) when mapping to the diploid genome. Requires genome generated with --genomeTransformType Diploid .\n***STARsolo:\n- CR CY UR UY ... sequences and quality scores of cell barcodes and UMIs for the solo* demultiplexing.\n- GX GN       ... gene ID and gene name for unique-gene reads.\n- gx gn       ... gene IDs and gene names for unique- and multi-gene reads.\n- CB UB       ... error-corrected cell barcodes and UMIs for solo* demultiplexing. Requires --out_sam_type BAM SortedByCoordinate.\n- sM          ... assessment of CB and UMI.\n- sS          ... sequence of the entire barcode (CB,UMI,adapter).\n- sQ          ... quality of the entire barcode.\n- sF          ... type of feature overlap and number of features for each alignment\n***Unsupported/undocumented:\n- rB          ... alignment block read/genomic coordinates.\n- vR          ... read coordinate of the variant.",
          "info" : {
            "orig_name" : "--outSAMattributes"
          },
          "example" : [
            "Standard"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_attr_ih_start",
          "description" : "start value for the IH attribute. 0 may be required by some downstream software, such as Cufflinks or StringTie.",
          "info" : {
            "orig_name" : "--outSAMattrIHstart"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_unmapped",
          "description" : "output of unmapped reads in the SAM format\n\n1st word:\n- None   ... no output\n- Within ... output unmapped reads within the main SAM file (i.e. Aligned.out.sam)\n2nd word:\n- KeepPairs ... record unmapped mate for each alignment, and, in case of unsorted output, keep it adjacent to its mapped mate. Only affects multi-mapping reads.",
          "info" : {
            "orig_name" : "--outSAMunmapped"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_order",
          "description" : "type of sorting for the SAM output\n\nPaired: one mate after the other for all paired alignments\nPairedKeepInputOrder: one mate after the other for all paired alignments, the order is kept the same as in the input FASTQ files",
          "info" : {
            "orig_name" : "--outSAMorder"
          },
          "example" : [
            "Paired"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_primary_flag",
          "description" : "which alignments are considered primary - all others will be marked with 0x100 bit in the FLAG\n\n- OneBestScore ... only one alignment with the best score is primary\n- AllBestScore ... all alignments with the best score are primary",
          "info" : {
            "orig_name" : "--outSAMprimaryFlag"
          },
          "example" : [
            "OneBestScore"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_read_id",
          "description" : "read ID record type\n\n- Standard ... first word (until space) from the FASTx read ID line, removing /1,/2 from the end\n- Number   ... read number (index) in the FASTx file",
          "info" : {
            "orig_name" : "--outSAMreadID"
          },
          "example" : [
            "Standard"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_mapq_unique",
          "description" : "0 to 255: the MAPQ value for unique mappers",
          "info" : {
            "orig_name" : "--outSAMmapqUnique"
          },
          "example" : [
            255
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_flag_or",
          "description" : "0 to 65535: sam FLAG will be bitwise OR'd with this value, i.e. FLAG=FLAG | outSAMflagOR. This is applied after all flags have been set by STAR, and after outSAMflagAND. Can be used to set specific bits that are not set otherwise.",
          "info" : {
            "orig_name" : "--outSAMflagOR"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_flag_and",
          "description" : "0 to 65535: sam FLAG will be bitwise AND'd with this value, i.e. FLAG=FLAG & outSAMflagOR. This is applied after all flags have been set by STAR, but before outSAMflagOR. Can be used to unset specific bits that are not set otherwise.",
          "info" : {
            "orig_name" : "--outSAMflagAND"
          },
          "example" : [
            65535
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_attr_rg_line",
          "description" : "SAM/BAM read group line. The first word contains the read group identifier and must start with \\"ID:\\", e.g. --out_sam_attr_rg_line ID:xxx CN:yy \\"DS:z z z\\".\n\nxxx will be added as RG tag to each output alignment. Any spaces in the tag values have to be double quoted.\nComma separated RG lines correspons to different (comma separated) input files in --readFilesIn. Commas have to be surrounded by spaces, e.g.\n--out_sam_attr_rg_line ID:xxx , ID:zzz \\"DS:z z\\" , ID:yyy DS:yyyy",
          "info" : {
            "orig_name" : "--outSAMattrRGline"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_header_hd",
          "description" : "@HD (header) line of the SAM header",
          "info" : {
            "orig_name" : "--outSAMheaderHD"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_header_pg",
          "description" : "extra @PG (software) line of the SAM header (in addition to STAR)",
          "info" : {
            "orig_name" : "--outSAMheaderPG"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_header_comment_file",
          "description" : "path to the file with @CO (comment) lines of the SAM header",
          "info" : {
            "orig_name" : "--outSAMheaderCommentFile"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_sam_filter",
          "description" : "filter the output into main SAM/BAM files\n\n- KeepOnlyAddedReferences ... only keep the reads for which all alignments are to the extra reference sequences added with --genome_fasta_files at the mapping stage.\n- KeepAllAddedReferences ...  keep all alignments to the extra reference sequences added with --genome_fasta_files at the mapping stage.",
          "info" : {
            "orig_name" : "--outSAMfilter"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_mult_nmax",
          "description" : "max number of multiple alignments for a read that will be output to the SAM/BAM files. Note that if this value is not equal to -1, the top scoring alignment will be output first\n\n- -1 ... all alignments (up to --out_filter_multimap_nmax) will be output",
          "info" : {
            "orig_name" : "--outSAMmultNmax"
          },
          "example" : [
            -1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sam_tlen",
          "description" : "calculation method for the TLEN field in the SAM/BAM files\n\n- 1 ... leftmost base of the (+)strand mate to rightmost base of the (-)mate. (+)sign for the (+)strand mate\n- 2 ... leftmost base of any mate to rightmost base of any mate. (+)sign for the mate with the leftmost base. This is different from 1 for overlapping mates with protruding ends",
          "info" : {
            "orig_name" : "--outSAMtlen"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_bam_compression",
          "description" : "-1 to 10  BAM compression level, -1=default compression (6?), 0=no compression, 10=maximum compression",
          "info" : {
            "orig_name" : "--outBAMcompression"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_bam_sorting_thread_n",
          "description" : ">=0: number of threads for BAM sorting. 0 will default to min(6,--runThreadN).",
          "info" : {
            "orig_name" : "--outBAMsortingThreadN"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_bam_sorting_bins_n",
          "description" : ">0:  number of genome bins for coordinate-sorting",
          "info" : {
            "orig_name" : "--outBAMsortingBinsN"
          },
          "example" : [
            50
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "BAM processing",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--bam_remove_duplicates_type",
          "description" : "mark duplicates in the BAM file, for now only works with (i) sorted BAM fed with inputBAMfile, and (ii) for paired-end alignments only\n\n- -                       ... no duplicate removal/marking\n- UniqueIdentical         ... mark all multimappers, and duplicate unique mappers. The coordinates, FLAG, CIGAR must be identical\n- UniqueIdenticalNotMulti  ... mark duplicate unique mappers but not multimappers.",
          "info" : {
            "orig_name" : "--bamRemoveDuplicatesType"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--bam_remove_duplicates_mate2bases_n",
          "description" : "number of bases from the 5' of mate 2 to use in collapsing (e.g. for RAMPAGE)",
          "info" : {
            "orig_name" : "--bamRemoveDuplicatesMate2basesN"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output Wiggle",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_wig_type",
          "description" : "type of signal output, e.g. \\"bedGraph\\" OR \\"bedGraph read1_5p\\". Requires sorted BAM: --out_sam_type BAM SortedByCoordinate .\n\n1st word:\n- None       ... no signal output\n- bedGraph   ... bedGraph format\n- wiggle     ... wiggle format\n2nd word:\n- read1_5p   ... signal from only 5' of the 1st read, useful for CAGE/RAMPAGE etc\n- read2      ... signal from only 2nd read",
          "info" : {
            "orig_name" : "--outWigType"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_wig_strand",
          "description" : "strandedness of wiggle/bedGraph output\n\n- Stranded   ...  separate strands, str1 and str2\n- Unstranded ...  collapsed strands",
          "info" : {
            "orig_name" : "--outWigStrand"
          },
          "example" : [
            "Stranded"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_wig_references_prefix",
          "description" : "prefix matching reference names to include in the output wiggle file, e.g. \\"chr\\", default \\"-\\" - include all references",
          "info" : {
            "orig_name" : "--outWigReferencesPrefix"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_wig_norm",
          "description" : "type of normalization for the signal\n\n- RPM    ... reads per million of mapped reads\n- None   ... no normalization, \\"raw\\" counts",
          "info" : {
            "orig_name" : "--outWigNorm"
          },
          "example" : [
            "RPM"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output Filtering",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_filter_type",
          "description" : "type of filtering\n\n- Normal  ... standard filtering using only current alignment\n- BySJout ... keep only those reads that contain junctions that passed filtering into SJ.out.tab",
          "info" : {
            "orig_name" : "--outFilterType"
          },
          "example" : [
            "Normal"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_filter_multimap_score_range",
          "description" : "the score range below the maximum score for multimapping alignments",
          "info" : {
            "orig_name" : "--outFilterMultimapScoreRange"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_filter_multimap_nmax",
          "description" : "maximum number of loci the read is allowed to map to. Alignments (all of them) will be output only if the read maps to no more loci than this value.\n\nOtherwise no alignments will be output, and the read will be counted as \\"mapped to too many loci\\" in the Log.final.out .",
          "info" : {
            "orig_name" : "--outFilterMultimapNmax"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_filter_mismatch_nmax",
          "description" : "alignment will be output only if it has no more mismatches than this value.",
          "info" : {
            "orig_name" : "--outFilterMismatchNmax"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--out_filter_mismatch_nover_lmax",
          "description" : "alignment will be output only if its ratio of mismatches to *mapped* length is less than or equal to this value.",
          "info" : {
            "orig_name" : "--outFilterMismatchNoverLmax"
          },
          "example" : [
            0.3
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--out_filter_mismatch_nover_read_lmax",
          "description" : "alignment will be output only if its ratio of mismatches to *read* length is less than or equal to this value.",
          "info" : {
            "orig_name" : "--outFilterMismatchNoverReadLmax"
          },
          "example" : [
            1.0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_filter_score_min",
          "description" : "alignment will be output only if its score is higher than or equal to this value.",
          "info" : {
            "orig_name" : "--outFilterScoreMin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--out_filter_score_min_over_lread",
          "description" : "same as outFilterScoreMin, but normalized to read length (sum of mates' lengths for paired-end reads)",
          "info" : {
            "orig_name" : "--outFilterScoreMinOverLread"
          },
          "example" : [
            0.66
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_filter_match_nmin",
          "description" : "alignment will be output only if the number of matched bases is higher than or equal to this value.",
          "info" : {
            "orig_name" : "--outFilterMatchNmin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--out_filter_match_nmin_over_lread",
          "description" : "sam as outFilterMatchNmin, but normalized to the read length (sum of mates' lengths for paired-end reads).",
          "info" : {
            "orig_name" : "--outFilterMatchNminOverLread"
          },
          "example" : [
            0.66
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_filter_intron_motifs",
          "description" : "filter alignment using their motifs\n\n- None                           ... no filtering\n- RemoveNoncanonical             ... filter out alignments that contain non-canonical junctions\n- RemoveNoncanonicalUnannotated  ... filter out alignments that contain non-canonical unannotated junctions when using annotated splice junctions database. The annotated non-canonical junctions will be kept.",
          "info" : {
            "orig_name" : "--outFilterIntronMotifs"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--out_filter_intron_strands",
          "description" : "filter alignments\n\n- RemoveInconsistentStrands      ... remove alignments that have junctions with inconsistent strands\n- None                           ... no filtering",
          "info" : {
            "orig_name" : "--outFilterIntronStrands"
          },
          "example" : [
            "RemoveInconsistentStrands"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output splice junctions (SJ.out.tab)",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_sj_type",
          "description" : "type of splice junction output\n\n- Standard    ... standard SJ.out.tab output\n- None        ... no splice junction output",
          "info" : {
            "orig_name" : "--outSJtype"
          },
          "example" : [
            "Standard"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Output Filtering: Splice Junctions",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--out_sj_filter_reads",
          "description" : "which reads to consider for collapsed splice junctions output\n\n- All     ... all reads, unique- and multi-mappers\n- Unique  ... uniquely mapping reads only",
          "info" : {
            "orig_name" : "--outSJfilterReads"
          },
          "example" : [
            "All"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sj_filter_overhang_min",
          "description" : "minimum overhang length for splice junctions on both sides for: (1) non-canonical motifs, (2) GT/AG and CT/AC motif, (3) GC/AG and CT/GC motif, (4) AT/AC and GT/AT motif. -1 means no output for that motif\n\ndoes not apply to annotated junctions",
          "info" : {
            "orig_name" : "--outSJfilterOverhangMin"
          },
          "example" : [
            30,
            12,
            12,
            12
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sj_filter_count_unique_min",
          "description" : "minimum uniquely mapping read count per junction for: (1) non-canonical motifs, (2) GT/AG and CT/AC motif, (3) GC/AG and CT/GC motif, (4) AT/AC and GT/AT motif. -1 means no output for that motif\n\nJunctions are output if one of outSJfilterCountUniqueMin OR outSJfilterCountTotalMin conditions are satisfied\ndoes not apply to annotated junctions",
          "info" : {
            "orig_name" : "--outSJfilterCountUniqueMin"
          },
          "example" : [
            3,
            1,
            1,
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sj_filter_count_total_min",
          "description" : "minimum total (multi-mapping+unique) read count per junction for: (1) non-canonical motifs, (2) GT/AG and CT/AC motif, (3) GC/AG and CT/GC motif, (4) AT/AC and GT/AT motif. -1 means no output for that motif\n\nJunctions are output if one of outSJfilterCountUniqueMin OR outSJfilterCountTotalMin conditions are satisfied\ndoes not apply to annotated junctions",
          "info" : {
            "orig_name" : "--outSJfilterCountTotalMin"
          },
          "example" : [
            3,
            1,
            1,
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sj_filter_dist_to_other_sj_min",
          "description" : "minimum allowed distance to other junctions' donor/acceptor\n\ndoes not apply to annotated junctions",
          "info" : {
            "orig_name" : "--outSJfilterDistToOtherSJmin"
          },
          "example" : [
            10,
            0,
            5,
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--out_sj_filter_intron_max_vs_read_n",
          "description" : "maximum gap allowed for junctions supported by 1,2,3,,,N reads\n\ni.e. by default junctions supported by 1 read can have gaps <=50000b, by 2 reads: <=100000b, by 3 reads: <=200000. by >=4 reads any gap <=alignIntronMax\ndoes not apply to annotated junctions",
          "info" : {
            "orig_name" : "--outSJfilterIntronMaxVsReadN"
          },
          "example" : [
            50000,
            100000,
            200000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Scoring",
      "arguments" : [
        {
          "type" : "integer",
          "name" : "--score_gap",
          "description" : "splice junction penalty (independent on intron motif)",
          "info" : {
            "orig_name" : "--scoreGap"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_gap_noncan",
          "description" : "non-canonical junction penalty (in addition to scoreGap)",
          "info" : {
            "orig_name" : "--scoreGapNoncan"
          },
          "example" : [
            -8
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_gap_gcag",
          "description" : "GC/AG and CT/GC junction penalty (in addition to scoreGap)",
          "info" : {
            "orig_name" : "--scoreGapGCAG"
          },
          "example" : [
            -4
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_gap_atac",
          "description" : "AT/AC  and GT/AT junction penalty  (in addition to scoreGap)",
          "info" : {
            "orig_name" : "--scoreGapATAC"
          },
          "example" : [
            -8
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_genomic_length_log2scale",
          "description" : "extra score logarithmically scaled with genomic length of the alignment: scoreGenomicLengthLog2scale*log2(genomicLength)",
          "info" : {
            "orig_name" : "--scoreGenomicLengthLog2scale"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_del_open",
          "description" : "deletion open penalty",
          "info" : {
            "orig_name" : "--scoreDelOpen"
          },
          "example" : [
            -2
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_del_base",
          "description" : "deletion extension penalty per base (in addition to scoreDelOpen)",
          "info" : {
            "orig_name" : "--scoreDelBase"
          },
          "example" : [
            -2
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_ins_open",
          "description" : "insertion open penalty",
          "info" : {
            "orig_name" : "--scoreInsOpen"
          },
          "example" : [
            -2
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_ins_base",
          "description" : "insertion extension penalty per base (in addition to scoreInsOpen)",
          "info" : {
            "orig_name" : "--scoreInsBase"
          },
          "example" : [
            -2
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--score_stitch_sj_shift",
          "description" : "maximum score reduction while searching for SJ boundaries in the stitching step",
          "info" : {
            "orig_name" : "--scoreStitchSJshift"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Alignments and Seeding",
      "arguments" : [
        {
          "type" : "integer",
          "name" : "--seed_search_start_lmax",
          "description" : "defines the search start point through the read - the read is split into pieces no longer than this value",
          "info" : {
            "orig_name" : "--seedSearchStartLmax"
          },
          "example" : [
            50
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--seed_search_start_lmax_over_lread",
          "description" : "seedSearchStartLmax normalized to read length (sum of mates' lengths for paired-end reads)",
          "info" : {
            "orig_name" : "--seedSearchStartLmaxOverLread"
          },
          "example" : [
            1.0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_search_lmax",
          "description" : "defines the maximum length of the seeds, if =0 seed length is not limited",
          "info" : {
            "orig_name" : "--seedSearchLmax"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_multimap_nmax",
          "description" : "only pieces that map fewer than this value are utilized in the stitching procedure",
          "info" : {
            "orig_name" : "--seedMultimapNmax"
          },
          "example" : [
            10000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_per_read_nmax",
          "description" : "max number of seeds per read",
          "info" : {
            "orig_name" : "--seedPerReadNmax"
          },
          "example" : [
            1000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_per_window_nmax",
          "description" : "max number of seeds per window",
          "info" : {
            "orig_name" : "--seedPerWindowNmax"
          },
          "example" : [
            50
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_none_loci_per_window",
          "description" : "max number of one seed loci per window",
          "info" : {
            "orig_name" : "--seedNoneLociPerWindow"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_split_min",
          "description" : "min length of the seed sequ''' + '''ences split by Ns or mate gap",
          "info" : {
            "orig_name" : "--seedSplitMin"
          },
          "example" : [
            12
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--seed_map_min",
          "description" : "min length of seeds to be mapped",
          "info" : {
            "orig_name" : "--seedMapMin"
          },
          "example" : [
            5
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_intron_min",
          "description" : "minimum intron size, genomic gap is considered intron if its length>=alignIntronMin, otherwise it is considered Deletion",
          "info" : {
            "orig_name" : "--alignIntronMin"
          },
          "example" : [
            21
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_intron_max",
          "description" : "maximum intron size, if 0, max intron size will be determined by (2^winBinNbits)*winAnchorDistNbins",
          "info" : {
            "orig_name" : "--alignIntronMax"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_mates_gap_max",
          "description" : "maximum gap between two mates, if 0, max intron gap will be determined by (2^winBinNbits)*winAnchorDistNbins",
          "info" : {
            "orig_name" : "--alignMatesGapMax"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_sj_overhang_min",
          "description" : "minimum overhang (i.e. block size) for spliced alignments",
          "info" : {
            "orig_name" : "--alignSJoverhangMin"
          },
          "example" : [
            5
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_sj_stitch_mismatch_nmax",
          "description" : "maximum number of mismatches for stitching of the splice junctions (-1: no limit).\n\n(1) non-canonical motifs, (2) GT/AG and CT/AC motif, (3) GC/AG and CT/GC motif, (4) AT/AC and GT/AT motif.",
          "info" : {
            "orig_name" : "--alignSJstitchMismatchNmax"
          },
          "example" : [
            0,
            -1,
            0,
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_sjdb_overhang_min",
          "description" : "minimum overhang (i.e. block size) for annotated (sjdb) spliced alignments",
          "info" : {
            "orig_name" : "--alignSJDBoverhangMin"
          },
          "example" : [
            3
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_spliced_mate_map_lmin",
          "description" : "minimum mapped length for a read mate that is spliced",
          "info" : {
            "orig_name" : "--alignSplicedMateMapLmin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--align_spliced_mate_map_lmin_over_lmate",
          "description" : "alignSplicedMateMapLmin normalized to mate length",
          "info" : {
            "orig_name" : "--alignSplicedMateMapLminOverLmate"
          },
          "example" : [
            0.66
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_windows_per_read_nmax",
          "description" : "max number of windows per read",
          "info" : {
            "orig_name" : "--alignWindowsPerReadNmax"
          },
          "example" : [
            10000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_transcripts_per_window_nmax",
          "description" : "max number of transcripts per window",
          "info" : {
            "orig_name" : "--alignTranscriptsPerWindowNmax"
          },
          "example" : [
            100
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--align_transcripts_per_read_nmax",
          "description" : "max number of different alignments per read to consider",
          "info" : {
            "orig_name" : "--alignTranscriptsPerReadNmax"
          },
          "example" : [
            10000
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--align_ends_type",
          "description" : "type of read ends alignment\n\n- Local             ... standard local alignment with soft-clipping allowed\n- EndToEnd          ... force end-to-end read alignment, do not soft-clip\n- Extend5pOfRead1   ... fully extend only the 5p of the read1, all other ends: local alignment\n- Extend5pOfReads12 ... fully extend only the 5p of the both read1 and read2, all other ends: local alignment",
          "info" : {
            "orig_name" : "--alignEndsType"
          },
          "example" : [
            "Local"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--align_ends_protrude",
          "description" : "allow protrusion of alignment ends, i.e. start (end) of the +strand mate downstream of the start (end) of the -strand mate\n\n1st word: int: maximum number of protrusion bases allowed\n2nd word: string:\n-                     ConcordantPair ... report alignments with non-zero protrusion as concordant pairs\n-                     DiscordantPair ... report alignments with non-zero protrusion as discordant pairs",
          "info" : {
            "orig_name" : "--alignEndsProtrude"
          },
          "example" : [
            "0    ConcordantPair"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--align_soft_clip_at_reference_ends",
          "description" : "allow the soft-clipping of the alignments past the end of the chromosomes\n\n- Yes ... allow\n- No  ... prohibit, useful for compatibility with Cufflinks",
          "info" : {
            "orig_name" : "--alignSoftClipAtReferenceEnds"
          },
          "example" : [
            "Yes"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--align_insertion_flush",
          "description" : "how to flush ambiguous insertion positions\n\n- None    ... insertions are not flushed\n- Right   ... insertions are flushed to the right",
          "info" : {
            "orig_name" : "--alignInsertionFlush"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Paired-End reads",
      "arguments" : [
        {
          "type" : "integer",
          "name" : "--pe_overlap_nbases_min",
          "description" : "minimum number of overlapping bases to trigger mates merging and realignment. Specify >0 value to switch on the \\"merginf of overlapping mates\\" algorithm.",
          "info" : {
            "orig_name" : "--peOverlapNbasesMin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--pe_overlap_mm_p",
          "description" : "maximum proportion of mismatched bases in the overlap area",
          "info" : {
            "orig_name" : "--peOverlapMMp"
          },
          "example" : [
            0.01
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Windows, Anchors, Binning",
      "arguments" : [
        {
          "type" : "integer",
          "name" : "--win_anchor_multimap_nmax",
          "description" : "max number of loci anchors are allowed to map to",
          "info" : {
            "orig_name" : "--winAnchorMultimapNmax"
          },
          "example" : [
            50
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--win_bin_nbits",
          "description" : "=log2(winBin), where winBin is the size of the bin for the windows/clustering, each window will occupy an integer number of bins.",
          "info" : {
            "orig_name" : "--winBinNbits"
          },
          "example" : [
            16
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--win_anchor_dist_nbins",
          "description" : "max number of bins between two anchors that allows aggregation of anchors into one window",
          "info" : {
            "orig_name" : "--winAnchorDistNbins"
          },
          "example" : [
            9
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--win_flank_nbins",
          "description" : "log2(winFlank), where win Flank is the size of the left and right flanking regions for each window",
          "info" : {
            "orig_name" : "--winFlankNbins"
          },
          "example" : [
            4
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "double",
          "name" : "--win_read_coverage_relative_min",
          "description" : "minimum relative coverage of the read sequence by the seeds in a window, for STARlong algorithm only.",
          "info" : {
            "orig_name" : "--winReadCoverageRelativeMin"
          },
          "example" : [
            0.5
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--win_read_coverage_bases_min",
          "description" : "minimum number of bases covered by the seeds in a window , for STARlong algorithm only.",
          "info" : {
            "orig_name" : "--winReadCoverageBasesMin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Chimeric Alignments",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--chim_out_type",
          "description" : "type of chimeric output\n\n- Junctions       ... Chimeric.out.junction\n- SeparateSAMold  ... output old SAM into separate Chimeric.out.sam file\n- WithinBAM       ... output into main aligned BAM files (Aligned.*.bam)\n- WithinBAM HardClip  ... (default) hard-clipping in the CIGAR for supplemental chimeric alignments (default if no 2nd word is present)\n- WithinBAM SoftClip  ... soft-clipping in the CIGAR for supplemental chimeric alignments",
          "info" : {
            "orig_name" : "--chimOutType"
          },
          "example" : [
            "Junctions"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_segment_min",
          "description" : "minimum length of chimeric segment length, if ==0, no chimeric output",
          "info" : {
            "orig_name" : "--chimSegmentMin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_score_min",
          "description" : "minimum total (summed) score of the chimeric segments",
          "info" : {
            "orig_name" : "--chimScoreMin"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_score_drop_max",
          "description" : "max drop (difference) of chimeric score (the sum of scores of all chimeric segments) from the read length",
          "info" : {
            "orig_name" : "--chimScoreDropMax"
          },
          "example" : [
            20
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_score_separation",
          "description" : "minimum difference (separation) between the best chimeric score and the next one",
          "info" : {
            "orig_name" : "--chimScoreSeparation"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_score_junction_non_gtag",
          "description" : "penalty for a non-GT/AG chimeric junction",
          "info" : {
            "orig_name" : "--chimScoreJunctionNonGTAG"
          },
          "example" : [
            -1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_junction_overhang_min",
          "description" : "minimum overhang for a chimeric junction",
          "info" : {
            "orig_name" : "--chimJunctionOverhangMin"
          },
          "example" : [
            20
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_segment_read_gap_max",
          "description" : "maximum gap in the read sequence between chimeric segments",
          "info" : {
            "orig_name" : "--chimSegmentReadGapMax"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--chim_filter",
          "description" : "different filters for chimeric alignments\n\n- None ... no filtering\n- banGenomicN ... Ns are not allowed in the genome sequence around the chimeric junction",
          "info" : {
            "orig_name" : "--chimFilter"
          },
          "example" : [
            "banGenomicN"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_main_segment_mult_nmax",
          "description" : "maximum number of multi-alignments for the main chimeric segment. =1 will prohibit multimapping main segments.",
          "info" : {
            "orig_name" : "--chimMainSegmentMultNmax"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_multimap_nmax",
          "description" : "maximum number of chimeric multi-alignments\n\n- 0 ... use the old scheme for chimeric detection which only considered unique alignments",
          "info" : {
            "orig_name" : "--chimMultimapNmax"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_multimap_score_range",
          "description" : "the score range for multi-mapping chimeras below the best chimeric score. Only works with --chim_multimap_nmax > 1",
          "info" : {
            "orig_name" : "--chimMultimapScoreRange"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_nonchim_score_drop_min",
          "description" : "to trigger chimeric detection, the drop in the best non-chimeric alignment score with respect to the read length has to be greater than this value",
          "info" : {
            "orig_name" : "--chimNonchimScoreDropMin"
          },
          "example" : [
            20
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--chim_out_junction_format",
          "description" : "formatting type for the Chimeric.out.junction file\n\n- 0 ... no comment lines/headers\n- 1 ... comment lines at the end of the file: command line and Nreads: total, unique/multi-mapping",
          "info" : {
            "orig_name" : "--chimOutJunctionFormat"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Quantification of Annotations",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--quant_mode",
          "description" : "types of quantification requested\n\n- -                ... none\n- TranscriptomeSAM ... output SAM/BAM alignments to transcriptome into a separate file\n- GeneCounts       ... count reads per gene",
          "info" : {
            "orig_name" : "--quantMode"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--quant_transcriptome_bam_compression",
          "description" : "-2 to 10  transcriptome BAM compression level\n\n- -2  ... no BAM output\n- -1  ... default compression (6?)\n-  0  ... no compression\n-  10 ... maximum compression",
          "info" : {
            "orig_name" : "--quantTranscriptomeBAMcompression"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--quant_transcriptome_sam_output",
          "description" : "alignment filtering for TranscriptomeSAM output\n\n- BanSingleEnd_BanIndels_ExtendSoftclip ... prohibit indels and single-end alignments, extend softclips - compatible with RSEM\n- BanSingleEnd               ... prohibit single-end alignments, allow indels and softclips\n- BanSingleEnd_ExtendSoftclip ... prohibit single-end alignments, extend softclips, allow indels",
          "info" : {
            "orig_name" : "--quantTranscriptomeSAMoutput"
          },
          "example" : [
            "BanSingleEnd_BanIndels_ExtendSoftclip"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "2-pass Mapping",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--twopass_mode",
          "description" : "2-pass mapping mode.\n\n- None        ... 1-pass mapping\n- Basic       ... basic 2-pass mapping, with all 1st pass junctions inserted into the genome indices on the fly",
          "info" : {
            "orig_name" : "--twopassMode"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--twopass1reads_n",
          "description" : "number of reads to process for the 1st step. Use very large number (or default -1) to map all reads in the first step.",
          "info" : {
            "orig_name" : "--twopass1readsN"
          },
          "example" : [
            -1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "WASP parameters",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--wasp_output_mode",
          "description" : "WASP allele-specific output type. This is re-implementation of the original WASP mappability filtering by Bryce van de Geijn, Graham McVicker, Yoav Gilad & Jonathan K Pritchard. Please cite the original WASP paper: Nature Methods 12, 1061-1063 (2015), https://www.nature.com/articles/nmeth.3582 .\n\n- SAMtag      ... add WASP tags to the alignments that pass WASP filtering",
          "info" : {
            "orig_name" : "--waspOutputMode"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "STARsolo (single cell RNA-seq) parameters",
      "arguments" : [
        {
          "type" : "string",
          "name" : "--solo_type",
          "description" : "type of single-cell RNA-seq\n\n- CB_UMI_Simple   ... (a.k.a. Droplet) one UMI and one Cell Barcode of fixed length in read2, e.g. Drop-seq and 10X Chromium.\n- CB_UMI_Complex  ... multiple Cell Barcodes of varying length, one UMI of fixed length and one adapter sequence of fixed length are allowed in read2 only (e.g. inDrop, ddSeq).\n- CB_samTagOut    ... output Cell Barcode as CR and/or CB SAm tag. No UMI counting. --readFilesIn cDNA_read1 [cDNA_read2 if paired-end] CellBarcode_read . Requires --out_sam_type BAM Unsorted [and/or SortedByCoordinate]\n- SmartSeq        ... Smart-seq: each cell in a separate FASTQ (paired- or single-end), barcodes are corresponding read-groups, no UMI sequences, alignments deduplicated according to alignment start and end (after extending soft-clipped bases)",
          "info" : {
            "orig_name" : "--soloType"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cb_type",
          "description" : "cell barcode type\n\nSequence: cell barcode is a sequence (standard option)\nString: cell barcode is an arbitrary string",
          "info" : {
            "orig_name" : "--soloCBtype"
          },
          "example" : [
            "Sequence"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cb_whitelist",
          "description" : "file(s) with whitelist(s) of cell barcodes. Only --solo_type CB_UMI_Complex allows more than one whitelist file.\n\n- None            ... no whitelist: all cell barcodes are allowed",
          "info" : {
            "orig_name" : "--soloCBwhitelist"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_cb_start",
          "description" : "cell barcode start base",
          "info" : {
            "orig_name" : "--soloCBstart"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_cb_len",
          "description" : "cell barcode length",
          "info" : {
            "orig_name" : "--soloCBlen"
          },
          "example" : [
            16
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_umi_start",
          "description" : "UMI start base",
          "info" : {
            "orig_name" : "--soloUMIstart"
          },
          "example" : [
            17
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_umi_len",
          "description" : "UMI length",
          "info" : {
            "orig_name" : "--soloUMIlen"
          },
          "example" : [
            10
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_barcode_read_length",
          "description" : "length of the barcode read\n\n- 1   ... equal to sum of soloCBlen+soloUMIlen\n- 0   ... not defined, do not check",
          "info" : {
            "orig_name" : "--soloBarcodeReadLength"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_barcode_mate",
          "description" : "identifies which read mate contains the barcode (CB+UMI) sequence\n\n- 0   ... barcode sequence is on separate read, which should always be the last file in the --readFilesIn listed\n- 1   ... barcode sequence is a part of mate 1\n- 2   ... barcode sequence is a part of mate 2",
          "info" : {
            "orig_name" : "--soloBarcodeMate"
          },
          "example" : [
            0
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cb_position",
          "description" : "position of Cell Barcode(s) on the barcode read.\n\nPresently only works with --solo_type CB_UMI_Complex, and barcodes are assumed to be on Read2.\nFormat for each barcode: startAnchor_startPosition_endAnchor_endPosition\nstart(end)Anchor defines the Anchor Base for the CB: 0: read start; 1: read end; 2: adapter start; 3: adapter end\nstart(end)Position is the 0-based position with of the CB start(end) with respect to the Anchor Base\nString for different barcodes are separated by space.\nExample: inDrop (Zilionis et al, Nat. Protocols, 2017):\n--solo_cb_position  0_0_2_-1  3_1_3_8",
          "info" : {
            "orig_name" : "--soloCBposition"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_umi_position",
          "description" : "position of the UMI on the barcode read, same as soloCBposition\n\nExample: inDrop (Zilionis et al, Nat. Protocols, 2017):\n--solo_cb_position  3_9_3_14",
          "info" : {
            "orig_name" : "--soloUMIposition"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_adapter_sequence",
          "description" : "adapter sequence to anchor barcodes. Only one adapter sequence is allowed.",
          "info" : {
            "orig_name" : "--soloAdapterSequence"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "integer",
          "name" : "--solo_adapter_mismatches_nmax",
          "description" : "maximum number of mismatches allowed in adapter sequence.",
          "info" : {
            "orig_name" : "--soloAdapterMismatchesNmax"
          },
          "example" : [
            1
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cb_match_wl_type",
          "description" : "matching the Cell Barcodes to the WhiteList\n\n- Exact                           ... only exact matches allowed\n- 1MM                             ... only one match in whitelist with 1 mismatched base allowed. Allowed CBs have to have at least one read with exact match.\n- 1MM_multi                       ... multiple matches in whitelist with 1 mismatched base allowed, posterior probability calculation is used choose one of the matches.\nAllowed CBs have to have at least one read with exact match. This option matches best with CellRanger 2.2.0\n- 1MM_multi_pseudocounts          ... same as 1MM_Multi, but pseudocounts of 1 are added to all whitelist barcodes.\n- 1MM_multi_Nbase_pseudocounts    ... same as 1MM_multi_pseudocounts, multimatching to WL is allowed for CBs with N-bases. This option matches best with CellRanger >= 3.0.0\n- EditDist_2                    ... allow up to edit distance of 3 fpr each of the barcodes. May include one deletion + one insertion. Only works with --solo_type CB_UMI_Complex. Matches to multiple passlist barcdoes are not allowed. Similar to ParseBio Split-seq pipeline.",
          "info" : {
            "orig_name" : "--soloCBmatchWLtype"
          },
          "example" : [
            "1MM_multi"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_input_sam_attr_barcode_seq",
          "description" : "when inputting reads from a SAM file (--readsFileType SAM SE/PE), these SAM attributes mark the barcode sequence (in proper order).\n\nFor instance, for 10X CellRanger or STARsolo BAMs, use --solo_input_sam_attr_barcode_seq CR UR .\nThis parameter is required when running STARsolo with input from SAM.",
          "info" : {
            "orig_name" : "--soloInputSAMattrBarcodeSeq"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_input_sam_attr_barcode_qual",
          "description" : "when inputting reads from a SAM file (--readsFileType SAM SE/PE), these SAM attributes mark the barcode qualities (in proper order).\n\nFor instance, for 10X CellRanger or STARsolo BAMs, use --solo_input_sam_attr_barcode_qual CY UY .\nIf this parameter is '-' (default), the quality 'H' will be assigned to all bases.",
          "info" : {
            "orig_name" : "--soloInputSAMattrBarcodeQual"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_strand",
          "description" : "strandedness of the solo libraries:\n\n- Unstranded  ... no strand information\n- Forward     ... read strand same as the original RNA molecule\n- Reverse     ... read strand opposite to the original RNA molecule",
          "info" : {
            "orig_name" : "--soloStrand"
          },
          "example" : [
            "Forward"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_features",
          "description" : "genomic features for which the UMI counts per Cell Barcode are collected\n\n- Gene            ... genes: reads match the gene transcript\n- SJ              ... splice junctions: reported in SJ.out.tab\n- GeneFull        ... full gene (pre-mRNA): count all reads overlapping genes' exons and introns\n- GeneFull_ExonOverIntron ... full gene (pre-mRNA): count all reads overlapping genes' exons and introns: prioritize 100% overlap with exons\n- GeneFull_Ex50pAS        ... full gene (pre-RNA): count all reads overlapping genes' exons and introns: prioritize >50% overlap with exons. Do not count reads with 100% exonic overlap in the antisense direction.",
          "info" : {
            "orig_name" : "--soloFeatures"
          },
          "example" : [
            "Gene"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_multi_mappers",
          "description" : "counting method for reads mapping to multiple genes\n\n- Unique     ... count only reads that map to unique genes\n- Uniform    ... uniformly distribute multi-genic UMIs to all genes\n- Rescue     ... distribute UMIs proportionally to unique+uniform counts (~ first iteration of EM)\n- PropUnique ... distribute UMIs proportionally to unique mappers, if present, and uniformly if not.\n- EM         ... multi-gene UMIs are distributed using Expectation Maximization algorithm",
          "info" : {
            "orig_name" : "--soloMultiMappers"
          },
          "example" : [
            "Unique"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_umi_dedup",
          "description" : "type of UMI deduplication (collapsing) algorithm\n\n- 1MM_All                     ... all UMIs with 1 mismatch distance to each other are collapsed (i.e. counted once).\n- 1MM_Directional_UMItools    ... follows the \\"directional\\" method from the UMI-tools by Smith, Heger and Sudbery (Genome Research 2017).\n- 1MM_Directional             ... same as 1MM_Directional_UMItools, but with more stringent criteria for duplicate UMIs\n- Exact                       ... only exactly matching UMIs are collapsed.\n- NoDedup                     ... no deduplication of UMIs, count all reads.\n- 1MM_CR                      ... CellRanger2-4 algorithm for 1MM UMI collapsing.",
          "info" : {
            "orig_name" : "--soloUMIdedup"
          },
          "example" : [
            "1MM_All"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_umi_filtering",
          "description" : "type of UMI filtering (for reads uniquely mapping to genes)\n\n- -                  ... basic filtering: remove UMIs with N and homopolymers (similar to CellRanger 2.2.0).\n- MultiGeneUMI       ... basic + remove lower-count UMIs that map to more than one gene.\n- MultiGeneUMI_All   ... basic + remove all UMIs that map to more than one gene.\n- MultiGeneUMI_CR    ... basic + remove lower-count UMIs that map to more than one gene, matching CellRanger > 3.0.0 .\nOnly works with --solo_umi_dedup 1MM_CR",
          "info" : {
            "orig_name" : "--soloUMIfiltering"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_out_file_names",
          "description" : "file names for STARsolo output:\n\nfile_name_prefix   gene_names   barcode_sequences   cell_feature_count_matrix",
          "info" : {
            "orig_name" : "--soloOutFileNames"
          },
          "example" : [
            "Solo.out/",
            "features.tsv",
            "barcodes.tsv",
            "matrix.mtx"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cell_filter",
          "description" : "cell filtering type and parameters\n\n- None            ... do not output filtered cells\n- TopCells        ... only report top cells by UMI count, followed by the exact number of cells\n- CellRanger2.2   ... simple filtering of CellRanger 2.2.\nCan be followed by numbers: number of expected cells, robust maximum percentile for UMI count, maximum to minimum ratio for UMI count\nThe harcoded values are from CellRanger: nExpectedCells=3000;  maxPercentile=0.99;  maxMinRatio=10\n- EmptyDrops_CR   ... EmptyDrops filtering in CellRanger flavor. Please cite the original EmptyDrops paper: A.T.L Lun et al, Genome Biology, 20, 63 (2019): https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1662-y\nCan be followed by 10 numeric parameters:  nExpectedCells   maxPercentile   maxMinRatio   indMin   indMax   umiMin   umiMinFracMedian   candMaxN   FDR   simN\nThe harcoded values are from CellRanger:             3000            0.99            10    45000    90000      500               0.01      20000  0.01  10000",
          "info" : {
            "orig_name" : "--soloCellFilter"
          },
          "example" : [
            "CellRanger2.2",
            "3000",
            "0.99",
            "10"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_out_format_features_gene_field3",
          "description" : "field 3 in the Gene features.tsv file. If \\"-\\", then no 3rd field is output.",
          "info" : {
            "orig_name" : "--soloOutFormatFeaturesGeneField3"
          },
          "example" : [
            "Gene Expression"
          ],
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "string",
          "name" : "--solo_cell_read_stats",
          "description" : "Output reads statistics for each CB\n\n- Standard    ... standard output",
          "info" : {
            "orig_name" : "--soloCellReadStats"
          },
          "required" : false,
          "direction" : "input",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Inputs",
      "arguments" : [
        {
          "type" : "file",
          "name" : "--input",
          "alternatives" : [
            "--readFilesIn"
          ],
          "description" : "The single-end or paired-end R1 FastQ files to be processed.",
          "example" : [
            "mysample_S1_L001_R1_001.fastq.gz"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : true,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--input_r2",
          "description" : "The paired-end R2 FastQ files to be processed. Only required if --input is a paired-end R1 file.",
          "example" : [
            "mysample_S1_L001_R2_001.fastq.gz"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "input",
          "multiple" : true,
          "multiple_sep" : ";"
        }
      ]
    },
    {
      "name" : "Outputs",
      "arguments" : [
        {
          "type" : "file",
          "name" : "--aligned_reads",
          "description" : "The output file containing the aligned reads.",
          "example" : [
            "aligned_reads.bam"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : true,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--reads_per_gene",
          "description" : "The output file containing the number of reads per gene.",
          "example" : [
            "reads_per_gene.tsv"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--unmapped",
          "description" : "The output file containing the unmapped reads.",
          "example" : [
            "unmapped.fastq"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--unmapped_r2",
          "description" : "The output file containing the unmapped R2 reads.",
          "example" : [
            "unmapped_r2.fastq"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--chimeric_junctions",
          "description" : "The output file containing the chimeric junctions.",
          "example" : [
            "chimeric_junctions.tsv"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--log",
          "description" : "The output file containing the log of the alignment process.",
          "example" : [
            "log.txt"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--splice_junctions",
          "description" : "The output file containing the splice junctions.",
          "example" : [
            "splice_junctions.tsv"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        },
        {
          "type" : "file",
          "name" : "--reads_aligned_to_transcriptome",
          "description" : "The output file containing the alignments to transcriptome in BAM formats. This file is generated when --quantMode is set to TranscriptomeSAM.",
          "example" : [
            "transcriptome_aligned.bam"
          ],
          "must_exist" : true,
          "create_parent" : true,
          "required" : false,
          "direction" : "output",
          "multiple" : false,
          "multiple_sep" : ";"
        }
      ]
    }
  ],
  "resources" : [
    {
      "type" : "python_script",
      "path" : "script.py",
      "is_executable" : true
    }
  ],
  "description" : "Aligns reads to a reference genome using STAR.\n",
  "test_resources" : [
    {
      "type" : "bash_script",
      "path" : "test.sh",
      "is_executable" : true
    }
  ],
  "status" : "enabled",
  "requirements" : {
    "commands" : [
      "ps"
    ]
  },
  "keywords" : [
    "align",
    "fasta",
    "genome"
  ],
  "license" : "MIT",
  "references" : {
    "doi" : [
      "10.1093/bioinformatics/bts635"
    ]
  },
  "links" : {
    "repository" : "https://github.com/alexdobin/STAR",
    "documentation" : "https://github.com/alexdobin/STAR/blob/master/doc/STARmanual.pdf"
  },
  "runners" : [
    {
      "type" : "executable",
      "id" : "executable",
      "docker_setup_strategy" : "ifneedbepullelsecachedbuild"
    },
    {
      "type" : "nextflow",
      "id" : "nextflow",
      "directives" : {
        "tag" : "$id"
      },
      "auto" : {
        "simplifyInput" : true,
        "simplifyOutput" : false,
        "transcript" : false,
        "publish" : false
      },
      "config" : {
        "labels" : {
          "mem1gb" : "memory = 1000000000.B",
          "mem2gb" : "memory = 2000000000.B",
          "mem5gb" : "memory = 5000000000.B",
          "mem10gb" : "memory = 10000000000.B",
          "mem20gb" : "memory = 20000000000.B",
          "mem50gb" : "memory = 50000000000.B",
          "mem100gb" : "memory = 100000000000.B",
          "mem200gb" : "memory = 200000000000.B",
          "mem500gb" : "memory = 500000000000.B",
          "mem1tb" : "memory = 1000000000000.B",
          "mem2tb" : "memory = 2000000000000.B",
          "mem5tb" : "memory = 5000000000000.B",
          "mem10tb" : "memory = 10000000000000.B",
          "mem20tb" : "memory = 20000000000000.B",
          "mem50tb" : "memory = 50000000000000.B",
          "mem100tb" : "memory = 100000000000000.B",
          "mem200tb" : "memory = 200000000000000.B",
          "mem500tb" : "memory = 500000000000000.B",
          "mem1gib" : "memory = 1073741824.B",
          "mem2gib" : "memory = 2147483648.B",
          "mem4gib" : "memory = 4294967296.B",
          "mem8gib" : "memory = 8589934592.B",
          "mem16gib" : "memory = 17179869184.B",
          "mem32gib" : "memory = 34359738368.B",
          "mem64gib" : "memory = 68719476736.B",
          "mem128gib" : "memory = 137438953472.B",
          "mem256gib" : "memory = 274877906944.B",
          "mem512gib" : "memory = 549755813888.B",
          "mem1tib" : "memory = 1099511627776.B",
          "mem2tib" : "memory = 2199023255552.B",
          "mem4tib" : "memory = 4398046511104.B",
          "mem8tib" : "memory = 8796093022208.B",
          "mem16tib" : "memory = 17592186044416.B",
          "mem32tib" : "memory = 35184372088832.B",
          "mem64tib" : "memory = 70368744177664.B",
          "mem128tib" : "memory = 140737488355328.B",
          "mem256tib" : "memory = 281474976710656.B",
          "mem512tib" : "memory = 562949953421312.B",
          "cpu1" : "cpus = 1",
          "cpu2" : "cpus = 2",
          "cpu5" : "cpus = 5",
          "cpu10" : "cpus = 10",
          "cpu20" : "cpus = 20",
          "cpu50" : "cpus = 50",
          "cpu100" : "cpus = 100",
          "cpu200" : "cpus = 200",
          "cpu500" : "cpus = 500",
          "cpu1000" : "cpus = 1000"
        }
      },
      "debug" : false,
      "container" : "docker"
    }
  ],
  "engines" : [
    {
      "type" : "docker",
      "id" : "docker",
      "image" : "python:3.12-slim",
      "target_registry" : "images.viash-hub.com",
      "target_tag" : "v0.2.0",
      "namespace_separator" : "/",
      "setup" : [
        {
          "type" : "apt",
          "packages" : [
            "procps",
            "gzip",
            "bzip2"
          ],
          "interactive" : false
        },
        {
          "type" : "docker",
          "run" : [
            "apt-get update && \\\\\n  apt-get install -y --no-install-recommends ${PACKAGES} && \\\\\n  cd /tmp && \\\\\n  wget --no-check-certificate https://github.com/alexdobin/STAR/archive/refs/tags/${STAR_VERSION}.zip && \\\\\n  unzip ${STAR_VERSION}.zip && \\\\\n  cd STAR-${STAR_VERSION}/source && \\\\\n  make STARstatic CXXFLAGS_SIMD=-std=c++11 && \\\\\n  cp STAR /usr/local/bin && \\\\\n  cd / && \\\\\n  rm -rf /tmp/STAR-${STAR_VERSION} /tmp/${STAR_VERSION}.zip && \\\\\n  apt-get --purge autoremove -y ${PACKAGES} && \\\\\n  apt-get clean\n"
          ],
          "env" : [
            "STAR_VERSION 2.7.11b",
            "PACKAGES gcc g++ make wget zlib1g-dev unzip xxd"
          ]
        },
        {
          "type" : "python",
          "user" : false,
          "packages" : [
            "pyyaml"
          ],
          "upgrade" : true
        },
        {
          "type" : "docker",
          "run" : [
            "STAR --version | sed 's#\\\\(.*\\\\)#star: \\"\\\\1\\"#' > /var/software_versions.txt\n"
          ]
        }
      ]
    },
    {
      "type" : "native",
      "id" : "native"
    }
  ],
  "build_info" : {
    "config" : "/workdir/root/repo/src/star/star_align_reads/config.vsh.yaml",
    "runner" : "nextflow",
    "engine" : "docker|native",
    "output" : "target/nextflow/star/star_align_reads",
    "viash_version" : "0.9.0",
    "git_commit" : "7e530218844c373048bc33de58f021b6460642e5",
    "git_remote" : "https://x-access-token:ghs_kiUBq39QrAlnG6IaeAcTcXhllzqpOV4LDB3e@github.com/viash-hub/biobox"
  },
  "package_config" : {
    "name" : "biobox",
    "version" : "v0.2.0",
    "description" : "A collection of bioinformatics tools for working with sequence data.\n",
    "viash_version" : "0.9.0",
    "source" : "src",
    "target" : "target",
    "config_mods" : [
      ".requirements.commands := ['ps']\n",
      ".engines += { type: \\"native\\" }",
      ".engines[.type == 'docker'].target_registry := 'images.viash-hub.com'",
      ".engines[.type == 'docker'].target_tag := 'v0.2.0'"
    ],
    "keywords" : [
      "bioinformatics",
      "modules",
      "sequencing"
    ],
    "license" : "MIT",
    "organization" : "vsh",
    "links" : {
      "repository" : "https://github.com/viash-hub/biobox",
      "issue_tracker" : "https://github.com/viash-hub/biobox/issues"
    }
  }
}'''))
]

// resolve dependencies dependencies (if any)


// inner workflow
// inner workflow hook
def innerWorkflowFactory(args) {
  def rawScript = '''set -e
tempscript=".viash_script.sh"
cat > "$tempscript" << VIASHMAIN
import tempfile
import subprocess
import shutil
from pathlib import Path
import yaml

## VIASH START
# The following code has been auto-generated by Viash.
par = {
  'run_rng_seed': $( if [ ! -z ${VIASH_PAR_RUN_RNG_SEED+x} ]; then echo "int(r'${VIASH_PAR_RUN_RNG_SEED//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'genome_dir': $( if [ ! -z ${VIASH_PAR_GENOME_DIR+x} ]; then echo "r'${VIASH_PAR_GENOME_DIR//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'genome_load': $( if [ ! -z ${VIASH_PAR_GENOME_LOAD+x} ]; then echo "r'${VIASH_PAR_GENOME_LOAD//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'genome_fasta_files': $( if [ ! -z ${VIASH_PAR_GENOME_FASTA_FILES+x} ]; then echo "r'${VIASH_PAR_GENOME_FASTA_FILES//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'genome_file_sizes': $( if [ ! -z ${VIASH_PAR_GENOME_FILE_SIZES+x} ]; then echo "list(map(int, r'${VIASH_PAR_GENOME_FILE_SIZES//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'genome_transform_output': $( if [ ! -z ${VIASH_PAR_GENOME_TRANSFORM_OUTPUT+x} ]; then echo "r'${VIASH_PAR_GENOME_TRANSFORM_OUTPUT//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'genome_chr_set_mitochondrial': $( if [ ! -z ${VIASH_PAR_GENOME_CHR_SET_MITOCHONDRIAL+x} ]; then echo "r'${VIASH_PAR_GENOME_CHR_SET_MITOCHONDRIAL//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'sjdb_file_chr_start_end': $( if [ ! -z ${VIASH_PAR_SJDB_FILE_CHR_START_END+x} ]; then echo "r'${VIASH_PAR_SJDB_FILE_CHR_START_END//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'sjdb_gtf_file': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_FILE+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_FILE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'sjdb_gtf_chr_prefix': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_CHR_PREFIX+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_CHR_PREFIX//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'sjdb_gtf_feature_exon': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_FEATURE_EXON+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_FEATURE_EXON//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'sjdb_gtf_tag_exon_parent_transcript': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_TRANSCRIPT+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_TRANSCRIPT//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'sjdb_gtf_tag_exon_parent_gene': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'sjdb_gtf_tag_exon_parent_gene_name': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE_NAME+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE_NAME//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'sjdb_gtf_tag_exon_parent_gene_type': $( if [ ! -z ${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE_TYPE+x} ]; then echo "r'${VIASH_PAR_SJDB_GTF_TAG_EXON_PARENT_GENE_TYPE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'sjdb_overhang': $( if [ ! -z ${VIASH_PAR_SJDB_OVERHANG+x} ]; then echo "int(r'${VIASH_PAR_SJDB_OVERHANG//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'sjdb_score': $( if [ ! -z ${VIASH_PAR_SJDB_SCORE+x} ]; then echo "int(r'${VIASH_PAR_SJDB_SCORE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'sjdb_insert_save': $( if [ ! -z ${VIASH_PAR_SJDB_INSERT_SAVE+x} ]; then echo "r'${VIASH_PAR_SJDB_INSERT_SAVE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'var_vcf_file': $( if [ ! -z ${VIASH_PAR_VAR_VCF_FILE+x} ]; then echo "r'${VIASH_PAR_VAR_VCF_FILE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'read_files_type': $( if [ ! -z ${VIASH_PAR_READ_FILES_TYPE+x} ]; then echo "r'${VIASH_PAR_READ_FILES_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'read_files_sam_attr_keep': $( if [ ! -z ${VIASH_PAR_READ_FILES_SAM_ATTR_KEEP+x} ]; then echo "r'${VIASH_PAR_READ_FILES_SAM_ATTR_KEEP//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'read_files_manifest': $( if [ ! -z ${VIASH_PAR_READ_FILES_MANIFEST+x} ]; then echo "r'${VIASH_PAR_READ_FILES_MANIFEST//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'read_files_prefix': $( if [ ! -z ${VIASH_PAR_READ_FILES_PREFIX+x} ]; then echo "r'${VIASH_PAR_READ_FILES_PREFIX//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'read_files_command': $( if [ ! -z ${VIASH_PAR_READ_FILES_COMMAND+x} ]; then echo "r'${VIASH_PAR_READ_FILES_COMMAND//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'read_map_number': $( if [ ! -z ${VIASH_PAR_READ_MAP_NUMBER+x} ]; then echo "int(r'${VIASH_PAR_READ_MAP_NUMBER//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'read_mates_lengths_in': $( if [ ! -z ${VIASH_PAR_READ_MATES_LENGTHS_IN+x} ]; then echo "r'${VIASH_PAR_READ_MATES_LENGTHS_IN//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'read_name_separator': $( if [ ! -z ${VIASH_PAR_READ_NAME_SEPARATOR+x} ]; then echo "r'${VIASH_PAR_READ_NAME_SEPARATOR//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'read_quality_score_base': $( if [ ! -z ${VIASH_PAR_READ_QUALITY_SCORE_BASE+x} ]; then echo "int(r'${VIASH_PAR_READ_QUALITY_SCORE_BASE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'clip_adapter_type': $( if [ ! -z ${VIASH_PAR_CLIP_ADAPTER_TYPE+x} ]; then echo "r'${VIASH_PAR_CLIP_ADAPTER_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'clip3p_nbases': $( if [ ! -z ${VIASH_PAR_CLIP3P_NBASES+x} ]; then echo "list(map(int, r'${VIASH_PAR_CLIP3P_NBASES//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'clip3p_adapter_seq': $( if [ ! -z ${VIASH_PAR_CLIP3P_ADAPTER_SEQ+x} ]; then echo "r'${VIASH_PAR_CLIP3P_ADAPTER_SEQ//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'clip3p_adapter_mm_p': $( if [ ! -z ${VIASH_PAR_CLIP3P_ADAPTER_MM_P+x} ]; then echo "list(map(float, r'${VIASH_PAR_CLIP3P_ADAPTER_MM_P//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'clip3p_after_adapter_nbases': $( if [ ! -z ${VIASH_PAR_CLIP3P_AFTER_ADAPTER_NBASES+x} ]; then echo "list(map(int, r'${VIASH_PAR_CLIP3P_AFTER_ADAPTER_NBASES//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'clip5p_nbases': $( if [ ! -z ${VIASH_PAR_CLIP5P_NBASES+x} ]; then echo "list(map(int, r'${VIASH_PAR_CLIP5P_NBASES//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'limit_genome_generate_ram': $( if [ ! -z ${VIASH_PAR_LIMIT_GENOME_GENERATE_RAM+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_GENOME_GENERATE_RAM//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_io_buffer_size': $( if [ ! -z ${VIASH_PAR_LIMIT_IO_BUFFER_SIZE+x} ]; then echo "list(map(int, r'${VIASH_PAR_LIMIT_IO_BUFFER_SIZE//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'limit_out_sam_one_read_bytes': $( if [ ! -z ${VIASH_PAR_LIMIT_OUT_SAM_ONE_READ_BYTES+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_OUT_SAM_ONE_READ_BYTES//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_out_sj_one_read': $( if [ ! -z ${VIASH_PAR_LIMIT_OUT_SJ_ONE_READ+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_OUT_SJ_ONE_READ//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_out_sj_collapsed': $( if [ ! -z ${VIASH_PAR_LIMIT_OUT_SJ_COLLAPSED+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_OUT_SJ_COLLAPSED//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_bam_sort_ram': $( if [ ! -z ${VIASH_PAR_LIMIT_BAM_SORT_RAM+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_BAM_SORT_RAM//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_sjdb_insert_nsj': $( if [ ! -z ${VIASH_PAR_LIMIT_SJDB_INSERT_NSJ+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_SJDB_INSERT_NSJ//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'limit_nreads_soft': $( if [ ! -z ${VIASH_PAR_LIMIT_NREADS_SOFT+x} ]; then echo "int(r'${VIASH_PAR_LIMIT_NREADS_SOFT//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_tmp_keep': $( if [ ! -z ${VIASH_PAR_OUT_TMP_KEEP+x} ]; then echo "r'${VIASH_PAR_OUT_TMP_KEEP//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_std': $( if [ ! -z ${VIASH_PAR_OUT_STD+x} ]; then echo "r'${VIASH_PAR_OUT_STD//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_reads_unmapped': $( if [ ! -z ${VIASH_PAR_OUT_READS_UNMAPPED+x} ]; then echo "r'${VIASH_PAR_OUT_READS_UNMAPPED//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_qs_conversion_add': $( if [ ! -z ${VIASH_PAR_OUT_QS_CONVERSION_ADD+x} ]; then echo "int(r'${VIASH_PAR_OUT_QS_CONVERSION_ADD//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_multimapper_order': $( if [ ! -z ${VIASH_PAR_OUT_MULTIMAPPER_ORDER+x} ]; then echo "r'${VIASH_PAR_OUT_MULTIMAPPER_ORDER//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_type': $( if [ ! -z ${VIASH_PAR_OUT_SAM_TYPE+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_TYPE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_mode': $( if [ ! -z ${VIASH_PAR_OUT_SAM_MODE+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_MODE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_strand_field': $( if [ ! -z ${VIASH_PAR_OUT_SAM_STRAND_FIELD+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_STRAND_FIELD//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_attributes': $( if [ ! -z ${VIASH_PAR_OUT_SAM_ATTRIBUTES+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_ATTRIBUTES//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_attr_ih_start': $( if [ ! -z ${VIASH_PAR_OUT_SAM_ATTR_IH_START+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_ATTR_IH_START//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_sam_unmapped': $( if [ ! -z ${VIASH_PAR_OUT_SAM_UNMAPPED+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_UNMAPPED//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_order': $( if [ ! -z ${VIASH_PAR_OUT_SAM_ORDER+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_ORDER//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_primary_flag': $( if [ ! -z ${VIASH_PAR_OUT_SAM_PRIMARY_FLAG+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_PRIMARY_FLAG//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_read_id': $( if [ ! -z ${VIASH_PAR_OUT_SAM_READ_ID+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_READ_ID//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_mapq_unique': $( if [ ! -z ${VIASH_PAR_OUT_SAM_MAPQ_UNIQUE+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_MAPQ_UNIQUE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_sam_flag_or': $( if [ ! -z ${VIASH_PAR_OUT_SAM_FLAG_OR+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_FLAG_OR//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_sam_flag_and': $( if [ ! -z ${VIASH_PAR_OUT_SAM_FLAG_AND+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_FLAG_AND//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_sam_attr_rg_line': $( if [ ! -z ${VIASH_PAR_OUT_SAM_ATTR_RG_LINE+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_ATTR_RG_LINE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_header_hd': $( if [ ! -z ${VIASH_PAR_OUT_SAM_HEADER_HD+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_HEADER_HD//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_header_pg': $( if [ ! -z ${VIASH_PAR_OUT_SAM_HEADER_PG+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_HEADER_PG//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_header_comment_file': $( if [ ! -z ${VIASH_PAR_OUT_SAM_HEADER_COMMENT_FILE+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_HEADER_COMMENT_FILE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sam_filter': $( if [ ! -z ${VIASH_PAR_OUT_SAM_FILTER+x} ]; then echo "r'${VIASH_PAR_OUT_SAM_FILTER//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_sam_mult_nmax': $( if [ ! -z ${VIASH_PAR_OUT_SAM_MULT_NMAX+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_MULT_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_sam_tlen': $( if [ ! -z ${VIASH_PAR_OUT_SAM_TLEN+x} ]; then echo "int(r'${VIASH_PAR_OUT_SAM_TLEN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_bam_compression': $( if [ ! -z ${VIASH_PAR_OUT_BAM_COMPRESSION+x} ]; then echo "int(r'${VIASH_PAR_OUT_BAM_COMPRESSION//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_bam_sorting_thread_n': $( if [ ! -z ${VIASH_PAR_OUT_BAM_SORTING_THREAD_N+x} ]; then echo "int(r'${VIASH_PAR_OUT_BAM_SORTING_THREAD_N//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_bam_sorting_bins_n': $( if [ ! -z ${VIASH_PAR_OUT_BAM_SORTING_BINS_N+x} ]; then echo "int(r'${VIASH_PAR_OUT_BAM_SORTING_BINS_N//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'bam_remove_duplicates_type': $( if [ ! -z ${VIASH_PAR_BAM_REMOVE_DUPLICATES_TYPE+x} ]; then echo "r'${VIASH_PAR_BAM_REMOVE_DUPLICATES_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'bam_remove_duplicates_mate2bases_n': $( if [ ! -z ${VIASH_PAR_BAM_REMOVE_DUPLICATES_MATE2BASES_N+x} ]; then echo "int(r'${VIASH_PAR_BAM_REMOVE_DUPLICATES_MATE2BASES_N//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_wig_type': $( if [ ! -z ${VIASH_PAR_OUT_WIG_TYPE+x} ]; then echo "r'${VIASH_PAR_OUT_WIG_TYPE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'out_wig_strand': $( if [ ! -z ${VIASH_PAR_OUT_WIG_STRAND+x} ]; then echo "r'${VIASH_PAR_OUT_WIG_STRAND//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_wig_references_prefix': $( if [ ! -z ${VIASH_PAR_OUT_WIG_REFERENCES_PREFIX+x} ]; then echo "r'${VIASH_PAR_OUT_WIG_REFERENCES_PREFIX//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_wig_norm': $( if [ ! -z ${VIASH_PAR_OUT_WIG_NORM+x} ]; then echo "r'${VIASH_PAR_OUT_WIG_NORM//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_filter_type': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_TYPE+x} ]; then echo "r'${VIASH_PAR_OUT_FILTER_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_filter_multimap_score_range': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MULTIMAP_SCORE_RANGE+x} ]; then echo "int(r'${VIASH_PAR_OUT_FILTER_MULTIMAP_SCORE_RANGE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_multimap_nmax': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MULTIMAP_NMAX+x} ]; then echo "int(r'${VIASH_PAR_OUT_FILTER_MULTIMAP_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_mismatch_nmax': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MISMATCH_NMAX+x} ]; then echo "int(r'${VIASH_PAR_OUT_FILTER_MISMATCH_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_mismatch_nover_lmax': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MISMATCH_NOVER_LMAX+x} ]; then echo "float(r'${VIASH_PAR_OUT_FILTER_MISMATCH_NOVER_LMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_mismatch_nover_read_lmax': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MISMATCH_NOVER_READ_LMAX+x} ]; then echo "float(r'${VIASH_PAR_OUT_FILTER_MISMATCH_NOVER_READ_LMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_score_min': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_SCORE_MIN+x} ]; then echo "int(r'${VIASH_PAR_OUT_FILTER_SCORE_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_score_min_over_lread': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_SCORE_MIN_OVER_LREAD+x} ]; then echo "float(r'${VIASH_PAR_OUT_FILTER_SCORE_MIN_OVER_LREAD//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_match_nmin': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MATCH_NMIN+x} ]; then echo "int(r'${VIASH_PAR_OUT_FILTER_MATCH_NMIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_match_nmin_over_lread': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_MATCH_NMIN_OVER_LREAD+x} ]; then echo "float(r'${VIASH_PAR_OUT_FILTER_MATCH_NMIN_OVER_LREAD//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'out_filter_intron_motifs': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_INTRON_MOTIFS+x} ]; then echo "r'${VIASH_PAR_OUT_FILTER_INTRON_MOTIFS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_filter_intron_strands': $( if [ ! -z ${VIASH_PAR_OUT_FILTER_INTRON_STRANDS+x} ]; then echo "r'${VIASH_PAR_OUT_FILTER_INTRON_STRANDS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sj_type': $( if [ ! -z ${VIASH_PAR_OUT_SJ_TYPE+x} ]; then echo "r'${VIASH_PAR_OUT_SJ_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sj_filter_reads': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_READS+x} ]; then echo "r'${VIASH_PAR_OUT_SJ_FILTER_READS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'out_sj_filter_overhang_min': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_OVERHANG_MIN+x} ]; then echo "list(map(int, r'${VIASH_PAR_OUT_SJ_FILTER_OVERHANG_MIN//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'out_sj_filter_count_unique_min': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_COUNT_UNIQUE_MIN+x} ]; then echo "list(map(int, r'${VIASH_PAR_OUT_SJ_FILTER_COUNT_UNIQUE_MIN//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'out_sj_filter_count_total_min': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_COUNT_TOTAL_MIN+x} ]; then echo "list(map(int, r'${VIASH_PAR_OUT_SJ_FILTER_COUNT_TOTAL_MIN//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'out_sj_filter_dist_to_other_sj_min': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_DIST_TO_OTHER_SJ_MIN+x} ]; then echo "list(map(int, r'${VIASH_PAR_OUT_SJ_FILTER_DIST_TO_OTHER_SJ_MIN//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'out_sj_filter_intron_max_vs_read_n': $( if [ ! -z ${VIASH_PAR_OUT_SJ_FILTER_INTRON_MAX_VS_READ_N+x} ]; then echo "list(map(int, r'${VIASH_PAR_OUT_SJ_FILTER_INTRON_MAX_VS_READ_N//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'score_gap': $( if [ ! -z ${VIASH_PAR_SCORE_GAP+x} ]; then echo "int(r'${VIASH_PAR_SCORE_GAP//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_gap_noncan': $( if [ ! -z ${VIASH_PAR_SCORE_GAP_NONCAN+x} ]; then echo "int(r'${VIASH_PAR_SCORE_GAP_NONCAN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_gap_gcag': $( if [ ! -z ${VIASH_PAR_SCORE_GAP_GCAG+x} ]; then echo "int(r'${VIASH_PAR_SCORE_GAP_GCAG//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_gap_atac': $( if [ ! -z ${VIASH_PAR_SCORE_GAP_ATAC+x} ]; then echo "int(r'${VIASH_PAR_SCORE_GAP_ATAC//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_genomic_length_log2scale': $( if [ ! -z ${VIASH_PAR_SCORE_GENOMIC_LENGTH_LOG2SCALE+x} ]; then echo "int(r'${VIASH_PAR_SCORE_GENOMIC_LENGTH_LOG2SCALE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_del_open': $( if [ ! -z ${VIASH_PAR_SCORE_DEL_OPEN+x} ]; then echo "int(r'${VIASH_PAR_SCORE_DEL_OPEN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_del_base': $( if [ ! -z ${VIASH_PAR_SCORE_DEL_BASE+x} ]; then echo "int(r'${VIASH_PAR_SCORE_DEL_BASE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_ins_open': $( if [ ! -z ${VIASH_PAR_SCORE_INS_OPEN+x} ]; then echo "int(r'${VIASH_PAR_SCORE_INS_OPEN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_ins_base': $( if [ ! -z ${VIASH_PAR_SCORE_INS_BASE+x} ]; then echo "int(r'${VIASH_PAR_SCORE_INS_BASE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'score_stitch_sj_shift': $( if [ ! -z ${VIASH_PAR_SCORE_STITCH_SJ_SHIFT+x} ]; then echo "int(r'${VIASH_PAR_SCORE_STITCH_SJ_SHIFT//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_search_start_lmax': $( if [ ! -z ${VIASH_PAR_SEED_SEARCH_START_LMAX+x} ]; then echo "int(r'${VIASH_PAR_SEED_SEARCH_START_LMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_search_start_lmax_over_lread': $( if [ ! -z ${VIASH_PAR_SEED_SEARCH_START_LMAX_OVER_LREAD+x} ]; then echo "float(r'${VIASH_PAR_SEED_SEARCH_START_LMAX_OVER_LREAD//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_search_lmax': $( if [ ! -z ${VIASH_PAR_SEED_SEARCH_LMAX+x} ]; then echo "int(r'${VIASH_PAR_SEED_SEARCH_LMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_multimap_nmax': $( if [ ! -z ${VIASH_PAR_SEED_MULTIMAP_NMAX+x} ]; then echo "int(r'${VIASH_PAR_SEED_MULTIMAP_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_per_read_nmax': $( if [ ! -z ${VIASH_PAR_SEED_PER_READ_NMAX+x} ]; then echo "int(r'${VIASH_PAR_SEED_PER_READ_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_per_window_nmax': $( if [ ! -z ${VIASH_PAR_SEED_PER_WINDOW_NMAX+x} ]; then echo "int(r'${VIASH_PAR_SEED_PER_WINDOW_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_none_loci_per_window': $( if [ ! -z ${VIASH_PAR_SEED_NONE_LOCI_PER_WINDOW+x} ]; then echo "int(r'${VIASH_PAR_SEED_NONE_LOCI_PER_WINDOW//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_split_min': $( if [ ! -z ${VIASH_PAR_SEED_SPLIT_MIN+x} ]; then echo "int(r'${VIASH_PAR_SEED_SPLIT_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'seed_map_min': $( if [ ! -z ${VIASH_PAR_SEED_MAP_MIN+x} ]; then echo "int(r'${VIASH_PAR_SEED_MAP_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_intron_min': $( if [ ! -z ${VIASH_PAR_ALIGN_INTRON_MIN+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_INTRON_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_intron_max': $( if [ ! -z ${VIASH_PAR_ALIGN_INTRON_MAX+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_INTRON_MAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_mates_gap_max': $( if [ ! -z ${VIASH_PAR_ALIGN_MATES_GAP_MAX+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_MATES_GAP_MAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_sj_overhang_min': $( if [ ! -z ${VIASH_PAR_ALIGN_SJ_OVERHANG_MIN+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_SJ_OVERHANG_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_sj_stitch_mismatch_nmax': $( if [ ! -z ${VIASH_PAR_ALIGN_SJ_STITCH_MISMATCH_NMAX+x} ]; then echo "list(map(int, r'${VIASH_PAR_ALIGN_SJ_STITCH_MISMATCH_NMAX//\\'/\\'\\"\\'\\"r\\'}'.split(';')))"; else echo None; fi ),
  'align_sjdb_overhang_min': $( if [ ! -z ${VIASH_PAR_ALIGN_SJDB_OVERHANG_MIN+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_SJDB_OVERHANG_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_spliced_mate_map_lmin': $( if [ ! -z ${VIASH_PAR_ALIGN_SPLICED_MATE_MAP_LMIN+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_SPLICED_MATE_MAP_LMIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_spliced_mate_map_lmin_over_lmate': $( if [ ! -z ${VIASH_PAR_ALIGN_SPLICED_MATE_MAP_LMIN_OVER_LMATE+x} ]; then echo "float(r'${VIASH_PAR_ALIGN_SPLICED_MATE_MAP_LMIN_OVER_LMATE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_windows_per_read_nmax': $( if [ ! -z ${VIASH_PAR_ALIGN_WINDOWS_PER_READ_NMAX+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_WINDOWS_PER_READ_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_transcripts_per_window_nmax': $( if [ ! -z ${VIASH_PAR_ALIGN_TRANSCRIPTS_PER_WINDOW_NMAX+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_TRANSCRIPTS_PER_WINDOW_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_transcripts_per_read_nmax': $( if [ ! -z ${VIASH_PAR_ALIGN_TRANSCRIPTS_PER_READ_NMAX+x} ]; then echo "int(r'${VIASH_PAR_ALIGN_TRANSCRIPTS_PER_READ_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'align_ends_type': $( if [ ! -z ${VIASH_PAR_ALIGN_ENDS_TYPE+x} ]; then echo "r'${VIASH_PAR_ALIGN_ENDS_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'align_ends_protrude': $( if [ ! -z ${VIASH_PAR_ALIGN_ENDS_PROTRUDE+x} ]; then echo "r'${VIASH_PAR_ALIGN_ENDS_PROTRUDE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'align_soft_clip_at_reference_ends': $( if [ ! -z ${VIASH_PAR_ALIGN_SOFT_CLIP_AT_REFERENCE_ENDS+x} ]; then echo "r'${VIASH_PAR_ALIGN_SOFT_CLIP_AT_REFERENCE_ENDS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'align_insertion_flush': $( if [ ! -z ${VIASH_PAR_ALIGN_INSERTION_FLUSH+x} ]; then echo "r'${VIASH_PAR_ALIGN_INSERTION_FLUSH//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'pe_overlap_nbases_min': $( if [ ! -z ${VIASH_PAR_PE_OVERLAP_NBASES_MIN+x} ]; then echo "int(r'${VIASH_PAR_PE_OVERLAP_NBASES_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'pe_overlap_mm_p': $( if [ ! -z ${VIASH_PAR_PE_OVERLAP_MM_P+x} ]; then echo "float(r'${VIASH_PAR_PE_OVERLAP_MM_P//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_anchor_multimap_nmax': $( if [ ! -z ${VIASH_PAR_WIN_ANCHOR_MULTIMAP_NMAX+x} ]; then echo "int(r'${VIASH_PAR_WIN_ANCHOR_MULTIMAP_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_bin_nbits': $( if [ ! -z ${VIASH_PAR_WIN_BIN_NBITS+x} ]; then echo "int(r'${VIASH_PAR_WIN_BIN_NBITS//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_anchor_dist_nbins': $( if [ ! -z ${VIASH_PAR_WIN_ANCHOR_DIST_NBINS+x} ]; then echo "int(r'${VIASH_PAR_WIN_ANCHOR_DIST_NBINS//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_flank_nbins': $( if [ ! -z ${VIASH_PAR_WIN_FLANK_NBINS+x} ]; then echo "int(r'${VIASH_PAR_WIN_FLANK_NBINS//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_read_coverage_relative_min': $( if [ ! -z ${VIASH_PAR_WIN_READ_COVERAGE_RELATIVE_MIN+x} ]; then echo "float(r'${VIASH_PAR_WIN_READ_COVERAGE_RELATIVE_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'win_read_coverage_bases_min': $( if [ ! -z ${VIASH_PAR_WIN_READ_COVERAGE_BASES_MIN+x} ]; then echo "int(r'${VIASH_PAR_WIN_READ_COVERAGE_BASES_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_out_type': $( if [ ! -z ${VIASH_PAR_CHIM_OUT_TYPE+x} ]; then echo "r'${VIASH_PAR_CHIM_OUT_TYPE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'chim_segment_min': $( if [ ! -z ${VIASH_PAR_CHIM_SEGMENT_MIN+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SEGMENT_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_score_min': $( if [ ! -z ${VIASH_PAR_CHIM_SCORE_MIN+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SCORE_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_score_drop_max': $( if [ ! -z ${VIASH_PAR_CHIM_SCORE_DROP_MAX+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SCORE_DROP_MAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_score_separation': $( if [ ! -z ${VIASH_PAR_CHIM_SCORE_SEPARATION+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SCORE_SEPARATION//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_score_junction_non_gtag': $( if [ ! -z ${VIASH_PAR_CHIM_SCORE_JUNCTION_NON_GTAG+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SCORE_JUNCTION_NON_GTAG//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_junction_overhang_min': $( if [ ! -z ${VIASH_PAR_CHIM_JUNCTION_OVERHANG_MIN+x} ]; then echo "int(r'${VIASH_PAR_CHIM_JUNCTION_OVERHANG_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_segment_read_gap_max': $( if [ ! -z ${VIASH_PAR_CHIM_SEGMENT_READ_GAP_MAX+x} ]; then echo "int(r'${VIASH_PAR_CHIM_SEGMENT_READ_GAP_MAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_filter': $( if [ ! -z ${VIASH_PAR_CHIM_FILTER+x} ]; then echo "r'${VIASH_PAR_CHIM_FILTER//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'chim_main_segment_mult_nmax': $( if [ ! -z ${VIASH_PAR_CHIM_MAIN_SEGMENT_MULT_NMAX+x} ]; then echo "int(r'${VIASH_PAR_CHIM_MAIN_SEGMENT_MULT_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_multimap_nmax': $( if [ ! -z ${VIASH_PAR_CHIM_MULTIMAP_NMAX+x} ]; then echo "int(r'${VIASH_PAR_CHIM_MULTIMAP_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_multimap_score_range': $( if [ ! -z ${VIASH_PAR_CHIM_MULTIMAP_SCORE_RANGE+x} ]; then echo "int(r'${VIASH_PAR_CHIM_MULTIMAP_SCORE_RANGE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_nonchim_score_drop_min': $( if [ ! -z ${VIASH_PAR_CHIM_NONCHIM_SCORE_DROP_MIN+x} ]; then echo "int(r'${VIASH_PAR_CHIM_NONCHIM_SCORE_DROP_MIN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'chim_out_junction_format': $( if [ ! -z ${VIASH_PAR_CHIM_OUT_JUNCTION_FORMAT+x} ]; then echo "int(r'${VIASH_PAR_CHIM_OUT_JUNCTION_FORMAT//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'quant_mode': $( if [ ! -z ${VIASH_PAR_QUANT_MODE+x} ]; then echo "r'${VIASH_PAR_QUANT_MODE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'quant_transcriptome_bam_compression': $( if [ ! -z ${VIASH_PAR_QUANT_TRANSCRIPTOME_BAM_COMPRESSION+x} ]; then echo "int(r'${VIASH_PAR_QUANT_TRANSCRIPTOME_BAM_COMPRESSION//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'quant_transcriptome_sam_output': $( if [ ! -z ${VIASH_PAR_QUANT_TRANSCRIPTOME_SAM_OUTPUT+x} ]; then echo "r'${VIASH_PAR_QUANT_TRANSCRIPTOME_SAM_OUTPUT//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'twopass_mode': $( if [ ! -z ${VIASH_PAR_TWOPASS_MODE+x} ]; then echo "r'${VIASH_PAR_TWOPASS_MODE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'twopass1reads_n': $( if [ ! -z ${VIASH_PAR_TWOPASS1READS_N+x} ]; then echo "int(r'${VIASH_PAR_TWOPASS1READS_N//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'wasp_output_mode': $( if [ ! -z ${VIASH_PAR_WASP_OUTPUT_MODE+x} ]; then echo "r'${VIASH_PAR_WASP_OUTPUT_MODE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_type': $( if [ ! -z ${VIASH_PAR_SOLO_TYPE+x} ]; then echo "r'${VIASH_PAR_SOLO_TYPE//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_cb_type': $( if [ ! -z ${VIASH_PAR_SOLO_CB_TYPE+x} ]; then echo "r'${VIASH_PAR_SOLO_CB_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_cb_whitelist': $( if [ ! -z ${VIASH_PAR_SOLO_CB_WHITELIST+x} ]; then echo "r'${VIASH_PAR_SOLO_CB_WHITELIST//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_cb_start': $( if [ ! -z ${VIASH_PAR_SOLO_CB_START+x} ]; then echo "int(r'${VIASH_PAR_SOLO_CB_START//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_cb_len': $( if [ ! -z ${VIASH_PAR_SOLO_CB_LEN+x} ]; then echo "int(r'${VIASH_PAR_SOLO_CB_LEN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_umi_start': $( if [ ! -z ${VIASH_PAR_SOLO_UMI_START+x} ]; then echo "int(r'${VIASH_PAR_SOLO_UMI_START//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_umi_len': $( if [ ! -z ${VIASH_PAR_SOLO_UMI_LEN+x} ]; then echo "int(r'${VIASH_PAR_SOLO_UMI_LEN//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_barcode_read_length': $( if [ ! -z ${VIASH_PAR_SOLO_BARCODE_READ_LENGTH+x} ]; then echo "int(r'${VIASH_PAR_SOLO_BARCODE_READ_LENGTH//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_barcode_mate': $( if [ ! -z ${VIASH_PAR_SOLO_BARCODE_MATE+x} ]; then echo "int(r'${VIASH_PAR_SOLO_BARCODE_MATE//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_cb_position': $( if [ ! -z ${VIASH_PAR_SOLO_CB_POSITION+x} ]; then echo "r'${VIASH_PAR_SOLO_CB_POSITION//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_umi_position': $( if [ ! -z ${VIASH_PAR_SOLO_UMI_POSITION+x} ]; then echo "r'${VIASH_PAR_SOLO_UMI_POSITION//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_adapter_sequence': $( if [ ! -z ${VIASH_PAR_SOLO_ADAPTER_SEQUENCE+x} ]; then echo "r'${VIASH_PAR_SOLO_ADAPTER_SEQUENCE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_adapter_mismatches_nmax': $( if [ ! -z ${VIASH_PAR_SOLO_ADAPTER_MISMATCHES_NMAX+x} ]; then echo "int(r'${VIASH_PAR_SOLO_ADAPTER_MISMATCHES_NMAX//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'solo_cb_match_wl_type': $( if [ ! -z ${VIASH_PAR_SOLO_CB_MATCH_WL_TYPE+x} ]; then echo "r'${VIASH_PAR_SOLO_CB_MATCH_WL_TYPE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_input_sam_attr_barcode_seq': $( if [ ! -z ${VIASH_PAR_SOLO_INPUT_SAM_ATTR_BARCODE_SEQ+x} ]; then echo "r'${VIASH_PAR_SOLO_INPUT_SAM_ATTR_BARCODE_SEQ//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_input_sam_attr_barcode_qual': $( if [ ! -z ${VIASH_PAR_SOLO_INPUT_SAM_ATTR_BARCODE_QUAL+x} ]; then echo "r'${VIASH_PAR_SOLO_INPUT_SAM_ATTR_BARCODE_QUAL//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_strand': $( if [ ! -z ${VIASH_PAR_SOLO_STRAND+x} ]; then echo "r'${VIASH_PAR_SOLO_STRAND//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'solo_features': $( if [ ! -z ${VIASH_PAR_SOLO_FEATURES+x} ]; then echo "r'${VIASH_PAR_SOLO_FEATURES//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_multi_mappers': $( if [ ! -z ${VIASH_PAR_SOLO_MULTI_MAPPERS+x} ]; then echo "r'${VIASH_PAR_SOLO_MULTI_MAPPERS//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_umi_dedup': $( if [ ! -z ${VIASH_PAR_SOLO_UMI_DEDUP+x} ]; then echo "r'${VIASH_PAR_SOLO_UMI_DEDUP//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_umi_filtering': $( if [ ! -z ${VIASH_PAR_SOLO_UMI_FILTERING+x} ]; then echo "r'${VIASH_PAR_SOLO_UMI_FILTERING//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_out_file_names': $( if [ ! -z ${VIASH_PAR_SOLO_OUT_FILE_NAMES+x} ]; then echo "r'${VIASH_PAR_SOLO_OUT_FILE_NAMES//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_cell_filter': $( if [ ! -z ${VIASH_PAR_SOLO_CELL_FILTER+x} ]; then echo "r'${VIASH_PAR_SOLO_CELL_FILTER//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_out_format_features_gene_field3': $( if [ ! -z ${VIASH_PAR_SOLO_OUT_FORMAT_FEATURES_GENE_FIELD3+x} ]; then echo "r'${VIASH_PAR_SOLO_OUT_FORMAT_FEATURES_GENE_FIELD3//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'solo_cell_read_stats': $( if [ ! -z ${VIASH_PAR_SOLO_CELL_READ_STATS+x} ]; then echo "r'${VIASH_PAR_SOLO_CELL_READ_STATS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'input': $( if [ ! -z ${VIASH_PAR_INPUT+x} ]; then echo "r'${VIASH_PAR_INPUT//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'input_r2': $( if [ ! -z ${VIASH_PAR_INPUT_R2+x} ]; then echo "r'${VIASH_PAR_INPUT_R2//\\'/\\'\\"\\'\\"r\\'}'.split(';')"; else echo None; fi ),
  'aligned_reads': $( if [ ! -z ${VIASH_PAR_ALIGNED_READS+x} ]; then echo "r'${VIASH_PAR_ALIGNED_READS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'reads_per_gene': $( if [ ! -z ${VIASH_PAR_READS_PER_GENE+x} ]; then echo "r'${VIASH_PAR_READS_PER_GENE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'unmapped': $( if [ ! -z ${VIASH_PAR_UNMAPPED+x} ]; then echo "r'${VIASH_PAR_UNMAPPED//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'unmapped_r2': $( if [ ! -z ${VIASH_PAR_UNMAPPED_R2+x} ]; then echo "r'${VIASH_PAR_UNMAPPED_R2//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'chimeric_junctions': $( if [ ! -z ${VIASH_PAR_CHIMERIC_JUNCTIONS+x} ]; then echo "r'${VIASH_PAR_CHIMERIC_JUNCTIONS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'log': $( if [ ! -z ${VIASH_PAR_LOG+x} ]; then echo "r'${VIASH_PAR_LOG//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'splice_junctions': $( if [ ! -z ${VIASH_PAR_SPLICE_JUNCTIONS+x} ]; then echo "r'${VIASH_PAR_SPLICE_JUNCTIONS//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'reads_aligned_to_transcriptome': $( if [ ! -z ${VIASH_PAR_READS_ALIGNED_TO_TRANSCRIPTOME+x} ]; then echo "r'${VIASH_PAR_READS_ALIGNED_TO_TRANSCRIPTOME//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi )
}
meta = {
  'name': $( if [ ! -z ${VIASH_META_NAME+x} ]; then echo "r'${VIASH_META_NAME//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'functionality_name': $( if [ ! -z ${VIASH_META_FUNCTIONALITY_NAME+x} ]; then echo "r'${VIASH_META_FUNCTIONALITY_NAME//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'resources_dir': $( if [ ! -z ${VIASH_META_RESOURCES_DIR+x} ]; then echo "r'${VIASH_META_RESOURCES_DIR//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'executable': $( if [ ! -z ${VIASH_META_EXECUTABLE+x} ]; then echo "r'${VIASH_META_EXECUTABLE//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'config': $( if [ ! -z ${VIASH_META_CONFIG+x} ]; then echo "r'${VIASH_META_CONFIG//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'temp_dir': $( if [ ! -z ${VIASH_META_TEMP_DIR+x} ]; then echo "r'${VIASH_META_TEMP_DIR//\\'/\\'\\"\\'\\"r\\'}'"; else echo None; fi ),
  'cpus': $( if [ ! -z ${VIASH_META_CPUS+x} ]; then echo "int(r'${VIASH_META_CPUS//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_b': $( if [ ! -z ${VIASH_META_MEMORY_B+x} ]; then echo "int(r'${VIASH_META_MEMORY_B//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_kb': $( if [ ! -z ${VIASH_META_MEMORY_KB+x} ]; then echo "int(r'${VIASH_META_MEMORY_KB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_mb': $( if [ ! -z ${VIASH_META_MEMORY_MB+x} ]; then echo "int(r'${VIASH_META_MEMORY_MB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_gb': $( if [ ! -z ${VIASH_META_MEMORY_GB+x} ]; then echo "int(r'${VIASH_META_MEMORY_GB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_tb': $( if [ ! -z ${VIASH_META_MEMORY_TB+x} ]; then echo "int(r'${VIASH_META_MEMORY_TB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_pb': $( if [ ! -z ${VIASH_META_MEMORY_PB+x} ]; then echo "int(r'${VIASH_META_MEMORY_PB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_kib': $( if [ ! -z ${VIASH_META_MEMORY_KIB+x} ]; then echo "int(r'${VIASH_META_MEMORY_KIB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_mib': $( if [ ! -z ${VIASH_META_MEMORY_MIB+x} ]; then echo "int(r'${VIASH_META_MEMORY_MIB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_gib': $( if [ ! -z ${VIASH_META_MEMORY_GIB+x} ]; then echo "int(r'${VIASH_META_MEMORY_GIB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_tib': $( if [ ! -z ${VIASH_META_MEMORY_TIB+x} ]; then echo "int(r'${VIASH_META_MEMORY_TIB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi ),
  'memory_pib': $( if [ ! -z ${VIASH_META_MEMORY_PIB+x} ]; then echo "int(r'${VIASH_META_MEMORY_PIB//\\'/\\'\\"\\'\\"r\\'}')"; else echo None; fi )
}
dep = {
  
}

## VIASH END

# read config
with open(meta["config"], 'r') as stream:
    config = yaml.safe_load(stream)
all_arguments = {
    arg["name"].lstrip('-'): arg
    for argument_group in config["argument_groups"]
    for arg in argument_group["arguments"]
}

##################################################
# check and process SE / PE R1 input files
input_r1 = par["input"]
readFilesIn = ",".join(par["input"])
par["input"] = None

# check and process PE R2 input files
input_r2 = par["input_r2"]
if input_r2 is not None:
    if len(input_r1) != len(input_r2):
        raise ValueError("The number of R1 and R2 files do not match.")
    readFilesIn = [readFilesIn, ",".join(par["input_r2"])]
    par["input_r2"] = None

# store readFilesIn
par["readFilesIn"] = readFilesIn

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

# determine readFilesCommand
if input_r1[0].endswith(".gz"):
    print(">> Input files are gzipped, setting readFilesCommand to zcat", flush=True)
    par["readFilesCommand"] = "zcat"
elif input_r1[0].endswith(".bz2"):
    print(">> Input files are bzipped, setting readFilesCommand to bzcat", flush=True)
    par["readFilesCommand"] = "bzcat"

##################################################
# store output paths
expected_outputs = {
    "aligned_reads": ["Aligned.out.sam", "Aligned.out.bam"],
    "reads_per_gene": "ReadsPerGene.out.tab",
    "chimeric_junctions": "Chimeric.out.junction",
    "log": "Log.final.out",
    "splice_junctions": "SJ.out.tab",
    "unmapped": "Unmapped.out.mate1",
    "unmapped_r2": "Unmapped.out.mate2", 
    "reads_aligned_to_transcriptome": "Aligned.toTranscriptome.out.bam"
}
output_paths = {name: par[name] for name in expected_outputs.keys()}
for name in expected_outputs.keys():
    par[name] = None

##################################################
# process other args
par["runMode"] = "alignReads"

if "cpus" in meta and meta["cpus"]:
    par["runThreadN"] = meta["cpus"]

##################################################
# run STAR and move output to final destination
with tempfile.TemporaryDirectory(prefix="star-", dir=meta["temp_dir"], ignore_cleanup_errors=True) as temp_dir:
    print(">> Constructing command", flush=True)

    # set output paths
    temp_dir = Path(temp_dir)
    par["outTmpDir"] = temp_dir / "tempdir"
    out_dir = temp_dir / "out"
    par["outFileNamePrefix"] = f"{out_dir}/" # star needs this slash

    # construct command
    cmd_args = [ "STAR" ]
    for name, value in par.items():
        if value is not None:
            if name in all_arguments:
                arg_info = all_arguments[name].get("info", {})
                cli_name = arg_info.get("orig_name", f"--{name}")
            else:
                cli_name = f"--{name}"
            val_to_add = value if isinstance(value, list) else [value]
            cmd_args.extend([cli_name] + [str(x) for x in val_to_add])
    print("", flush=True)

    # run command
    print(">> Running STAR with command:", flush=True)
    print(f"+ {' '.join(cmd_args)}", end="\\\\n\\\\n", flush=True)
    subprocess.run(
        cmd_args,
        check=True
    )
    print(">> STAR finished successfully", end="\\\\n\\\\n", flush=True)

    # move output to final destination
    print(">> Moving output to final destination", flush=True)
    for name, paths in expected_outputs.items():
        for expected_path in [paths] if isinstance(paths, str) else paths:
            expected_full_path = out_dir / expected_path
            if output_paths[name] and expected_full_path.is_file():
                print(f">> Moving {expected_path} to {output_paths[name]}", flush=True)
                shutil.move(expected_full_path, output_paths[name])
VIASHMAIN
python -B "$tempscript"
'''
  
  return vdsl3WorkflowFactory(args, meta, rawScript)
}



/**
  * Generate a workflow for VDSL3 modules.
  * 
  * This function is called by the workflowFactory() function.
  * 
  * Input channel: [id, input_map]
  * Output channel: [id, output_map]
  * 
  * Internally, this workflow will convert the input channel
  * to a format which the Nextflow module will be able to handle.
  */
def vdsl3WorkflowFactory(Map args, Map meta, String rawScript) {
  def key = args["key"]
  def processObj = null

  workflow processWf {
    take: input_
    main:

    if (processObj == null) {
      processObj = _vdsl3ProcessFactory(args, meta, rawScript)
    }
    
    output_ = input_
      | map { tuple ->
        def id = tuple[0]
        def data_ = tuple[1]

        if (workflow.stubRun) {
          // add id if missing
          data_ = [id: 'stub'] + data_
        }

        // process input files separately
        def inputPaths = meta.config.allArguments
          .findAll { it.type == "file" && it.direction == "input" }
          .collect { par ->
            def val = data_.containsKey(par.plainName) ? data_[par.plainName] : []
            def inputFiles = []
            if (val == null) {
              inputFiles = []
            } else if (val instanceof List) {
              inputFiles = val
            } else if (val instanceof Path) {
              inputFiles = [ val ]
            } else {
              inputFiles = []
            }
            if (!workflow.stubRun) {
              // throw error when an input file doesn't exist
              inputFiles.each{ file -> 
                assert file.exists() :
                  "Error in module '${key}' id '${id}' argument '${par.plainName}'.\n" +
                  "  Required input file does not exist.\n" +
                  "  Path: '$file'.\n" +
                  "  Expected input file to exist"
              }
            }
            inputFiles 
          } 

        // remove input files
        def argsExclInputFiles = meta.config.allArguments
          .findAll { (it.type != "file" || it.direction != "input") && data_.containsKey(it.plainName) }
          .collectEntries { par ->
            def parName = par.plainName
            def val = data_[parName]
            if (par.multiple && val instanceof Collection) {
              val = val.join(par.multiple_sep)
            }
            if (par.direction == "output" && par.type == "file") {
              val = val
                .replaceAll('\\$id', id)
                .replaceAll('\\$\\{id\\}', id)
                .replaceAll('\\$key', key)
                .replaceAll('\\$\\{key\\}', key)
            }
            [parName, val]
          }

        [ id ] + inputPaths + [ argsExclInputFiles, meta.resources_dir ]
      }
      | processObj
      | map { output ->
        def outputFiles = meta.config.allArguments
          .findAll { it.type == "file" && it.direction == "output" }
          .indexed()
          .collectEntries{ index, par ->
            def out = output[index + 1]
            // strip dummy '.exitcode' file from output (see nextflow-io/nextflow#2678)
            if (!out instanceof List || out.size() <= 1) {
              if (par.multiple) {
                out = []
              } else {
                assert !par.required :
                    "Error in module '${key}' id '${output[0]}' argument '${par.plainName}'.\n" +
                    "  Required output file is missing"
                out = null
              }
            } else if (out.size() == 2 && !par.multiple) {
              out = out[1]
            } else {
              out = out.drop(1)
            }
            [ par.plainName, out ]
          }
        
        // drop null outputs
        outputFiles.removeAll{it.value == null}

        [ output[0], outputFiles ]
      }
    emit: output_
  }

  return processWf
}

// depends on: session?
def _vdsl3ProcessFactory(Map workflowArgs, Map meta, String rawScript) {
  // autodetect process key
  def wfKey = workflowArgs["key"]
  def procKeyPrefix = "${wfKey}_process"
  def scriptMeta = nextflow.script.ScriptMeta.current()
  def existing = scriptMeta.getProcessNames().findAll{it.startsWith(procKeyPrefix)}
  def numbers = existing.collect{it.replace(procKeyPrefix, "0").toInteger()}
  def newNumber = (numbers + [-1]).max() + 1

  def procKey = newNumber == 0 ? procKeyPrefix : "$procKeyPrefix$newNumber"

  if (newNumber > 0) {
    log.warn "Key for module '${wfKey}' is duplicated.\n",
      "If you run a component multiple times in the same workflow,\n" +
      "it's recommended you set a unique key for every call,\n" +
      "for example: ${wfKey}.run(key: \"foo\")."
  }

  // subset directives and convert to list of tuples
  def drctv = workflowArgs.directives

  // TODO: unit test the two commands below
  // convert publish array into tags
  def valueToStr = { val ->
    // ignore closures
    if (val instanceof CharSequence) {
      if (!val.matches('^[{].*[}]$')) {
        '"' + val + '"'
      } else {
        val
      }
    } else if (val instanceof List) {
      "[" + val.collect{valueToStr(it)}.join(", ") + "]"
    } else if (val instanceof Map) {
      "[" + val.collect{k, v -> k + ": " + valueToStr(v)}.join(", ") + "]"
    } else {
      val.inspect()
    }
  }

  // multiple entries allowed: label, publishdir
  def drctvStrs = drctv.collect { key, value ->
    if (key in ["label", "publishDir"]) {
      value.collect{ val ->
        if (val instanceof Map) {
          "\n$key " + val.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ")
        } else if (val == null) {
          ""
        } else {
          "\n$key " + valueToStr(val)
        }
      }.join()
    } else if (value instanceof Map) {
      "\n$key " + value.collect{ k, v -> k + ": " + valueToStr(v) }.join(", ")
    } else {
      "\n$key " + valueToStr(value)
    }
  }.join()

  def inputPaths = meta.config.allArguments
    .findAll { it.type == "file" && it.direction == "input" }
    .collect { ', path(viash_par_' + it.plainName + ', stageAs: "_viash_par/' + it.plainName + '_?/*")' }
    .join()

  def outputPaths = meta.config.allArguments
    .findAll { it.type == "file" && it.direction == "output" }
    .collect { par ->
      // insert dummy into every output (see nextflow-io/nextflow#2678)
      if (!par.multiple) {
        ', path{[".exitcode", args.' + par.plainName + ']}'
      } else {
        ', path{[".exitcode"] + args.' + par.plainName + '}'
      }
    }
    .join()

  // TODO: move this functionality somewhere else?
  if (workflowArgs.auto.transcript) {
    outputPaths = outputPaths + ', path{[".exitcode", ".command*"]}'
  } else {
    outputPaths = outputPaths + ', path{[".exitcode"]}'
  }

  // create dirs for output files (based on BashWrapper.createParentFiles)
  def createParentStr = meta.config.allArguments
    .findAll { it.type == "file" && it.direction == "output" && it.create_parent }
    .collect { par -> 
      def contents = "args[\"${par.plainName}\"] instanceof List ? args[\"${par.plainName}\"].join('\" \"') : args[\"${par.plainName}\"]"
      "\${ args.containsKey(\"${par.plainName}\") ? \"mkdir_parent '\" + escapeText(${contents}) + \"'\" : \"\" }"
    }
    .join("\n")

  // construct inputFileExports
  def inputFileExports = meta.config.allArguments
    .findAll { it.type == "file" && it.direction.toLowerCase() == "input" }
    .collect { par ->
      def contents = "viash_par_${par.plainName} instanceof List ? viash_par_${par.plainName}.join(\"${par.multiple_sep}\") : viash_par_${par.plainName}"
      "\n\${viash_par_${par.plainName}.empty ? \"\" : \"export VIASH_PAR_${par.plainName.toUpperCase()}='\" + escapeText(${contents}) + \"'\"}"
    }

  // NOTE: if using docker, use /tmp instead of tmpDir!
  def tmpDir = java.nio.file.Paths.get(
    System.getenv('NXF_TEMP') ?: 
    System.getenv('VIASH_TEMP') ?: 
    System.getenv('VIASH_TMPDIR') ?: 
    System.getenv('VIASH_TEMPDIR') ?: 
    System.getenv('VIASH_TMP') ?: 
    System.getenv('TEMP') ?: 
    System.getenv('TMPDIR') ?: 
    System.getenv('TEMPDIR') ?:
    System.getenv('TMP') ?: 
    '/tmp'
  ).toAbsolutePath()

  // construct stub
  def stub = meta.config.allArguments
    .findAll { it.type == "file" && it.direction == "output" }
    .collect { par -> 
      "\${ args.containsKey(\"${par.plainName}\") ? \"touch2 \\\"\" + (args[\"${par.plainName}\"] instanceof String ? args[\"${par.plainName}\"].replace(\"_*\", \"_0\") : args[\"${par.plainName}\"].join('\" \"')) + \"\\\"\" : \"\" }"
    }
    .join("\n")

  // escape script
  def escapedScript = rawScript.replace('\\', '\\\\').replace('$', '\\$').replace('"""', '\\"\\"\\"')

  // publishdir assert
  def assertStr = (workflowArgs.auto.publish == true) || workflowArgs.auto.transcript ? 
    """\nassert task.publishDir.size() > 0: "if auto.publish is true, params.publish_dir needs to be defined.\\n  Example: --publish_dir './output/'" """ :
    ""

  // generate process string
  def procStr = 
  """nextflow.enable.dsl=2
  |
  |def escapeText = { s -> s.toString().replaceAll("'", "'\\\"'\\\"'") }
  |process $procKey {$drctvStrs
  |input:
  |  tuple val(id)$inputPaths, val(args), path(resourcesDir, stageAs: ".viash_meta_resources")
  |output:
  |  tuple val("\$id")$outputPaths, optional: true
  |stub:
  |\"\"\"
  |touch2() { mkdir -p "\\\$(dirname "\\\$1")" && touch "\\\$1" ; }
  |$stub
  |\"\"\"
  |script:$assertStr
  |def parInject = args
  |  .findAll{key, value -> value != null}
  |  .collect{key, value -> "export VIASH_PAR_\${key.toUpperCase()}='\${escapeText(value)}'"}
  |  .join("\\n")
  |\"\"\"
  |# meta exports
  |export VIASH_META_RESOURCES_DIR="\${resourcesDir}"
  |export VIASH_META_TEMP_DIR="${['docker', 'podman', 'charliecloud'].any{ it == workflow.containerEngine } ? '/tmp' : tmpDir}"
  |export VIASH_META_NAME="${meta.config.name}"
  |# export VIASH_META_EXECUTABLE="\\\$VIASH_META_RESOURCES_DIR/\\\$VIASH_META_NAME"
  |export VIASH_META_CONFIG="\\\$VIASH_META_RESOURCES_DIR/.config.vsh.yaml"
  |\${task.cpus ? "export VIASH_META_CPUS=\$task.cpus" : "" }
  |\${task.memory?.bytes != null ? "export VIASH_META_MEMORY_B=\$task.memory.bytes" : "" }
  |if [ ! -z \\\${VIASH_META_MEMORY_B+x} ]; then
  |  export VIASH_META_MEMORY_KB=\\\$(( (\\\$VIASH_META_MEMORY_B+999) / 1000 ))
  |  export VIASH_META_MEMORY_MB=\\\$(( (\\\$VIASH_META_MEMORY_KB+999) / 1000 ))
  |  export VIASH_META_MEMORY_GB=\\\$(( (\\\$VIASH_META_MEMORY_MB+999) / 1000 ))
  |  export VIASH_META_MEMORY_TB=\\\$(( (\\\$VIASH_META_MEMORY_GB+999) / 1000 ))
  |  export VIASH_META_MEMORY_PB=\\\$(( (\\\$VIASH_META_MEMORY_TB+999) / 1000 ))
  |  export VIASH_META_MEMORY_KIB=\\\$(( (\\\$VIASH_META_MEMORY_B+1023) / 1024 ))
  |  export VIASH_META_MEMORY_MIB=\\\$(( (\\\$VIASH_META_MEMORY_KIB+1023) / 1024 ))
  |  export VIASH_META_MEMORY_GIB=\\\$(( (\\\$VIASH_META_MEMORY_MIB+1023) / 1024 ))
  |  export VIASH_META_MEMORY_TIB=\\\$(( (\\\$VIASH_META_MEMORY_GIB+1023) / 1024 ))
  |  export VIASH_META_MEMORY_PIB=\\\$(( (\\\$VIASH_META_MEMORY_TIB+1023) / 1024 ))
  |fi
  |
  |# meta synonyms
  |export VIASH_TEMP="\\\$VIASH_META_TEMP_DIR"
  |export TEMP_DIR="\\\$VIASH_META_TEMP_DIR"
  |
  |# create output dirs if need be
  |function mkdir_parent {
  |  for file in "\\\$@"; do 
  |    mkdir -p "\\\$(dirname "\\\$file")"
  |  done
  |}
  |$createParentStr
  |
  |# argument exports${inputFileExports.join()}
  |\$parInject
  |
  |# process script
  |${escapedScript}
  |\"\"\"
  |}
  |""".stripMargin()

  // TODO: print on debug
  // if (workflowArgs.debug == true) {
  //   println("######################\n$procStr\n######################")
  // }

  // write process to temp file
  def tempFile = java.nio.file.Files.createTempFile("viash-process-${procKey}-", ".nf")
  addShutdownHook { java.nio.file.Files.deleteIfExists(tempFile) }
  tempFile.text = procStr

  // create process from temp file
  def binding = new nextflow.script.ScriptBinding([:])
  def session = nextflow.Nextflow.getSession()
  def parser = new nextflow.script.ScriptParser(session)
    .setModule(true)
    .setBinding(binding)
  def moduleScript = parser.runScript(tempFile)
    .getScript()

  // register module in meta
  def module = new nextflow.script.IncludeDef.Module(name: procKey)
  scriptMeta.addModule(moduleScript, module.name, module.alias)

  // retrieve and return process from meta
  return scriptMeta.getProcess(procKey)
}

// defaults
meta["defaults"] = [
  // key to be used to trace the process and determine output names
  key: null,

  // fixed arguments to be passed to script
  args: [:],

  // default directives
  directives: readJsonBlob('''{
  "container" : {
    "registry" : "images.viash-hub.com",
    "image" : "vsh/biobox/star/star_align_reads",
    "tag" : "v0.2.0"
  },
  "tag" : "$id"
}'''),

  // auto settings
  auto: readJsonBlob('''{
  "simplifyInput" : true,
  "simplifyOutput" : false,
  "transcript" : false,
  "publish" : false
}'''),

  // Apply a map over the incoming tuple
  // Example: `{ tup -> [ tup[0], [input: tup[1].output] ] + tup.drop(2) }`
  map: null,

  // Apply a map over the ID element of a tuple (i.e. the first element)
  // Example: `{ id -> id + "_foo" }`
  mapId: null,

  // Apply a map over the data element of a tuple (i.e. the second element)
  // Example: `{ data -> [ input: data.output ] }`
  mapData: null,

  // Apply a map over the passthrough elements of a tuple (i.e. the tuple excl. the first two elements)
  // Example: `{ pt -> pt.drop(1) }`
  mapPassthrough: null,

  // Filter the channel
  // Example: `{ tup -> tup[0] == "foo" }`
  filter: null,

  // Choose whether or not to run the component on the tuple if the condition is true.
  // Otherwise, the tuple will be passed through.
  // Example: `{ tup -> tup[0] != "skip_this" }`
  runIf: null,

  // Rename keys in the data field of the tuple (i.e. the second element)
  // Will likely be deprecated in favour of `fromState`.
  // Example: `[ "new_key": "old_key" ]`
  renameKeys: null,

  // Fetch data from the state and pass it to the module without altering the current state.
  // 
  // `fromState` should be `null`, `List[String]`, `Map[String, String]` or a function. 
  // 
  // - If it is `null`, the state will be passed to the module as is.
  // - If it is a `List[String]`, the data will be the values of the state at the given keys.
  // - If it is a `Map[String, String]`, the data will be the values of the state at the given keys, with the keys renamed according to the map.
  // - If it is a function, the tuple (`[id, state]`) in the channel will be passed to the function, and the result will be used as the data.
  // 
  // Example: `{ id, state -> [input: state.fastq_file] }`
  // Default: `null`
  fromState: null,

  // Determine how the state should be updated after the module has been run.
  // 
  // `toState` should be `null`, `List[String]`, `Map[String, String]` or a function.
  // 
  // - If it is `null`, the state will be replaced with the output of the module.
  // - If it is a `List[String]`, the state will be updated with the values of the data at the given keys.
  // - If it is a `Map[String, String]`, the state will be updated with the values of the data at the given keys, with the keys renamed according to the map.
  // - If it is a function, a tuple (`[id, output, state]`) will be passed to the function, and the result will be used as the new state.
  //
  // Example: `{ id, output, state -> state + [counts: state.output] }`
  // Default: `{ id, output, state -> output }`
  toState: null,

  // Whether or not to print debug messages
  // Default: `false`
  debug: false
]

// initialise default workflow
meta["workflow"] = workflowFactory([key: meta.config.name], meta.defaults, meta)

// add workflow to environment
nextflow.script.ScriptMeta.current().addDefinition(meta.workflow)

// anonymous workflow for running this module as a standalone
workflow {
  // add id argument if it's not already in the config
  // TODO: deep copy
  def newConfig = deepClone(meta.config)
  def newParams = deepClone(params)

  def argsContainsId = newConfig.allArguments.any{it.plainName == "id"}
  if (!argsContainsId) {
    def idArg = [
      'name': '--id',
      'required': false,
      'type': 'string',
      'description': 'A unique id for every entry.',
      'multiple': false
    ]
    newConfig.arguments.add(0, idArg)
    newConfig = processConfig(newConfig)
  }
  if (!newParams.containsKey("id")) {
    newParams.id = "run"
  }

  helpMessage(newConfig)

  channelFromParams(newParams, newConfig)
    // make sure id is not in the state if id is not in the args
    | map {id, state ->
      if (!argsContainsId) {
        [id, state.findAll{k, v -> k != "id"}]
      } else {
        [id, state]
      }
    }
    | meta.workflow.run(
      auto: [ publish: "state" ]
    )
}

// END COMPONENT-SPECIFIC CODE
