/**
  * Replace "^" prefixed tags (eg. ^forename) in a template with values from an array
  *
  * @param string $ps_template String with embedded tags. Tags are just alphanumeric strings prefixed with a caret ("^")
  * @param string $pm_tablename_or_num Table name or number of table from which values are being formatted
  * @param string $pa_row_ids An array of primary key values in the specified table to be pulled into the template
  * @param array $pa_options Supported options are:
  *		returnAsArray = if true an array of processed template values is returned, otherwise the template values are returned as a string joined together with a delimiter. Default is false.
  *		delimiter = value to string together template values with when returnAsArray is false. Default is ';' (semicolon)
  *		placeholderPrefix = attribute container to implicitly place primary record fields into. Ex. if the table is "ca_entities" and the placeholder is "address" then tags like ^city will resolve to ca_entities.address.city
  *		requireLinkTags = if set then links are only added when explicitly defined with <l> tags. [Default is true]
  *		primaryIDs = row_ids for primary rows in related table, keyed by table name; when resolving ambiguous relationships the row_ids will be excluded from consideration. This option is rarely used and exists primarily to take care of a single
  *						edge case: you are processing a template relative to a self-relationship such as ca_entities_x_entities that includes references to the subject table (ca_entities, in the case of ca_entities_x_entities). There are
  *						two possible paths to take in this situations; primaryIDs lets you specify which ones you *don't* want to take by row_id. For interstitial editors, the ids will be set to a single id: that of the subject (Eg. ca_entities) row
  *						from which the interstitial was launched.
  *		sort = optional list of tag values to sort repeating values within a row template on. The tag must appear in the template. You can specify more than one tag by separating the tags with semicolons.
  *		sortDirection = The direction of the sort of repeating values within a row template. May be either ASC (ascending) or DESC (descending). [Default is ASC]
  *		linkTarget = Optional target to use when generating <l> tag-based links. By default links point to standard detail pages, but plugins may define linkTargets that point elsewhere.
  * 		skipIfExpression = skip the elements in $pa_row_ids for which the given expression does not evaluate true
  *		includeBlankValuesInArray = include blank template values in primary template and all <unit>s in returned array when returnAsArray is set. If you need the returned array of values to line up with the row_ids in $pa_row_ids this should be set. [Default is false]
  *		includeBlankValuesInTopLevelForPrefetch = include blank template values in *primary template* (not <unit>s) in returned array when returnAsArray is set. Used by template prefetcher to ensure returned values align with id indices. [Default is false]
  *		forceValues = Optional array of values indexed by placeholder without caret (eg. ca_objects.idno) and row_id. When present these values will be used in place of the placeholders, rather than whatever value normal processing would result in. [Default is null]
  *		aggregateUnique = Remove duplicate values. If set then array of evaluated templates may not correspond one-to-one with the original list of row_ids set in $pa_row_ids. [Default is false]
  *
  * @return mixed Output of processed templates
  *
  * TODO: sort and sortDirection are not currently supported! They are ignored for the time being
  */
 public static function process($ps_template, $pm_tablename_or_num, array $pa_row_ids, array $pa_options = null)
 {
     // Set up options
     foreach (array('request', 'template', 'restrict_to_relationship_types', 'restrictToRelationshipTypes', 'excludeRelationshipTypes', 'useLocaleCodes') as $vs_k) {
         unset($pa_options[$vs_k]);
     }
     if (!isset($pa_options['convertCodesToDisplayText'])) {
         $pa_options['convertCodesToDisplayText'] = true;
     }
     $pb_return_as_array = (bool) caGetOption('returnAsArray', $pa_options, false);
     unset($pa_options['returnAsArray']);
     if (($pa_sort = caGetOption('sort', $pa_options, null)) && !is_array($pa_sort)) {
         $pa_sort = explode(";", $pa_sort);
     }
     $ps_sort_direction = caGetOption('sortDirection', $pa_options, null, array('forceUppercase' => true));
     if (!in_array($ps_sort_direction, array('ASC', 'DESC'))) {
         $ps_sort_direction = 'ASC';
     }
     $ps_delimiter = caGetOption('delimiter', $pa_options, '; ');
     $pb_include_blanks = caGetOption('includeBlankValuesInArray', $pa_options, false);
     $pb_include_blanks_for_prefetch = caGetOption('includeBlankValuesInTopLevelForPrefetch', $pa_options, false);
     // Bail if no rows or template are set
     if (!is_array($pa_row_ids) || !sizeof($pa_row_ids) || !$ps_template) {
         return $pb_return_as_array ? array() : "";
     }
     // Parse template
     if (!is_array($va_template = DisplayTemplateParser::parse($ps_template, $pa_options))) {
         return null;
     }
     $o_dm = Datamodel::load();
     $ps_tablename = is_numeric($pm_tablename_or_num) ? $o_dm->getTableName($pm_tablename_or_num) : $pm_tablename_or_num;
     $t_instance = $o_dm->getInstanceByTableName($ps_tablename, true);
     $vs_pk = $t_instance->primaryKey();
     // Prefetch related items for <units>
     if (!$pa_options['isUnit'] && !caGetOption('dontPrefetchRelated', $pa_options, false)) {
         DisplayTemplateParser::prefetchAllRelatedIDs($va_template['tree']->children, $ps_tablename, $pa_row_ids, $pa_options);
     }
     $qr_res = caMakeSearchResult($ps_tablename, $pa_row_ids);
     if (!$qr_res) {
         return $pb_return_as_array ? array() : "";
     }
     $pa_check_access = $t_instance->hasField('access') ? caGetOption('checkAccess', $pa_options, null) : null;
     if (!is_array($pa_check_access) || !sizeof($pa_check_access)) {
         $pa_check_access = null;
     }
     $ps_skip_if_expression = caGetOption('skipIfExpression', $pa_options, false);
     $va_skip_if_expression_tags = caGetTemplateTags($ps_skip_if_expression);
     $va_proc_templates = [];
     while ($qr_res->nextHit()) {
         // check access
         if ($pa_check_access && !in_array($qr_res->get("{$ps_tablename}.access"), $pa_check_access)) {
             continue;
         }
         // check if we skip this row because of skipIfExpression
         if (strlen($ps_skip_if_expression) > 0) {
             $va_expression_vars = [];
             foreach ($va_skip_if_expression_tags as $vs_expression_tag) {
                 if (!isset($va_expression_vars[$vs_expression_tag])) {
                     $va_expression_vars[$vs_expression_tag] = $qr_res->get($vs_expression_tag, ['assumeDisplayField' => true, 'returnIdno' => true, 'delimiter' => $ps_delimiter]);
                 }
             }
             if (ExpressionParser::evaluate($ps_skip_if_expression, $va_expression_vars)) {
                 continue;
             }
         }
         if ($pa_options['relativeToContainer']) {
             $va_vals = DisplayTemplateParser::_getValues($qr_res, $va_template['tags'], $pa_options);
             if (isset($pa_options['sort']) && is_array($pa_options['sort'])) {
                 $va_vals = caSortArrayByKeyInValue($va_vals, array('__sort__'), $pa_options['sortDirection'], array('dontRemoveKeyPrefixes' => true));
             }
             foreach ($va_vals as $vn_index => $va_val_list) {
                 $va_proc_templates[] = is_array($va_val_list) ? DisplayTemplateParser::_processChildren($qr_res, $va_template['tree']->children, $va_val_list, array_merge($pa_options, ['index' => $vn_index, 'returnAsArray' => $pa_options['aggregateUnique']])) : '';
             }
         } else {
             $va_proc_templates[] = DisplayTemplateParser::_processChildren($qr_res, $va_template['tree']->children, DisplayTemplateParser::_getValues($qr_res, $va_template['tags'], $pa_options), array_merge($pa_options, ['returnAsArray' => $pa_options['aggregateUnique']]));
         }
     }
     if ($pa_options['aggregateUnique']) {
         $va_acc = [];
         foreach ($va_proc_templates as $va_val_list) {
             if (is_array($va_val_list)) {
                 $va_acc = array_merge($va_acc, $va_val_list);
             } else {
                 $va_acc[] = $va_val_list;
             }
         }
         $va_proc_templates = array_unique($va_acc);
     }
     if (!$pb_include_blanks && !$pb_include_blanks_for_prefetch) {
         $va_proc_templates = array_filter($va_proc_templates, 'strlen');
     }
     // Transform links
     $va_proc_templates = caCreateLinksFromText($va_proc_templates, $ps_tablename, $pa_row_ids, null, caGetOption('linkTarget', $pa_options, null), array_merge(['addRelParameter' => true, 'requireLinkTags' => true], $pa_options));
     if (!$pb_include_blanks && !$pb_include_blanks_for_prefetch) {
         $va_proc_templates = array_filter($va_proc_templates, 'strlen');
     }
     if (!$pb_return_as_array) {
         return join($ps_delimiter, $va_proc_templates);
     }
     return $va_proc_templates;
 }