<?php
/** ---------------------------------------------------------------------
 * app/helpers/displayHelpers.php : miscellaneous functions
 * ----------------------------------------------------------------------
 * CollectiveAccess
 * Open-source collections management software
 * ----------------------------------------------------------------------
 *
 * Software by Whirl-i-Gig (http://www.whirl-i-gig.com)
 * Copyright 2009-2025 Whirl-i-Gig
 *
 * For more information visit http://www.CollectiveAccess.org
 *
 * This program is free software; you may redistribute it and/or modify it under
 * the terms of the provided license as published by Whirl-i-Gig
 *
 * CollectiveAccess is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTIES whatsoever, including any implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * This source code is free and modifiable under the terms of
 * GNU General Public License. (http://www.gnu.org/copyleft/gpl.html). See
 * the "license.txt" file for details, or visit the CollectiveAccess web site at
 * http://www.CollectiveAccess.org
 * 
 * @package CollectiveAccess
 * @subpackage utils
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License version 3
 * 
 * ----------------------------------------------------------------------
 */
require_once(__CA_LIB_DIR__.'/Parsers/TimeExpressionParser.php');
require_once(__CA_LIB_DIR__.'/Parsers/ExpressionParser.php');
require_once(__CA_LIB_DIR__."/ApplicationPluginManager.php");
require_once(__CA_LIB_DIR__.'/Parsers/DisplayTemplateParser.php');
require_once(__CA_LIB_DIR__.'/Media/MediaInfoCoder.php');
require_once(__CA_APP_DIR__.'/helpers/searchHelpers.php');

# ------------------------------------------------------------------------------------------------
/**
 * @param $ps_item_locale -
 * @param $pa_preferred_locales -
 * @return Array - returns an associative array defining which locales should be used when displaying values; suitable for use with caExtractValuesByLocale()
 */
$g_user_locale_rules = array();
function caGetUserLocaleRules($ps_item_locale=null, $pa_preferred_locales=null) {
	global $g_ui_locale, $g_ui_locale_id, $g_user_locale_rules;

	if (isset($g_user_locale_rules[$ps_item_locale])) { return $g_user_locale_rules[$ps_item_locale]; }

	$o_config = Configuration::load();
	$va_default_locales = $o_config->getList('locale_defaults');

	$va_preferred_locales = [];

	if (is_array($pa_preferred_locales)) {
		foreach($pa_preferred_locales as $vs_preferred_locale) {
			if(is_numeric($vs_preferred_locale)) {
				$vs_preferred_locale = ca_locales::IDToCode($vs_preferred_locale);
			}
			$va_preferred_locales[$vs_preferred_locale] = true;
		}
	}
	
	$va_similar_locales = [];
	if ($ps_item_locale) {
		// if item locale is passed as locale_id we need to convert it to a code
		if (is_numeric($ps_item_locale)) {
			$t_locales = new ca_locales();
			if ($t_locales->load($ps_item_locale)) {
				$ps_item_locale = $t_locales->getCode();
			} else {
				$ps_item_locale = null;
			}
		}
		if ($ps_item_locale) {
			$va_preferred_locales[$ps_item_locale] = true;
		}
		$va_similar_locales = ca_locales::localesForLanguage($ps_item_locale, ['codesOnly' => true]);
	}

	$va_fallback_locales = array();
	if (is_array($va_default_locales)) {
		foreach($va_default_locales as $vs_fallback_locale) {
			$va_similar_locales = array_merge($va_similar_locales, ca_locales::localesForLanguage($vs_fallback_locale, ['codesOnly' => true]));
			if (!isset($va_preferred_locales[$vs_fallback_locale]) || !$va_preferred_locales[$vs_fallback_locale]) {
				$va_fallback_locales[$vs_fallback_locale] = true;
			}
		}
	}
	// add locales with same language
	foreach($va_similar_locales as $vs_similar_locale) {
		$va_fallback_locales[$vs_similar_locale] = true;
	}

	if ($g_ui_locale && !is_array($va_preferred_locales) || !sizeof($va_preferred_locales)) {
		if (!isset($va_preferred_locales[$g_ui_locale]) || !$va_preferred_locales[$g_ui_locale]) {
			$va_preferred_locales[$g_ui_locale] = true;
		}
	}
	$va_fallback_locales = array_filter($va_fallback_locales, function($v, $k) use ($ps_item_locale, $va_fallback_locales, $va_preferred_locales) { return !isset($va_preferred_locales[$k]) && ($k !== $ps_item_locale); }, ARRAY_FILTER_USE_BOTH);

	$va_rules = array(
		'preferred' => $va_preferred_locales,	/* all of these locales will display if available */
		'fallback' => $va_fallback_locales		/* the first of these that is available will display, but only if none of the preferred locales are available */
	);

	if($ps_item_locale){ $g_user_locale_rules[$ps_item_locale] = $va_rules; }

	return $va_rules;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 * @param $pa_locale_rules - Associative array defining which locales to extract, and how to fall back to alternative locales should your preferred locales not exist in $pa_values
 * @param $pa_values - Associative array keyed by unique item_id and then locale code (eg. en_US) or locale_id; the values can be anything - string, numbers, objects, arrays, etc.
 * @param $pa_options [optional] - Associative array of options; available options are:
 *		returnList = return an indexed array of found values rather than an associative array keys on unique item_id. [Default is false]
 *		debug = print debugging information. [Default is false]
 *		noFallback = don't use fallback locales. [Default is false
 * @return Array - an array of found values keyed by unique item_id; or an indexed list of found values if option 'returnList' is passed in $pa_options
 */
function caExtractValuesByLocale($pa_locale_rules, $pa_values, $pa_options=null) {
	if (!is_array($pa_values)) { return array(); }
	$va_locales = ca_locales::getLocaleList();
	
	$no_fallback = isset($pa_options['noFallback']) && (bool)$pa_options['noFallback'];

	if (!is_array($pa_options)) { $pa_options = array(); }
	if (!isset($pa_options['returnList'])) { $pa_options['returnList'] = false; }

	if (!is_array($pa_values)) { return array(); }
	$va_values = array();
	foreach($pa_values as $vm_id => $va_value_list_by_locale) {
		if(!is_array($va_value_list_by_locale)) { continue; }
		if ((sizeof($va_value_list_by_locale) == 1) && (!$no_fallback || (in_array(ca_locales::idToCode(array_key_first($va_value_list_by_locale)), array_keys($pa_locale_rules['preferred']))))) {		// Don't bother looking if there's just a single value
			$va_values[$vm_id] = array_pop($va_value_list_by_locale);
			continue;
		}
		foreach($va_value_list_by_locale as $pm_locale => $vm_value) {
			// convert locale_id to locale string
			if (is_numeric($pm_locale)) {
				if (!$va_locales[$pm_locale]) { continue; }	// invalid locale_id?
				$vs_locale = $va_locales[$pm_locale]['language'].'_'.$va_locales[$pm_locale]['country'];
			} else {
				$vs_locale = $pm_locale;
			}

			// try to find values for preferred locale
			if (isset($pa_locale_rules['preferred'][$vs_locale]) && $pa_locale_rules['preferred'][$vs_locale]) {
				$va_values[$vm_id] = $vm_value;
				break;
			}

			if(!$no_fallback) {
				// try fallback locales
				if (isset($pa_locale_rules['fallback'][$vs_locale]) && $pa_locale_rules['fallback'][$vs_locale]) {
					$va_values[$vm_id] = $vm_value;
				}
			}
		}

		if (!$no_fallback && !isset($va_values[$vm_id])) {
			// desperation mode: pick an available locale
			$va_values[$vm_id] = array_pop($va_value_list_by_locale);
		}
	}
	return ($pa_options['returnList']) ? array_values($va_values) : $va_values;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caExtractValuesByUserLocale($pa_values, $ps_item_locale=null, $pa_preferred_locales=null, $pa_options=null) {
	return caExtractValuesByLocale(caGetUserLocaleRules($ps_item_locale, $pa_preferred_locales), $pa_values, $pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Takes the output of BaseModel->getHierarchyAncestors() and tries to extract the appropriate values for the current user's locale.
 * This is designed for the common case where you want to get a list of ancestors with their labels in the appropriate language,
 * so you call getHierarchyAncestors() with the 'getHierarchyAncestors' option set to the label table. What you get back is a simple
 * list where each item is a node with the table fields + the label fields; if a node has labels in several languages then you'll get back
 * dupes - one for each language. 
 *
 * This function takes that list with dupes and returns an array key'ed upon the primary key containing a single entry for each node
 * and the label set to the appropriate language - no dupes!
 *
 * @param array - the list of ancestor hierarchy nodes as returned by BaseModel->getHierarchyAncestors()
 * @param string - the field name of the primary key of the hierarchy (eg. 'place_id' for ca_places)
 * @return array - the list of ancestors with labels in the appropriate language; array is indexed by the primary key
 */
function caExtractValuesByUserLocaleFromHierarchyAncestorList($pa_list, $ps_primary_key_name, $ps_label_display_field, $ps_use_if_no_label_field, $ps_default_text='???') {
	if (!is_array($pa_list)) { return array(); }
	$va_values = array();
	foreach($pa_list as $vn_i => $va_item) {
		if (!isset($va_item[$ps_label_display_field]) || !$va_item[$ps_label_display_field]) {
			if (!isset($va_item[$ps_use_if_no_label_field]) || !($va_item[$ps_label_display_field] = $va_item[$ps_use_if_no_label_field])) {
				$va_item[$ps_label_display_field] = $ps_default_text;
			}
		}
		$va_values[$va_item['NODE'][$ps_primary_key_name]][$va_item['NODE']['locale_id']] = $va_item;
	}

	return caExtractValuesByUserLocale($va_values);
}
# ------------------------------------------------------------------------------------------------
/**
 * Return string value from locale-indexed array of values for a locale-aware setting
 *
 * @param string $setting
 * @param array $setting_values
 * @param array $options Options include:
 *		default = Default value to return if no setting value is available. [Default is null]
 *	
 * @return string
 */
function caExtractSettingsValueByUserLocale(string $setting, array $setting_values, ?array $options=null) {
	global $g_ui_locale;
	if(!isset($setting_values[$setting])) { return caGetOption('default', $options, null); }
	if (!is_array($v = $setting_values[$setting])) { return strlen($v) ? $v : caGetOption('default', $options, null); }

	if (isset($v[$g_ui_locale])) {
		return $v[$g_ui_locale];
	}
	
	// Try to find setting with same locale
	if($setting_values[$setting][$g_ui_locale] ?? null) {
		return $setting_values[$setting][$g_ui_locale];
	}
	
	// Fall back and try to find setting with same language
	$l = explode('_', $g_ui_locale);
	$l = $l[0];
	foreach($setting_values[$setting] as $locale => $val) {
		if(preg_match("!^{$l}(_[A-Za-z]{2,3}|$)$!", $locale)) {
			return $val;
		}
	}
	
	// No language match, so just return the first value
	if (!is_null($val = array_shift($v))) { return $val; }
	
	// If all else fails, try to return the default
	return caGetOption('default', $options, null);
}
# ------------------------------------------------------------------------------------------------
function caExtractValuesByUserLocaleFromHierarchyChildList($pa_list, $ps_primary_key_name, $ps_label_display_field, $ps_use_if_no_label_field, $ps_default_text='???') {
	if (!is_array($pa_list)) { return array(); }
	$va_values = array();
	foreach($pa_list as $vn_i => $va_item) {
		if (!$va_item[$ps_label_display_field]) {
			if (!($va_item[$ps_label_display_field] = $va_item[$ps_use_if_no_label_field])) {
				$va_item[$ps_label_display_field] = $ps_default_text;
			}
		}
		$va_values[$va_item[$ps_primary_key_name]][$va_item['locale_id']] = $va_item;
	}

	return caExtractValuesByUserLocale($va_values);
}
# ------------------------------------------------------------------------------------------------
function caFormatFieldErrorsAsHTML($pa_errors, $ps_css_class) {

	$vs_output = "<ul class='{$ps_css_class}'>\n";
	foreach($pa_errors as $o_e) {
		$vs_output .= '<li class="'.$ps_css_class.'"><img src=""/> ';
		$vs_output .= $o_e->getErrorMessage()."</li>";
	}
	$vs_output .= "</ul>\n";


	return $vs_output;
}
# ------------------------------------------------------------------------------------------------
function caFormControlBox($ps_left_content, $ps_middle_content, $ps_right_content, $ps_second_row_content='') {
	$vs_output = '<div class="control-box rounded">
	<div class="control-box-left-content">'.$ps_left_content;

	$vs_output .= '</div>
	<div class="control-box-right-content">'.$ps_right_content;

	$vs_output .= '</div><div class="control-box-middle-content">'.$ps_middle_content.'</div>';

	if ($ps_second_row_content) {
		$vs_output .= '<div class="clear"><!--empty--></div>'.$ps_second_row_content;
	}

	$vs_output .= '</div>
<div class="clear"><!--empty--></div>'."\n";

	return $vs_output;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caDeleteWarningBox($po_request, $t_instance, $ps_item_name, $ps_module_path, $ps_controller, $ps_cancel_action, $pa_parameters) {
	if ($vs_warning = isset($pa_parameters['warning']) ? $pa_parameters['warning'] : null) {
		$vs_warning = '<br/>'.$vs_warning;
	}

	$vs_remapping_controls = caDeleteRemapper($po_request, $t_instance);
	$vs_output = caFormTag($po_request, 'Delete', 'caDeleteForm', null, 'post', 'multipart/form-data', '_top', array('noCSRFToken' => false,'disableUnsavedChangesWarning' => true));
	$vs_output .= "<div class='delete-control-box'>".caFormControlBox(
		"<div class='delete_warning_box'>"._t('Really delete "%1"?', $ps_item_name)."</div>".
		($vs_remapping_controls ? "<div class='delete_remapping_controls'>{$vs_remapping_controls}</div>" : ''),
		$vs_warning,
		caFormSubmitButton($po_request, __CA_NAV_ICON_DELETE__, _t("Delete"), 'caDeleteForm', array()).
		caFormNavButton($po_request, __CA_NAV_ICON_CANCEL__, _t("Cancel"), '', $ps_module_path, $ps_controller, $ps_cancel_action, $pa_parameters)
	)."</div>\n";


	foreach(array_merge($pa_parameters, array('confirm' => 1)) as $vs_f => $vs_v) {
		$vs_output .= caHTMLHiddenInput($vs_f, array('value' => $vs_v));
	}
	$vs_output .= caHTMLHiddenInput($t_instance->primaryKey(), array('value' => $t_instance->getPrimaryKey()));
	$vs_output .= "</form>\n";

	return $vs_output;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caDeleteMultipleWarningBox($po_request, $t_instance, $row_ids, $message, $ps_module_path, $ps_controller, $ps_cancel_action, $pa_parameters) {
	if ($vs_warning = isset($pa_parameters['warning']) ? $pa_parameters['warning'] : null) {
		$vs_warning = '<br/>'.$vs_warning;
	}

	$vs_output = caFormTag($po_request, 'Delete', 'caDeleteForm', null, 'post', 'multipart/form-data', '_top', array('noCSRFToken' => false,'disableUnsavedChangesWarning' => true));
	$vs_output .= "<div class='delete-control-box'>".caFormControlBox(
		"<div class='delete_warning_box'>"._t('Really delete %1?', $message)."</div>",
		$vs_warning,
		caFormSubmitButton($po_request, __CA_NAV_ICON_DELETE__, _t("Delete"), 'caDeleteForm', array()).
		caFormNavButton($po_request, __CA_NAV_ICON_CANCEL__, _t("Cancel"), '', $ps_module_path, $ps_controller, $ps_cancel_action, $pa_parameters)
	)."</div>\n";


	foreach(array_merge($pa_parameters, array('confirm' => 1)) as $vs_f => $vs_v) {
		$vs_output .= caHTMLHiddenInput($vs_f, array('value' => $vs_v));
	}
	foreach($row_ids as $row_id) {
		$vs_output .= caHTMLHiddenInput("row_id[]", array('value' => $row_id));
	}
	$vs_output .= "</form>\n";

	return $vs_output;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caDeleteRemapper($po_request, $t_instance) {
	$vs_instance_table = $t_instance->tableName();

	$vn_reference_to_count = $vn_reference_from_count = $vn_child_count = 0;
	$va_reference_to_buf = $va_reference_from_buf = array();
	switch($vs_instance_table) {
		case 'ca_relationship_types':
			// get # of relationships using this type
			$vn_rel_count = $t_instance->getRelationshipCountForType();
			$t_rel_instance = Datamodel::getInstanceByTableNum($t_instance->get('table_num'));
			if (!$t_rel_instance->load($t_instance->get('table_num'))) { return ''; }
			if ($vn_rel_count == 1) {
				$va_reference_to_buf[] = _t("Type is used by %1 %2", $vn_rel_count, $t_rel_instance->getProperty('NAME_PLURAL'))."<br>\n";
			} else {
				$va_reference_to_buf[] = _t("Type is used by %1 %2", $vn_rel_count, $t_rel_instance->getProperty('NAME_PLURAL'))."<br>\n";
			}
			$vn_reference_to_count += $vn_rel_count;

			$vs_typename = _t('relationship type');
			break;
		default:
			// Check relationships
			$va_tables = [
				'ca_objects', 'ca_object_lots', 'ca_entities', 'ca_places', 'ca_occurrences', 'ca_collections', 
				'ca_storage_locations', 'ca_list_items', 'ca_loans', 'ca_movements', 'ca_tours', 'ca_tour_stops', 
				'ca_object_representations'
			];

			if (!in_array($t_instance->tableName(), $va_tables)) { return null; }

			foreach($va_tables as $vs_table) {
				if (!is_array($va_items = $t_instance->getRelatedItems($vs_table))) { $va_items = []; }

				if (!($vn_c = sizeof($va_items))) { continue; }
				if ($vn_c == 1) {
					$va_reference_to_buf[] = _t("Has %1 relationship to %2", $vn_c, caGetTableDisplayName($vs_table, true))."<br>\n";
				} else {
					$va_reference_to_buf[] = _t("Has %1 relationships to %2", $vn_c, caGetTableDisplayName($vs_table, true))."<br>\n";
				}
				$vn_reference_to_count += $vn_c;
			}

			// Check attributes *using* this row
			if ($vn_datatype = $t_instance->authorityElementDatatype()) {
				if ($vn_c = $t_instance->getAuthorityElementReferences(array('countOnly' => true))) {
					if ($vn_c == 1) {
						$va_reference_to_buf[] = _t("Is referenced %1 time", $vn_c)."<br>\n";
					} else {
						$va_reference_to_buf[] = _t("Is referenced %1 times", $vn_c)."<br>\n";
					}
					$vn_reference_to_count += $vn_c;
				}
			}

			$vs_typename = $t_instance->getTypeName();

			// Check for authority references that are *part* of this row
			if (is_array($va_references_from = $t_instance->getAuthorityElementList()) && sizeof($va_references_from)) {
				foreach($va_references_from as $va_ref) {
					if (!($t_element = ca_metadata_elements::getInstance($va_ref['hier_element_id']))) { continue; }
					$va_reference_from_buf[] = _t(($va_ref['count'] == 1) ? "%1 reference in %2" : "%1 references in %2", $va_ref['count'], $t_element->getLabelForDisplay());
					$vn_reference_from_count += $va_ref['count'];
				}
			}

			// Check for child records in hierarchy
			if ($t_instance->isHierarchical() && is_array($va_children = call_user_func($t_instance->tableName()."::getHierarchyChildrenForIDs", [$t_instance->getPrimaryKey()]))) {
				$vn_child_count = sizeof($va_children);
				if ($vn_child_count == 1) {
					$va_reference_to_buf[] = _t("Has %1 child", $vn_child_count)."<br>\n";
				} else {
					$va_reference_to_buf[] = _t("Has %1 children", $vn_child_count)."<br>\n";
				}
				$vn_reference_to_count += $vn_child_count;
			}
			break;
	}

	// get default setting
	$o_config = $po_request->getAppConfig();
	if (!in_array($vs_default = strtolower($po_request->user->getPreference('cataloguing_delete_reference_handling_default')), ['transfer', 'remap', 'remove', 'delete'])) {
		if (!in_array($vs_default = strtolower($o_config->get("{$vs_instance_table}_delete_reference_handling_default")), ['transfer', 'remap', 'remove', 'delete'])) {
			if (!in_array($vs_default = strtolower($o_config->get('delete_reference_handling_default')), ['transfer', 'remap', 'remove', 'delete'])) {
				$vs_default = null;
			}
		}
	}

	switch($vs_default) {
		case 'transfer':
		case 'remap':
			$vs_default = 'remap';
			break;
		case 'remove':
		case 'delete':
			$vs_default = 'delete';
			break;

	}

	$vs_output = '';
	// add autocompleter for remapping
	if ($vn_reference_to_count == 1) {
		$vs_output .= "<h3 id='caReferenceHandlingToCount'>"._t('This %1 is referenced %2 time', $vs_typename, $vn_reference_to_count).". "._t('When deleting this %1:', $vs_typename)."</h3>\n";
	} elseif($vn_reference_to_count > 1) {
		$vs_output .= "<h3 id='caReferenceHandlingToCount'>"._t('This %1 is referenced %2 times', $vs_typename, $vn_reference_to_count).". "._t('When deleting this %1:', $vs_typename)."</h3>\n";
	} else {
		$vs_output .= "<h3 id='caReferenceHandlingToCount'>"._t('When deleting this %1:', $vs_typename)."</h3>\n";
	}

	$va_delete_opts = ['value' => 'delete', 'id' => 'caReferenceHandlingToDelete'];
	$va_remap_opts = ['value' => 'remap', 'id' => 'caReferenceToHandlingRemap'];
	$va_remap_lookup_opts = ['value' => '', 'size' => 40, 'id' => 'caReferenceHandlingToRemapTo', 'class' => 'lookupBg'];
	if ($vs_default === 'delete') { 
		$va_delete_opts['checked'] = 1; 
		$va_remap_lookup_opts['disabled'] = 1; 
	} else {
		$va_remap_opts['checked'] = 1; 
	}
	
	if ($vn_reference_to_count > 0) {
		$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingTo', $va_delete_opts).' '._t('remove all references')."<br/>\n";
	} else {
		$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingTo', $va_delete_opts).' '._t('remove all associated metadata')."<br/>\n";
	}

	if ($vs_instance_table === 'ca_storage_locations') {
		AssetLoadManager::register('hierBrowser');
		$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingTo', $va_remap_opts).' '._t('transfer references to').' ';

		$vs_output .= "<div id=\"caReferenceHandlingToRemapToHierBrowser\" class=\"hierarchyBrowserSmall\" style=\"width: 700px;\">
				<!-- Content for hierarchy browser is dynamically inserted here by ca.hierbrowser -->
			</div><!-- end hierbrowser -->";
		$vs_output .= "
			<div class=\"hierarchyBrowserFind\">
				"._t('Find').": <input type=\"text\" id=\"caReferenceHandlingToRemapToHierBrowserSearch\" name=\"search\" value=\"\" size=\"25\"/>
			</div>";

	} else {
		if ($vn_reference_to_count > 0) {
			$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingTo', $va_remap_opts).' '._t('transfer references to').' '.caHTMLTextInput('caReferenceHandlingToRemapTo', $va_remap_lookup_opts)."<br/>\n";
			$vs_output .= caHTMLCheckboxInput('caReferenceHandlingMetadata', ['id' => 'caReferenceHandlingMetadata', 'value' => 1, 'checked' => 1], []).' '._t('also transfer metadata');
		} else {
			$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingTo', $va_remap_opts).' '._t('transfer metadata to').' '.caHTMLTextInput('caReferenceHandlingToRemapTo', $va_remap_lookup_opts)."<br/>\n";
			$vs_output .= caHTMLHiddenInput('caReferenceHandlingMetadata', ['id' => 'caReferenceHandlingMetadata', 'value' => 1], []);
		}
		
	}
	$vs_output .= "<a href='#' class='button' onclick='jQuery(\"#caReferenceHandlingToRemapToID\").val(\"\"); jQuery(\"#caReferenceHandlingToRemapTo\").val(\"\"); jQuery(\"#caReferenceHandlingToClear\").css(\"display\", \"none\"); return false;' style='display: none;' id='caReferenceHandlingToClear'>"._t('Clear').'</a>';
	$vs_output .= caHTMLHiddenInput('caReferenceHandlingToRemapToID', array('value' => '', 'id' => 'caReferenceHandlingToRemapToID'));

	if ($vn_child_count > 0) {
		$vs_output .= '<p class="formLabelWarning" id="caChildDeletionWarning"><i class="caIcon fa fa-info-circle fa-1x"></i> '._t('Child records will be deleted')."</p>\n";
	}

	$vs_output .= "<script type='text/javascript'>";

	$va_service_info = caJSONLookupServiceUrl($po_request, $t_instance->tableName(), array('noSymbols' => 1, 'noInline' => 1, 'exclude' => (int)$t_instance->getPrimaryKey(), 'table_num' => (int)$t_instance->get('table_num')));

	if ($vs_instance_table === 'ca_storage_locations') {
		$vs_output .= "
var caReferenceHandlingToRemapToHierBrowser = null;

jQuery(document).ready(function() {
if (!caReferenceHandlingToRemapToHierBrowser) {
	caReferenceHandlingToRemapToHierBrowser = caUI.initHierBrowser('caReferenceHandlingToRemapToHierBrowser', {
		levelDataUrl: ".json_encode($va_service_info['levelList']).",
		initDataUrl: ".json_encode($va_service_info['ancestorList']).",

		readOnly: true,
		editButtonIcon: ".json_encode(caNavIcon(__CA_NAV_ICON_RIGHT_ARROW__, 1)).",
		disabledButtonIcon: ".json_encode(caNavIcon(__CA_NAV_ICON_DOT__, 1)).",

		allowDragAndDropSorting: false,

		initItemID: ".(int)$t_instance->get("{$vs_instance_table}.parent_id").",

		excludeItemIDs: [".(int)$t_instance->getPrimaryKey()."],
		indicator: ".json_encode(caNavIcon(__CA_NAV_ICON_SPINNER__, 1)).",
		displayCurrentSelectionOnLoad: false,

		onSelection: function(id) {
			jQuery(\"#caReferenceHandlingToRemapToID\").val(id);
		}
	});
}
jQuery(\"#caReferenceHandlingToRemapToHierBrowserSearch, #caReferenceHandlingMetadata\").attr(\"disabled\", true);
jQuery('#caReferenceToHandlingRemap').click(function() {
	caReferenceHandlingToRemapToHierBrowser.isReadOnly(false);
	jQuery(\"#caReferenceHandlingToRemapToHierBrowserSearch, #caReferenceHandlingMetadata\").attr(\"disabled\", false);
	jQuery('#caChildDeletionWarning').hide();
});
jQuery('#caReferenceHandlingToDelete').click(function() {
	caReferenceHandlingToRemapToHierBrowser.isReadOnly(true);
	jQuery(\"#caReferenceHandlingToRemapToHierBrowserSearch, #caReferenceHandlingMetadata\").attr(\"disabled\", true);

	jQuery('#caChildDeletionWarning').show();
});
jQuery('#caReferenceHandlingToRemapToHierBrowserSearch').autocomplete(
	{
		source: ".json_encode($va_service_info['search']).", minLength: 3, delay: 800, html: true,
		select: function( event, ui ) {
			if (ui.item.id) {
				caReferenceHandlingToRemapToHierBrowser.setUpHierarchy(ui.item.id);	// jump browser to selected item
				jQuery('#caReferenceHandlingToRemapToID').val(ui.item.id);
			}
			event.preventDefault();
			jQuery('#caReferenceHandlingToRemapToHierBrowserSearch').val('');
		}
	}
).click(function() { this.select() });
})";
	} else {
		$vs_output .= "jQuery(document).ready(function() {";
		$vs_output .= "jQuery(\"#caReferenceHandlingToRemapToHierBrowserSearch, #caReferenceHandlingMetadata\").attr(\"disabled\", true);\n";
		$vs_output .= "jQuery('#caReferenceHandlingToRemapTo').autocomplete(
				{
					source: ".json_encode($va_service_info['search']).", html: true,
					minLength: 3, delay: 800,
					select: function(event, ui) {
						jQuery('#caReferenceHandlingToRemapToID').val(ui.item.id);
						jQuery('#caReferenceHandlingClear').css('display', 'inline');
					}
				}
			);";

		$vs_output .= "jQuery('#caReferenceToHandlingRemap').click(function() {
			jQuery('#caReferenceHandlingToRemapTo, #caReferenceHandlingMetadata').attr('disabled', false);
			jQuery('#caChildDeletionWarning').hide();
		});
		jQuery('#caReferenceHandlingToDelete').click(function() {
			jQuery('#caReferenceHandlingToRemapTo, #caReferenceHandlingMetadata').attr('disabled', true);
			jQuery('#caChildDeletionWarning').show();
		});
		";
		$vs_output .= "});";
	}
	$vs_output .= "</script>\n";

	TooltipManager::add('#caReferenceHandlingToCount', "<div class='tooltipHead'>"._t('References to this %1', $t_instance->getProperty('NAME_SINGULAR'))."</div>\n".join("\n", $va_reference_to_buf));


	if (sizeof($va_reference_from_buf)) {
		// add autocompleter for remapping
		if ($vn_reference_from_count == 1) {
			$vs_output .= "<h3 id='caReferenceHandlingFromCount'>"._t('This %1 references %2 other item in metadata', $vs_typename, $vn_reference_from_count).". "._t('When deleting this %1:', $vs_typename)."</h3>\n";
		} else {
			$vs_output .= "<h3 id='caReferenceHandlingFromCount'>"._t('This %1 references %2 other items in metadata', $vs_typename, $vn_reference_from_count).". "._t('When deleting this %1:', $vs_typename)."</h3>\n";
		}

		$va_delete_opts = ['value' => 'delete', 'id' => 'caReferenceHandlingFromDelete'];
		$va_remap_opts = ['value' => 'remap', 'id' => 'caReferenceHandlingFromRemap'];
		$va_remap_lookup_opts = ['value' => '', 'size' => 40, 'id' => 'caReferenceHandlingToRemapFrom', 'class' => 'lookupBg'];
		if ($vs_default === 'delete') { 
			$va_delete_opts['checked'] = 1; 
			$va_remap_lookup_opts['disabled'] = 1; 
		} else {
			$va_remap_opts['checked'] = 1; 
		}
		$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingFrom', $va_delete_opts).' '._t('remove these references')."<br/>\n";
		$vs_output .= caHTMLRadioButtonInput('caReferenceHandlingFrom', $va_remap_opts).' '._t('transfer these references and accompanying metadata to').' '.caHTMLTextInput('caReferenceHandlingToRemapFrom', $va_remap_lookup_opts);
		$vs_output .= "<a href='#' class='button' onclick='jQuery(\"#caReferenceHandlingToRemapFromID\").val(\"\"); jQuery(\"#caReferenceHandlingToRemapFrom\").val(\"\"); jQuery(\"#caReferenceHandlingClear\").css(\"display\", \"none\"); return false;' style='display: none;' id='caReferenceHandlingClear'>"._t('Clear').'</a>';
		$vs_output .= caHTMLHiddenInput('caReferenceHandlingToRemapFromID', array('value' => '', 'id' => 'caReferenceHandlingToRemapFromID'));
		$vs_output .= "<script type='text/javascript'>";

		$va_service_info = caJSONLookupServiceUrl($po_request, $t_instance->tableName(), array('noSymbols' => 1, 'noInline' => 1, 'exclude' => (int)$t_instance->getPrimaryKey(), 'table_num' => (int)$t_instance->get('table_num')));
		$vs_output .= "jQuery(document).ready(function() {";
		$vs_output .= "jQuery('#caReferenceHandlingToRemapFrom').autocomplete(
				{
					source: '".$va_service_info['search']."', html: true,
					minLength: 3, delay: 800,
					select: function(event, ui) {
						jQuery('#caReferenceHandlingToRemapFromID').val(ui.item.id);
						jQuery('#caReferenceHandlingClear').css('display', 'inline');
					}
				}
			);";

		$vs_output .= "jQuery('#caReferenceHandlingFromRemap').click(function() {
			jQuery('#caReferenceHandlingToRemapFrom').attr('disabled', false);
		});
		jQuery('#caReferenceHandlingFromDelete').click(function() {
			jQuery('#caReferenceHandlingToRemapFrom').attr('disabled', true);
		});
		";
		$vs_output .= "});";
		$vs_output .= "</script>\n";

		TooltipManager::add('#caReferenceHandlingFromCount', "<div class='tooltipHead'>"._t('References by this %1', $t_instance->getProperty('NAME_SINGULAR'))."</div>\n".join("<br/>\n", $va_reference_from_buf));
	}

	return $vs_output;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caHierarchyToolsMergeWarningBox($request, $t_instance, $item_name, $parameters) {
	$table_name = $t_instance->tableName();
	$config = Configuration::load();
	//$container_types = $config->get("{$table_name}_container_types"); 
	$component_types = $config->get("{$table_name}_component_types"); 
	
	$target_type = mb_strtolower($t_instance->getTypeName($component_types[0], ['useSingular' => false]));
	
	if ($warning = isset($parameters['warning']) ? $parameters['warning'] : null) {
		$warning = '<span class="formLabelWarning"><i class="caIcon fa fa-exclamation-triangle fa-1x"></i> '.$warning.'</span><br/>';
	} else {
		$warning = '<span class="formLabelWarning"><i class="caIcon fa fa-exclamation-triangle fa-1x"></i> '._t('Proceeding will permanently delete all component %1', $target_type).'</span><br/>';
	}
	
	$warn_metadata = false;
	
	$child_ids = $t_instance->getHierarchyChildren(null, ['idsOnly' => true]);
	if(is_array($child_ids) && sizeof($child_ids)) {
		$t_child = Datamodel::getInstance($t_instance->tableName());
		
		foreach($child_ids as $child_id) {
			if($t_child->load($child_id)) {
				if($warn_metadata = $t_child->metadataIsSet()) {
					break;
				}
			}
		}	
	}
	
	if($warn_metadata) {
		$warning .= '<span class="formLabelWarning"><i class="caIcon fa fa-exclamation-triangle fa-1x"></i> '._t("Metadata set on component %1 will be lost", $target_type).'</span><br/>';
	}
	
	$editor_url = caEditorUrl($request, $t_instance->tableName(), $t_instance->getPrimaryKey(), true);

	$output = caFormTag($request, 'merge', 'caHierarchyMergeForm', null, 'post', 'multipart/form-data', '_top', ['noCSRFToken' => false,'disableUnsavedChangesWarning' => true]);
	$output .= "<div class='delete-control-box'>".caFormControlBox(
		"<div class='delete_warning_box'>"._t('Really merge component %1 into %2? ', $target_type, $item_name)."</div>".
		'',
		$warning,
		caFormSubmitButton($request, __CA_NAV_ICON_DELETE__, _t("Merge"), 'caHierarchyMergeForm', []).
		caFormNavButton($request, __CA_NAV_ICON_CANCEL__, _t("Cancel"), '', $editor_url['module'], $editor_url['controller'], $editor_url['action'], [$editor_url['_pk'] => $editor_url['id']])
	)."</div>\n";

	$parameters['t'] = $t_instance->tableName();
	foreach(array_merge($parameters, ['confirm' => 1]) as $f => $v) {
		$output .= caHTMLHiddenInput($f, ['value' => $v]);
	}
	$output .= caHTMLHiddenInput($t_instance->primaryKey(), ['value' => $t_instance->getPrimaryKey()]);
	$output .= "</form>\n";

	return $output;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caHierarchyToolsSplitWarningBox($request, $t_instance, $item_name, $parameters) {
	if(!method_exists($t_instance, 'getRepresentationCount')) { return null; }
	
	$table_name = $t_instance->tableName();
	$config = Configuration::load();
	$container_types = $config->get("{$table_name}_container_types"); 
	$component_types = $config->get("{$table_name}_component_types"); 
	
	$item_type = mb_strtolower($t_instance->getTypeName($component_types[0], ['useSingular' => false]));
	$target_type = mb_strtolower($t_instance->getTypeName($container_types[0], ['useSingular' => true]));
	
	if ($warning = isset($parameters['warning']) ? $parameters['warning'] : null) {
		$warning = '<span class="formLabelWarning"><i class="caIcon fa fa-exclamation-triangle fa-1x"></i> '.$warning.'</span><br/>';;
	} else {
		$warning = '<span class="formLabelWarning"><i class="caIcon fa fa-exclamation-triangle fa-1x"></i> '._t("Proceeding will create %1 new %2", $t_instance->getRepresentationCount(), $item_type).'</span><br/>';
	}

	$editor_url = caEditorUrl($request, $t_instance->tableName(), $t_instance->getPrimaryKey(), true);

	$output = caFormTag($request, 'split', 'caHierarchySplitForm', null, 'post', 'multipart/form-data', '_top', ['noCSRFToken' => false,'disableUnsavedChangesWarning' => true]);
	$output .= "<div class='delete-control-box'>".caFormControlBox(
		"<div class='delete_warning_box'>"._t('Really split %1 into %2? ', $target_type, $item_type)."</div>".
		'',
		$warning,
		caFormSubmitButton($request, __CA_NAV_ICON_DELETE__, _t("Split"), 'caHierarchySplitForm', []).
		caFormNavButton($request, __CA_NAV_ICON_CANCEL__, _t("Cancel"), '', $editor_url['module'], $editor_url['controller'], $editor_url['action'], [$editor_url['_pk'] => $editor_url['id']])
	)."</div>\n";

	$parameters['t'] = $t_instance->tableName();
	foreach(array_merge($parameters, ['confirm' => 1]) as $f => $v) {
		$output .= caHTMLHiddenInput($f, ['value' => $v]);
	}
	$output .= caHTMLHiddenInput($t_instance->primaryKey(), ['value' => $t_instance->getPrimaryKey()]);
	$output .= "</form>\n";

	return $output;
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns HTML <img> tag displaying spinning "I'm doing something" icon
 */
function caBusyIndicatorIcon($po_request, $pa_attributes=null) {
	return caNavIcon(__CA_NAV_ICON_SPINNER__, 1, $pa_attributes);
}
# ------------------------------------------------------------------------------------------------
/**
 * 
 */
function caGetMediaInfoForDisplay($pm_media, $ps_version) {
	$o_coder = (is_a($pm_media, "MediaInfoCoder")) ? $pm_media : new MediaInfoCoder($pm_media);

	$va_ret = [];
	$va_media_info = $o_coder->getMediaInfo();
	$va_dimensions = [];
	if (isset($va_media_info[$ps_version]['WIDTH']) && isset($va_media_info[$ps_version]['HEIGHT'])) {
		if (($vn_w = $va_media_info[$ps_version]['WIDTH']) && ($vn_h = $va_media_info[$ps_version]['WIDTH'])) {
			$va_dimensions[] = $va_media_info[$ps_version]['WIDTH'].'p x '.$va_media_info[$ps_version]['HEIGHT'].'p';
		}
	}
	if (isset($va_media_info[$ps_version]['PROPERTIES']['bitdepth']) && ($vn_depth = $va_media_info[$ps_version]['PROPERTIES']['bitdepth'])) {
		$va_dimensions[] = $va_ret['bitdepth'] = intval($vn_depth).' bpp';
	}
	if (isset($va_media_info[$ps_version]['PROPERTIES']['colorspace']) && ($vs_colorspace = $va_media_info[$ps_version]['PROPERTIES']['colorspace'])) {
		$va_dimensions[] = $va_ret['colorspace'] = $vs_colorspace;
	}
	if (isset($va_media_info[$ps_version]['PROPERTIES']['resolution']) && is_array($va_resolution = $va_media_info[$ps_version]['PROPERTIES']['resolution'])) {
		if (isset($va_resolution['x']) && isset($va_resolution['y']) && $va_resolution['x'] && $va_resolution['y']) {
			// TODO: units for resolution? right now assume pixels per inch
			if ($va_resolution['x'] == $va_resolution['y']) {
				$va_dimensions[] = $va_ret['resolution'] = $va_resolution['x'].'ppi';
			} else {
				$va_dimensions[] = $va_ret['resolution'] = $va_resolution['x'].'x'.$va_resolution['y'].'ppi';
			}
		}
	}
	if (isset($va_media_info[$ps_version]['PROPERTIES']['duration']) && ($vn_duration = $va_media_info[$ps_version]['PROPERTIES']['duration'])) {
		$va_dimensions[] = $va_ret['duration'] = sprintf("%4.1f", $vn_duration).'s';
	}
	if (isset($va_media_info[$ps_version]['PROPERTIES']['pages']) && ($vn_pages = $va_media_info[$ps_version]['PROPERTIES']['pages'])) {
		$va_dimensions[] = $va_ret['pages'] = $vn_pages.' '.(($vn_pages == 1) ? _t('page') : _t('pages'));
	}
	if (!isset($va_media_info[$ps_version]['PROPERTIES']['filesize']) || !($vn_filesize = $va_media_info[$ps_version]['PROPERTIES']['filesize'])) {
		$va_ret['filesize'] = $vn_filesize = @filesize($o_coder->getMediaPath($ps_version));
	}
	if ($vn_filesize) {
		$va_dimensions[] = sprintf("%4.1f", $vn_filesize/(1024*1024)).'mb';
	}
	$va_ret['dimensions'] = join('; ', $va_dimensions);

	$va_ret['MD5'] = $va_media_info[$ps_version]['MD5'];
	$va_ret['mimetype'] = $va_media_info[$ps_version]['MIMETYPE'];
	$va_ret['type'] = Media::getTypenameForMimetype($va_media_info[$ps_version]['MIMETYPE']);
	$va_ret['filename'] = $va_media_info['ORIGINAL_FILENAME'];
	$va_ret['thumbnail'] = $o_coder->getMediaTag('thumbnail');
	$va_ret['info'] = $va_media_info;

	return $va_ret;
}
# ------------------------------------------------------------------------------------------------
/**
 * Formats extracted media metadata for display to user.
 *
 * @param $pa_metadata array - array key'ed by metadata system (eg. EXIF, DPX, IPTC) where values are arrays containing key/value metadata pairs
 *
 * @return string - formated metadata for display to user
 */
function caFormatMediaMetadata($pa_metadata) {
	$vs_buf = "<table>\n";

	$vn_metadata_rows = 0;
	if (is_array($pa_metadata) && sizeof($pa_metadata)) {
		foreach($pa_metadata as $vs_metadata_type => $va_metadata_data) {
			if (isset($va_metadata_data) && is_array($va_metadata_data)) {
				$vs_buf .= "<tr><th>".preg_replace('!^METADATA_!', '', $vs_metadata_type)."</th><th colspan='2'><!-- empty --></th></tr>\n";
				foreach($va_metadata_data as $vs_key => $vm_value) {
					if(!in_array($vs_key, ['FILE', 'IFD0', 'EXIF'])) { continue; }
					$vs_buf .=  "<tr valign='top'><td><!-- empty --></td><td>{$vs_key}</td><td>"._caFormatMediaMetadataArray($vm_value, 0, $vs_key)."</td></tr>\n";
					$vn_metadata_rows++;
				}
			}
		}
	}

	if (!$vn_metadata_rows) {
		$vs_buf .=  "<tr valign='top'><td colspan='3'>"._t('No embedded metadata was extracted from the media')."</td></tr>\n";
	}
	$vs_buf .= "</table>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Formats extracted media metadata for display to user.
 *
 * @param $pa_metadata array - array key'ed by metadata system (eg. EXIF, DPX, IPTC) where values are arrays containing key/value metadata pairs
 *
 * @return string - formated metadata for display to user
 */
function _caFormatMediaMetadataArray($pa_array, $pn_level=0, $ps_key=null, ?array $options=null) {
	if(!is_array($pa_array)) { return $pa_array; }

	$no_table = caGetOption('noTable', $options, false);
	
	$vs_buf = $no_table ? '' : "<div style='width: 100%; overflow: auto;'><table style='margin-left: ".($pn_level * 10)."px;'>";
	foreach($pa_array as $vs_key => $vs_val) {
		if(is_array($vs_val)) {
			$vs_buf .= _caFormatMediaMetadataArray($vs_val, $pn_level++, null, ['noTable' => true]);
			continue;
		}
		if (preg_match("!^Undefined!i", $vs_key)) { continue; }
		$vs_val = preg_replace('![^A-Za-z0-9 \-_\+\!\@\#\$\%\^\&\*\(\)\[\]\{\}\?\<\>\,\.\"\'\=]+!', '', $vs_val);
		switch($vs_key) {
			case 'MakerNote':	// EXIF tags to skip output of
			case 'ImageResourceInformation':
			case 'ImageSourceData':
			case 'ICC_Profile':
				continue(2);
				break;
			case 'FileDateTime':
				$vs_val = caGetLocalizedDate($vs_val, ['timeOmit' => false]);
				break;
		}
		$vs_buf .= "<tr><td width='130'>{$vs_key}</td><td>"._caFormatMediaMetadataArray($vs_val, $pn_level + 1, $vs_key)."</td></tr>";
	}
	$vs_buf .= $no_table ? '' : "</table></div>\n";
	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates next/previous/back-to-results navigation HTML for bundleable editors
 *
 * @param $po_request RequestHTTP The current request
 * @param $po_instance BaseModel An instance containing the currently edited record
 * @param $po_result_context ResultContext The current result content
 * @param $pa_options array An optional array of options. Supported options are:
 *		backText = a string to use as the "back" button text; default is "Results"
 *
 * @return string HTML implementing the navigation element
 */
function caEditorFindResultNavigation($po_request, $po_instance, $po_result_context, $pa_options=null) {
	$vn_item_id 			= $po_instance->getPrimaryKey();
	$vs_pk 					= $po_instance->primaryKey();
	$vs_table_name			= $po_instance->tableName();
	if (($vs_priv_table_name = $vs_table_name) == 'ca_list_items') {
		$vs_priv_table_name = 'ca_lists';
	}

	$va_found_ids 			= $po_result_context->getResultList();
	$vn_current_pos			= $po_result_context->getIndexInResultList($vn_item_id);
	$vn_prev_id 			= $po_result_context->getPreviousID($vn_item_id);
	$vn_next_id 			= $po_result_context->getNextID($vn_item_id);

	if (isset($pa_options['backText']) && $pa_options['backText']) {
		$vs_back_text = $pa_options['backText'];
	} else {
		$vs_back_text = "<span class='resultLink'>"._t('Results')."</span>";
	}

	$vs_buf = '';
	if (is_array($va_found_ids) && sizeof($va_found_ids)) {
		$default_to_summary_view_conf = $po_request->config->getList("{$vs_table_name}_editor_defaults_to_summary_view");
		if(is_array($default_to_summary_view_conf) && sizeof($default_to_summary_view_conf)) {
			$t_table = Datamodel::getInstance($vs_table_name, true);
			$t_ui = ca_editor_uis::loadDefaultUI($vs_table_name, $po_request, $t_table->getTypeID($vn_item_id));
			$default_to_summary_view = $t_ui ? in_array($t_ui->get('editor_code'), $default_to_summary_view_conf, true) : false;
		} else {
			$default_to_summary_view = (bool)$po_request->config->get("{$vs_table_name}_editor_defaults_to_summary_view");
		}
		if ($vn_prev_id > 0) {
			if(
				$po_request->user->canAccess($po_request->getModulePath(),$po_request->getController(),"Edit",array($vs_pk => $vn_prev_id))
				&&
				!$default_to_summary_view
			){
				$vs_buf .= caNavLink($po_request, caNavIcon(__CA_NAV_ICON_SCROLL_LT__, 2), 'prev record', $po_request->getModulePath(), $po_request->getController(), 'Edit'.'/'.$po_request->getActionExtra(), array($vs_pk => $vn_prev_id)).'&nbsp;';
			} else {
				$vs_buf .= caNavLink($po_request, caNavIcon(__CA_NAV_ICON_SCROLL_LT__, 2), 'prev record', $po_request->getModulePath(), $po_request->getController(), 'Summary', array($vs_pk => $vn_prev_id)).'&nbsp;';
			}
			TooltipManager::add(".prev.record", "Previous"); 
		} else {
			$vs_buf .=  '<span class="prev disabled">'.caNavIcon(__CA_NAV_ICON_SCROLL_LT__, 2).'</span>';
		}

		$vs_buf .= "<span class='resultCount'>".ResultContext::getResultsLinkForLastFind($po_request, $vs_table_name,  $vs_back_text, ''). " (".($vn_current_pos)."/".sizeof($va_found_ids).")</span>";

		if (!$vn_next_id && sizeof($va_found_ids)) { $vn_next_id = $va_found_ids[0]; }
		if ($vn_next_id > 0) {
			if(
				$po_request->user->canAccess($po_request->getModulePath(),$po_request->getController(),"Edit",array($vs_pk => $vn_next_id))
				&&
				!$default_to_summary_view
			){
				$vs_buf .= '&nbsp;'.caNavLink($po_request,caNavIcon(__CA_NAV_ICON_SCROLL_RT__, 2), 'next record', $po_request->getModulePath(), $po_request->getController(), 'Edit'.'/'.$po_request->getActionExtra(), array($vs_pk => $vn_next_id));
			} else {
				$vs_buf .= '&nbsp;'.caNavLink($po_request,caNavIcon(__CA_NAV_ICON_SCROLL_RT__, 2), 'next record', $po_request->getModulePath(), $po_request->getController(), 'Summary', array($vs_pk => $vn_next_id));
			}
			TooltipManager::add(".next.record", "Next");
		} else {
			$vs_buf .=  '<span class="next disabled">'.caNavIcon(__CA_NAV_ICON_SCROLL_RT__, 2).'</span>';
		}
	} elseif ($vn_item_id) {
		$vs_buf .= "<span class='resultCount'>".ResultContext::getResultsLinkForLastFind($po_request, $vs_table_name,  $vs_back_text, '')."</span>";
	}

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param array $pa_bundle_list
 * @param array $pa_options Optional array of options. Supported options are:
 *		NONE
 *
 * @return string
 */
function caSetupEditorScreenOverlays($po_request, $pt_subject, $pa_bundle_list, $pa_options=null) {
	$vs_buf = '';
	if ($pt_subject && $pt_subject->isHierarchical()) {
		$vs_buf .= caEditorHierarchyOverview($po_request, $pt_subject->tableName(), $pt_subject->getPrimaryKey(), $pa_options);
	}
	$vs_buf .= caEditorFieldList($po_request, $pt_subject, $pa_bundle_list, $pa_options);

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * 
 *
 * @param array $pa_bundle_list 
 * @param array $pa_options Optional array of options. Supported options are:
 *		NONE
 *
 * @return string
 */
function caEditorFieldList($po_request, $pt_subject, $pa_bundle_list, $pa_options=null) {
	$vs_buf = "<script type=\"text/javascript\">
	jQuery(document).ready(function() {
		jQuery(document).on('keydown.ctrl_f', function() {
			caHierarchyOverviewPanel.hidePanel({dontCloseMask:1});
			caEditorFieldList.onOpenCallback = function(){
				var selector = '#' + caEditorFieldList.panelID + ' a.editorFieldListLink:link';
				jQuery(selector).first().focus();
			};
			caEditorFieldList.showPanel();
		});
		jQuery('#editorFieldListContentArea').html(jQuery(\"#editorFieldListHTML\").html());
		jQuery('#editorFieldListContentArea a').click(function() {
			caEditorFieldList.hidePanel();
		});

		if (typeof caBundleVisibilityManager !== 'undefined') { caBundleVisibilityManager.setAll(); }
		if (typeof caBundleUpdateManager !== 'undefined') { caBundleUpdateManager = caUI.initBundleUpdateManager({url:'".caNavUrl($po_request, '*', '*', 'reload')."', screen:'".$po_request->getActionExtra()."', key:'".$pt_subject->primaryKey()."', id: ".(int)$pt_subject->getPrimaryKey()."}); }
		caBundleUpdateManager.registerBundles(".json_encode($pa_bundle_list).");
	});
</script>
<div id=\"editorFieldListHTML\">";
	if (is_array($pa_bundle_list)) { 
		foreach($pa_bundle_list as $vs_anchor => $va_info) {
			$vs_buf .= "<a href=\"#\" onclick=\"jQuery.scrollTo('a[name={$vs_anchor}]', {duration: 350, offset: -80 , onAfter : function(selector, data){jQuery(selector).parent('.bundleLabel').find('a:link').first().focus();}}); return false;\" class=\"editorFieldListLink\">".$va_info['name']."</a><br/>";
		}
	}
	$vs_buf .= "</div>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param array $pa_bundle_list
 * @param array $pa_options Optional array of options. Supported options are:
 *		NONE
 *
 * @return string
 */
function caEditorHierarchyOverview($po_request, $ps_table, $pn_id, $pa_options=null) {
	$t_subject = Datamodel::getInstanceByTableName($ps_table, true);
	$vs_buf = "<script type=\"text/javascript\">
	jQuery(document).ready(function() {
		jQuery(document).on('keydown.ctrl_h', function() {
			caEditorFieldList.hidePanel({dontCloseMask:1});

			var url;
			if (jQuery('#caHierarchyOverviewContentArea').html().length == 0) {
				url = '".caNavUrl($po_request, $po_request->getModulePath(), $po_request->getController(), 'getHierarchyForDisplay', array($t_subject->primaryKey() => $pn_id))."';
			}
			caHierarchyOverviewPanel.showPanel(url, null, false);
		});
		jQuery('#caHierarchyOverviewContentArea').html('');
		jQuery('#caHierarchyOverviewContentArea a').click(function() {
			caHierarchyOverviewPanel.hidePanel();
		});
	});
</script>
\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates standard-format inspector panels for editors
 *
 * @param View $po_view Inspector view object
 * @param array $pa_options Optional array of options. Supported options are:
 *		backText = a string to use as the "back" button text; default is "Results"
 *
 * @return string HTML implementing the inspector
 */
function caEditorInspector($view, $options=null) {
	$style              = null;
	$idno               = null;
	$num_objects		= 0;

	$t_item 				= $view->getVar('t_item');
	$table_name = $t_item->tableName();
	if (($priv_table_name = $table_name) == 'ca_list_items') {
		$priv_table_name = 'ca_lists';
		$style = "style='padding-top:10px;'";
	}
	
	$components_tools = [];
	$component_count = 0;

	$item_id 			= $t_item->getPrimaryKey();
	$o_result_context		= $view->getVar('result_context');
	$t_ui 					= $view->getVar('t_ui');
	$t_type 				= method_exists($t_item, "getTypeInstance") ? $t_item->getTypeInstance() : null;
	$type_name			= method_exists($t_item, "getTypeName") ? $t_item->getTypeName() : '';
	if (!$type_name) { $type_name = $t_item->getProperty('NAME_SINGULAR'); }

	if (!is_array($va_reps = $view->getVar('representations'))) { $va_reps = []; }

	if ($t_item->isHierarchical()) {
		$ancestors 		= $view->getVar('ancestors');
		$parent_id		= $t_item->get($t_item->getProperty('HIERARCHY_PARENT_ID_FLD'));
	} else {
		$ancestors = array();
		$parent_id = null;
	}

	// action extra to preserve currently open screen across next/previous links
	$buf = '';
	if (($item_id) || ($view->request->getAction() === 'Delete')) {
		$buf = "<h3 class='nextPrevious' {$style}>".caEditorFindResultNavigation($view->request, $t_item, $o_result_context, $options)."</h3>\n";
	}

	$color = null;
	if ($t_type) { $color = trim($t_type->get('color')); }
	if (!$color && $t_ui) { $color = trim($t_ui->get('color')); }
	if (!$color) { $color = "FFFFFF"; }

	$buf .= "<h4><div id='caColorbox' style='border: 6px solid #{$color};'>\n";

	$icon = null;
	if ($t_type) { $icon = $t_type->getMediaTag('icon', 'icon'); }
	if (!$icon && $t_ui) { $icon = $t_ui->getMediaTag('icon', 'icon'); }

	if ($icon){
		$buf .= "<div id='inspectoricon' style='border-right: 6px solid #{$color}; border-bottom: 6px solid #{$color}; -moz-border-radius-bottomright: 8px; -webkit-border-bottom-right-radius: 8px;'>\n{$icon}</div>\n";
	}

	if (($view->request->getAction() === 'Delete') && ($view->request->getParameter('confirm', pInteger))) { 
		$buf .= "<strong>"._t("Deleted %1", $type_name)."</strong>\n";
		$buf .= "<br style='clear: both;'/></div></h4>\n";
	} else {
		if ($item_id) {
			if(!$view->request->config->get("{$priv_table_name}_inspector_disable_headline")) {
				if(!method_exists($t_item, 'isSaveable') || $t_item->isSaveable($view->request)) {
					$buf .= "<strong>"._t("Editing %1", $type_name).": </strong>\n";
				}else{
					$buf .= "<strong>"._t("Viewing %1", $type_name).": </strong>\n";
				}
			}

			if ($t_item->hasField('is_deaccessioned') && $t_item->get('is_deaccessioned') && ($t_item->get('deaccession_date', array('getDirectDate' => true)) <= caDateToHistoricTimestamp(_t('now')))) {
				// If currently deaccessioned then display deaccession message
				$buf .= "<br/><div class='inspectorDeaccessioned'>"._t('Deaccessioned %1', $t_item->get('deaccession_date'))."</div>\n";
				if ($vs_deaccession_notes = $t_item->get('deaccession_notes')) { TooltipManager::add(".inspectorDeaccessioned", $vs_deaccession_notes); }
			} else {
				if (($table_name === 'ca_storage_locations') || $view->request->user->canDoAction('can_see_current_location_in_inspector_'.$table_name)) {
					$is_home = $t_item->isInHomeLocation();
					$inspector_current_value = null;
					if (method_exists($t_item, "getHistory") && ($inspector_current_value_label = $t_item->getInspectorHistoryTrackingDisplayPolicy('label'))) {
						if ($inspector_current_value = $t_item->getCurrentValueForDisplay()) { $buf .= "<div class='inspectorCurrentLocation'><strong>{$inspector_current_value_label}:</strong><br/>{$inspector_current_value}".(($is_home) ? ' '._t('[HOME]') : '')."</div>"; }
					}

					if ((!$is_home || !$inspector_current_value) && caHomeLocationsEnabled($t_item->tableName(), $t_item->getTypeCode()) && ($home_location_id = $t_item->get('home_location_id')) && ($t_home_loc = ca_storage_locations::find($home_location_id, ['returnAs' => 'firstModelInstance']))) {
						if (!($template = $view->request->config->get('inspector_home_location_display_template'))) { $template = '^ca_storage_locations.hierarchy.preferred_labels.name%delimiter=_➜_'; }
						$buf .= "<div class='inspectorCurrentLocation'><strong>"._t('Home location').":</strong><br/>".$t_home_loc->getWithTemplate($template)."</div>"; 
					}
				}
			}

			//
			// Display flags; expressions for these are defined in app.conf in the <table_name>_inspector_display_flags directive
			//
			if (is_array($va_display_flags = $view->request->config->getAssoc("{$table_name}_inspector_display_flags"))) {
				$display_flag_buf = array();
				foreach($va_display_flags as $vs_exp => $vs_display_flag) {
					$exp_vars = array();
					foreach(ExpressionParser::getVariableList($vs_exp) as $vs_var_name) {
						$exp_vars[$vs_var_name] = $t_item->get($vs_var_name, array('convertCodesToIdno' => true));
					}

					if (ExpressionParser::evaluate($vs_exp, $exp_vars)) {
						$display_flag_buf[] = $t_item->getWithTemplate("{$vs_display_flag}");
					}
				}

				if(!($vs_display_flag_delim = $view->request->config->get("{$table_name}_inspector_display_flags_delimiter"))) {
					$vs_display_flag_delim = '; ';
				}
				if (sizeof($display_flag_buf) > 0) { $buf .= join($vs_display_flag_delim, $display_flag_buf); }
			}

			$label = '';
			$dont_use_labels_for_ca_objects = (bool)$t_item->getAppConfig()->get('ca_objects_dont_use_labels');
			if(!(($table_name === 'ca_objects') && $dont_use_labels_for_ca_objects)){
				if ($vs_get_spec = $view->request->config->get("{$table_name}_inspector_display_title")) {
					$label = caProcessTemplateForIDs($vs_get_spec, $table_name, array($t_item->getPrimaryKey()));
				} else {
					$va_object_collection_collection_ancestors = $view->getVar('object_collection_collection_ancestors');
					if (
						($t_item->tableName() == 'ca_objects') &&
						$t_item->getAppConfig()->get('ca_objects_x_collections_hierarchy_enabled') &&
						is_array($va_object_collection_collection_ancestors) && sizeof($va_object_collection_collection_ancestors)
					) {
						$va_collection_links = array();
						foreach($va_object_collection_collection_ancestors as $va_collection_ancestor) {
							$va_collection_links[] = caEditorLink($view->request, $va_collection_ancestor['label'], '', 'ca_collections', $va_collection_ancestor['collection_id']);
						}
						$label .= join(" / ", $va_collection_links).' &gt; ';
					}

					if (method_exists($t_item, 'getLabelForDisplay') && ($t_item->getLabelTableInstance())) {
						$vn_parent_index = (sizeof($ancestors) - 1);
						if ($parent_id && (($table_name != 'ca_places') || ($vn_parent_index > 0))) {
							$va_parent = $ancestors[$vn_parent_index];
							$vs_disp_fld = $t_item->getLabelDisplayField();

							if ($va_parent['NODE'][$vs_disp_fld] && ($vs_editor_link = caEditorLink($view->request, $va_parent['NODE'][$vs_disp_fld], '', $table_name, $va_parent['NODE'][$t_item->primaryKey()]))) {
								$label .= $vs_editor_link.' &gt; '.$t_item->getLabelForDisplay();
							} else {
								$label .= ($va_parent['NODE'][$vs_disp_fld] ? $va_parent['NODE'][$vs_disp_fld].' &gt; ' : '').$t_item->getLabelForDisplay();
							}
						} else {
							$label .= $t_item->getLabelForDisplay();
							if (($table_name === 'ca_editor_uis') && (in_array($view->request->getAction(), array('EditScreen', 'DeleteScreen', 'SaveScreen')))) {
								$t_screen = new ca_editor_ui_screens($view->request->getParameter('screen_id', pInteger));
								if (!($vs_screen_name = $t_screen->getLabelForDisplay())) {
									$vs_screen_name = _t('new screen');
								}
								$label .= " &gt; ".$vs_screen_name;
							}

						}
					} else {
						$label .= $t_item->hasField('name') ? $t_item->get('name') : $t_item->get(array_shift($t_item->getProperty('LIST_FIELDS')));
					}
				}
			}

			$show_idno = (!$view->request->config->get("{$table_name}_inspector_dont_display_idno")) && (bool)($idno = $t_item->get($t_item->getProperty('ID_NUMBERING_ID_FIELD')));

			if (!$label) {
				switch($table_name) {
					default:
						if (($table_name === 'ca_objects') && $dont_use_labels_for_ca_objects) {
							$label = $idno;
							$show_idno = false;
						} else {
							$label =  '['.caGetBlankLabelText($table_name).']';
						}
						break;
				}
			}

			$buf .= "<div class='recordTitle {$table_name}' style='width:190px; overflow:hidden;'>{$label}";
			
			if($show_idno) {
				$buf .= " ({$idno}) ";
			}
			$buf .= "</div>";
			if (($table_name === 'ca_object_lots') && $t_item->getPrimaryKey()) {
				$buf .= "<div id='inspectorLotMediaDownload'><strong>".((($num_objects = $t_item->numObjects(null, ['excludeChildObjects' => $view->request->config->get("exclude_child_objects_in_inspector_log_count")])) == 1) ? _t('Lot contains %1 object', $num_objects) : _t('Lot contains %1 objects', $num_objects))."</strong>\n";
			}
			if ($view->request->config->get("include_custom_inspector")) {
				if(file_exists($view->request->getViewsDirectoryPath()."/bundles/inspector_info.php")) {
					$inspector_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
					$inspector_view->setVar('t_item', $t_item);
					$buf .= $inspector_view->render('inspector_info.php');
				}
			}	
		} else {
			$parent_name = '';
			if ($parent_id = $view->request->getParameter('parent_id', pInteger)) {
				$t_parent = clone $t_item;
				$t_parent->load($parent_id);
				$parent_name = $t_parent->getLabelForDisplay();
			}
			$buf .= "<div class='creatingNew'>"._t("Creating new %1", $type_name)." ".($parent_name ?  _t("%1 &gt; New %2", $parent_name, $type_name) : '')."</div>\n";
			$buf .= "<br/>\n";
		}

	// -------------------------------------------------------------------------------------

	if($t_item->getPrimaryKey()) {
		if (is_array($va_reps) && (sizeof($va_reps) > 0)) {	
			$imgs = array();

			$buf .= "<div id='inspectorMedia'>";

			$vn_r = $vn_primary_index = 0;
			foreach($va_reps as $va_rep) {
				if (!($va_rep['info']['preview170']['WIDTH'] && $va_rep['info']['preview170']['HEIGHT'])) { continue; }

				if ($is_primary = (isset($va_rep['is_primary']) && (bool)$va_rep['is_primary'])) {
					$vn_primary_index = $vn_r;
				}

				$imgs[] = "{url:'".$va_rep['urls']['preview170']."', width: ".$va_rep['info']['preview170']['WIDTH'].", height: ".
				$va_rep['info']['preview170']['HEIGHT'].", link: '#', onclick:  'caMediaPanel.showPanel(\'".
				caNavUrl($view->request, '*', '*', 'GetMediaOverlay', array($t_item->primaryKey() => $item_id, 'representation_id' => $va_rep['representation_id']))."\')'}";

				$vn_r++;
			}

				if (sizeof($va_reps) > 1) {
					$buf .= "
				<div class='leftScroll'>
					<a href='#' onclick='inspectorInfoRepScroller.scrollToPreviousImage(); return false;'>".caNavIcon(__CA_NAV_ICON_SCROLL_LT__, '16px')."</a>
				</div>
	";
				}

			if (sizeof($imgs) > 0) {
				$buf .= "
			<div id='inspectorInfoRepScrollingViewer' style='position: relative;'>
				<div id='inspectorInfoRepScrollingViewerContainer'>
					<div id='inspectorInfoRepScrollingViewerImageContainer'></div>
				</div>
			</div>
	";
				if (sizeof($va_reps) > 1) {
					$buf .= "
				<div class='rightScroll'>
					<a href='#' onclick='inspectorInfoRepScroller.scrollToNextImage(); return false;'>".caNavIcon(__CA_NAV_ICON_SCROLL_RT__, '16px')."</a>
				</div>
	";
				}
				TooltipManager::add(".leftScroll", _t('Previous Image'));
				TooltipManager::add(".rightScroll", _t('Next Image'));

				$buf .= "<script type='text/javascript'>";
				$buf .= "
				var inspectorInfoRepScroller = caUI.initImageScroller([".join(",", $imgs)."], 'inspectorInfoRepScrollingViewerImageContainer', {
						containerWidth: 170, containerHeight: 170,
						imageCounterID: 'inspectorInfoRepScrollingViewerCounter',
						scrollingImageClass: 'inspectorInfoRepScrollerImage',
						scrollingImagePrefixID: 'inspectorInfoRep',
						initialIndex: {$vn_primary_index}

				});
			</script>";

			}
			$buf .= "</div>\n";

			if ($vs_get_spec = $view->request->config->get("{$table_name}_inspector_display_below_media")) {
				$buf .= caProcessTemplateForIDs($vs_get_spec, $table_name, array($t_item->getPrimaryKey()));
			}
		}

		//
		// Output configurable additional info from config, if set
		//

		if ($additional_info = $view->request->config->get("{$table_name}_inspector_additional_info")) {
			if(is_array($additional_info)){
				$buf .= "<br/>";
				foreach($additional_info as $vs_info){
					$buf .= caProcessTemplateForIDs($vs_info, $table_name, array($t_item->getPrimaryKey()), ['requireLinkTags' => true])."<br/>\n";
				}
			} else {
				$buf .= "<br/>".caProcessTemplateForIDs($additional_info, $table_name, array($t_item->getPrimaryKey()), ['requireLinkTags' => true])."<br/>\n";
			}
		}

		$tools = [];
		if ($item_id) {
			# --- watch this link
			$vs_watch = "";
			if (in_array($table_name, array('ca_objects', 'ca_object_lots', 'ca_entities', 'ca_places', 'ca_occurrences', 'ca_collections', 'ca_storage_locations'))) {
				$t_watch_list = new ca_watch_list();
				$watch = "<div class='watchThis inspectorActionButton'><div><a href='#' title='"._t('Add/remove item to/from watch list.')."' onclick='caToggleItemWatch(); return false;' id='caWatchItemButton'>".caNavIcon($t_watch_list->isItemWatched($item_id, $t_item->tableNum(), $view->request->user->get("user_id")) ? __CA_NAV_ICON_UNWATCH__ : __CA_NAV_ICON_WATCH__, '20px')."</a></div></div>";

				$tools[] = "{$watch}\n<script type='text/javascript'>
	function caToggleItemWatch() {
		var url = '".caNavUrl($view->request, $view->request->getModulePath(), $view->request->getController(), 'toggleWatch', array($t_item->primaryKey() => $item_id))."';

		jQuery.getJSON(url, {'csrfToken': ".json_encode(caGenerateCSRFToken($view->request))."}, function(data, status) {
			if (data['status'] == 'ok') {
				jQuery('#caWatchItemButton').html((data['state'] == 'watched') ? '".addslashes(caNavIcon(__CA_NAV_ICON_UNWATCH__, '20px'))."' : '".addslashes(caNavIcon(__CA_NAV_ICON_WATCH__, '20px'))."');
			} else {
				console.log('Error toggling watch status for item: ' + data['errors']);
			}
		});
	}
	</script>\n";
				TooltipManager::add("#caWatchItemButton", _t('Watch/Unwatch this record'));
			}

			if ($view->request->user->canDoAction("can_change_type_{$table_name}") && (sizeof($t_item->getTypeList()) >= 1)) {
				$tools[] = "<div id='inspectorChangeType' class='inspectorActionButton'><div id='inspectorChangeTypeButton'><a href='#' onclick='caTypeChangePanel.showPanel(); return false;'>".caNavIcon(__CA_NAV_ICON_CHANGE__, '20px', array('title' => _t('Change type')))."</a></div></div>\n";

				$change_type_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
				$change_type_view->setVar('t_item', $t_item);

				FooterManager::add($change_type_view->render("change_type_html.php"));
				TooltipManager::add("#inspectorChangeType", _t('Change Record Type'));
			}

			if ($t_item->getPrimaryKey() && $view->request->config->get("{$table_name}_show_add_child_control_in_inspector")) {
				$show_add_child_control = true;
				if (is_array($va_restrict_add_child_control_to_types = $view->request->config->getList($table_name.'_restrict_child_control_in_inspector_to_types')) && sizeof($va_restrict_add_child_control_to_types)) {
					$t_type_instance = $t_item->getTypeInstance();
					if (!in_array($t_type_instance->get('idno'), $va_restrict_add_child_control_to_types) && !in_array($t_type_instance->getPrimaryKey(), $va_restrict_add_child_control_to_types)) {
						$show_add_child_control = false;
					}
				}
				if ($show_add_child_control) {
					if ((bool)$view->request->config->get($table_name.'_enforce_strict_type_hierarchy')) {
						// strict menu
						$vs_type_list = $t_item->getTypeListAsHTMLFormElement('type_id', array('style' => 'width: 90px; font-size: 9px;'), array('childrenOfCurrentTypeOnly' => true, 'directChildrenOnly' => ($view->request->config->get($table_name.'_enforce_strict_type_hierarchy') == '~') ? false : true, 'returnHierarchyLevels' => true, 'access' => __CA_BUNDLE_ACCESS_EDIT__));
					} else {
						// all types
						$vs_type_list = $t_item->getTypeListAsHTMLFormElement('type_id', array('style' => 'width: 90px; font-size: 9px;'), array('access' => __CA_BUNDLE_ACCESS_EDIT__));
					}

					if ($vs_type_list) {
						$tools[] = "<div id='inspectorCreateChild' class='inspectorActionButton'><div id='inspectorCreateChildButton'><a href='#' onclick='caCreateChildPanel.showPanel(); return false;'>".caNavIcon(__CA_NAV_ICON_CHILD__, '20px', array('title' => _t('Create Child Record')))."</a></div></div>\n";

						$create_child_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
						$create_child_view->setVar('t_item', $t_item);
						$create_child_view->setVar('type_list', $vs_type_list);

						FooterManager::add($create_child_view->render("create_child_html.php"));
						TooltipManager::add("#inspectorCreateChildButton", _t('Create a child record under this one'));
					}
				}
			}
		}

		if($view->request->user->canDoAction("can_duplicate_{$table_name}") && $t_item->getPrimaryKey()) {
			$tools[] = "<div id='caDuplicateItemButton' class='inspectorActionButton'>".
							caFormTag($view->request, 'Edit', 'DuplicateItemForm', $view->request->getModulePath().'/'.$view->request->getController(), 'post', 'multipart/form-data', '_top', ['noCSRFToken' => false, 'disableUnsavedChangesWarning' => true, 'noTimestamp' => true]).
							"<div>".caFormSubmitLink($view->request, caNavIcon(__CA_NAV_ICON_DUPLICATE__, '20px'), '', 'DuplicateItemForm', null, ['aria-label' => _t('Duplicate item')])."</div>".
							caHTMLHiddenInput($t_item->primaryKey(), array('value' => $t_item->getPrimaryKey())).
							caHTMLHiddenInput('mode', array('value' => 'dupe')).
						"	</form>
						</div>\n";

			TooltipManager::add("#caDuplicateItemButton", _t('Duplicate this %1', mb_strtolower($type_name, 'UTF-8')));
		}

		if (method_exists($t_item, 'getTypeCode') && caHomeLocationsEnabled($t_item->tableName(), $t_item->getTypeCode()) && $view->request->user->canDoAction("can_set_home_location_".$table_name)) {	
			$tools[] = "<div id='inspectorSetHomeLocation' class='inspectorActionButton'><div id='inspectorSetHomeLocationButton'><a href='#' onclick='_initSetHomeLocationHierarchyBrowser(); return false;'>".caNavIcon(__CA_NAV_ICON_HOME__, '20px', array('title' => _t('Set home location')))."</a></div></div>\n";

			$change_type_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
			$change_type_view->setVar('t_item', $t_item);

			FooterManager::add($change_type_view->render("set_home_location_html.php"));
			TooltipManager::add("#inspectorSetHomeLocation", _t('Set home location'));
		}

		// Download media in lot ($num_objects is only set for object lots)
		if ($num_objects > 0) {
			$tools[] = "<div id='inspectorLotMediaDownloadButton' class='inspectorActionButton'>".caNavLink($view->request, caNavIcon(__CA_NAV_ICON_DOWNLOAD__, '20px'), "button", $view->request->getModulePath(), $view->request->getController(), 'getLotMedia', array('lot_id' => $t_item->getPrimaryKey(), 'download' => 1), array())."</div>\n";
			TooltipManager::add('#inspectorLotMediaDownloadButton', _t("Download all media associated with objects in this lot"));
		}
		
		// Download media in set
		if(($table_name == 'ca_sets') && (sizeof($t_item->getItemRowIDs() ?? []) > 0)) {
			$tools [] = "<div id='inspectorSetMediaDownloadButton' class='inspectorActionButton'>".caNavLink($view->request, caNavIcon(__CA_NAV_ICON_DOWNLOAD__, '20px'), "button", $view->request->getModulePath(), $view->request->getController(), 'getSetMedia', array('set_id' => $t_item->getPrimaryKey(), 'download' => 1), array())."</div>\n";

			TooltipManager::add('#inspectorSetMediaDownloadButton', _t("Download all media associated with records in this set"));
		}
	
		// Auto-delete set?
		if(($table_name == 'ca_sets') && ($view->request->user->getPreference('autodelete_sets') === 'yes')) {
			$autodelete = "<div class='inspectorActionButton'><div><a href='#' title='"._t('Set auto-deletion of set.')."' onclick='caToggleAutoDelete(); return false;' id='inspectorSetAutoDeleteButton'>".caNavIcon($t_item->willAutoDelete() ? __CA_NAV_ICON_AUTO_DELETE__ : __CA_NAV_ICON_NO_AUTO_DELETE__, '20px')."</a></div></div>";

				$tools[] = "{$autodelete}\n<script type='text/javascript'>
	function caToggleAutoDelete() {
		var url = '".caNavUrl($view->request, $view->request->getModulePath(), $view->request->getController(), 'toggleAutoDelete', [$t_item->primaryKey() => $item_id])."';

		jQuery.getJSON(url, {'csrfToken': ".json_encode(caGenerateCSRFToken($view->request))."}, function(data, status) {
			if (data['status'] == 'ok') {
				jQuery('#inspectorSetAutoDeleteButton').html((data['state'] == 'autodelete') ? ".json_encode(caNavIcon(__CA_NAV_ICON_AUTO_DELETE__, '20px'))." : ".json_encode(caNavIcon(__CA_NAV_ICON_NO_AUTO_DELETE__, '20px')).");
				jQuery('#inspectorAutoDeleteMessage').html(data['message'] ?? '');
			} else {
				console.log('Error toggling autodelete status for item: ' + data['errors']);
			}
		});
	}
	</script>\n";
			TooltipManager::add('#inspectorSetAutoDeleteButton', _t("Auto-delete set when older than %1 days?", 10));
		}


		$more_info = '';

		// list of sets in which item is a member
		$t_set = new ca_sets();
		if (is_array($va_sets = caExtractValuesByUserLocale($t_set->getSetsForItem($t_item->tableNum(), $t_item->getPrimaryKey(), array('user_id' => $view->request->getUserID(), 'access' => __CA_SET_READ_ACCESS__)))) && sizeof($va_sets)) {
			$va_links = array();

			$last_set_id = Session::getVar('last_set_id');
			foreach($va_sets as $vn_set_id => $va_set) {
				$class = ($last_set_id == $vn_set_id) ? "class='currentSet'" : "";
				$va_links[] = "<a {$class} href='".caEditorUrl($view->request, 'ca_sets', $vn_set_id)."'>".$va_set['name']."</a>";
			}
			$more_info .= "<div><strong>".((sizeof($va_links) == 1) ? _t("In set") : _t("In sets"))."</strong> ".join(", ", $va_links)."</div>\n";
		}
		
		if(($table_name === 'ca_sets') && ($view->request->user->getPreference('autodelete_sets'))) {
			$autodelete_info = ca_sets::getAutoDeleteInfo($t_item, $view->request->user);
			$msg = $autodelete_info['message'] ?? null;
			$more_info .= "<div id='inspectorAutoDeleteMessage' class='inspectorAutodeleteSet'>{$msg}</div>\n";
		}
		
		$creation = $t_item->getCreationTimestamp();
		$last_change = $t_item->getLastChangeTimestamp();

		if ($creation['timestamp'] || $last_change['timestamp']) {
			$more_info .= "<div class='inspectorChangeDateList'>";

			if($creation['timestamp']) {
				if (!trim($vs_name = $creation['fname'].' '.$creation['lname'])) { $vs_name = null; }
				$vs_interval = (($vn_t = (time() - $creation['timestamp'])) == 0) ? _t('Just now') : _t('%1 ago', caFormatInterval($vn_t , 2));

				$more_info .= "<div class='inspectorChangeDateListLine'  id='caInspectorCreationDate'>".
					($vs_name ? _t('<strong>Created</strong><br/>%1 by %2', $vs_interval, $vs_name) : _t('<strong>Created</strong><br/>%1', $vs_interval)).
					"</div>";

				TooltipManager::add("#caInspectorCreationDate", "<div class='tooltipHead'>"._t('Created on')."</div>"._t('Created on %1', caGetLocalizedDate($creation['timestamp'], array('dateFormat' => 'delimited'))));
			}

			if ($last_change['timestamp'] && ($creation['timestamp'] != $last_change['timestamp'])) {
				if (!trim($vs_name = $last_change['fname'].' '.$last_change['lname'])) { $vs_name = null; }
				$vs_interval = (($vn_t = (time() - $last_change['timestamp'])) == 0) ? _t('Just now') : _t('%1 ago', caFormatInterval($vn_t , 2));

				$more_info .= "<div class='inspectorChangeDateListLine' id='caInspectorChangeDate'>".
					($vs_name ? _t('<strong>Last changed</strong><br/>%1 by %2', $vs_interval, $vs_name) : _t('<strong>Last changed</strong><br/>%1', $vs_interval)).
					"</div>";

				TooltipManager::add("#caInspectorChangeDate", "<div class='tooltipHead'>"._t('Last changed on')."</div>"._t('Last changed on %1', caGetLocalizedDate($last_change['timestamp'], array('dateFormat' => 'delimited'))));
			}

			if (method_exists($t_item, 'getMetadataDictionaryRuleViolations') && is_array($violations = $t_item->getMetadataDictionaryRuleViolations()) && (($total_num_violations = (sizeof($violations))) > 0)) {
				$screen_id = $view->request->getActionExtra();
				if (!$screen_id || !is_array($violations_for_current_screen = $t_item->getMetadataDictionaryRuleViolations(null, ['screen_id' => $screen_id]))) { $violations_for_current_screen = []; }
				$total_num_violations_for_current_screen = sizeof($violations_for_current_screen);

				$violation_messages = [];
				foreach($violations as $violation_id => $violation) {
					$label = $t_item->getDisplayLabel($violation['bundle_name']);
					$screen = '';
					$in_this_tab = false;
					if (isset($violations_for_current_screen[$violation_id])) {
						$screen = _t('(on this tab)');
						$in_this_tab = true;
					} else {
						$b = explode(".", $violation['bundle_name']);
						$bundle = array_pop($b);
							
						$placements = $t_ui->getPlacementsForBundle($violation['bundle_name'], $view->request, []);
						if (!is_array($placements) || !sizeof($placements)) {
							$placements = $t_ui->getPlacementsForBundle("ca_attribute_{$bundle}", $view->request, []);
							if (!is_array($placements) || !sizeof($placements)) {
								$placements = $t_ui->getPlacementsForBundle($bundle, $view->request, []);
							}
						}
						if(is_array($placements)) {
							$placement = array_shift($placements);
							$screen = _t("(on tab <em>%1</em>)", $placement['screen_label']);
						}
					}
					$violation_messages[] = "<li ".($in_this_tab ? "class='caMetadataDictionaryViolationInCurrentTab'" : "class='caMetadataDictionaryViolationNotInCurrentTab'")."><em><u>{$label}</u></em> ".caExtractSettingsValueByUserLocale('violationMessage', $violation)." {$screen}</li>";
				}

				$num_violations_display = null;
				if($total_num_violations_for_current_screen > 0) {
					if ($total_num_violations_for_current_screen != $total_num_violations) {
						$more_info .= "<div id='caInspectorViolationsList'>".caNavIcon(__CA_NAV_ICON_ALERT__, "14px")." ".($num_violations_display = (($total_num_violations_for_current_screen > 1) ? _t('%1 problems on this tab require attention (of %2 total)', $total_num_violations_for_current_screen, $total_num_violations) : _t('%1 problem on this tab requires attention (of %2 total)', $total_num_violations_for_current_screen, $total_num_violations)))."</div>\n"; 
					} else {
						$more_info .= "<div id='caInspectorViolationsList'>".caNavIcon(__CA_NAV_ICON_ALERT__, "14px")." ".($num_violations_display = (($total_num_violations_for_current_screen > 1) ? _t('%1 problems on this tab require attention', $total_num_violations) : _t('%1 problem on this tab requires attention', $total_num_violations)))."</div>\n"; 
					}
				} else {
					$more_info .= "<div id='caInspectorViolationsList'>".caNavIcon(__CA_NAV_ICON_ALERT__, "14px")." ".($num_violations_display = (($total_num_violations > 1) ? _t('%1 problems require attention', $total_num_violations) : _t('%1 problem requires attention', $total_num_violations)))."</div>\n"; 
				}
				if($num_violations_display) { 
					TooltipManager::add("#caInspectorViolationsList", "<div class='tooltipHead'>{$num_violations_display}</div><ol>".join("\n", $violation_messages)."</ol>\n");
				}
			}

			$more_info .= "</div>\n";
		}

		if ($get_spec = $view->request->config->get("{$table_name}_inspector_display_more_info")) {
			$more_info .= caProcessTemplateForIDs($get_spec, $table_name, [$t_item->getPrimaryKey()]);
		}
		if (strlen($more_info) > 0) {
			$tools[] = "
<div id='inspectorMoreInfo' class='inspectorActionButton'>
	<div id='inspectorMoreInfoButton'>
		<a href='#'>".caNavIcon(__CA_NAV_ICON_INFO__, '20px')."</a>
	</div>
</div>\n";

			TooltipManager::add("#inspectorMoreInfo", _t('See more information about this record'));
		}
		
		// Get component information
		$object_container_types = $view->request->config->getList('ca_objects_container_types');
		$object_component_types = $view->request->config->getList('ca_objects_component_types');
		$takes_components = (method_exists($t_item, "getTypeCode") && (in_array($t_item->getTypeCode(), $object_container_types) || in_array('*', $object_container_types)));
		
		$can_add_component = (($table_name === 'ca_objects') && $t_item->getPrimaryKey() && ($view->request->user->canDoAction('can_create_ca_objects')) && ($t_item->canTakeComponents() || $t_item->isComponent()));

		if((($takes_components && isset($object_component_types[0])) || ($takes_components && in_array($t_item->getTypeCode(), $object_component_types)))) {
			if (method_exists($t_item, 'getComponentCount')) {
				$component_count = $t_item->getComponentCount();
				if ($t_ui && ($component_list_screen = $t_ui->getScreenWithBundle("ca_objects_components_list", $view->request)) && ($vs_component_list_screen !== $view->request->getActionExtra())) { 
					$component_count_link = caNavLink($view->request, $component_count, '', '*', '*', $view->request->getAction().'/'.$component_list_screen, [$t_item->primaryKey() => $t_item->getPrimaryKey()]);
				} else {
					$component_count_link = $component_count;
				}
				$components_tools[] = "<div><strong>"._t('Components').":</strong> {$component_count_link}</div>";
			}
			
			// Component hierarchy tools
			if(($table_name === 'ca_objects') && $view->request->config->get("ca_objects_component_allow_merge_unmerge") && (($takes_components && isset($object_component_types[0])) || in_array($t_item->getTypeCode(), $object_component_types))) {
				$container_type_name = mb_strtolower($t_item->getTypeName());
				$container_target_type_name = $object_container_types[0];
				$component_type_name = mb_strtolower(caGetListItemForDisplay('object_types', $object_component_types[0], ['return' => 'plural']));
				if($component_count > 0) {
					$tools [] = "<div id='inspectorHierarchyMerge' class='inspectorActionButton'>".caNavLink($view->request, caNavIcon(__CA_NAV_ICON_MERGE__, '20px'), "button", 'editor', 'HierarchyTools', 'merge', ['t' => 'ca_objects', 'id' => $t_item->getPrimaryKey()], [])."</div>\n";
					TooltipManager::add('#inspectorHierarchyMerge', _t("Merge media from component %1 into this %2 and delete %1", $component_type_name, $container_type_name));
				} elseif($t_item->getRepresentationCount() > 0) {
					$tools [] = "<div id='inspectorHierarchySplit' class='inspectorActionButton'>".caNavLink($view->request, caNavIcon(__CA_NAV_ICON_SPLIT__, '20px'), "button", 'editor', 'HierarchyTools', 'split', ['t' => 'ca_objects', 'id' => $t_item->getPrimaryKey()], [])."</div>\n";
					TooltipManager::add('#inspectorHierarchySplit', _t("Split media in this %1 into %2 with component %3", $container_type_name, $container_target_type_name, $component_type_name));
				}
			}
		}	
		if ($can_add_component) {
			$components_tools[] = '<div><a href="#" onclick=\'caObjectComponentPanel.showPanel("'.caNavUrl($view->request, '*', 'ObjectComponent', 'Form', ['parent_id' => $t_item->getPrimaryKey()]).'"); return false;\')>'.caNavIcon(__CA_NAV_ICON_ADD__, '12px').'</a></div>';

			$change_type_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
			$change_type_view->setVar('t_item', $t_item);

			FooterManager::add($change_type_view->render("create_component_html.php"));
		}
		
		$t_set_type = $t_item->getTypeInstance();		
		$type_settings = $t_set_type ? $t_set_type->getSettings() : [];
		if(($table_name === 'ca_sets') && (caGetOption('random_generation_mode', $type_settings, 0) > 0)) {
			$tools[] = "<div id='inspectorRandomButton' class='inspectorActionButton'><a href='#' onclick='caRandomSetGenerationPanel.showPanel(); return false;'>".caNavIcon(__CA_NAV_ICON_RANDOM__, '20px', ['title' => _t('Add random items')])."</a></div>\n";

			$random_set_view = new View($view->request, $view->request->getViewsDirectoryPath()."/bundles/");
			$random_set_view->setVar('t_item', $t_item);
			$random_set_view->setVar('userCanSetExclusion', ((int)$t_set_type->getSetting('random_generation_mode') === 3));
			FooterManager::add($random_set_view->render("random_set_generation_html.php"));
			TooltipManager::add('#inspectorRandomButton', _t("Add random items"));
		}

		if(sizeof($tools) > 0) {
			$buf .= "<div id='toolIcons'>".join(" ", $tools)."</div><!--End tooIcons-->";
		}
		
		$next_by_idno = $prev_by_idno = null;
		if($view->request->config->get("{$table_name}_inspector_display_idno_sequence_navigation")) {
			$next_by_idno = $t_item->getAdjacentByIdno('next');
			$prev_by_idno = $t_item->getAdjacentByIdno('previous');
		}					
		if($prev_by_idno || $next_by_idno) {
			$buf .= "<div style='text-align: center;'><strong>"._t('Sequence:')."</strong><br>\n";	
			if($prev_by_idno) {
				$buf .= caEditorLink($view->request, '&lt; '.$prev_by_idno['idno'], '', $table_name, $prev_by_idno['id']);
			}
			if($prev_by_idno && $next_by_idno) { $buf .= ' | '; }
			if($next_by_idno) {
				$buf .= caEditorLink($view->request, $next_by_idno['idno'].' &gt;', '', $table_name, $next_by_idno['id']);
			}	
			$buf .= "</div>";
		}
		
		if($more_info) {
			$buf .= "<div id='inspectorInfo' >{$more_info}</div>\n";
		}
	}

	// -------------------------------------------------------------------------------------
	//
	// Item-specific information
	//
		//
		// Output info for related items
		//
		if(!$t_item->getPrimaryKey()) { // only applies to new records
			$rel_table_name = $view->request->getParameter('rel_table', pString);
			$vn_rel_type_id = $view->request->getParameter('rel_type_id', pString);
			$vn_rel_id = $view->request->getParameter('rel_id', pInteger);
			if($rel_table_name && Datamodel::tableExists($rel_table_name) && $vn_rel_type_id && $vn_rel_id) {
				$t_rel = Datamodel::getInstance($rel_table_name);
				if($t_rel && $t_rel->load($vn_rel_id)){
					$buf .= '<strong>'._t("Will be related to %1", $t_rel->getTypeName()).'</strong>: '.$t_rel->getLabelForDisplay();
				}
			}
		}

		//
		// Output lot info for ca_objects
		//
		$is_currently_part_of_lot = true;
		if (!($vn_lot_id = $t_item->get('lot_id'))) {
			$vn_lot_id = $view->request->getParameter('lot_id', pInteger);
			$is_currently_part_of_lot = false;
		}

		if (($table_name === 'ca_objects') && ($vn_lot_id)) {
			$lot_types = caGetTypeListForUser('ca_object_lots', ['access' => __CA_BUNDLE_ACCESS_READONLY__]);
			$t_lot = new ca_object_lots($vn_lot_id);
			if (($t_lot->get('deleted') == 0) && (in_array($t_lot->get('type_id'), $lot_types))) {
				if(!($vs_lot_displayname = $t_lot->get('idno_stub'))) {
					if((!$vs_lot_displayname = $t_lot->getLabelForDisplay())){
						$vs_lot_displayname = "Lot {$vn_lot_id}";
					}
				}
				if ($vs_lot_displayname) {
					if(!($vs_part_of_lot_msg = $view->request->config->get("ca_objects_inspector_part_of_lot_msg"))){
						$vs_part_of_lot_msg = _t('Part of lot');
					}
					if(!($vs_will_be_part_of_lot_msg = $view->request->config->get("ca_objects_inspector_will_be_part_of_lot_msg"))){
						$vs_will_be_part_of_lot_msg = _t('Will be part of lot');
					}
					$buf .= "<br/><strong>".($is_currently_part_of_lot ? $vs_part_of_lot_msg : $vs_will_be_part_of_lot_msg)."</strong>: " . caNavLink($view->request, $vs_lot_displayname, '', 'editor/object_lots', 'ObjectLotEditor', 'Edit', array('lot_id' => $vn_lot_id));
				}
			}
		}

		if(sizeof($components_tools) > 0) {
			$buf .= "<div id='toolComponentControls'>".join(" ", $components_tools)."</div>\n";
		}

		//
		// Output lot info for ca_object_lots
		//
		if (($table_name === 'ca_object_lots') && $t_item->getPrimaryKey()) {
			$va_component_types = $view->request->config->getList('ca_objects_component_types');
			if (is_array($va_component_types) && sizeof($va_component_types)) {
				$buf .= "<br/><strong>".((($num_objects = $t_item->numObjects(null, array('return' => 'objects', 'excludeChildObjects' => $view->request->config->get("exclude_child_objects_in_inspector_log_count")))) == 1) ? _t('Lot contains %1 object', $num_objects) : _t('Lot contains %1 objects', $num_objects))."</strong>\n";
				$buf .= "<br/><strong>".((($vn_num_components = $t_item->numObjects(null, array('return' => 'components'))) == 1) ? _t('Lot contains %1 component', $vn_num_components) : _t('Lot contains %1 components', $vn_num_components))."</strong>\n";
			} else {
				$buf .= "<br/><strong>".((($num_objects = $t_item->numObjects(null, ['excludeChildObjects' => $view->request->config->get("exclude_child_objects_in_inspector_log_count")])) == 1) ? _t('Lot contains %1 object', $num_objects) : _t('Lot contains %1 objects', $num_objects))."</strong>\n";
			}

			if (((bool)$view->request->config->get('allow_automated_renumbering_of_objects_in_a_lot')) && ($va_nonconforming_objects = $t_item->getObjectsWithNonConformingIdnos())) {

				$buf .= '<br/><br/><em>'. ((($vn_c = sizeof($va_nonconforming_objects)) == 1) ? _t('There is %1 object with non-conforming numbering', $vn_c) : _t('There are %1 objects with non-conforming numbering', $vn_c))."</em>\n";

				$buf .= "<a href='#' onclick='jQuery(\"#inspectorNonConformingNumberList\").toggle(250); return false;'>".caNavIcon(__CA_NAV_ICON_FOLDER_OPEN__, '16px');

				$buf .= "<div id='inspectorNonConformingNumberList' class='inspectorNonConformingNumberList'><div class='inspectorNonConformingNumberListScroll'><ol>\n";
				foreach($va_nonconforming_objects as $vn_object_id => $va_object_info) {
					$buf .= '<li>'.caEditorLink($view->request, $va_object_info['idno'], '', 'ca_objects', $vn_object_id)."</li>\n";
				}
				$buf .= "</ol></div>";
				$buf .= caNavLink($view->request, caNavIcon(__CA_NAV_ICON_GO__, '16px').' '._t('Re-number objects'), 'button', $view->request->getModulePath(), $view->request->getController(), 'renumberObjects', array('lot_id' => $t_item->getPrimaryKey()));
				$buf .= "</div>\n";
			}

			$t_object = new ca_objects();

			if(!(bool)$view->request->config->get('disable_add_object_to_lot_inspector_controls')) {
				$buf .= "<div class='inspectorLotObjectTypeControls'><form action='#' id='caAddObjectToLotForm'>";
				if ((bool)$view->request->config->get('ca_objects_enforce_strict_type_hierarchy')) {
					// strict menu
					$buf .= _t('Add new %1', $t_object->getTypeListAsHTMLFormElement('type_id', array('id' => 'caAddObjectToLotForm_type_id'), array('childrenOfCurrentTypeOnly' => true, 'directChildrenOnly' => ($view->request->config->get('ca_objects_enforce_strict_type_hierarchy') == '~') ? false : true, 'returnHierarchyLevels' => true, 'access' => __CA_BUNDLE_ACCESS_EDIT__)));
				} else {
					// all types
					$buf .= _t('Add new %1', $t_object->getTypeListAsHTMLFormElement('type_id', array('id' => 'caAddObjectToLotForm_type_id'), array('access' => __CA_BUNDLE_ACCESS_EDIT__)));
				}

				$buf .= "&nbsp;<a href='#' onclick='caAddObjectToLotForm()'>" . caNavIcon(__CA_NAV_ICON_ADD__, '16px') . '</a>';
				$buf .= "</form></div>\n";

				$buf .= "<script type='text/javascript'>
function caAddObjectToLotForm() { 
	window.location='" . caEditorUrl($view->request, 'ca_objects', 0, false, array('lot_id' => $t_item->getPrimaryKey(), 'rel' => 1, 'type_id' => '')) . "' + jQuery('#caAddObjectToLotForm_type_id').val();
}
jQuery(document).ready(function() {
	jQuery('#objectLotsNonConformingNumberList').hide();
});
</script>\n";
			}
		}

		//
		// Output related counts
		//
		if (is_array($show_counts_for = $view->request->config->getList($t_item->tableName().'_show_related_counts_in_inspector_for')) && sizeof($show_counts_for)) {
			$links = [];
			foreach($show_counts_for as $rel_table) {
				$show_counts_config = caParseTableTypesFromSpecification($t_item->tableName(), $rel_table);
				if(is_array($show_counts_config) && sizeof($show_counts_config)) {
					if(sizeof($show_counts_config['types']) > 0) {
						foreach($show_counts_config['types'] as $type_id => $type_info) {
							if(($count = (int)$t_item->getRelatedItems($show_counts_config['table'], ['returnAs' => 'count', 'limit' => 100000, 'restrictToTypes' => [$type_id]])) > 0) {
								$links[$show_counts_config['table'].'/'.$type_info['idno']] = caSearchLink($view->request, _t('%1 related %2', $count, ($count === 1) ? $type_info['name_singular'] : $type_info['name_plural']), '', $show_counts_config['table'], $t_item->primaryKey(true).":".$t_item->getPrimaryKey(), ['type_id' => $type_id]);
							}
						}
					} elseif (($count = (int)$t_item->getRelatedItems($show_counts_config['table'], ['returnAs' => 'count', 'limit' => 100000])) > 0) {
						$links[$show_counts_config['table']] = caSearchLink($view->request, _t('%1 related %2', $count, Datamodel::getTableProperty($show_counts_config['table'], ($count === 1) ? 'NAME_SINGULAR' : 'NAME_PLURAL')), '', $show_counts_config['table'], $t_item->primaryKey(true).":".$t_item->getPrimaryKey());
					}
				} 
			}
			ksort($links, SORT_NATURAL|SORT_FLAG_CASE);
			$buf .= join("<br/>\n", $links);
		}

		//
		// Output related objects for ca_object_representations
		//
		if ($table_name === 'ca_object_representations') {
			foreach(array('ca_objects', 'ca_object_lots', 'ca_entities', 'ca_places', 'ca_occurrences', 'ca_collections', 'ca_storage_locations', 'ca_loans', 'ca_movements') as $rel_table_name) {
				if (sizeof($va_objects = $t_item->getRelatedItems($rel_table_name))) {
					$buf .= "<div><strong>"._t("Related %1", Datamodel::getTableProperty($rel_table_name, 'NAME_PLURAL'))."</strong>: <br/>\n";

					$vs_screen = '';
					if ($t_ui = ca_editor_uis::loadDefaultUI($rel_table_name, $view->request, null)) {
						$vs_screen = $t_ui->getScreenWithBundle('ca_object_representations', $view->request);
					}
					foreach($va_objects as $vn_rel_id => $va_rel_info) {
						if ($label = array_shift($va_rel_info['labels'])) {
							$buf .= caEditorLink($view->request, '&larr; '.$label.' ('.$va_rel_info['idno'].')', '', $rel_table_name, $va_rel_info[Datamodel::primaryKey($rel_table_name)], array(), array(), array('action' => 'Edit'.($vs_screen ? "/{$vs_screen}" : "")))."<br/>\n";
						}
					}
					$buf .= "</div>\n";
				}
			}
		}

		//
		// Output related object reprsentation for ca_representation_annotation
		//
		if ($table_name === 'ca_representation_annotations') {
			if ($vn_representation_id = $t_item->get('representation_id')) {
				$buf .= "<div><strong>"._t("Applied to representation")."</strong>: <br/>\n";
				$t_rep = new ca_object_representations($vn_representation_id);
				$buf .= caNavLink($view->request, '&larr; '.$t_rep->getLabelForDisplay(), '', 'editor/object_representations', 'ObjectRepresentationEditor', 'Edit/'.$view->getVar('representation_editor_screen'), array('representation_id' => $vn_representation_id)).'<br/>';

				$buf .= "</div>\n";
			}
		}

		//
		// Output extra useful info for sets
		//
		if ($table_name === 'ca_sets') {

			$vn_set_item_count = $t_item->getItemCount(array('user_id' => $view->request->getUserID()));

			if (($vn_set_item_count > 0) && ($view->request->user->canDoAction('can_batch_edit_'.Datamodel::getTableName($t_item->get('table_num'))))) {
				$buf .= caNavButton($view->request, __CA_NAV_ICON_BATCH_EDIT__, _t('Batch edit'), 'editorBatchSetEditorLink', 'batch', 'Editor', 'Edit', array('id' => 'ca_sets:'.$t_item->getPrimaryKey()), array(), array('icon_position' => __CA_NAV_ICON_ICON_POS_LEFT__, 'no_background' => true, 'dont_show_content' => true));
			}
			TooltipManager::add(".editorBatchSetEditorLink", _t('Batch Edit'));

			$buf .= "<div><strong>"._t("Number of items")."</strong>: {$vn_set_item_count}<br/>\n";

			$vn_set_table_num = $t_item->get('table_num');
			$vs_set_table_name = Datamodel::getTableName($vn_set_table_num);
			if ($t_item->getPrimaryKey()) {

				$buf .= "<strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_set_table_num)."<br/>\n";

				$buf .= "</div>\n";

				if(!(bool)$view->request->config->get('ca_sets_disable_duplication_of_items') && $view->request->user->canDoAction('can_duplicate_items_in_sets') && $view->request->user->canDoAction('can_duplicate_' . $vs_set_table_name)) {
					$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px; text-align: right;" ></div>';
					$buf .= caFormTag($view->request, 'DuplicateItems', 'caDupeSetItemsForm', 'manage/sets/SetEditor', 'post', 'multipart/form-data', '_top', array('noCSRFToken' => false, 'disableUnsavedChangesWarning' => true));
					$buf .= _t("Duplicate items in this set and add to") . " ";
					$buf .= caHTMLSelect('setForDupes', array(
						_t('current set') => 'current',
						_t('new set') => 'new',
					));
					$buf .= caHTMLHiddenInput('set_id', array('value' => $t_item->getPrimaryKey()));
					$buf .= caFormSubmitLink($view->request, caNavIcon(__CA_NAV_ICON_GO__, "16px"), "button", "caDupeSetItemsForm", null, ['aria-label' => _t('Duplicate items')]);
					$buf .= "</form>";
					$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px; text-align: right;" ></div>';
				}
			} else {
				if ($vn_set_table_num = $view->request->getParameter('table_num', pInteger)) {
					$buf .= "<div><strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_set_table_num)."<br/>\n";

					$buf .= "</div>\n";
				}
			}
			$t_user = new ca_users(($vn_user_id = $t_item->get('user_id')) ? $vn_user_id : $view->request->getUserID());
			if ($t_user->getPrimaryKey()) {
				$buf .= "<div><strong>"._t('Owner')."</strong>: ".$t_user->get('fname').' '.$t_user->get('lname')."</div>\n";
			}

			if($view->request->user->canDoAction('can_export_'.$vs_set_table_name) && $t_item->getPrimaryKey() && (sizeof(ca_data_exporters::getExporters($vn_set_table_num))>0)) {
				$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px; text-align: right;" id="caExportItemButton">';

				$buf .= _t('Export this set of records')."&nbsp; ";
				$buf .= "<a class='button' onclick='jQuery(\"#exporterFormList\").show();' style='text-align:right;' href='#'>".caNavIcon(__CA_NAV_ICON_EXPORT_SMALL__, '16px')."</a>";

				$buf .= caFormTag($view->request, 'ExportData', 'caExportForm', 'manage/MetadataExport', 'post', 'multipart/form-data', '_top', array('noCSRFToken' => true, 'disableUnsavedChangesWarning' => true));
				$buf .= "<div id='exporterFormList'>";
				$buf .= ca_data_exporters::getExporterListAsHTMLFormElement('exporter_id', $vn_set_table_num, array('id' => 'caExporterList'),array('width' => '135px'));
				$buf .= caHTMLHiddenInput('set_id', array('value' => $t_item->getPrimaryKey()));
				$buf .= caFormSubmitLink($view->request, _t('Export')." &rsaquo;", "button", "caExportForm", null, ['aria-label' => _t('Export')]);
				$buf .= "</div>\n";
				$buf .= "</form>";

				$buf .= "</div>";

				$buf .= "<script type='text/javascript'>";
				$buf .= "jQuery(document).ready(function() {";
				$buf .= "jQuery(\"#exporterFormList\").hide();";
				$buf .= "});";
				$buf .= "</script>";
			}
		}

		//
		// Output extra useful info for set items
		//
		if ($table_name === 'ca_set_items') {
			AssetLoadManager::register("panel");
			$t_set = new ca_sets();
			if ($t_set->load($vn_set_id = $t_item->get('set_id'))) {
				$buf .= "<div><strong>"._t("Part of set")."</strong>: ".caEditorLink($view->request, $t_set->getLabelForDisplay(), '', 'ca_sets', $vn_set_id)."<br/>\n";

				$t_content_instance = Datamodel::getInstanceByTableNum($vn_item_table_num = $t_item->get('table_num'));
				if ($t_content_instance->load($vn_row_id = $t_item->get('row_id'))) {
					$label = $t_content_instance->getLabelForDisplay();
					if ($vs_id_fld = $t_content_instance->getProperty('ID_NUMBERING_ID_FIELD')) {
						$label .= " (".$t_content_instance->get($vs_id_fld).")";
					}
					$buf .= "<strong>"._t("Is %1", caGetTableDisplayName($vn_item_table_num, false)."</strong>: ".caEditorLink($view->request, $label, '', $vn_item_table_num, $vn_row_id))."<br/>\n";
				}

				$buf .= "</div>\n";
			}
		}

		//
		// Output extra useful info for metadata alert rules
		if (($table_name == 'ca_metadata_alert_rules')) {
			if ($t_item->getPrimaryKey()) {
				$buf .= "<div><strong>"._t("Rule content type")."</strong>: ".caGetTableDisplayName($t_item->get('table_num'))."<br/></div>\n";
			} else {
				if ($vn_set_table_num = $view->request->getParameter('table_num', pInteger)) {
					$buf .= "<div><strong>"._t("Rule content type")."</strong>: ".caGetTableDisplayName($vn_set_table_num)."<br/>\n";

					$buf .= "</div>\n";
				}
			}
		}

		//
		// Output extra useful info for lists
		//
		if (($table_name === 'ca_lists') && $t_item->getPrimaryKey()) {
			$buf .= "<strong>"._t("Number of items")."</strong>: ".$t_item->numItemsInList()."<br/>\n";
				$t_list_item = new ca_list_items();
				$t_list_item->load(array('list_id' => $t_item->getPrimaryKey(), 'parent_id' => null));
				$vs_type_list = $t_list_item->getTypeListAsHTMLFormElement('type_id', array('style' => 'width: 90px; font-size: 9px;'), array('access' => __CA_BUNDLE_ACCESS_EDIT__));

				if ($vs_type_list) {
					$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px;">';
					$buf .= caFormTag($view->request, 'Edit', 'NewChildForm', 'administrate/setup/list_item_editor/ListItemEditor', 'post', 'multipart/form-data', '_top', array('noCSRFToken' => false, 'disableUnsavedChangesWarning' => true));
					$buf .= _t('Add a %1 to this list', $vs_type_list).caHTMLHiddenInput($t_list_item->primaryKey(), array('value' => '0')).caHTMLHiddenInput('parent_id', array('value' => $t_list_item->getPrimaryKey()));
					$buf .= caFormSubmitLink($view->request, caNavIcon(__CA_NAV_ICON_ADD__, '16px'), '', 'NewChildForm', null, ['aria-label' => _t('Add a %1 to this list', $vs_type_list)]);
					$buf .= "</form></div>\n";
				}
		}

		//
		// Output containing list for list items
		//
		if ($table_name === 'ca_list_items') {
			if ($t_list = $view->getVar('t_list')) {
				$vn_list_id = $t_list->getPrimaryKey();
				$buf .= "<div><strong>"._t("Part of")."</strong>: ".caEditorLink($view->request, $t_list->getLabelForDisplay(), '', 'ca_lists', $vn_list_id) ."</div>\n";
				if ($t_item->get('is_default')) {
					$buf .= "<div><strong>"._t("Is default for list")."</strong></div>";
				}
			}
		}

		//
		// Output containing relationship type name for relationship types
		//
		if ($table_name === 'ca_relationship_types') {
			if (!($t_rel_instance = Datamodel::getInstanceByTableNum($t_item->get('table_num'), true))) {
				if ($parent_id = $view->request->getParameter('parent_id', pInteger)) {
					$t_rel_type = new ca_relationship_types($parent_id);
					$t_rel_instance = Datamodel::getInstanceByTableNum($t_rel_type->get('table_num'), true);
				}
			}

			if ($t_rel_instance) {
				$buf .= "<div><strong>"._t("Is a")."</strong>: ".$t_rel_instance->getProperty('NAME_SINGULAR')."<br/></div>\n";
			}
		}

		//
		// Output extra useful info for metadata elements
		//
		if (($table_name === 'ca_metadata_elements') && $t_item->getPrimaryKey()) {
			$buf .= "<div><strong>"._t("Element code")."</strong>: ".$t_item->get('element_code')."<br/></div>\n";

			if (sizeof($va_uis = $t_item->getUIs()) > 0) {
				$buf .= "<div><strong>"._t("Referenced by user interfaces")."</strong>:<br/>\n";
				foreach($va_uis as $vn_ui_id => $va_ui_info) {
					$buf .= caNavLink($view->request, $va_ui_info['name'], '', 'administrate/setup/interface_screen_editor', 'InterfaceScreenEditor', 'Edit', array('ui_id' => $vn_ui_id, 'screen_id' => $va_ui_info['screen_id']));
					$buf .= " (".Datamodel::getTableProperty($va_ui_info['editor_type'], 'NAME_PLURAL').")<br/>\n";
				}
				$buf .= "</div>\n";
			}
		}

		//
		// Output related objects for ca_editor_uis and ca_editor_ui_screens
		//
		if ($table_name === 'ca_editor_uis') {
			$buf .= "<div><strong>"._t("Number of screens")."</strong>: ".(int)$t_item->getScreenCount()."\n";

			if ($t_item->getPrimaryKey()) {
				$buf .= "<div><strong>"._t("Edits")."</strong>: ".caGetTableDisplayName($t_item->get('editor_type'))."<br/>\n";
			} else {
				$buf .= "<div><strong>"._t("Edits")."</strong>: ".caGetTableDisplayName($view->request->getParameter('editor_type', pInteger))."<br/>\n";
			}
			$buf .= "</div>\n";
		}

		//
		// Output related objects for ca_editor_uis and ca_editor_ui_screens
		//
		if ($table_name === 'ca_editor_ui_screens') {
			$t_ui = new ca_editor_uis($vn_ui_id = $t_item->get('ui_id'));
			$buf .= "<div><strong>"._t("Part of")."</strong>: ".caNavLink($view->request, $t_ui->getLabelForDisplay(), '',  'administrate/setup/interface_editor', 'InterfaceEditor', 'Edit', array('ui_id' => $vn_ui_id))."\n";

			$buf .= "</div>\n";
		}

		//
		// Output extra useful info for bundle displays
		//
		if ($table_name === 'ca_bundle_displays') {
			$buf .= "<div><strong>"._t("Number of placements")."</strong>: ".$t_item->getPlacementCount(array('user_id' => $view->request->getUserID()))."<br/>\n";

			if ($t_item->getPrimaryKey()) {

				$vn_content_table_num = $t_item->get('table_num');
				$buf .= "<strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."\n";

				$buf .= "</div>\n";
			} else {
				if ($vn_content_table_num = $view->request->getParameter('table_num', pInteger)) {
					$buf .= "<div><strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."\n";

					$buf .= "</div>\n";
				}
			}

			$t_user = new ca_users(($vn_user_id = $t_item->get('user_id')) ? $vn_user_id : $view->request->getUserID());
			if ($t_user->getPrimaryKey()) {
				$buf .= "<div><strong>"._t('Owner')."</strong>: ".$t_user->get('fname').' '.$t_user->get('lname')."</div>\n";
			}
		}

		//
		// Output extra useful info for search forms
		//
		if ($table_name === 'ca_search_forms') {
			$buf .= "<div><strong>"._t("Number of placements")."</strong>: ".$t_item->getPlacementCount(array('user_id' => $view->request->getUserID()))."<br/>\n";

			if ($t_item->getPrimaryKey()) {

				$vn_content_table_num = $t_item->get('table_num');
				$buf .= "<strong>"._t("Searches for")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."\n";
				$buf .= "</div>\n";
			} else {
				if ($vn_content_table_num = $view->request->getParameter('table_num', pInteger)) {
					$buf .= "<strong>"._t("Searches for")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."\n";
					$buf .= "</div>\n";
				}
			}
			$t_user = new ca_users(($vn_user_id = $t_item->get('user_id')) ? $vn_user_id : $view->request->getUserID());
			if ($t_user->getPrimaryKey()) {
				$buf .= "<div><strong>"._t('Owner')."</strong>: ".$t_user->get('fname').' '.$t_user->get('lname')."</div>\n";
			}
		}

		//
		// Output extra useful info for tours
		//
		if (($table_name === 'ca_tours') && $t_item->getPrimaryKey()) {
			$buf .= "<br/><strong>"._t("Number of stops")."</strong>: ".$t_item->getStopCount()."<br/>\n";
		}

		//
		// Output containing tour for tour stops
		//
		if ($table_name === 'ca_tour_stops') {
			$t_tour = new ca_tours($vn_tour_id = $t_item->get('tour_id'));
			$buf .= "<strong>"._t("Part of")."</strong>: ".caEditorLink($view->request, $t_tour->getLabelForDisplay(), '', 'ca_tours', $vn_tour_id) ."<br/>\n";
		}

		//
		// Output extra useful info for bundle mappings
		//
		if ($table_name === 'ca_bundle_mappings') {
			if ($t_item->getPrimaryKey()) {
				$vn_content_table_num = $t_item->get('table_num');
				$buf .= "<br/><strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."<br/>\n";
				$buf .= "<strong>"._t("Type")."</strong>: ".$t_item->getChoiceListValue('direction', $t_item->get('direction'))."<br/>\n";
				$buf .= "<strong>"._t("Target format")."</strong>: ".$t_item->get('target')."<br/>\n";

				$va_stats = $t_item->getMappingStatistics();
				$buf .= "<div><strong>"._t("Number of groups")."</strong>: ".$va_stats['groupCount']."<br/>\n";
				$buf .= "<strong>"._t("Number of rules")."</strong>: ".$va_stats['ruleCount']."<br/>\n";


				$buf .= "</div>\n";
			} else {
				if ($vn_content_table_num = $view->request->getParameter('table_num', pInteger)) {
					$buf .= "<div><strong>"._t("Type of content")."</strong>: ".caGetTableDisplayName($vn_content_table_num)."<br/>\n";
					$buf .= "<strong>"._t("Type")."</strong>: ".$t_item->getChoiceListValue('direction', $view->request->getParameter('direction', pString))."<br/>\n";
					$buf .= "<strong>"._t("Target format")."</strong>: ".$view->request->getParameter('target', pString)."<br/>\n";

					$buf .= "<div><strong>"._t("Number of groups")."</strong>: 0<br/>\n";
					$buf .= "<strong>"._t("Number of rules")."</strong>: 0</div>\n";

					$buf .= "</div>\n";
				}
			}
		}
	// -------------------------------------------------------------------------------------
	// Export


	if ($t_item->getPrimaryKey() && $view->request->config->get($table_name.'_show_add_child_control_in_inspector')) {
		$show_add_child_control = true;
		if (is_array($va_restrict_add_child_control_to_types = $view->request->config->getList($table_name.'_restrict_child_control_in_inspector_to_types')) && sizeof($va_restrict_add_child_control_to_types)) {
			$t_type_instance = $t_item->getTypeInstance();
			if (!in_array($t_type_instance->get('idno'), $va_restrict_add_child_control_to_types) && !in_array($t_type_instance->getPrimaryKey(), $va_restrict_add_child_control_to_types)) {
				$show_add_child_control = false;
			}
		}
	}

	if($view->request->user->canDoAction('can_export_'.$table_name) && $t_item->getPrimaryKey()) {
		if (ca_data_exporters::getExporters($t_item->tableNum(), ['countOnly' => true]) > 0) {
			$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px; text-align: right;" id="caExportItemButton">';

			$buf .= _t('Export data')." ";
			$buf .= "<a class='button' onclick='jQuery(\"#exporterFormList\").show();' style='text-align:right;' href='#'>".caNavIcon(__CA_NAV_ICON_EXPORT_SMALL__, '16px')."</a>";


			$buf .= caFormTag($view->request, 'ExportSingleData', 'caExportForm', 'manage/MetadataExport', 'post', 'multipart/form-data', '_top', array('disableUnsavedChangesWarning' => true));
			$buf .= "<div id='exporterFormList'>";
			$buf .= ca_data_exporters::getExporterListAsHTMLFormElement('exporter_id', $t_item->tableNum(), array('id' => 'caExporterList'), array('width' => '120px', 'recordType' => $t_item->getTypeCode(), 'value' => Session::getVar('exporter_id')));
			$buf .= caHTMLHiddenInput('item_id', array('value' => $t_item->getPrimaryKey()));
			$buf .= caFormSubmitLink($view->request, _t('Export')." &rsaquo;", "button", "caExportForm", null, ['aria-label' => _t('Export data')]);
			$buf .= "</div>\n";
			$buf .= "</form>";

			$buf .= "</div>";

			$buf .= "<script type='text/javascript'>";
			$buf .= "jQuery(document).ready(function() {";
			$buf .= "jQuery(\"#exporterFormList\").hide();";
			$buf .= "});";
			$buf .= "</script>";
		}

		require_once(__CA_LIB_DIR__."/ExternalExportManager.php");
		if (ExternalExportManager::getTargets(['countOnly' => true, 'table' => $t_item->tableNum(), 'restrictToTypes' => [$t_item->getTypeCode()]]) > 0) {
			$buf .= '<div style="border-top: 1px solid #aaaaaa; margin-top: 5px; font-size: 10px; text-align: right;" id="caExternalExportItemButton">';
			$buf .= _t('Export to external repository')." ";
			$buf .= "<a class='button' onclick='jQuery(\"#externalExporterFormList\").show();' style='text-align:right;' href='#'>".caNavIcon(__CA_NAV_ICON_EXPORT_SMALL__, '16px')."</a>";

			$buf .= caFormTag($view->request, 'ExternalExportSingle', 'caExternalExportForm', 'manage/MetadataExport', 'post', 'multipart/form-data', '_top', array('disableUnsavedChangesWarning' => true));
			$buf .= "<div id='externalExporterFormList'>";
			$buf .= ExternalExportManager::getTargetListAsHTMLFormElement('target', $t_item->tableNum(), array('id' => 'caExternalExporterList'), array('width' => '120px', 'restrictToTypes' => [$t_item->getTypeCode()]));
			$buf .= caHTMLHiddenInput('item_id', array('value' => $t_item->getPrimaryKey()));
			$buf .= caFormSubmitLink($view->request, _t('Export')." &rsaquo;", "button", "caExternalExportForm", null, ['aria-label' => _t('Export to external repository')]);
			$buf .= "</div>\n";
			$buf .= "</form>";
			$buf .= "</div>";

			$buf .= "<script type='text/javascript'>";
			$buf .= "jQuery(document).ready(function() {";
			$buf .= "jQuery(\"#externalExporterFormList\").hide();";
			$buf .= "});";
			$buf .= "</script>";
		}
	}
	
	$buf .= "</div></h4>\n";

	$buf .= "<script type='text/javascript'>
		var inspectorCookieJar = jQuery.cookieJar('caCookieJar');";
		if($t_item->getPrimaryKey()) {
			if ($more_info) {
				$buf .= "			
		if (inspectorCookieJar.get('inspectorMoreInfoIsOpen') == undefined) {		// default is to have info open
			inspectorCookieJar.set('inspectorMoreInfoIsOpen', 1);
		}
		if (inspectorCookieJar.get('inspectorMoreInfoIsOpen') == 1) {
			jQuery('#inspectorInfo').toggle(0);
			jQuery('#inspectorMoreInfoButton').html('".addslashes(caNavIcon(__CA_NAV_ICON_COLLAPSE__, '20px'))."');
		}

		jQuery('#inspectorMoreInfo').click(function() {
			jQuery('#inspectorInfo').slideToggle(350, function() { 
				inspectorCookieJar.set('inspectorMoreInfoIsOpen', (this.style.display == 'block') ? 1 : 0); 
				jQuery('#inspectorMoreInfoButton').html((this.style.display == 'block') ? '".addslashes(caNavIcon(__CA_NAV_ICON_COLLAPSE__, '20px'))."' : '".addslashes(caNavIcon(__CA_NAV_ICON_INFO__, '20px'))."');
				caResizeSideNav();
			}); 
			return false;
		});
	";
			}

			if (sizeof($va_reps)) {
				$buf .= "
	if (inspectorCookieJar.get('inspectorShowMediaIsOpen') == undefined) {		// default is to have media open
		inspectorCookieJar.set('inspectorShowMediaIsOpen', 1);
	}

	if (inspectorCookieJar.get('inspectorShowMediaIsOpen') == 1) {
		jQuery('#inspectorMedia').toggle();
	}

	jQuery('#caColorbox').on('click', function(e) {
		if (e.altKey) {
			jQuery('#inspectorMedia').slideToggle(200, function() { 
				inspectorCookieJar.set('inspectorShowMediaIsOpen', (this.style.display == 'block') ? 1 : 0); 
					caResizeSideNav();
			}); 
			return false;
		}
	});
				";
			}
		}

		$buf .= "</script>\n";
	}
	
	// Search results debug
	if($view->request->user->getPreference('show_search_result_desc') === 'show') {
		$result_desc = $o_result_context->getResultDesc() ?? null;
		if(is_array($result_desc) && sizeof($result_desc)) {
			if($result_desc_text = ($t_item->getPrimaryKey() ? caFormatSearchResultDesc($t_item->getPrimaryKey(), $result_desc, ['maxTitleLength' => 20, 'request' => $view->request]) : null)) {
				$buf .= "<div class='searchResultDesc'><span class='searchResultDescHeading'>"._t('Search <em>%1</em> matched on', $o_result_context->getSearchExpression()).':</span><br/>'.$result_desc_text."</div>\n";
			}
		}
	}
	$o_app_plugin_manager = new ApplicationPluginManager();
	$va_hookAppend = $o_app_plugin_manager->hookAppendToEditorInspector(array("t_item"=>$t_item));
	if (is_string($va_hookAppend["caEditorInspectorAppend"] ?? null)) {
		$buf .= $va_hookAppend["caEditorInspectorAppend"];
	}

	return $buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Parse table and types from specifier
 *
 * @pram string $table Table for which related values are to be fetched
 * @param string $spec Related table specification
 * @param array $options Options include:
 *		None available yes
 *
 * @return array|null
 */
function caParseTableTypesFromSpecification(string $table, string $spec, ?array $options=null) : ?array {
	$config = Configuration::load();
	$tmp = explode('/', $spec);
	
	$rel_table = $types = null;
	switch(sizeof($tmp)) {
		case 1:
			$rel_table = $spec;
			break;
		case 2:
		default:
			$rel_table = $tmp[0];
			$types = preg_split('![;,]+!', $tmp[1]);
			break;
	}
	
	if(!($t_rel_instance = Datamodel::getInstance($rel_table, true))) {
		return null;
	}
	$by_type = (bool)$config->get($rel_table.'_breakout_find_by_type_in_menu');
	$dont_expand_hierarchically = $config->getList($rel_table.'_find_dont_expand_hierarchically') ?? [];
	
	if($by_type && !$types) { $types = ['*']; }
	
	$types_proc = [];
	if(is_array($types)) {
		$type_list = $t_rel_instance->getTypeList();
		if($types[0] === '*') {
			$types_proc = $type_list;
		} else {
			$type_ids = [];
			foreach($types as $type) {
				$type_ids = array_merge($type_ids, caMakeTypeIDList($rel_table, $type, ['dontIncludeSubtypesInTypeRestriction' => in_array($type, $dont_expand_hierarchically)]) ?? []);
			}
			foreach($type_ids as $type_id) {
				if(!($type_list[$type_id] ?? null)) { continue; }
				$types_proc[$type_id] = $type_list[$type_id];
			}
		}
	}
	return ['table' => $rel_table, 'types' => $types_proc];
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates access control list (ACL) editor for item
 *
 * @param View $view Inspector view object
 * @param BaseModel $t_instance Model instance representing the item for which ACL is being managed
 * @param array $options None implemented yet
 *
 * @return string HTML implementing the inspector
 */
function caEditorACLEditor(View $view, BaseModel $t_instance, ?array $options=null) : ?string {
	$view_path = (isset($options['viewPath']) && $options['viewPath']) ? $options['viewPath'] : $view->request->getViewsDirectoryPath();
	$o_view = new View($view->request, "{$view_path}/bundles/");

	$o_view->setVar('t_instance', $t_instance);
	
	// Get inheritance usage stats
	$o_view->setVar('statistics', ca_acl::getStatisticsForRow($t_instance, $t_instance->getPrimaryKey()));
	
	return $o_view->render('ca_acl_access.php');
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates standard-format inspector panels for editors
 *
 * @param View $po_view Inspector view object
 * @param array $pa_options Optional array of options. Supported options are:
 *		backText = a string to use as the "back" button text; default is "Results"
 *
 * @return string HTML implementing the inspector
 */
function caBatchEditorInspector(View $po_view, ?array $pa_options=null) : ?string {
	$rs 					= $po_view->getVar('record_selection');
	$t_item 				= $po_view->getVar('t_item');
	$vs_table_name = $t_item->tableName();
	if (($vs_priv_table_name = $vs_table_name) == 'ca_list_items') {
		$vs_priv_table_name = 'ca_lists';
	}

	$o_result_context		= $po_view->getVar('result_context');
	$t_ui 					= $po_view->getVar('t_ui');
	
	$vs_buf = '<h3 class="nextPrevious"><span class="resultCount" style="padding-top:10px;">'.$rs->getResultsLink($po_view->request)."</span></h3>\n";

	$vs_color = $vs_type_name = null;

	$t_type = method_exists($t_item, "getTypeInstance") ? $t_item->getTypeInstance() : null;
	if ($t_type) {
		$vs_color = trim($t_type->get('color'));
		$vs_type_name = $t_type->getTypeName();
	}
	if (!$vs_color && $t_ui) { $vs_color = trim($t_ui->get('color')); }
	if (!$vs_color) { $vs_color = "444444"; }

	$vs_buf .= "<h4><div id='caColorbox' style='border: 6px solid #{$vs_color}; padding-bottom:15px;'>\n";
	
	$vn_item_count = $rs->getItemCount(['user_id' => $po_view->request->getUserID()]);
	$vs_item_name = ($vn_item_count == 1) ? $t_item->getProperty("NAME_SINGULAR"): $t_item->getProperty("NAME_PLURAL");

	$vs_buf .= "<strong>"._t("Batch editing %1 %2", $vn_item_count, $vs_item_name).": </strong>\n";


	if (!($vs_label = $rs->name())) {
		$vs_label = '['.caGetBlankLabelText('ca_sets').']';
	}

	if($editor_link = $rs->getEditorLink($po_view->request, $vs_label)) {
		$vs_label = $editor_link;
	}

	$vs_buf .= " {$vs_label}\n";


	// -------------------------------------------------------------------------------------

	$vs_buf .= "<div>"._t('Batch contains <em>%1</em>', join(", ", $rs->getTypesForItems()))."</div>\n";

	// -------------------------------------------------------------------------------------
	// Nav link for batch delete
	// -------------------------------------------------------------------------------------

	if (($vn_item_count > 0) && ($po_view->request->user->canDoAction('can_batch_delete_'.Datamodel::getTableName($rs->tableNum())))) {

		$vs_buf .= "<div class='button' style='text-align:right;'><a href='#' id='inspectorMoreInfo'>"._t("More options")."</a> &rsaquo;</div>
			<div id='inspectorInfo' class='setDelete'>";
		$vs_buf .= caNavLink($po_view->request, 
			caNavIcon(__CA_NAV_ICON_NUKE__, '24px', array('style' => 'margin-top:7px; vertical-align: text-bottom;'))." "._t("Delete <strong><em>all</em></strong> records in set")
			, null, 'batch', 'Editor', 'Delete', array('id' => $rs->ID())
		);
		if ($po_view->request->user->canDoAction("can_change_type_{$vs_table_name}")) {

			$vs_buf .= "<a href='#' onclick='caTypeChangePanel.showPanel(); return false;'>".caNavIcon(__CA_NAV_ICON_CHANGE__, '20px', array('style' => 'margin: 7px 4px 0 0; vertical-align: text-bottom;'))." "._t("Set type for records in set")."</a>\n";

			$vo_change_type_view = new View($po_view->request, $po_view->request->getViewsDirectoryPath()."/bundles/");
			$vo_change_type_view->setVar('t_item', $t_item);
			$vo_change_type_view->setVar('record_selection', $rs);
			$vo_change_type_view->setVar('id', $rs->ID());

			FooterManager::add($vo_change_type_view->render("batch_change_type_html.php"));
			TooltipManager::add("#inspectorChangeType", _t('Change Record Type'));
		}

		$vs_buf .= "</div>\n";

		$vs_buf .= "<script type='text/javascript'>
			jQuery('#inspectorMoreInfo').click(function() {
				jQuery('#inspectorInfo').slideToggle(350, function() { 
					jQuery('#inspectorMoreInfo').html((this.style.display == 'block') ? '".addslashes(_t('Close options'))."' : '".addslashes(_t('More options'))."');
				}); 
				return false;
			});
		</script>";

	}

	// -------------------------------------------------------------------------------------

	$vs_buf .= "</div></h4>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates standard-format inspector panels for editors
 *
 * @param View $po_view Inspector view object
 * @param array $pa_options Optional array of options. Supported options are:
 *		backText = a string to use as the "back" button text; default is "Results"
 *
 * @return string HTML implementing the inspector
 */
function caBatchMediaImportInspector($po_view, $pa_options=null) {
	$vs_color = "444444";
	$vs_buf = "<h4><div id='caColorbox' style='border: 6px solid #{$vs_color}; padding-bottom:15px;'>\n";
	$vs_buf .= "<strong>"._t("Batch import media")."</strong>\n";

	$global_batch_media_import_root_directory = caGetSharedMediaUploadPath();
	$user_batch_media_import_root_directory = caGetMediaUploadPathForUser($po_view->request->getUserID());
	$vs_buf .= "<p class='abbreviatedPath' title='{$global_batch_media_import_root_directory}'>"._t('<strong>Directory (all users):</strong> %1', caTruncateStringWithEllipsis($global_batch_media_import_root_directory, 30, 'middle'))."</p>\n";
	$vs_buf .= "<p class='abbreviatedPath' title='{$user_batch_media_import_root_directory}'>"._t('<strong>Directory (user):</strong> %1', caTruncateStringWithEllipsis($user_batch_media_import_root_directory, 30, 'middle'))."</p>\n";

	$vs_buf .= "</div></h4>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
 * Generates standard-format inspector panels for exporters
 *
 * @param View $po_view Inspector view object
 *
 * @return string HTML implementing the inspector
 */
function caBatchMetadataExportInspector($po_view) {
	$vs_color = "444444";
	$vs_buf = "<h4><div id='caColorbox' style='border: 6px solid #{$vs_color}; padding-bottom:15px;'>\n";

	$vs_buf .= "<strong>"._t("Batch export metadata")."</strong>\n";

	$t_item = $po_view->getVar("t_item");
	$vs_buf .= "<p>"._t("Selected exporter").":<br />".$t_item->getLabelForDisplay()."</p>";

	if($vn_id = $po_view->request->getParameter('item_id', pInteger)) {
		$vs_buf .= "<p>".caEditorLink($po_view->request, _t("Back to record"), 'caResultsEditorEditLink', $t_item->getTargetTableName(), $vn_id)."</p>";
	}

	$vs_buf .= "</div></h4>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------------------------------------
/**
  *
  */
function caTableIsActive($pm_table) {
	$t_instance = is_numeric($pm_table) ? Datamodel::getInstanceByTableNum($pm_table, true) : Datamodel::getInstanceByTableName($pm_table, true);
	if (!$t_instance) { return null; }

	$vs_table_name = $t_instance->tableName();

	$o_config = Configuration::load();
	if (is_subclass_of($t_instance, "BaseRelationshipModel")) {
		$vs_left_table_name = $t_instance->getLeftTableName();
		if ($vs_left_table_name == 'ca_tour_stops') { $vs_left_table_name = 'ca_tours'; }
		$vs_right_table_name = $t_instance->getRightTableName();
		if ($vs_right_table_name == 'ca_tour_stops') { $vs_right_table_name = 'ca_tours'; }

		if ((int)($o_config->get("{$vs_left_table_name}_disable"))) { return false; }
		if ((int)($o_config->get("{$vs_right_table_name}_disable"))) { return false; }
	} else {
		switch($vs_table_name) {
			case 'ca_tour_stops':
				if ((int)($o_config->get('ca_tours_disable'))) { return false; }
				break;
			default:
				if ((int)($o_config->get($vs_table_name.'_disable'))) { return false; }
				break;
		}
		
	}

	return true;
}
# ------------------------------------------------------------------------------------------------
/**
  *
  */
function caFilterTableList($pa_tables, $pa_options=null) {
	$o_config = Configuration::load();
	
	// allow ca_object_representations by default as it's always active as object media, even when
	// disabled as a top-level record type
	if(!is_array($force_table_nums = caGetOption('alwaysAllowTableNums', $pa_options, [56]))) {
		$force_table_nums = [];
	}
	
	// assume table display names (*not actual database table names*) are keys and table_nums are values
	$va_filtered_tables = array();
	foreach($pa_tables as $vs_display_name => $vn_table_num) {
		$vs_display_name = mb_strtolower($vs_display_name, 'UTF-8');

		if (!caTableIsActive($vn_table_num) && !in_array($vn_table_num, $force_table_nums, true)) { continue; }
		$vs_table_name = Datamodel::getTableName($vn_table_num);
		$va_filtered_tables[$vs_display_name] = $vn_table_num;
	}

	if (caGetOption("sort", $pa_options, true)) {
		ksort($va_filtered_tables);
	}

	return $va_filtered_tables;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caGetTableDisplayName($pm_table_name_or_num, $pb_use_plural=true) {
	$vs_table = Datamodel::getTableName($pm_table_name_or_num);

	switch($vs_table) {
		case 'ca_occurrences':
			$t_occ = new ca_occurrences();
				$va_types = $t_occ->getTypeList();
				$va_type_labels = array();
				foreach($va_types as $vn_item_id => $va_type_info) {
					$va_type_labels[] = $va_type_info[($pb_use_plural ? 'name_plural' : 'name_singular')];
				}

				return join('/', $va_type_labels);
			break;
		default:
			if($t_instance = Datamodel::getInstanceByTableName($vs_table, true)) {
				return $t_instance->getProperty(($pb_use_plural ? 'NAME_PLURAL' : 'NAME_SINGULAR'));
			}
			break;
	}

	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param
 *
 * @return
 */
function caGetMediaDisplayConfig() {
	$o_config = Configuration::load();
	return Configuration::load(__CA_APP_DIR__.'/conf/media_display.conf');
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param
 *
 * @return
 */
function caGetMediaDisplayInfo($ps_context, $ps_mimetype) {
	$o_config = Configuration::load();
	$o_media_display_config = caGetMediaDisplayConfig();

	if (!is_array($va_context = $o_media_display_config->getAssoc($ps_context))) { return null; }

	if (!$ps_mimetype) { return $va_context; }
	foreach($va_context as $vs_media_class => $va_media_class_info) {
		if (!is_array($va_mimetypes = $va_media_class_info['mimetypes'])) { continue; }

		if (in_array($ps_mimetype, $va_mimetypes)) {
			return $va_media_class_info;
		}
	}
	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param
 *
 * @return
 */
function caGetMediaDisplayInfoForMimetype(string $context, ?string $mimetype) : ?array {
	$o_config = Configuration::load();
	$o_media_display_config = caGetMediaDisplayConfig();

	if (!is_array($context = $o_media_display_config->getAssoc($context))) { return null; }

	if (!$mimetype) { return $context; }
	foreach($context as $display_class => $display_class_info) {
		if (!is_array($mimetypes = $display_class_info['mimetypes'])) { continue; }

		if (in_array($mimetype, $mimetypes)) {
			$display_class_info['display_class'] = $display_class;
			return $display_class_info;
		}
	}
	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param
 *
 * @return
 */
function caGetMediaDisplayInfoForDisplayClass(string $context, string $display_class) : ?array {
	$o_config = Configuration::load();
	$o_media_display_config = caGetMediaDisplayConfig();

	if (!is_array($context = $o_media_display_config->getAssoc($context))) { return null; }

	if(!$display_class) { return $context; }
	if(isset($context[$display_class]) && is_array($context[$display_class])) {
		return $context[$display_class];
	}
	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 *
 * @param
 *
 * @return
 */
function caGetDefaultMediaViewer(string $mimetype) : ?string {
	$o_config = Configuration::load();
	$o_media_display_config = caGetMediaDisplayConfig();

	if (!is_array($defaults = $o_media_display_config->getAssoc('default_viewers'))) { return null; }

	foreach($defaults as $media_class => $info) {
		if (!is_array($mimetypes = $info['mimetypes'])) { continue; }

		if (in_array($mimetype, $mimetypes)) {
			return $info['viewer'];
		}
	}
	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns a list of "^" prefixed-tags (eg. ^forename) present in a template
 *
 * @param string $ps_template
 * @param array $pa_options
 *		stripOptions = Remove all options from returned tags. [Default is false]
 *		parseOptions = Parse tag options are return an array where each value is an array of options; the tag itself is put in the 'originalTag' key. [Default is false]
 *		firstPartOnly = Return a list of first elements in the tags. For the template "^ca_entities.entity_id ^ca_objects_x_entities.source_text" the list  ["ca_entites", "ca_objects_x_entities"] would be returned. [Default is false]
 *
 * @return array An array of tags, or an array of arrays when parseOptions option is set.
 */
function caGetTemplateTags($ps_template, $pa_options=null) {
	if(!strlen($ps_template)) { return []; }
	$key = caMakeCacheKeyFromOptions($pa_options ?? [], $ps_template);
	if(MemoryCache::contains($key, 'DisplayTemplateParserUtils')) { return MemoryCache::fetch($key, 'DisplayTemplateParserUtils'); }
	
	$va_tags = caExtractTagsFromTemplate($ps_template, $pa_options);

	if (caGetOption('firstPartOnly', $pa_options, false)) {
		foreach($va_tags as $vn_i => $vs_tag) {
			$va_tmp = explode('.', $vs_tag);
			$va_tags[$vn_i] = array_shift($va_tmp);
		}
		return array_unique($va_tags);
	} elseif (caGetOption('stripOptions', $pa_options, false)) {
		foreach($va_tags as $vn_i => $vs_tag) {
			$va_opts = caParseTagOptions($vs_tag);

			$va_tags[$vn_i] = $va_opts['tag'];
		}
	} elseif (caGetOption('parseOptions', $pa_options, false)) {
		foreach($va_tags as $vn_i => $vs_tag) {
			$va_opts = caParseTagOptions($vs_tag);

			$va_tags[$vn_i] = array_merge(array('originalTag' => $vs_tag), $va_opts);
		}
	}

	MemoryCache::save($key, $va_tags, 'DisplayTemplateParserUtils');
	return $va_tags;
}
# ------------------------------------------------------------------------------------------------
/**
 * Replace "^" prefix-edtags (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 array $pa_values Array of values; keys must match tag names
 * @param array $pa_options Supported options are:
 *			prefix = string to add to beginning of tags extracted from template before doing lookup into value array
 *			removePrefix = string to remove from tags extracted from template before doing lookup into value array
 *			getFrom = a model instance to draw data from. If set, $pa_values is ignored.
 *			quote = quote replacement values (Eg. ^ca_objects.idno becomes "2015.001" rather than 2015.001). Value containing quotes will be escaped with a backslash. [Default is false]
 *
 * @return string Output of processed template
 */
function caProcessTemplate($ps_template, $pa_values, $pa_options=null) {
	$key = caMakeCacheKeyFromOptions(['options' => $pa_options, 'values' => $pa_values], $ps_template);
	if(MemoryCache::contains($key, 'DisplayTemplateParserUtils')) { return MemoryCache::fetch($key, 'DisplayTemplateParserUtils'); }
	
	$ret = DisplayTemplateParser::processTemplate($ps_template, $pa_values, $pa_options);
	MemoryCache::save($key, $ret, 'DisplayTemplateParserUtils');
	
	return $ret;
}
# ------------------------------------------------------------------------------------------------
/**
 * Modified tag value based upon settings in supported tag directives
 * The directive syntax is <directive>:<directive parameters>
 * Supported directives include:
 *		LP = left pad value; parameters are in the format <padding>/<length>. Ex. LP:0/10 will left pad a string with zeros to a length of 10.
 *		RP = right pad value; parameters are in the format <padding>/<length>
 *		PREFIX = add prefix to value if not empty; parameter is the prefix text
 *		SUFFIX = add suffix to value if not empty;  parameter is the suffix text
 *
 * @param string $ps_value
 * @param array $pa_directives
 * @param array $pa_options Options include:
 *      omitUnits = Omit unit specifier on dimensional quantities. [Default is false]
 *      forceEnglishUnits =
 *      forceMetricUnits =
 *
 * @return string
 */
function caProcessTemplateTagDirectives($ps_value, $pa_directives, $pa_options=null) {
	global $g_ui_locale;
	if (!is_array($pa_directives) || !sizeof($pa_directives)) { return $ps_value; }
	
	$key = caMakeCacheKeyFromOptions(['options' => $pa_options, 'directives' => $pa_directives], $ps_value);
	if(MemoryCache::contains($key, 'DisplayTemplateParserUtils')) { return MemoryCache::fetch($key, 'DisplayTemplateParserUtils'); }

	$pb_omit_units = caGetOption('omitUnits', $pa_options, false);
	$force_english_units = caGetOption('forceEnglishUnits', $pa_options, null, ['validValues' => ['ft', 'in']]);
	$force_metric_units = caGetOption('forceMetricUnits', $pa_options, null, ['validValues' => ['m', 'cm', 'mm']]);

	$o_dimensions_config = Configuration::load(__CA_APP_DIR__."/conf/dimensions.conf");
	$va_add_periods_list = $o_dimensions_config->get('add_period_after_units');

	$vn_precision = ini_get('precision');
	ini_set('precision', 12);

	foreach($pa_directives as $vs_directive) {
		$va_tmp = explode(":", $vs_directive);
		switch(strtoupper($va_tmp[0])) {
			case 'UNITS':
				try {
					$vo_measurement = caParseLengthDimension($ps_value);

					$vs_measure_conv = null;
					$vs_units = strtolower($va_tmp[1]);
					if ($force_english_units && in_array($vs_units, ['in', 'in.', 'inches', 'inch', '"', 'ft', 'ft.', 'foot', 'feet', "'"])) {
						$vs_units = $force_english_units;
					}
					if ($force_metric_units && in_array($vs_units, ['m', 'cm'])) {
						$vs_units = $force_metric_units;
					}
					switch($vs_units) {
						case 'infrac':
						case 'english':
						case 'fractionalenglish':
							$vn_maximum_denominator = array_reduce($o_dimensions_config->get('display_fractions_for'), function($acc, $v) {
								$t = explode("/", $v); return ((int)$t[1] > $acc) ? (int)$t[1] : $acc;
							}, 0);

							$vs_in_inches = $vo_measurement->convertTo(Zend_Measure_Length::INCH, 15);
							$vn_value_in_inches = (float)preg_replace("![^0-9\.]+!", "", $vs_in_inches);
							$va_measure_conv = [];

							if (!$force_english_units && ($vn_value_in_inches > $o_dimensions_config->get('use_feet_for_display_up_to'))){
								if ($vn_in_miles = (int)($vn_value_in_inches / (12 * 5280))) { $va_measure_conv[] = "{$vn_in_miles} miles".((!$pb_omit_units && in_array('MILE', $va_add_periods_list)) ? '.' : ''); }
								$vn_value_in_inches -= ($vn_in_miles * (12 * 5280));
							}
							if (($force_english_units == 'ft') || (!$force_english_units && ($vn_value_in_inches > $o_dimensions_config->get('use_inches_for_display_up_to')))) {
								if ($vn_in_feet = (int)($vn_value_in_inches / 12)) { $va_measure_conv[] = "{$vn_in_feet} ft".((!$pb_omit_units && in_array('FEET', $va_add_periods_list)) ? '.' : ''); }
								$vn_value_in_inches -= (12 * $vn_in_feet);
							}
							if ($vn_value_in_inches > 0) {
								$vo_inches = caParseLengthDimension("{$vn_value_in_inches} in");
								$va_measure_conv[] = (in_array($vs_units, ['fractionalenglish', 'infrac']) ? caLengthToFractions($vn_value_in_inches, $vn_maximum_denominator, true) : $vo_inches->convertTo(Zend_Measure_Length::INCH, $o_dimensions_config->get('inch_decimal_precision'))).((!$pb_omit_units && in_array('INCH', $va_add_periods_list)) ? '.' : '');
							}
							$vs_measure_conv = join(" ", $va_measure_conv);

							break;
						case 'metric':
							$vs_in_cm = $vo_measurement->convertTo(Zend_Measure_Length::CENTIMETER, 15);
							$vn_value_in_cm = (float)preg_replace("![^0-9\.]+!", "", $vs_in_cm);

							if (($force_metric_units == 'mm') || (!$force_metric_units && ($vn_value_in_cm <= $o_dimensions_config->get('use_millimeters_for_display_up_to')))) {
								$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::MILLIMETER, (int)$o_dimensions_config->get('millimeter_decimal_precision')).((!$pb_omit_units && in_array('MILLIMETER', $va_add_periods_list)) ? '.' : '');
							} elseif (($force_metric_units == 'cm') || (!$force_metric_units && ($vn_value_in_cm <= $o_dimensions_config->get('use_centimeters_for_display_up_to')))) {
								$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::CENTIMETER, (int)$o_dimensions_config->get('centimeter_decimal_precision')).((!$pb_omit_units && in_array('CENTIMETER', $va_add_periods_list)) ? '.' : '');
							} elseif (($force_metric_units == 'm') || (!$force_metric_units && ($vn_value_in_cm <= $o_dimensions_config->get('use_meters_for_display_up_to')))) {
								$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::METER, (int)$o_dimensions_config->get('meter_decimal_precision')).((!$pb_omit_units && in_array('METER', $va_add_periods_list)) ? '.' : '');
							} else {
								$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::KILOMETER, $o_dimensions_config->get('kilometer_decimal_precision')).((!$pb_omit_units && in_array('KILOMETER', $va_add_periods_list)) ? '.' : '');
							}
							break;
						case 'm':
							$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::METER, $o_dimensions_config->get('meter_decimal_precision')).((!$pb_omit_units && in_array('METER', $va_add_periods_list)) ? '.' : '');
							break;
						case 'cm':
							$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::CENTIMETER, $o_dimensions_config->get('centimeter_decimal_precision')).((!$pb_omit_units && in_array('CENTIMETER', $va_add_periods_list)) ? '.' : '');
							break;
						case 'ft':
						case 'ft.':
						case 'foot':
						case 'feet':
						case "'":
							$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::FEET, $o_dimensions_config->get('feet_decimal_precision')).((!$pb_omit_units && in_array('FEET', $va_add_periods_list)) ? '.' : '');
							break;
						case 'in':
						case 'inch':
						case 'inches':
						case 'in.':
						case '"':
							$vs_measure_conv = $vo_measurement->convertTo(Zend_Measure_Length::INCH, $o_dimensions_config->get('inch_decimal_precision')).((!$pb_omit_units && in_array('INCH', $va_add_periods_list)) ? '.' : '');
							break;
					}
					
					if(preg_match("!\.[0]+[ ]+!", $vs_measure_conv)) { // remove trailing zero decimals
						$vs_measure_conv = preg_replace("!\.[0]+[ ]+!", " ", $vs_measure_conv);
					}

					if ($vs_measure_conv) {
						if ($pb_omit_units) { $vs_measure_conv = trim(preg_replace("![^\d\-\.\/ ]+!", "", $vs_measure_conv)); }
						$ps_value = "{$vs_measure_conv}";
					}
					if(is_array($pa_options['displayUnits'])) { 
						foreach($pa_options['displayUnits'] as $b => $a) {
							$ps_value = preg_replace("!".preg_quote($b, '!')."\.*$!u", $a, $ps_value);
						}
					}
				} catch (Exception $e) {
					// noop
				}
				break;
			case 'LP':		// left padding
				$va_params = explode("/", $va_tmp[1]);
				$vn_len = (int)$va_params[1];
				$vs_str = (string)$va_params[0];
				if (($vn_len > 0) && strlen($vs_str)) {
					$ps_value = str_pad($ps_value, $vn_len, $vs_str, STR_PAD_LEFT);
				}
				break;
			case 'RP':		// right padding
				$va_params = explode("/", $va_tmp[1]);
				$vn_len = (int)$va_params[1];
				$vs_str = (string)$va_params[0];
				if (($vn_len > 0) && strlen($vs_str)) {
					$ps_value = str_pad($ps_value, $vn_len, $vs_str, STR_PAD_RIGHT);
				}
				break;
			case 'PREFIX':
			case 'PX':
				if ((strlen($ps_value) > 0) && (strlen($va_tmp[1]))) {
					$ps_value = $va_tmp[1].$ps_value;
				}
				break;
			case 'SUFFIX':
			case 'SX':
				if ((strlen($ps_value) > 0) && (strlen($va_tmp[1]))) {
					$ps_value = $ps_value.$va_tmp[1];
				}
				break;
			case 'LOWER':
				$ps_value = strtolower($ps_value);
				break;
			case 'UPPER':
				$ps_value = strtoupper($ps_value);
				break;
			case 'NL2BR':
				$ps_value = nl2br($ps_value);
				break;
			case 'TRUNCATE':
				$ellipsis = ((bool)$va_tmp[2]);
				if((int)$va_tmp[1] > 0) { 
					$ellipsis = $ellipsis && (mb_strlen($ps_value) > (int)$va_tmp[1]);
					$ps_value = mb_substr($ps_value, 0, (int)$va_tmp[1]); 
					if($ellipsis) { $ps_value .= '...'; }
				}
				
				break;
		}
	}

	ini_set('precision', $vn_precision);
	MemoryCache::save($key, $ps_value, 'DisplayTemplateParserUtils');
	return $ps_value;
}
# ------------------------------------------------------------------------------------------------
/**
 * 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 array $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 to make the entire text a link in the absence of <l> tags.
 *		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]
 * 		indexWithIDs = Return array with indexes set to row_ids. [Default is false; use numeric indices starting with zero]
 *
 * @return mixed Output of processed templates
 */
function caProcessTemplateForIDs($ps_template, $pm_tablename_or_num, $pa_row_ids, $pa_options=null) {
	return DisplayTemplateParser::evaluate($ps_template, $pm_tablename_or_num, $pa_row_ids, $pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns display string for relationship bundles. Used by themes/default/bundles/ca_entities.php and the like.
 *
 * @param RequestHTTP $po_request
 * @param string $ps_table
 * @param array $pa_attributes
 * @param array $pa_options Options include:
 *		prefix = Bundle prefix to use when generating UI elements. [Default is null]
 *		makeLink = Render text as link to related item. [Default is false]
 *		display = Name of bundle field to use as display text for related item. [Default is "_display"]
 *		relationshipTypeDisplayPosition = Position to render relationship type in relative to display text. Valid values are "left", "right" and "none". [Default is "right"]
 *		editableRelationshipType = Render relationship type as drop-down to support in-place editing. [Default is to use table-specific settings in app.conf]
 *
 * @return string
 */
function caGetRelationDisplayString($po_request, $ps_table, $pa_attributes=null, $pa_options=null) {
	$o_config = Configuration::load();

	$ps_prefix = caGetOption('prefix', $pa_options, null);

	if (!($vs_relationship_type_display_position = caGetOption('relationshipTypeDisplayPosition', $pa_options, null))) {
		$vs_relationship_type_display_position = strtolower($o_config->get($ps_table.'_lookup_relationship_type_position'));
	}

	$vs_attr_str = _caHTMLMakeAttributeString(is_array($pa_attributes) ? $pa_attributes : []);
	$vs_display = "{".caGetOption('display', $pa_options, '_display')."}";
	if (caGetOption('makeLink', $pa_options, false)) {
		$vs_display = "<a href='".urldecode(caEditorUrl($po_request, $ps_table, '{'.Datamodel::primaryKey($ps_table).'}', false, array('rel' => true)))."' {$vs_attr_str}>{$vs_display}</a>";
	}

	$vs_reltype_disp = caGetOption('editableRelationshipType', $pa_options, (bool)$o_config->get("{$ps_table}_lookup_relationship_type_editable")) ? "<select name='{$ps_prefix}_type_id{n}' id='{$ps_prefix}_type_id{n}' class='listRelRelationshipTypeEdit'></select>" : "({{relationship_typename}}) <input type='hidden' name='{$ps_prefix}_type_id{n}' id='{$ps_prefix}_type_id{n}' value='{type_id}'/>";

	switch($vs_relationship_type_display_position) {
		case 'left':
			return "{$vs_reltype_disp} {$vs_display}";
			break;
		case 'none':
			return "{$vs_display}";
			break;
		default:
		case 'right':
			return "{$vs_display} {$vs_reltype_disp}";
			break;
	}
}
# ------------------------------------------------------------------------------------------------
/**
 * Determines if template includes link tags (<l>).
 *
 * @param string $template
 
 * @return bool
 */
function caTemplateHasLinks($template) {
	return preg_match('!<l>!', $template);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date/time as a localized string for display, subject to the settings in the app/conf/datetime.conf configuration
 *
 * @param null|int $pn_timestamp Unix timestamp for date/time to localize; if omitted defaults to current date and time.
 * @param null|array $pa_options All options supported by TimeExpressionParser::getText() are supported
 * @return string Localized date/time expression
 */
function caGetLocalizedDate($pn_timestamp=null, $pa_options=null) {
	if (!$pn_timestamp) { $pn_timestamp = time(); }
	$o_tep = new TimeExpressionParser();

	$o_tep->setUnixTimestamps($pn_timestamp, $pn_timestamp);

	return $o_tep->getText($pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date/time as a localized string for display, subject to the settings in the app/conf/datetime.conf configuration
 *
 * @param int $pn_timestamp Historic timestamp for date/time to localize; if omitted defaults to current date and time.
 * @param array $pa_options All options supported by TimeExpressionParser::getText() are supported
 *
 * @return string Localized date/time expression
 */
function caGetLocalizedHistoricDate($pn_timestamp=null, $pa_options=null) {
	if (!$pn_timestamp) { return ''; }
	$o_tep = new TimeExpressionParser();

	$o_tep->setHistoricTimestamps($pn_timestamp, $pn_timestamp);

	return $o_tep->getText($pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date range as a localized string for display, subject to the settings in the app/conf/datetime.conf configuration
 *
 * @param int $pn_start_timestamp Historic start timestamp for date range to localize
 * @param int $pn_end_timestamp Historic end timestamp for date range to localize
 * @param array $pa_options All options supported by TimeExpressionParser::getText() are supported
 *
 * @return string Localized date/time expression
 */
function caGetLocalizedHistoricDateRange($pn_start_timestamp, $pn_end_timestamp, $pa_options=null) {
	$o_tep = new TimeExpressionParser();

	$o_tep->setHistoricTimestamps($pn_start_timestamp, $pn_end_timestamp);

	return $o_tep->getText($pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date range as a localized string for display, subject to the settings in the app/conf/datetime.conf configuration
 *
 * @param int $pn_start_timestamp Start of date range, as Unix timestamp
 * @param int $pn_end_timestamp End of date range, as Unix timestamp
 * @param array $pa_options All options supported by TimeExpressionParser::getText() are supported
 *
 * @return string Localized date range expression
 */
function caGetLocalizedDateRange($pn_start_timestamp, $pn_end_timestamp, $pa_options=null) {
	$o_tep = new TimeExpressionParser();

	$o_tep->setUnixTimestamps($pn_start_timestamp, $pn_end_timestamp);

	return $o_tep->getText($pa_options);
}
# ------------------------------------------------------------------------------------------------
/**
 * Normalize arbitrarily precise date expression to century, decade, year, month or day
 *
 * @param string $ps_expression A valid date expression parseable by the TimeExpressionParser class
 * @param string $ps_normalization Level to normalize to. Valid values are centuries, decades, years, months, days
 * @param array $pa_options
 *			delimiter = A string to join multiple values with when returning normalized date range as a string. Default is semicolon followed by space ("; ")
 *			returnAsArray = If set an array of normalized values will be returned rather than a string. Default is false.
 * @return array|bool The normalized expression. If the expression normalizes to multiple values (eg. a range of years being normalized to months) then the values will be joined with a delimiter and returned as a string unless the "returnAsArray" option is set.
 */
function caNormalizeDateRange($ps_expression, $ps_normalization, $pa_options=null) {
	$o_tep = new TimeExpressionParser();
	if ($o_tep->parse($ps_expression)) {
		$va_dates = $o_tep->getHistoricTimestamps();
		$va_vals= $o_tep->normalizeDateRange($va_dates['start'], $va_dates['end'], $ps_normalization);

		if (isset($pa_options['returnAsArray']) && $pa_options['returnAsArray']) {
			return $va_vals;
		} else {
			$vs_delimiter = isset($pa_options['returnAsArray']) ? $pa_options['returnAsArray'] : "; ";
			return join($vs_delimiter, $va_vals);
		}
	}
	return null;
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date range for TimelineJS
 *
 * @param array $pa_historic_timestamps
 * @return array
 */
function caGetDateRangeForTimelineJS($pa_historic_timestamps) {
	$o_tep = new TimeExpressionParser();

	$va_start = $o_tep->getHistoricDateParts($pa_historic_timestamps[0]);
	$va_end = $o_tep->getHistoricDateParts($pa_historic_timestamps[1]);

	if ($va_start['year'] <= -2000000) { $va_start['year'] = -50000; }
	if ($va_end['year'] >= 2000000) { $va_end['year'] = date("Y"); }

	if (
		($va_start['year'] == $va_end['year'])
		&&
		($va_start['month'] == 1) && ($va_end['month'] == 12)
		&&
		($va_start['day'] == 1) && ($va_end['day'] == 31)
	) {
		return [
			'start_date' => [
				'year' => $va_start['year']
			],
			'end_date' => [
				'year' => $va_end['year']
			],
		];
	} elseif(
		($va_start['year'] == $va_end['year'])
		&&
		($va_start['month'] == $va_end['month'])
		&&
		(($va_start['day'] == 1) && ($va_end['day'] == $o_tep->daysInMonth($va_end['end'])))
	) {
		return [
			'start_date' => [
				'year' => $va_start['year'],
				'month' => $va_start['month']
			],
			'end_date' => [
				'year' => $va_end['year'],
				'month' => $va_end['month']
			],
		];
	} else {
		return [
			'start_date' => [
				'year' => $va_start['year'],
				'month' => $va_start['month'],
				'day' => $va_start['day']
			],
			'end_date' => [
				'year' => $va_end['year'],
				'month' => $va_end['month'],
				'day' => $va_end['day']
			],
		];
	}
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns date range for calendar display. Return value is an array with the following keys:
 *
 * start = An array with keys for each component of the starting date/time (Eg. day, month, year)
 * end = An array with keys for each component of the ending date/time (Eg. day, month, year)
 * start_iso = The starting date/time as an ISO-8601 format string
 * end_iso = The ending date/time as an ISO-8601 format string
 *
 * @param int $pn_start_timestamp Start of date range, as Unix timestamp
 * @param array $pa_options All options supported by TimeExpressionParser::getText() are supported
 *
 * @return array
 */
function caGetDateRangeForCalendar($pa_historic_timestamps, $pa_options=null) {
	$o_tep = new TimeExpressionParser();

	$va_start = $o_tep->getHistoricDateParts($pa_historic_timestamps[0]);
	$va_end = $o_tep->getHistoricDateParts($pa_historic_timestamps[1]);

	if ($va_start['year'] < 0) { $va_start['year'] = 1900; }
	if ($va_end['year'] >= 2000000) { $va_end['year'] = date("Y"); }

	return array(
		'start'=> $va_start,
		'end' => $va_end,
		'start_iso' => $o_tep->getISODateTime($va_start, 'FULL'),
		'end_iso' => $o_tep->getISODateTime($va_end, 'FULL')
	);
}
# ------------------------------------------------------------------------------------------------
/**
 * Returns text describing dimensions of object representation
 *
 * @param DbResult or ca_object_representations instance $po_rep An object containing representation data. Can be either a DbResult object (ie. a query result) or ca_object_representations instance (an instance representing a row in the ca_object_representation class)
 * @param string $ps_version the name of the media version to return dimensions information for
 * @param array $pa_options Array of options, including:
 *		returnAsArray = if set an array with elements of the dimensions display text is returned
 *
 * @return mixed Text ready for display describing dimensions of the representation's media. Can be array if 'returnAsArray' option is set.
 */
function caGetRepresentationDimensionsForDisplay($po_rep, $ps_version, $pa_options=null) {
	$va_tmp = $po_rep->getMediaInfo('media', $ps_version);
	$va_dimensions = array();
		if (isset($va_tmp['WIDTH']) && isset($va_tmp['HEIGHT'])) {
		if (($vn_w = $va_tmp['WIDTH']) && ($vn_h = $va_tmp['HEIGHT'])) {
			$va_dimensions[] = $va_tmp['WIDTH'].'p x '.$va_tmp['HEIGHT'].'p';
		}
	}
	if (isset($va_tmp['PROPERTIES']['bitdepth']) && ($vn_depth = $va_tmp['PROPERTIES']['bitdepth'])) {
		$va_dimensions[] = intval($vn_depth).' bpp';
	}
	if (isset($va_tmp['PROPERTIES']['colorspace']) && ($vs_colorspace = $va_tmp['PROPERTIES']['colorspace'])) {
		$va_dimensions[] = $vs_colorspace;
	}
	if (isset($va_tmp['PROPERTIES']['resolution']) && is_array($va_resolution = $va_tmp['PROPERTIES']['resolution'])) {
		if (isset($va_resolution['x']) && isset($va_resolution['y']) && $va_resolution['x'] && $va_resolution['y']) {
			// TODO: units for resolution? right now assume pixels per inch
			if ($va_resolution['x'] == $va_resolution['y']) {
				$va_dimensions[] = $va_resolution['x'].'ppi';
			} else {
				$va_dimensions[] = $va_resolution['x'].'x'.$va_resolution['y'].'ppi';
			}
		}
	}
	if (isset($va_tmp['PROPERTIES']['duration']) && ($vn_duration = $va_tmp['PROPERTIES']['duration'])) {
		$va_dimensions[] = caFormatInterval($vn_duration);
	}
	if (isset($va_tmp['PROPERTIES']['pages']) && ($vn_pages = $va_tmp['PROPERTIES']['pages'])) {
		$va_dimensions[] = $vn_pages.' '.(($vn_pages == 1) ? _t('page') : _t('pages'));
	}
	if (!isset($va_tmp['PROPERTIES']['filesize']) || !($vn_filesize = $va_tmp['PROPERTIES']['filesize'])) {
		$vn_filesize = @filesize($po_rep->getMediaPath('media', $ps_version));
	}
	if ($vn_filesize) {
		$va_dimensions[] = caFormatFileSize($vn_filesize);
	}

	if(isset($pa_options['returnAsArray']) && $pa_options['returnAsArray']) {
		return $va_dimensions;
	}
	return join('; ', $va_dimensions);
}
# ------------------------------------------------------------------------------------------------
/**
 *
 * @return string
 */
function caFormatFileSize($pn_bytes) {
	if ($pn_bytes >= 1073741824) {
		$pn_bytes = number_format($pn_bytes/1073741824, 2).'gb';
	}
	elseif ($pn_bytes >= 1048576) {
		$pn_bytes = number_format($pn_bytes/1048576, 2).'mb';
	} elseif ($pn_bytes >= 1024) {
		$pn_bytes = number_format($pn_bytes/1024, 2).'kb';
	} elseif ($pn_bytes > 1) {
		$pn_bytes = $pn_bytes.'b';
	} elseif ($pn_bytes == 1) {
		$pn_bytes = $pn_bytes.'b';
	} else {
		$pn_bytes = '0b';
	}

	return $pn_bytes;
}
# ------------------------------------------------------------------------------------------------
/**
 * Converts result set into display labels for relationship lookup
 *
 * @param SearchResult $qr_rel_items
 * @param BaseModel $pt_rel
 * @param array $pa_options Array of options, including:
 *		stripTags = default is false
 * 		exclude = list of primary key values to omit from returned list
 *		config =
 *		limit = maximum number of items to return; if omitted all items are returned
 *		inlineCreateMessage =
 *		inlineCreateQuery =
 *		inlineCreateMessageDoesNotExist =
 *		template =
 *		relatedItems = 
 *		sortOrder = 
 *		self_id = ID of record being related to when relationship is to self (Eg. object-object). This id will be omitted from returned items.
 *		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.
 * @return mixed
 */
function caProcessRelationshipLookupLabel($qr_rel_items, $pt_rel, $pa_options=null) {
	$va_initial_values = array();

	$vs_hier_fld 					= $pt_rel->getProperty('HIERARCHY_ID_FLD');
	$vs_idno_fld 					= $pt_rel->getProperty('ID_NUMBERING_ID_FIELD');
	$vs_idno_sort_fld 				= $pt_rel->getProperty('ID_NUMBERING_SORT_FIELD');
	$vs_rel_pk            			= caGetOption('primaryKey', $pa_options, $pt_rel->primaryKey());
	$vs_rel_table         			= caGetOption('table', $pa_options, $pt_rel->tableName());
	$self_id         				= caGetOption('self_id', $pa_options, null, ['castTo' => 'int']);

	$o_config = (!isset($pa_options['config']) || !is_object($pa_options['config'])) ? Configuration::load() : $pa_options['config'];

	$pn_limit = 								caGetOption('limit', $pa_options, null);
	$ps_inline_create_message = 				caGetOption('inlineCreateMessage', $pa_options, null);
	$ps_inline_create_does_not_exist_message = 	caGetOption('inlineCreateMessageDoesNotExist', $pa_options, null);
	$ps_inline_create_query = 					caGetOption('inlineCreateQuery', $pa_options, null);
	$ps_inline_create_query_lc = 				mb_strtolower($ps_inline_create_query);

	$ps_empty_result_message = 					caGetOption('emptyResultMessage', $pa_options, null);
	$ps_empty_result_query = 					caGetOption('emptyResultQuery', $pa_options, null);

	$vs_template =								caGetOption('template', $pa_options, null);

	$va_exclude = 								caGetOption('exclude', $pa_options, array(), array('castTo' => 'array'));
	$po_request = 								caGetOption('request', $pa_options, null);
	if(!$po_request) { global $g_request; $po_request = $g_request; }

	if($self_id) { $va_exclude[] = $self_id; }

	if (!is_array($va_display_format = $o_config->getList("{$vs_rel_table}_lookup_settings"))) { $va_display_format = ['^label']; }
	if (!($vs_display_delimiter = $o_config->get("{$vs_rel_table}_lookup_delimiter"))) { $vs_display_delimiter = ''; }
	if (!$vs_template) { $vs_template = join($vs_display_delimiter, $va_display_format); }

	$va_related_item_info = $va_parent_ids = $va_hierarchy_ids = array();
	$va_items = array();

	$va_primary_ids = (method_exists($pt_rel, "isSelfRelationship") && ($vb_is_self_rel = $pt_rel->isSelfRelationship())) ? caGetOption("primaryIDs", $pa_options, null) : null;

	$t_rel = Datamodel::getInstanceByTableName($vs_rel_table, true);
	/** @var ca_sets $t_set */
	$t_set = Datamodel::getInstance('ca_sets', true);
	$vs_type_id_fld = method_exists($t_rel, 'getTypeFieldName') ? $t_rel->getTypeFieldName() : null;

	$vn_c = 0;
	$vb_include_inline_add_does_not_exist_message = $vb_include_empty_result_message = false;
	$vb_include_inline_add_message = true;

	if (is_object($qr_rel_items)) {
		if (!$qr_rel_items->numHits()) {
			if ($ps_inline_create_does_not_exist_message) {
				$vb_include_inline_add_does_not_exist_message = true;
				$vb_include_inline_add_message = false;
			} else {
				if ($ps_empty_result_message) {
					$vb_include_empty_result_message = true;
				}
			}
		} else {
			$vs_table = 	$qr_rel_items->tableName();
			$vs_pk = 		$qr_rel_items->primaryKey();

			$vs_idno_fld = Datamodel::getTableProperty($vs_table, 'ID_NUMBERING_ID_FIELD');
			$va_primary_ids = (method_exists($pt_rel, "isSelfRelationship") && ($vb_is_self_rel = $pt_rel->isSelfRelationship())) ? caGetOption("primaryIDs", $pa_options, null) : null;

			while($qr_rel_items->nextHit()) {
				$vn_id = $qr_rel_items->get("{$vs_rel_table}.{$vs_rel_pk}");
				if(($qr_rel_items->tableName() == 'ca_sets') && ($po_request instanceof RequestHTTP)) {
					if(!$t_set->haveAccessToSet($po_request->getUserID(), __CA_ACL_EDIT_ACCESS__, $vn_id)) {
						continue;
					}
				}
				if(in_array($vn_id, $va_exclude)) { continue; }

				$va_item = array(
					'id' => $vn_id,
					$vs_rel_pk => $vn_id
				);

				if ($vs_type_id_fld) {
					$va_item['type_id'] = $qr_rel_items->get("{$vs_rel_table}.{$vs_type_id_fld}");
				}

				$va_item['_display'] = caProcessTemplateForIDs( $vs_template, $vs_table,
					array( $qr_rel_items->get( "{$vs_table}.{$vs_pk}" ) ),
					array(
						'returnAsArray' => false,
						'returnAsLink' => false,
						'delimiter' => caGetOption( 'delimiter', $pa_options, $vs_display_delimiter ),
						'resolveLinksUsing' => $vs_rel_table,
						'primaryIDs' => $va_primary_ids
					) );
				$va_item['label'] = mb_strtolower($qr_rel_items->get("{$vs_table}.preferred_labels"));
				if ($vs_idno_fld) { $va_item['idno'] = mb_strtolower($qr_rel_items->get("{$vs_table}.{$vs_idno_fld}")); }

				$va_items[$vn_id] = $va_item;

				$vn_c++;
				if (($pn_limit) && ($pn_limit <= $vn_c)) {
					break;
				}
			}
		}
	}

	if (isset($pa_options['relatedItems']) && is_array($pa_options['relatedItems']) && sizeof($pa_options['relatedItems'])) {
		$va_tmp = array();
		foreach ($pa_options['relatedItems'] as $vn_relation_id => $va_relation) {
			$va_items[$va_relation[$vs_rel_pk]]['relation_id'] = $va_relation['relation_id'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['relationship_type_id'] = $va_items[$va_relation[$vs_rel_pk]]['type_id'] = ($va_relation['relationship_type_id'] ?? null) ? ($va_relation['direction']) ?  $va_relation['direction'].'_'.$va_relation['relationship_type_id'] : $va_relation['relationship_type_id'] : null;
			$va_items[$va_relation[$vs_rel_pk]]['rel_type_id'] = $va_relation['relationship_type_id'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['item_type_id'] = $va_relation['item_type_id'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['relationship_typename'] = $va_relation['relationship_typename'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['idno'] = $va_relation[$vs_idno_fld] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['idno_sort'] = $va_relation[$vs_idno_sort_fld] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['label'] = $va_relation['label'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['direction'] = $va_relation['direction'] ?? null;
			$va_items[$va_relation[$vs_rel_pk]]['effective_date'] = $va_relation['effective_date'] ?? null;

			if (isset($va_relation['surname'])) {		// pass forename and surname entity label fields to support proper sorting by name
				$va_items[$va_relation[$vs_rel_pk]]['surname'] = $va_relation['surname'];
				$va_items[$va_relation[$vs_rel_pk]]['forename'] = $va_relation['forename'];
			}

			if (!isset($va_items[$va_relation[$vs_rel_pk]][$vs_rel_pk]) || !$va_items[$va_relation[$vs_rel_pk]][$vs_rel_pk]) {
				$va_items[$va_relation[$vs_rel_pk]][$vs_rel_pk] = $va_items[$va_relation[$vs_rel_pk]]['id'] = $va_relation[$vs_rel_pk];
			}

			if ($vs_template) {
				$pk = is_object($qr_rel_items) ? $qr_rel_items->primaryKey() : null;
				$va_items[$va_relation[$vs_rel_pk]]['_display'] = caProcessTemplateForIDs($vs_template, $pt_rel->tableName(), array($va_relation['relation_id'] ?? null ? $va_relation['relation_id'] : $va_relation[$pk] ?? null), array('returnAsArray' => false, 'returnAsLink' => false, 'delimiter' => caGetOption('delimiter', $pa_options, $vs_display_delimiter), 'resolveLinksUsing' => $vs_rel_table, 'primaryIDs' => $va_primary_ids));
			} else {
				$va_items[$va_relation[$vs_rel_pk]]['_display'] = $va_items[$va_relation[$vs_rel_pk]]['label'];
			}

			$va_tmp[$vn_relation_id] = $va_items[$va_relation[$vs_rel_pk]];
		}
		$va_items = $va_tmp;
		unset($va_tmp);
	} elseif(method_exists($pt_rel, 'isSelfRelationship') && $pt_rel->isSelfRelationship()) {
		foreach($va_items as $k => $item) {
			if(!isset($item['relation_id'])) { continue; }
			if($pt_rel->load($item['relation_id'])) {
				$va_items[$k]['direction'] = strtolower($pt_rel->getOrientationForRelationship($va_primary_ids));
				$va_items[$k]['relationship_type_id'] = $va_items[$k]['type_id'] = (($va_items[$k]['type_id'] ?? null) ? ($va_items[$k]['direction'] ? $va_items[$k]['direction'].'_'.$va_items[$k]['type_id'] : $va_items[$k]['type_id']) : null);
			}
		}
	}
	if(is_array($pa_options['sortOrder'] ?? null)) {
		$va_items_sorted = [];
		foreach($pa_options['sortOrder'] as $id) {
			$va_items_sorted[$id] = $va_items[$id];
		}
		$va_items = $va_items_sorted;
	}

	foreach ($va_items as $va_item) {
		$vn_id = $va_item[$vs_rel_pk];
		if(in_array($vn_id, $va_exclude)) { continue; }


		$vs_display = html_entity_decode($va_item['_display'], ENT_HTML5, "UTF-8");
		if (isset($pa_options['stripTags']) && $pa_options['stripTags']) {
			if (preg_match('!(<[A-Za-z0-9]+[ ]+[A-Za-z0-9 ,;\&\-_]*>)!', $vs_display, $va_matches)) {	// convert text in <> to non-tags if the text has only letters, numbers and spaces in it
				array_shift($va_matches);
				foreach($va_matches as $vs_match) {
					$vs_display = str_replace($vs_match, htmlspecialchars($vs_match), $vs_display);
				}
			}
			$vs_display = trim(strip_tags($vs_display));

			$vs_label = $va_item['label'];
			if (preg_match('!(<[A-Za-z0-9]+[ ]+[A-Za-z0-9 ,;\&\-_]*>)!', $vs_label, $va_matches)) {	// convert text in <> to non-tags if the text has only letters, numbers and spaces in it
				array_shift($va_matches);
				foreach($va_matches as $vs_match) {
					$vs_label = str_replace($vs_match, htmlspecialchars($vs_match), $vs_label);
				}
			}
			$va_item['label'] = trim(strip_tags($vs_label));

		}

		$vs_display_lc = mb_strtolower($vs_display);
		if (($vs_display_lc == $ps_inline_create_query_lc) || (isset($va_item['label']) && ($va_item['label'] == $ps_inline_create_query_lc))) {
			$vb_include_inline_add_message = false;
		}

		$po_request = caGetOption('request',$pa_options);
		if($po_request && ca_editor_uis::loadDefaultUI($pt_rel->tableName(),$po_request,$va_item['rel_type_id'] ?? null)) {
			$va_item['hasInterstitialUI'] = true;
		} else {
			$va_item['hasInterstitialUI'] = false;
		}

		if(!($va_item['relation_id'] ?? null)) {
			$va_item['relation_id'] = $va_item[$vs_rel_pk] ?? null;
		}
		$va_item['relation_id'] = (int)$va_item['relation_id'] ;
		
		$va_initial_values[$va_item['relation_id']] = array_merge(
			$va_item,
			array(
				'label' => $vs_display
			)
		);
	}

	if($vb_include_inline_add_message && $ps_inline_create_message) {
		array_unshift($va_initial_values, 
				array(
					'label' => $ps_inline_create_message,
					'id' => 0,
					$vs_rel_pk => 0,
					'_query' => $ps_inline_create_query
				)
		);
	} elseif ($vb_include_inline_add_does_not_exist_message && $ps_inline_create_does_not_exist_message) {
		array_unshift($va_initial_values,
				array(
					'label' => $ps_inline_create_does_not_exist_message,
					'id' => 0,
					$vs_rel_pk => 0,
					'_query' => $ps_inline_create_query
				)
		);
	} elseif ($vb_include_empty_result_message) {
		array_unshift($va_initial_values,
			array(
				'label' => $ps_empty_result_message,
				'id' => -1,
				$vs_rel_pk => -1,
				'_query' => $ps_empty_result_query
			)
		);
	}

	return $va_initial_values;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caGetMediaMimetypeToDisplayClassMap($ps_context) {
	$o_config = Configuration::load();
	$o_media_display_config = caGetMediaDisplayConfig();

	if (!is_array($va_context = $o_media_display_config->getAssoc($ps_context))) { return null; }

	$va_map = array();
	foreach($va_context as $vs_media_class => $va_media_class_info) {
		if (!is_array($va_mimetypes = $va_media_class_info['mimetypes'])) { continue; }

		foreach($va_mimetypes as $vs_mimetype) {
			$va_map[$vs_mimetype] = $vs_media_class;
		}
	}
	return $va_map;
}
# ------------------------------------------------------------------------------------------------
/**
 *
 */
function caObjectsDisplayDownloadLink($po_request, $pn_object_id = null, $pt_representation = null) {
	$o_config = caGetDetailConfig();
	$vn_can_download = false;
	if($vs_allow = $o_config->get(['allowObjectRepresentationDownload', 'allow_ca_objects_representation_download'])){
		switch($vs_allow){
			case "anyone":
				$vn_can_download = true;
				break;
			# ------------------------------------------
			case "logged_in":
				if ($po_request->isLoggedIn()) {
					$vn_can_download = true;
				}
				break;
			# ------------------------------------------
			case "logged_in_privileged":
				if (($po_request->isLoggedIn()) && ($po_request->user->canDoAction('can_download_media'))) {
					$vn_can_download = true;
				}
				break;
			# ------------------------------------------
			default:
			case "never":
				$vn_can_download = false;
				break;
			# ------------------------------------------
		}
	}

	$va_types = $o_config->get(['restrictObjectRepresentationDownloadToObjectTypes', 'allow_ca_objects_representation_download_types']);
	if($pn_object_id && $vn_can_download && is_array($va_types) && sizeof($va_types)){
		# --- see if current object's type is in the confirgured array
		$t_object = new ca_objects($pn_object_id);
		$t_list_item = new ca_list_items($t_object->get("type_id"));
		$va_object_type_code = $t_list_item->get("idno");
		if(!in_array($va_object_type_code, $va_types)){
			$vn_can_download = false;
		}
	}
	$va_download_access_settings = $po_request->config->get("download_access_settings");
	if((!$po_request->config->get("dont_enforce_access_settings")) && $pt_representation && is_array($va_download_access_settings) && sizeof($va_download_access_settings)){
		$vn_rep_access = $pt_representation->get("access");
		if($vn_rep_access && !in_array($vn_rep_access, $va_download_access_settings)){
			$vn_can_download = false;
		}
	}
	return $vn_can_download;
}
# ------------------------------------------------------------------
/**
 * Creates links to the appropriate editor (in Providence) or detail page (in Pawtucket) from supplied text and ids.
 * Used in SearchResult::get() and BundlableLabelableBaseModelWithAttributes::get() to automatically generate links when fetching
 * information from related tables.
 *
 * @param array $pa_text An array of strings to create links for
 * @param string $ps_table_name The name of the table/record to which the links refer
 * @param array $pa_row_ids Array of row_ids to link to. Values must correspond by index with those in $pa_text
 * @param string $ps_class Optional CSS class to apply to links
 * @param string $ps_target
 * @param array $pa_options Supported options are:
 *		requireLinkTags = If set then links are only added when explicitly defined with <l> tags. Default is to make the entire text a link in the absence of <l> tags.
 * 		addRelParameter =
 *      absolute = Return absolute urls [Default is false]
 *		bundle = When generating an editor link, will cause link to be to screen containing the specified bundle. If omitted or the bundle does not exist in the editor the default screen will be shown. [Default is null]
 *		attributes = array of attributes to set on returned link tags. [Default is null]
 *
 * @return array A list of HTML links
 */
function caCreateLinksFromText($pa_text, $ps_table_name, $pa_row_ids, $ps_class=null, $ps_target=null, $pa_options=null) {
	if (!in_array(__CA_APP_TYPE__, array('PROVIDENCE', 'PAWTUCKET'))) { return $pa_text; }
	if (__CA_APP_TYPE__ == 'PAWTUCKET') {
		$o_config = Configuration::load();
	}

	$pb_add_rel = caGetOption('addRelParameter', $pa_options, false);

	$vb_can_handle_target = false;
	if ($ps_target) {
		$o_app_plugin_manager = new ApplicationPluginManager();
		$vb_can_handle_target = $o_app_plugin_manager->hookCanHandleGetAsLinkTarget(array('target' => $ps_target));
	}

	$va_links = array();
	$va_link_opts = ['absolute' => $pa_options['absolute'] ?? false, 'bundle' => $pa_options['bundle'] ?? 'ca_object_components_list'];

	global $g_request;
	if (!$g_request) { return $pa_text; }

	foreach($pa_text as $vn_i => $vs_text) {
		$vs_text = preg_replace("!([A-Za-z0-9]+)='([^']*)'!", "$1=\"$2\"", $vs_text);
		$va_l_tags = array();

		$o_doc = str_get_dom($vs_text);
		$o_links = $o_doc('l');

		foreach($o_links as $o_link) {
			if (!$o_link) { continue; }
			$vs_html = $o_link->html();

			$vs_content = preg_replace("!^<[^\>]+>!", "", $vs_html);
			$vs_content = preg_replace("!<[^\>]+>$!", "", $vs_content);

			$link_attributes = $o_link->attributes ?? [];
			unset($link_attributes['relativeTo']);
			$va_l_tags[] = ['directive' => html_entity_decode($vs_html), 'content' => $vs_content, 'attributes' => $link_attributes];
		}

		if (sizeof($va_l_tags)) {
			$vs_content = html_entity_decode($vs_text);
			$vs_content = preg_replace_callback("/(&#[0-9]+;)/", function($m) { return mb_convert_encoding($m[1], "UTF-8", "HTML-ENTITIES"); }, $vs_content);
			foreach($va_l_tags as $va_l) {
				if ($vb_can_handle_target) {
					$va_params = array('request' => $g_request, 'content' => $va_l['content'], 'table' => $ps_table_name, 'id' => $pa_row_ids[$vn_i], 'classname' => $ps_class, 'target' => $ps_target, 'additionalParameters' => null, 'options' => $va_link_opts);
					$va_params = $o_app_plugin_manager->hookGetAsLink($va_params);
					$vs_link_text = $va_params['tag'];
				} else {
					switch(__CA_APP_TYPE__) {
						case 'PROVIDENCE':
							$vs_link_text= caEditorLink($g_request, $va_l['content'], $ps_class, $ps_table_name, $pa_row_ids[$vn_i], ($pb_add_rel ? array('rel' => true) : []), $va_l['attributes'], $va_link_opts);
							break;
						case 'PAWTUCKET':
							$vs_link_text= caDetailLink($g_request, $va_l['content'], $ps_class, $ps_table_name, $pa_row_ids[$vn_i], null, $va_l['attributes'], $va_link_opts);
							break;
					}
				}

				if ($vs_link_text) {
					$vs_content = str_replace($va_l['directive'], $vs_link_text, $vs_content);
				} else {
					$vs_content = str_replace($va_l['directive'], $va_l['content'], $vs_content);
				}
			}
			$va_links[$vn_i] = $vs_content;
		} else {
			if (isset($pa_options['requireLinkTags']) && $pa_options['requireLinkTags']) {
				$va_links[$vn_i] = $vs_text;
				continue;
			}
			if ($vb_can_handle_target) {
				$va_params = array('request' => $g_request, 'content' => $vs_text, 'table' => $ps_table_name, 'id' => $pa_row_ids[$vn_i], 'classname' => $ps_class, 'target' => $ps_target, 'additionalParameters' => null, 'options' => $va_link_opts);
				$va_params = $o_app_plugin_manager->hookGetAsLink($va_params);
				$va_links[$vn_i]  = $va_params['tag'];
			} else {
				$link_attributes = caGetOption('attributes', $pa_options, null);
				switch(__CA_APP_TYPE__) {
					case 'PROVIDENCE':
						$va_links[$vn_i] = ($vs_link = caEditorLink($g_request, $vs_text, $ps_class, $ps_table_name, $pa_row_ids[$vn_i], ($pb_add_rel ? ['rel' => true] : []), $link_attributes, $va_link_opts)) ? $vs_link : $vs_text;
						break;
					case 'PAWTUCKET':
						$va_links[$vn_i] = ($vs_link = caDetailLink($g_request, $vs_text, $ps_class, $ps_table_name, $pa_row_ids[$vn_i], null, $link_attributes, $va_link_opts)) ? $vs_link : $vs_text;
						break;
					default:
						$va_links[$vn_i] = $vs_text;
						break;
				}
			}
		}
	}
	return $va_links;
}
# ------------------------------------------------------------------
/**
 *
 *
 * @param BaseModel $pt_subject
 * @param string $ps_related_table
 * @param array $pa_bundle_settings
 * @param array $pa_options Supported options are:
 *
 *
 * @return string
 */
function caGetBundleDisplayTemplate($pt_subject, $ps_related_table, $pa_bundle_settings, $pa_options=null) {
	$vs_template = null;
	if(strlen(trim($pa_bundle_settings['display_template'] ?? null))) {
		$vs_template = trim($pa_bundle_settings['display_template']);
	}

	// If no display_template set try to get a default out of the app.conf file
	if (!$vs_template) {
		if(!trim($vs_template = $pt_subject->getAppConfig()->get("{$ps_related_table}_default_editor_display_template"))) {	// use explicit setting
			if (is_array($va_lookup_settings = $pt_subject->getAppConfig()->getList("{$ps_related_table}_lookup_settings"))) {	// fall back to derive from lookup setting
				if (!($vs_lookup_delimiter = $pt_subject->getAppConfig()->get("{$ps_related_table}_lookup_delimiter"))) { $vs_lookup_delimiter = ''; }
				$vs_template = join($vs_lookup_delimiter, $va_lookup_settings);
			}
		}
	}

	// If no app.conf default then just show preferred_labels
	if (!$vs_template) {
		$vs_template = "^preferred_labels";
	}
	return $vs_template;
}
# ---------------------------------------
/**
 * Generates show/hide control HTML for bundles
 *
 * @param RequestHTTP $po_request
 * @param string $ps_id_prefix
 * @param array $pa_settings bundle placement option array
 * @param bool $pb_has_value
 * @param string $ps_preview_init string to initialize bundle preview content section with
 *
 * @return string HTML implementing the control
 */
function caEditorBundleShowHideControl($po_request, $ps_id_prefix, $pa_settings=null, $pb_has_value=false, $ps_preview_init="&nbsp;") {
	if (caGetOption('dont_allow_bundle_show_hide', $pa_settings, false)) { return ''; }
	$vs_expand_collapse_value = caGetOption('expand_collapse_value', $pa_settings, 'dont_force');
	$vs_expand_collapse_no_value = caGetOption('expand_collapse_no_value', $pa_settings, 'dont_force');
	$vs_expand_collapse = caGetOption('expand_collapse', $pa_settings, false);



	if(!$vs_expand_collapse) {
		$vs_expand_collapse = ($pb_has_value ? $vs_expand_collapse_value : $vs_expand_collapse_no_value);
	}

	switch(strtolower($vs_expand_collapse)) {
		case 'collapse':
			$vs_force = 'closed';
			break;
		case 'expand':
			$vs_force = 'open';
			break;
		case 'dont_force':
		default:
			$vs_force = '';
			break;
	}

	$ps_preview_id_prefix = preg_replace("/[0-9]+\_rel/", "", $ps_id_prefix);

	$vs_buf  = "<span class='bundleContentPreview' id='{$ps_preview_id_prefix}_BundleContentPreview'>{$ps_preview_init}</span>";
	$vs_buf .= "<span class='iconButton'>";
	$vs_buf .= "<a href='#' onclick='caBundleVisibilityManager.toggle(\"{$ps_id_prefix}\");  return false;' aria-label='" . _t('Toggle visibility') . "'>".caNavIcon(__CA_NAV_ICON_VISIBILITY_TOGGLE__, '18px', array('id' =>"{$ps_id_prefix}VisToggleButton"))."</a>";
	$vs_buf .= "</span>\n";
	$vs_buf .= "<script type='text/javascript'>jQuery(document).ready(function() { caBundleVisibilityManager.registerBundle('{$ps_id_prefix}', '{$vs_force}'); }); </script>";

	return $vs_buf;
}
# ---------------------------------------
/**
 * Generates metadata dictionary control HTML for bundles
 *
 * @param RequestHTTP $po_request
 * @param string $ps_id_prefix
 * @param array $pa_settings
 *
 * @return string HTML implementing the control
 */
function caEditorBundleMetadataDictionary($po_request, $ps_id_prefix, $pa_settings) {
	global $g_ui_locale;

	$definition = caGetOption($g_ui_locale, $pa_settings['definition'] ?? null, null);
	if(is_array($definition)) { $definition = join ("", $definition); }
	if (!($vs_definition = trim($definition))) { return ''; }

	$vs_buf = '';
	$vs_buf .= "<span class='iconButton'>";
	$vs_buf .= "<a href='#' class='caMetadataDictionaryDefinitionToggle' onclick='caBundleVisibilityManager.toggleDictionaryEntry(\"{$ps_id_prefix}\");  return false;'>".caNavIcon(__CA_NAV_ICON_INFO__, 1, array('id' => "{$ps_id_prefix}MetadataDictionaryToggleButton"))."</a>";

	$vs_buf .= "<div id='{$ps_id_prefix}DictionaryEntry' class='caMetadataDictionaryDefinition'>{$vs_definition}</div>";
	$vs_buf .= "<script type='text/javascript'>jQuery(document).ready(function() { caBundleVisibilityManager.registerBundle('{$ps_id_prefix}'); }); </script>";	
	$vs_buf .= "</span>\n";	

	return $vs_buf;
}
# ---------------------------------------
/**
 *
 */
function caGetDefaultEditorBundleSortConfiguration(string $table, string $related_table, array $settings) : array {
	$config = Configuration::load();
	$default_sorts = $config->getAssoc("{$related_table}_default_bundle_display_sorts");

	$type_specific_sorts = null;
	
	if(!is_array($default_sorts) || !is_array($default_sort_options = caGetOption('options', $default_sorts, null))) { $default_sort_options = []; }
	if(is_array($rel_types = caGetOption(['restrict_to_types', 'restrictToTypes'], $settings, null)) && sizeof($rel_types)) {
		$path = array_keys(Datamodel::getPath($table, $related_table));
		if(is_array($type_codes = caMakeTypeList($related_table, $rel_types))) {
			foreach($type_codes as $t) {
				if(is_array($type_specific_sorts = $config->get("{$related_table}_{$t}_bundle_display_sorts")) && is_array($type_specific_sort_options = caGetOption('options', $type_specific_sorts, null))) {
					$default_sort_options = array_merge($default_sort_options, $type_specific_sort_options);
				}
			}
		}
	}
	return ['defaultSorts' => $default_sorts, 'typeSpecificSorts' => $type_specific_sorts, 'sortOptions' => $default_sort_options];
}
# ---------------------------------------
/**
 * Generates sort control HTML for relation bundles (Eg. ca_entities, ca_occurrences)
 *
 * @param RequestHTTP $po_request
 * @param string $ps_id_prefix
 * @param string $ps_table
 * @param string $ps_related_table
 * @param array $pa_options
 *		sort = 
 *		sortDirection = 
 * 
 * @return string HTML implementing the control
 */
function caEditorBundleSortControls($request, $id_prefix, $table, $related_table, $options=null) {
	if (!is_array($options)) { $options = []; }

	if(!$table) { return '???'; }

	$sort = caGetOption('sort', $options, null);
	$sort_direction = caGetOption('sortDirection', $options, null);
	$user_set_sort = caGetOption('userSetSort', $options, false);
	
	$type_id = null;
	if($type_ids = caGetOption(['restrict_to_types', 'restrictToTypes'], $options, null)) {
		$type_id = is_array($type_ids) ? array_shift($type_ids) : $type_ids;
	}
	
	if (!is_array($sort_fields = caGetAvailableSortFields($table, $type_id, array_merge(['request' => $request], $options, ['naturalSortLabel' => _t('current order'), 'includeInterstitialSortsFor' => $related_table]))) || !sizeof($sort_fields)) { return ''; }

	$allowed_sorts = caGetOption('allowedSorts', $options, null);
	$user_set_sort = caGetOption('userSetSort', $options, false);
	
	if(!is_array($allowed_sorts) || !sizeof($allowed_sorts)) {
		// apply global settings
		$default_sort_config = caGetDefaultEditorBundleSortConfiguration($related_table, $table, $options);
		$default_sorts = $default_sort_config['defaultSorts'];
		$type_specific_sorts = $default_sort_config['typeSpecificSorts'];
		$default_sort_options = $default_sort_config['sortOptions'];
		
		if(!$sort) {
			$sort = caGetOption('sort', $type_specific_sorts ? $type_specific_sorts : $default_sorts, null);
			$sort_direction = caGetOption('direction', $type_specific_sorts ? $type_specific_sorts : $default_sorts, null);
		}
		
		// Translate truncated sorts to their standard multi-field equivalents
		// (Eg. ca_entities.type_id => ca_entities.type_id;ca_entities.preferred_labels.surname;ca_entities.preferred_labels.forename)
		$sort_field_trans = [];
		foreach($sort_fields as $sf => $n) {
			$t = explode(';', $sf);
			if(sizeof($t) > 1) {
				for($i=1; $i < sizeof($t); $i++) {
					$sort_field_trans[join(';', array_slice($t, 0, $i))] = $sf;
				}
			}
		}
		
		if(is_array($type_specific_sorts) ) {
			$default_sort_options = $type_specific_sorts['options'];
		}
		
		// Expand global sort list to include parents when sort is on container field
		$default_sort_options = array_merge(['_natural' => false], array_reduce($default_sort_options, function($carry, $item) use ($sort_field_trans) { 
			if(array_key_exists($item, $sort_field_trans)) { $item = $sort_field_trans[$item]; }	// rewrite truncated fields 
			
			$tmp = explode('.', $item);
			if(sizeof($tmp) > 2) {
				$carry[join('.', array_slice($tmp, 0, 2))] =  true;
			}
			$carry[$item] = false;
			
		
			return $carry;
		}, []));
	
		if(is_array($default_sort_options) && sizeof($default_sort_options)) {
			$sort_fields = array_filter(
				$sort_fields,
				function ($v) use ($default_sort_options) {
					return array_key_exists($v, $default_sort_options);
				},
				ARRAY_FILTER_USE_KEY
			);
		}
	} else {
		$allowed_sorts = array_map(function($v) {
			return strlen($v) ? $v : '_natural';
		}, $allowed_sorts);
		
		if($sort && !$user_set_sort) {
			$allowed_sorts = array_filter($allowed_sorts, function($v) {
				return ($v !== '_natural');
			});
		}
		
		$sort_fields_proc = $default_sort_options = [];
	
		// Expand global sort list to include parents when sort is on container field
		foreach($sort_fields as $sf => $n) {
			if(!in_array($sf, $allowed_sorts)) { continue; }
			$tmp = explode('.', $sf);
			if((sizeof($tmp) > 2) && !in_array($tmp[1], ['preferred_labels', 'nonpreferred_labels']))  {
				if(!($label = ca_metadata_elements::getElementLabel($tmp[1]))) { continue; }
				$sort_fields_proc[$k=join('.', array_slice($tmp, 0, 2))] = $label;
				$default_sort_options[$k] = true;
			}
			$sort_fields_proc[$sf] = $n;
				
		}
		$sort_fields = $sort_fields_proc;
	}

	$sort_fields = array_map(function($v) { return mb_strtolower($v); }, $sort_fields);
	return "<div class='editorBundleSortControl'>"._t('Sort using %1 %2', caHTMLSelect("{$id_prefix}_RelationBundleSortControl", 
			array_flip($sort_fields), 
			[
				'onChange' => "caRelationBundle{$id_prefix}.sort(jQuery(this).val())", 
				'id' => "{$id_prefix}_RelationBundleSortControl", 
				'class' => 'caItemListSortControlTrigger dontTriggerUnsavedChangeWarning'], 
			['value' => $sort, 'disabledOptions' => $default_sort_options]
		), 
		caHTMLSelect(
			"{$id_prefix}_RelationBundleSortDirectionControl", 
			[_t('↑') => 'ASC', _t('↓') => 'DESC'], 
			[
				'onChange' => "caRelationBundle{$id_prefix}.sort(jQuery('#{$id_prefix}_RelationBundleSortControl').val())", 
				'id' => "{$id_prefix}_RelationBundleSortDirectionControl", 
				'class' => 'caItemListSortControlTrigger dontTriggerUnsavedChangeWarning'
			], 
			['value' => strtoupper($sort_direction)]
		)
	)."</div>";
}
# ---------------------------------------
/**
 * Generates batch editing control HTML for relation bundles (Eg. ca_entities, ca_occurrences)
 *
 * @param RequestHTTP $request
 * @param int $placement_id
 * @param BaseModel $t_instance
 * @param string $related_table
 * @param array $options
 * 
 * @return string HTML implementing the control
 */
function caEditorBundleBatchEditorControls($request, $placement_id, $t_instance, $related_table, $options=null) {
	if (!is_array($options)) { $options = []; }
	
	$buf = '';
	if(caGetOption('showBatchEditorButton', $options, false)) {
		$buf = '<div class="button batchEdit">'.caNavLink($request, "<span class='form-button'>".caNavIcon(__CA_NAV_ICON_BATCH_EDIT__, '15px')._t(' Batch edit')."</span>", 'form-button', '*', '*', 'BatchEdit', ['placement_id' => $placement_id, 'primary_id' => $t_instance->getPrimaryKey(), 'screen' => $request->getActionExtra()]).'</div>';
	}
	return $buf;
}
# ---------------------------------------
/** 
 * Check if home location functionality is enabled for a given table and, optionally, type
 *
 * @param string $table 
 * @param mixed $type Type code or type_id
 *
 * @return bool
 */
function caHomeLocationsEnabled(string $table, $type=null, array $options=null) {
	if(!in_array($table, ['ca_objects', 'ca_object_lots', 'ca_object_representations', 'ca_collections', 'ca_storage_locations'], true)) { return false; }
	$o_config = Configuration::load();
	if($type && (bool)$o_config->get("{$table}_{$type}_enable_home_location")) { return true; }
	if($type && is_numeric($type) && ($t_instance = Datamodel::getInstance($table, true))) {	
		// Try converting numeric type to type code
		$type = $t_instance->getTypeCodeForID((int)$type);
		if($type && (bool)$o_config->get("{$table}_{$type}_enable_home_location")) { return true; }
	}
	if(caGetOption('enableIfAnyTypeSet', $options, false) && ($t_instance = Datamodel::getInstance($table, true))) {
		if(is_array($types = array_map(function($v) { return $v['idno']; }, $t_instance->getTypeList()))) {
			foreach($types as $type) {
				if((bool)$o_config->get("{$table}_{$type}_enable_home_location")) { return true; }
			}
		}
	}

	if((bool)$o_config->get("{$table}_enable_home_location")) { return true; }

	return false;
}
# ---------------------------------------
/** 
 * Used by ca_objects, ca_collections and ca_object_lots bundles
 */
function caReturnToHomeLocationControlForRelatedBundle($po_request, $ps_id_prefix, $pt_primary, $ps_policy, $initial_values) {
	if(!is_array($pconfig = ca_objects::getPolicyConfig($ps_policy))) { return null; }
	if (!($pt_related = Datamodel::getInstance($pconfig['table'], true))) {  return null; }

	$target = $pt_related->tableName();
	if(!$pt_related->getAppConfig()->get("{$target}_enable_home_location")) { return null; }
	$policies = array_filter(ca_objects::getHistoryTrackingCurrentValuePolicies($target), function($v) { return array_key_exists('ca_storage_locations', $v['elements']); });
	if(!is_array($policies) || !sizeof($policies)) { return ''; }
	if (!$ps_policy) { $ps_policy = $target::getDefaultHistoryTrackingCurrentValuePolicy(); }
	if(is_object($initial_values)) {
		$iv = [];
		while($initial_values->nextHit()) {
			$iv[] = ['object_id' => $initial_values->get('ca_objects.object_id')];
		}
		$initial_values->seek(0);
		$initial_values = $iv;
	}

	$settings = $target::policy2bundleconfig(['policy' => $ps_policy]);
	$interstitials = caGetOption('ca_storage_locations_setInterstitialElementsOnAdd', $settings, null);

	$vs_buf = "<div id='{$ps_id_prefix}_editor_bundle_return_to_home_button' class='editorBundleReturnToHomeControl'>".
		caJSButton($po_request, __CA_NAV_ICON_HOME__, _t("Return home"), "{$ps_id_prefix}_return_to_home_locations", ['onclick' => "caReturnToHomeLocationToggleForm{$ps_id_prefix}(); return false;"], ['size' => '15px']).
		"</div>";

	$vs_buf .= "<div id='{$ps_id_prefix}_editor_bundle_return_to_home_controls_message' class='editorBundleReturnToHomeControlsMessage'></div>\n";

	$primary_table = $pt_primary->tableName();
	$primary_id = $pt_primary->getPrimaryKey();

	$vs_buf .= "<div id='{$ps_id_prefix}_editor_bundle_return_to_home_controls' class='editorBundleReturnToHomeControls'>".
		ca_storage_locations::getHistoryTrackingChronologyInterstitialElementAddHTMLForm($po_request, $ps_id_prefix, $pt_related->tableName(), $settings, ['placement_code' => $ps_id_prefix, 'noTemplate' => true]).
		caJSButton($po_request, __CA_NAV_ICON_GO__, _t("Apply"), "{$ps_id_prefix}_return_to_home_locations_execute", ['onclick' => "caReturnToHomeLocation{$ps_id_prefix}(); return false;"], ['size' => '15px']).caJSButton($po_request, __CA_NAV_ICON_CANCEL__, _t("Cancel"), "{$ps_id_prefix}_return_to_home_locations_execute", ['onclick' => "caReturnToHomeLocationToggleForm{$ps_id_prefix}(); return false;"], ['size' => '15px']).
		"</div>\n"; 
	$vs_buf .= "
		<script type='text/javascript'>
			function caReturnToHomeLocationToggleForm{$ps_id_prefix}() {
				jQuery('#{$ps_id_prefix}_editor_bundle_return_to_home_controls').slideToggle(250);
			}
			function caReturnToHomeLocation{$ps_id_prefix}() {
				var interstitials = ".json_encode($interstitials).";
				var data = { 'table': '{$primary_table}', 'id': {$primary_id}, 'policy': '{$ps_policy}', 'target': '{$target}'};
				for(var i in interstitials) {
					data[interstitials[i]] = jQuery('#{$ps_id_prefix}_ca_storage_locations__' + interstitials[i]).val();
				}

				jQuery('#{$ps_id_prefix}_editor_bundle_return_to_home_button').hide();
				jQuery.post('".caNavUrl($po_request, '*', '*', 'ReturnToHomeLocations')."', data, function(data) {
						jQuery('#{$ps_id_prefix}_editor_bundle_return_to_home_controls').hide();
						var e = jQuery('#{$ps_id_prefix}_editor_bundle_return_to_home_controls_message');
						if(data && (data.ok == 1)) {
							jQuery(e).html(data.message).show();
						} else if(data) {
							jQuery(e).html('Error: ' + data.message).show(250);
							jQuery('#{$ps_id_prefix}_editor_bundle_return_to_home_button').show();
						}

						setTimeout(function() { 
							jQuery(e).fadeOut(250);
						}, 5000);
						if(caBundleUpdateManager) { 
							setTimeout(function() { 
								caBundleUpdateManager.reloadBundle('history_tracking_current_contents'); 
								caBundleUpdateManager.reloadBundle('ca_storage_locations_current_contents'); 
								caBundleUpdateManager.reloadBundle('{$target}'); 
							}, 2000);
						}
				}, 'json');
			}
		</script>
	";
	return $vs_buf;
}
# ---------------------------------------
/**
 * 
 */
function caProcessBottomLineTemplateForDisplay($po_request, $pt_display, $pr_res, $pa_options=null) {
	if (!$pr_res) { return null; }
	if (!$pt_display) { return null; }
	$vs_template = $pt_display->getSetting('bottom_line');

	$va_bundles_by_code = [];
	if (!is_array($va_bundles = $pt_display->getPlacementsInDisplay())) { return null; }
	foreach($va_bundles as $vn_placement_id => $va_placement) {
		$va_bundles_by_code[$va_placement['bundle']] = $va_placement;
	}

	$va_tags = caGetTemplateTags($vs_template, ['parseOptions' => true]);
	$vb_is_set = false;

	foreach($va_tags as $va_tag) {
		$va_fields = preg_split("/[ ;,]+/", $va_tag['options']['fields']);

		$va_tag_bits = explode(':', $va_tag['tag']);
		switch(strtolower($va_tag_bits[0])) {
			case 'sum':
				$va_placements = [];
				foreach($va_fields as $vs_field) {
					if (!isset($va_bundles_by_code[$vs_field])) { continue; }
					$va_placements[] = $va_bundles_by_code[$vs_field];
				}

				$vs_val = caProcessBottomLineTemplateForPlacement($po_request, $va_placements, $pr_res, ['template' => '^SUM'.(isset($va_tag_bits[1]) ? ":{$va_tag_bits[1]}" : ""), 'multiple' => true]);

				$vs_template = str_replace("^".$va_tag['originalTag'], $vs_val, $vs_template);

				$vb_is_set = true;
				break;
		}
	}
	return $vb_is_set ? $vs_template : null;
}
# ---------------------------------------
/**
 *
 */
function caProcessBottomLineTemplateForPlacement($po_request, $pa_placement, $pr_res, $pa_options=null) {
	global $g_ui_units_pref, $g_ui_locale;

	if (!$pr_res) { return null; }

	if ($vb_is_multiple = caGetOption('multiple', $pa_options, false)) {
		$pa_placements = $pa_placement;
	} else {
		$pa_placements = [$pa_placement];
	}

	if (($vn_current_index = $pr_res->currentIndex()) < 0) { $vn_current_index = 0; }

	$pn_page_start = caGetOption('pageStart', $pa_options, 0);
	$pn_page_end = caGetOption('pageEnd', $pa_options, $pr_res->numHits());

	$va_tags_to_process = $va_subelements_to_process = $va_tag_values = [];

	foreach($pa_placements as $pa_placement) {
		$pr_res->seek(0);

		if (!($vs_template = caGetOption('template', $pa_options, $pa_placement['settings']['bottom_line']))) { 
			$pr_res->seek($vn_current_index);	// Restore current position of search result
			return null; 
		}

		$vs_bundle_name = $pa_placement['bundle'];

		$va_tmp = explode(".", $vs_bundle_name);

		if (!($t_instance = Datamodel::getInstanceByTableName($va_tmp[0], true))) {
			$pr_res->seek($vn_current_index);	// Restore current position of search result
			return null;
		}

		$vn_datatype = ca_metadata_elements::getElementDatatype($va_tmp[1]);
		if (is_null($vn_datatype)) { continue; }

		if (!($vs_user_currency = $po_request->user ? $po_request->user->getPreference('currency') : 'USD')) {
			$vs_user_currency = 'USD';
		}
		$vs_user_currency = caGetCurrencySymbol($vs_user_currency, $va_tmp[1]);

		// Parse out tags and optional sub-elements from template
		//		we have to pull each sub-element separately
		//
		//		Ex. 	^SUM:valuation = sum of "valuation" sub-element
		//				^SUM = sum of primary value in non-container element
		if (!preg_match("!(\^[A-Z]+[\:]{0,1}[A-Za-z0-9\_\-]*)!", $vs_template, $va_tags)) {
			$pr_res->seek($vn_current_index);	// Restore current position of search result
			return $vs_template;
		}


		if ($vn_datatype == 0) {	// container
			foreach($va_tags as $vs_raw_tag) {
				$va_tmp = explode(":", $vs_raw_tag);
				$vs_tag = $va_tmp[0];
				if (sizeof($va_tmp) == 2) {
					$vs_subelement = $va_tmp[1];
				} else {
					continue;
				}

				$va_tags_to_process[$vs_raw_tag] = true;
				$va_subelements_to_process["{$vs_bundle_name}.{$vs_subelement}"] = ca_metadata_elements::getElementDatatype($vs_subelement);
			}
		} else {
			$va_tmp = explode(".", $vs_bundle_name);
			if (sizeof($va_tmp) == 2) { $vs_bundle_name .= ".".array_pop($va_tmp); }
			$va_subelements_to_process[$vs_bundle_name] = $vn_datatype;
		}

		$vn_c = 0;
		$vn_page_len = 0;
		$vb_has_timecode = false;

		$vn_min = $vn_max = null;
		$vn_page_min = $vn_page_max = null;

		$va_tag_values = array();
		while($pr_res->nextHit()) {
			foreach($va_subelements_to_process as $vs_subelement => $vn_subelement_datatype) {
				$vs_value_name = ($vb_is_multiple) ? "Value_{$vn_subelement_datatype}" : $vs_subelement;

				if (!is_array($va_tag_values[$vs_value_name])) {
					$va_tag_values[$vs_value_name]['SUM'] = 0;
					$va_tag_values[$vs_value_name]['PAGESUM'] = 0;
					$va_tag_values[$vs_value_name]['MIN'] = null;
					$va_tag_values[$vs_value_name]['PAGEMIN'] = null;
					$va_tag_values[$vs_value_name]['MAX'] = null;
					$va_tag_values[$vs_value_name]['PAGEMAX'] = null;
					$va_tag_values[$vs_value_name]['AVG'] = 0;
					$va_tag_values[$vs_value_name]['PAGEAVG'] = 0;
				}

				switch($vn_subelement_datatype) {
					case 6:		// currency
						$va_values = $pr_res->get($vs_subelement, array('returnAsDecimalWithCurrencySpecifier' => true, 'returnAsArray' => true));

						if(is_array($va_values)) {
							foreach($va_values as $vs_value) {
								$vn_value = (float)caConvertCurrencyValue($vs_value, $vs_user_currency, array('numericValue' => true));

								$va_tag_values[$vs_value_name]['SUM'] += $vn_value;
								if (is_null($va_tag_values[$vs_value_name]['MIN']) || ($vn_value < $va_tag_values[$vs_value_name]['MIN'])) { $va_tag_values[$vs_value_name]['MIN'] = $vn_value; }
								if (is_null($va_tag_values[$vs_value_name]['MAX']) || ($vn_value > $va_tag_values[$vs_value_name]['MAX'])) { $va_tag_values[$vs_value_name]['MAX'] = $vn_value; }

								if (($vn_c >= $pn_page_start) && ($vn_c <= $pn_page_end)) {
									$va_tag_values[$vs_value_name]['PAGESUM'] += $vn_value;
									if (is_null($va_tag_values[$vs_value_name]['PAGEMIN']) || ($vn_value < $va_tag_values[$vs_value_name]['PAGEMIN'])) { $va_tag_values[$vs_value_name]['PAGEMIN'] = $vn_value; }
									if (is_null($va_tag_values[$vs_value_name]['PAGEMAX']) || ($vn_value > $va_tag_values[$vs_value_name]['PAGEMAX'])) { $va_tag_values[$vs_value_name]['PAGEMAX'] = $vn_value; }
									$vn_page_len++;
								}
							}
						}
						break;
					case 8:		// length
					case 9:		// weight
						$va_values = $pr_res->get($vs_subelement, array('returnAsDecimalMetric' => true, 'returnAsArray' => true));

						if(is_array($va_values)) {
							foreach($va_values as $vs_value) {
								$vn_value = (float)$vs_value;
								$va_tag_values[$vs_value_name]['SUM'] += $vn_value;
								if (is_null($va_tag_values[$vs_value_name]['MIN']) || ($vn_value < $va_tag_values[$vs_value_name]['MIN'])) { $va_tag_values[$vs_value_name]['MIN'] = $vn_value; }
								if (is_null($va_tag_values[$vs_value_name]['MAX']) || ($vn_value > $va_tag_values[$vs_value_name]['MAX'])) { $va_tag_values[$vs_value_name]['MAX'] = $vn_value; }

								if (($vn_c >= $pn_page_start) && ($vn_c <= $pn_page_end)) {
									$va_tag_values[$vs_value_name]['PAGESUM'] += $vn_value;
									if (is_null($va_tag_values[$vs_value_name]['PAGEMIN']) || ($vn_value < $va_tag_values[$vs_value_name]['PAGEMIN'])) { $va_tag_values[$vs_value_name]['PAGEMIN'] = $vn_value; }
									if (is_null($va_tag_values[$vs_value_name]['PAGEMAX']) || ($vn_value > $va_tag_values[$vs_value_name]['PAGEMAX'])) { $va_tag_values[$vs_value_name]['PAGEMAX'] = $vn_value; }
									$vn_page_len++;
								}
							}
						}
						break;
					case 10:	// timecode
						$va_values = $pr_res->get($vs_subelement, array('returnAsDecimal' => true, 'returnAsArray' => true));

						if(is_array($va_values)) {
							foreach($va_values as $vn_value) {
								$va_tag_values[$vs_value_name]['SUM'] += $vn_value;
								if (is_null($vn_min) || ($vn_value < $vn_min)) { $vn_min = $vn_value; }
								if (is_null($vn_max) || ($vn_value > $vn_max)) { $vn_max = $vn_value; }

								if (($vn_c >= $pn_page_start) && ($vn_c <= $pn_page_end)) {
									$va_tag_values[$vs_value_name]['PAGESUM'] += $vn_value;
									if (is_null($vn_page_min) || ($vn_value < $vn_page_min)) { $vn_page_min = $vn_value; }
									if (is_null($vn_page_max) || ($vn_value > $vn_page_max)) { $vn_page_max = $vn_value; }
									$vn_page_len++;
								}
							}
						}
						$vb_has_timecode = true;
						break;
					case 11:	// integer
					case 12:	// numeric (decimal)
						$va_values = $pr_res->get($vs_subelement, array('returnAsArray' => true));

						if(is_array($va_values)) {
							foreach($va_values as $vs_value) {
								$vn_value = (float)$vs_value;
								$va_tag_values[$vs_value_name]['SUM'] += $vn_value;
								if (is_null($vn_min) || ($vn_value < $vn_min)) { $vn_min = $vn_value; }
								if (is_null($vn_max) || ($vn_value > $vn_max)) { $vn_max = $vn_value; }

								if (($vn_c >= $pn_page_start) && ($vn_c <= $pn_page_end)) {
									$va_tag_values[$vs_value_name]['PAGESUM'] += $vn_value;
									if (is_null($vn_page_min) || ($vn_value < $vn_page_min)) { $vn_page_min = $vn_value; }
									if (is_null($vn_page_max) || ($vn_value > $vn_page_max)) { $vn_page_max = $vn_value; }
									$vn_page_len++;
								}
							}
						}
						break;
					default:
						break(2);
				}
			}
			$vn_c++;
		}
	}

	if ($vb_has_timecode) {
		$o_tcp = new TimecodeParser();
		$o_config = Configuration::load();
		if (!($vs_timecode_format = $o_config->get('timecode_output_format'))) { $vs_timecode_format = 'HOURS_MINUTES_SECONDS'; }
	}

	if ($vb_is_multiple) {
		$va_subelements_to_process = [];
		foreach(array_keys($va_tag_values) as $vs_value_name) {
			$vn_datatype = preg_match("!_([\d]+)$!", $vs_value_name, $va_matches) ? (int)$va_matches[1] : 0;
			$va_subelements_to_process[$vs_value_name] =  $vn_datatype;
		}
	}

	// Post processing
	foreach($va_subelements_to_process as $vs_subelement => $vn_subelement_datatype) {
		$vs_value_name = ($vb_is_multiple) ? "Value_{$vn_subelement_datatype}" : $vs_subelement;

		switch($vn_subelement_datatype) {
			case 6:		// currency
				$va_tag_values[$vs_value_name]['PAGEAVG'] = ($vn_page_len > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['PAGESUM']/$vn_page_len) : 0;
				$va_tag_values[$vs_value_name]['AVG'] = ($vn_c > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['SUM']/$vn_c) : "0.00";

				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					$va_tag_values[$vs_value_name][$vs_tag] = "{$vs_user_currency} ".$va_tag_values[$vs_value_name][$vs_tag];
				}

				break;
			case 8:		// length
				$va_tag_values[$vs_value_name]['PAGEAVG'] = ($vn_page_len > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['PAGESUM']/$vn_page_len) : 0;
				$va_tag_values[$vs_value_name]['AVG'] = ($vn_c > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['SUM']/$vn_c) : "0.00";

				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					$vo_measurement = new Zend_Measure_Length((float)$vn_val, 'METER', $g_ui_locale);
					$va_tag_values[$vs_value_name][$vs_tag] = $vo_measurement->convertTo(($g_ui_units_pref == 'metric') ? Zend_Measure_Length::METER :  Zend_Measure_Length::FEET, 4);
				}

				break;
			case 9:		// weight
				$va_tag_values[$vs_value_name]['PAGEAVG'] = ($vn_page_len > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['PAGESUM']/$vn_page_len) : 0;
				$va_tag_values[$vs_value_name]['AVG'] = ($vn_c > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['SUM']/$vn_c) : "0.00";

				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					$vo_measurement = new Zend_Measure_Length((float)$vn_val, 'KILOGRAM', $g_ui_locale);
					$va_tag_values[$vs_value_name][$vs_tag] = $vo_measurement->convertTo(($g_ui_units_pref == 'metric') ? Zend_Measure_Weight::KILOGRAM :  Zend_Measure_Weight::POUND, 4);
				}

				break;
			case 10:	// timecode
				$va_tag_values[$vs_value_name]['PAGEAVG'] = ($vn_page_len > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['PAGESUM']/$vn_page_len) : 0;
				$va_tag_values[$vs_value_name]['AVG'] = ($vn_c > 0) ? sprintf("%1.2f", $va_tag_values[$vs_value_name]['SUM']/$vn_c) : 0;

				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					if (!$vb_has_timecode) { $va_tag_values[$vs_value_name][$vs_tag] = ''; continue; }
					$o_tcp->setParsedValueInSeconds($vn_val);
					$va_tag_values[$vs_value_name][$vs_tag] = $o_tcp->getText($vs_timecode_format);
				}

				break;
			case 11:	// integer
				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					$va_tag_values[$vs_value_name][$vs_tag] = (int)$va_tag_values[$vs_value_name][$vs_tag];
				}

				break;
			case 12:	// numeric (decimal)
				foreach($va_tag_values[$vs_value_name] as $vs_tag => $vn_val) {
					$va_tag_values[$vs_value_name][$vs_tag] = (float)$va_tag_values[$vs_value_name][$vs_tag];
				}

				break;
		}

		foreach($va_tag_values as $vs_value_name => $va_tag_data) {
			foreach($va_tag_data as $vs_tag => $vs_tag_value) {
				if(strpos($vs_value_name, '.') !== false) {
					$va_tmp = explode(".", $vs_value_name);
					$vs_template = str_replace("^{$vs_tag}:".array_pop($va_tmp), $vs_tag_value, $vs_template);
				} elseif($vb_is_multiple && preg_match("!^Value_([\d]+)$!", $vs_value_name, $va_matches)) {
					$vs_name = null;
					switch((int)$va_matches[1]) {
						case 6:
							$vs_name = 'currency';
							break;
						case 8:
							$vs_name = 'length';
							break;
						case 9:
							$vs_name = 'weight';
							break;
						case 10:
							$vs_name = 'timecode';
							break;
						case 11:
							$vs_name = 'integer';
							break;
						case 12:
							$vs_name = 'numeric';
							break;
					}
					if ($vs_name) { $vs_template = str_replace("^{$vs_tag}:{$vs_name}", $vs_tag_value, $vs_template); }
				}
				$vs_template = str_replace("^{$vs_tag}", $vs_tag_value, $vs_template);
			}
		}
	}

	// Restore current position of search result
	$pr_res->seek($vn_current_index);

	return $vs_template;
}
# ------------------------------------------------------------------
/**
 * Return list of representations
 *
 * @param RequestHTTP $request The current request
 * @param RepresentableBaseModel $subject = A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $options Options include:
 *		primaryOnly = return only primary representations. [Default is false]
 *		currentRepClass = CSS class to apply to thumbnail of currently visible representation. [Default is "active"]
 *		dontShowPlaceholder = Don't use placeholder when no representation is available. [Default is false]
 *		display = media_display.conf display version to use. [Default is 'detail']
 *		displayAnnotations = Mode of display for annotations on representation. Valid values are: viewer (in viewer), div (in external div with class #detailAnnotations), none (no display) [Default is none]
 *		displayAnnotationTemplate = Template to use when formatting list of annotations [Default is the annotation title (^ca_representation_annotations.preferred_labels.name)]
 *
 * @return string HTML output
 *
 * @see caGetMediaViewerHTML
 */
function caRepresentationList($request, $subject, ?array $options=null) : ?array {
	$detail_config = caGetDetailConfig()->get($subject->tableName());
	$access_values = caGetUserAccessValues($request);
	
	$show_only_media_types = caGetOption('showOnlyMediaTypes', $options, null);
	
	$allow_download = caGetOption('allowMediaDownload', $options, false);
	if(caGetOption('requireLoginForMediaDownload', $options, false) && !$request->isLoggedIn()) { 
		$allow_download = false;
	} else {
		$require_roles_for_download = caGetOption('requireRoleForMediaDownload', $options, null);
		if(is_array($require_roles_for_download) && sizeof($require_roles_for_download)) {
			$allow_download = false;
			if($request->isLoggedIn()) {
				foreach($require_roles_for_download as $rrole) {
					if($request->user->hasUserRole($rrole)) {
						$allow_download = true;
						break;
					}
				}
			}
		}
	}
	
	// options
	$versions = caGetOption('versions', $options, ['icon', 'small']);
	$display_type = caGetOption('display', $options, 'detail');
	
	if($show_only_media_types && !is_array($show_only_media_types)) { $show_only_media_types = [$show_only_media_types]; }
	if(is_array($show_only_media_types)) {
		$show_only_media_types = array_map('strtolower', $show_only_media_types);
	}
	
	$t_instance = null;
	if(is_a($subject, 'RepresentableBaseModel')) {
		$t_instance = $subject;
	} elseif(is_a($subject, 'SearchResult')) {
		$t_instance = $subject->getInstance();
	} else {
		return null;
	}
	if (method_exists($t_instance, 'filterNonPrimaryRepresentations')) { $t_instance->filterNonPrimaryRepresentations(false); }
	
	$qr = $t_instance->getRepresentationsAsSearchResult($options);
	
	$t_rep = new ca_object_representations();
	$reps = [];
	
	$caption_template = caGetOption('mediaCaptionTemplate', $options, '???');
	while($qr->nextHit()) {
		$rep_id = $qr->get('ca_object_representations.representation_id');
		$mimetype = $qr->get('ca_object_representations.mimetype');
		
		if(!($display_info = caGetMediaDisplayInfoForMimetype($display_type, $mimetype))) {
			continue;
		}
		$download_info = caGetMediaDisplayInfoForMimetype('download', $mimetype);
		if(is_array($show_only_media_types) && sizeof($show_only_media_types) && !in_array(strtolower($display_info['viewer']), $show_only_media_types)) {
			continue;
		}
		
		$display_version = $display_info['display_version'] ?? null;
		
		$iiif_url = $request->getBaseUrlPath().'/service/IIIF/representation:'.$rep_id.'/info.json';
		
		$vtt_captions = null;
		if(is_array($vtt_caption_list = $t_rep->getCaptionFileList($rep_id))) {
			$vtt_captions = [];
			foreach($vtt_caption_list as $vtt_caption) {
				$vtt_captions[] = [
					'url' => $vtt_caption['url'],
					'locale' => $vtt_caption['locale'],
					'language' => substr($vtt_caption['locale_code'], 0, 2),
					'locale_code' => $vtt_caption['locale_code']
				];
			}
		}
		$rep = [
			'representation_id' => $rep_id,
			'mimetype' => $mimetype,
			'media_class' => caGetMediaClass($qr->get('ca_object_representations.mimetype')),
			'original_url' => $qr->get("ca_object_representations.media.original.url"),
			'url' => $qr->get("ca_object_representations.media.{$display_version}.url"),
			'original_tag' => $qr->get("ca_object_representations.media.original.tag"),
			'icon_tag' => $qr->get("ca_object_representations.media.iconlarge.tag"),
			'tag' => $qr->get("ca_object_representations.media.{$display_version}.tag"),
			'iiifUrl' => $iiif_url,
			'no_overlay' => (bool)($display_info['no_overlay'] ?? false),
			'caption' => caProcessTemplateForIDs($caption_template, $qr->tableName(), [$rep_id]),
			'vttCaptions' => $vtt_captions,
			'download_version' => $allow_download ? caGetOption('download_version', $download_info, null) : null
		];
		
		if(is_array($file_list = $qr->getFileList()) && sizeof($file_list)) {
			$pages = array_map(function($v) {
				return $v['preview_url'];
			}, $file_list);	
			$rep['pages_previews'] = array_values($pages);
			
			$rep['pages'] = [];
			foreach($rep['pages_previews'] as $i => $p) {
				$rep['pages'][] = $request->getBaseUrlPath().'/service/IIIF/representation:'.$rep_id.':'.($i+1).'/info.json';
			}
		}
		
		
		$rep['display_class'] = $display_info['display_class'] ?? null;
		
		$opts = MediaViewerManager::viewerOptionsForDisplayClass($display_type, $display_info['display_class']);
		$rep['options'] = $opts;
		
		$opts = MediaViewerManager::viewerOptionsForDisplayClass('overlay', $display_info['display_class']);
		$rep['overlay_options'] = $opts;
		
		
		if(is_array($versions)) {
			foreach($versions as $version) {
				$rep[$version] = $qr->get("ca_object_representations.media.{$version}.url");
			}
		}
		
		$reps[] = $rep;
	}
	return $reps;
}
# ------------------------------------------------------------------
/**
 * Returns media presentation viewer HTML for one or more records. This is the entry point when rendering media
 * using per-mimetype settings in media_display.conf. Renders HTML using  views/bundles/representation_viewer_html.php.
 * This will render media viewers for many items. To render a viewer for a specific item see caGetMediaViewerHTML()
 *
 * @param RequestHTTP $po_request The current request
 * @param BaseModel|SearchResult $po_data A model instance (ca_object_representations or a model inheriting from RepresentableBaseModel) or a search result (for a RepresentableBaseModel table) for which to render the viewer. 
 * @param RepresentableBaseModel $pt_subject = A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *		primaryOnly = return only primary representations. [Default is false]
 *		currentRepClass = CSS class to apply to thumbnail of currently visible representation. [Default is "active"]
 *		dontShowPlaceholder = Don't use placeholder when no representation is available. [Default is false]
 *		display = media_display.conf display version to use. [Default is 'detail']
 *		displayAnnotations = Mode of display for annotations on representation. Valid values are: viewer (in viewer), div (in external div with class #detailAnnotations), none (no display) [Default is none]
 *		displayAnnotationTemplate = Template to use when formatting list of annotations [Default is the annotation title (^ca_representation_annotations.preferred_labels.name)]
 *
 * @return string HTML output
 *
 * @see caGetMediaViewerHTML
 */
# DEPRECATED: Still used by Pawtucket; will be removed in next version of Pawtucket
function caRepresentationViewer($po_request, $po_data, $pt_subject, $pa_options=null) {
	$o_view = new View($po_request, $po_request->getViewsDirectoryPath().'/bundles/');
	
	$va_detail_config = caGetDetailConfig()->get($po_data->tableName());
	$va_access_values = caGetUserAccessValues($po_request);

	// options
	$pb_primary_only 					= caGetOption('primaryOnly', $pa_options, false);
	
	$show_only_media_types 				= caGetOption('representationViewerShowOnlyMediaTypes', $pa_options, null);
	if(($show_only_media_types) && !is_array($show_only_media_types)) { $show_only_media_types = [$show_only_media_types]; }
	
	$show_only_media_types_when_present = caGetOption('representationViewerShowOnlyMediaTypesWhenPresent', $pa_options, null);
	if(($show_only_media_types_when_present) && !is_array($show_only_media_types_when_present)) { $show_only_media_types_when_present = [$show_only_media_types_when_present]; }

	
	$ps_active_representation_class 	= caGetOption('currentRepClass', $pa_options, 'active');
	$pb_dont_show_placeholder 			= caGetOption('dontShowPlaceholder', $pa_options, false);
	$ps_display_annotations	 			= caGetOption('displayAnnotations', $pa_options, false);
	$ps_annotation_display_template 	= caGetOption('displayAnnotationTemplate', $pa_options, caGetOption('displayAnnotationTemplate', $va_detail_config['options'], '^ca_representation_annotations.preferred_labels.name'));
	$default_annotation_id		 		= caGetOption('defaultAnnotationID', $pa_options, null);
	$start_timecode		 				= caGetOption('startTimecode', $pa_options, null);
	$ps_display_type		 			= caGetOption('display', $pa_options, false);
	$always_use_clover_viewer		 	= caGetOption('alwaysUseCloverViewer', $pa_options, false);

	$vs_slides = '';
	$slide_list = [];
	
	$t_instance = Datamodel::getInstanceByTableName($po_data->tableName(), true);
	
	$vo_data = null;
	if(is_a($po_data, 'SearchResult') && ($t_instance) && (is_a($t_instance, 'RepresentableBaseModel'))) {
		$vo_data = $po_data;
	} elseif(is_a($po_data, 'ca_object_representations')) {
		$vo_data = caMakeSearchResult('ca_object_representations', [$po_data->getPrimaryKey()]);
	} elseif(is_a($po_data, 'RepresentableBaseModel')) {
		$vo_data = caMakeSearchResult($po_data->tableName(), [$po_data->getPrimaryKey()]);
	} else {
		return _t('No media');
	}
	
	$o_view->setVar('t_subject', $pt_subject);
	$o_view->setVar('active_representation_class', $ps_active_representation_class);
	$o_view->setVar('context', ($vs_context = $po_request->getParameter('context', pString)) ? $vs_context : $vs_context = $po_request->getAction());

	$va_rep_ids = array();
	if (method_exists($vo_data, 'filterNonPrimaryRepresentations')) { $vo_data->filterNonPrimaryRepresentations(false); }
	while($vo_data->nextHit()) {
		if (!($vn_representation_id = $vo_data->get('ca_object_representations.representation_id', ['checkAccess' => $va_access_values, 'limit' => 1]))) { continue; }
		$t_instance->load($vo_data->getPrimaryKey());
		if($t_instance->getPrimaryRepresentationId()){
			$vn_representation_id = $t_instance->getPrimaryRepresentationId();
		}
		if($pn_representation_id = $po_request->getParameter("representation_id", pInteger)){
			$vn_representation_id = $pn_representation_id;
		}
					
		// Assemble id's for representations to display
		if($pb_primary_only){
			$va_rep_ids[] = $vn_representation_id;
		}elseif(sizeof($va_rep_ids = $t_instance->getRepresentationIDs(["checkAccess" => $va_access_values]))) {
			# --- are there multiple reps?
			if($vn_primary_id = array_search(1, $va_rep_ids)){
				unset($va_rep_ids[$vn_primary_id]);
				$va_rep_ids = array_merge([$vn_primary_id], array_keys($va_rep_ids));
			}else{
				$va_rep_ids = array_keys($va_rep_ids);
			}
		}
		
		// Fetch representations for display
		if(sizeof($va_rep_ids) > 0){
			$qr_reps = caMakeSearchResult('ca_object_representations', $va_rep_ids);
			$va_rep_tags = $qr_reps->getRepresentationViewerHTMLBundles($po_request, $pt_subject, array_merge($pa_options, ['context' => $vs_context]));

			$va_rep_info = array();

			$qr_reps->seek(0);
			$mimetypes_present = $show_only_media_types_when_present_reduced = [];

			if ($show_only_media_types_when_present) {
				while($qr_reps->nextHit()) {
					$mimetypes_present[$qr_reps->getMediaInfo('ca_object_representations.media', 'original', 'mimetype')] = true;
				}
				foreach($show_only_media_types_when_present as $t) {
					if (caMimetypeIsValid($t, array_keys($mimetypes_present))) {
						$show_only_media_types_when_present_reduced[] = $t;
					}
				}
			}
			$qr_reps->seek(0);

			$filtered_rep_ids = [];
			while($qr_reps->nextHit()) {
				if(!($mimetype = $qr_reps->getMediaInfo('ca_object_representations.media', 'original', 'mimetype'))) { continue; }
				if($show_only_media_types && !caMimetypeIsValid($mimetype, $show_only_media_types)) { continue; }

				if($show_only_media_types_when_present_reduced && !caMimetypeIsValid($mimetype, $show_only_media_types_when_present_reduced)) { continue; }

				$filtered_rep_ids[] =  $vn_rep_id = $qr_reps->get('representation_id');

				$vn_index = null;
				if($vn_rep_id == $vn_primary_id){
					$vn_index = 0;
				}elseif (!($vn_index = (int)$qr_reps->get(RepresentableBaseModel::getRepresentationRelationshipTableName($pt_subject->tableName()).'.rank'))) {
					$vn_index = $qr_reps->get('ca_object_representations.representation_id');
				}
				$va_rep_info[$vn_index] = array("rep_id" => $vn_rep_id, "tag" => $va_rep_tags[$vn_rep_id]);
			}
			$va_rep_ids = $filtered_rep_ids;

			// reset rep_ids  to ensure same order as slides as order may change if primary is not in first location
			$o_view->setVar('representation_ids', array_values(array_map(function($v) { return $v['rep_id']; }, $va_rep_info)));

			$vn_count = 0;

			$slide_list = [];
			foreach($va_rep_info as $vn_order => $va_rep){
				if(sizeof($va_rep_ids) > 1){ 
					$vs_slides .= "<li id='slide{$va_rep['rep_id']}' class='{$va_rep['rep_id']}'>"; 
				}
				$vs_slides .= ($vn_count == 0) ? "<div id='slideContent{$va_rep['rep_id']}'>".$va_rep["tag"]."</div>" : "<div id='slideContent{$va_rep['rep_id']}'></div>";	// initially only load first one

				if(sizeof($va_rep_ids) > 1) { 
					$vs_slides .= "</li>"; 
				}
				$slide_list[] = $va_rep["tag"];

				$vn_count++;
			}
		} elseif(!$pb_dont_show_placeholder) {
			if(!$po_request->config->get("disable_lightbox")){
				$o_lightbox_config = caGetLightboxConfig();

				if(!($vs_lightbox_icon = $o_lightbox_config->get("addToLightboxIcon"))){
					$vs_lightbox_icon = "<i class='fa fa-suitcase'></i>";
				}
				$va_lightboxDisplayName = caGetLightboxDisplayName($o_lightbox_config);
				$vs_lightbox_displayname = $va_lightboxDisplayName["singular"];
				$vs_lightbox_displayname_plural = $va_lightboxDisplayName["plural"];
				$vs_tool_bar = "<div id='detailMediaToolbar'>";
				if ($po_request->isLoggedIn()) {
					$vs_tool_bar .= " <a href='#' onclick='caMediaPanel.showPanel(\"".caNavUrl($po_request, '', 'Lightbox', 'addItemForm', array($pt_subject->primaryKey() => $pt_subject->getPrimaryKey()))."\"); return false;' aria-label='"._t("Add item to %1", $vs_lightbox_displayname)."' title='"._t("Add item to %1", $vs_lightbox_displayname)."'>".$vs_lightbox_icon."</a>\n";
				}else{
					$vs_tool_bar .= " <a href='#' onclick='caMediaPanel.showPanel(\"".caNavUrl($po_request, '', 'LoginReg', 'LoginForm')."\"); return false;' aria-label='"._t("Login to add item to %1", $vs_lightbox_displayname)."' title='"._t("Login to add item to %1", $vs_lightbox_displayname)."'>".$vs_lightbox_icon."</a>\n";
				}
				$vs_tool_bar .= "</div><!-- end detailMediaToolbar -->\n";
			}

			$vs_placeholder = "<div class='detailMediaPlaceholder' aria-label='No media available'>".caGetPlaceholder($pt_subject->getTypeCode(), "placeholder_large_media_icon")."</div>".$vs_tool_bar;
		}
	}	
	
	$o_view->setVar('representation_id', $vn_representation_id);
	$o_view->setVar('representation_count', sizeof($va_rep_ids));
	$o_view->setVar('representation_ids', $va_rep_ids);
	$o_view->setVar('placeholder', $vs_placeholder);
	$o_view->setVar('slides', $vs_slides);
	$o_view->setVar('slide_list', $slide_list);
	$o_view->setVar('display_annotations', $ps_display_annotations);
	$o_view->setVar('default_annotation_id', $default_annotation_id);
	$o_view->setVar('start_timecode', $start_timecode);

	return $o_view->render('representation_viewer_html.php');
}
# ---------------------------------------
/*
 * Toolbar for representation when displayed on object detail pages and in gallery
 *
 * @param RequestHTTP $po_request The current request
 * @param ca_object_representations $pt_representation  A ca_object_representations instance to render the toolbar for
 * @param RepresentableBaseModel|int $pt_subject = A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record) or an integer object_id
 
 * @param $pa_options array includes:
 *			display = media_display.conf display version to use. [Default is 'detail']
 *			context = viewer context value to pass in toolbar. For Pawtucket details this is the detail name. [Default is null]
 *			set_id = ID for set item being display is part of. [Default is null]
 *
 * @return string HTML toolbar output
 */
function caRepToolbar($po_request, $pt_representation, $pt_subject, $pa_options=null){
	$ps_display_type 		= caGetOption('display', $pa_options, 'detail');
	$ps_context 			= caGetOption('context', $pa_options, null);
	$o_media_display_config = caGetMediaDisplayConfig();

	$ps_table = is_object($pt_subject) ? $pt_subject->tablename() : "ca_objects";
	$pn_subject_id = is_object($pt_subject) ? $pt_subject->getPrimaryKey() : (int)$pt_subject;

	$va_rep_display_info = caGetMediaDisplayInfo($ps_display_type, $pt_representation->getMediaInfo('media', 'INPUT', 'MIMETYPE'));
	$va_rep_display_info['poster_frame_url'] = $pt_representation->getMediaUrl('media', $va_rep_display_info['poster_frame_version']);

	$va_add_to_set_link_info = caGetAddToSetInfo($po_request);

	$vs_tool_bar = "<div class='detailMediaToolbar'>";
	$vn_rep_id = $pt_representation->getPrimaryKey();

	$va_detail_type_config = caGetDetailTypeConfig($ps_context);

	if (!caGetOption(['no_overlay'], $va_rep_display_info, false)) {
		$overlay_icon = $o_media_display_config->get('overlay_icon');
		
		$apm = new ApplicationPluginManager();
		$toolbar_dtl = $apm->hookRepToolBarZoomButton([
			'request' => $po_request, 'representation_id' => $vn_rep_id, 
			'table' => $ps_table, 'subject_id' => $pn_subject_id, 
			'options' => $pa_options,
			'zoomParams' => $zoom_params = ['context' => $ps_context, 'id' => $pn_subject_id, 'representation_id' => $vn_rep_id, 'set_id' => caGetOption('set_id', $pa_options, 0), 'overlay' => 1]
		]);
		if(isset($toolbar_dtl['zoomParams']) && is_array($toolbar_dtl['zoomParams'])) {
			$zoom_params = $toolbar_dtl['zoomParams'];
		}
		$vs_tool_bar .= "<a href='#' class='zoomButton' onclick='caMediaPanel.showPanel(\"".caNavUrl($po_request, '', 'Detail', 'GetMediaOverlay', $zoom_params)."\", function() { var url = jQuery(\"#\" + caMediaPanel.getPanelID()).data(\"reloadUrl\"); if(url) { window.location = url; } }); return false;' aria-label='"._t("Open Media View")."' title='"._t("Open Media View")."'>{$overlay_icon}</a>\n";	
	}
	if (is_null($vb_show_compare = caGetOption('compare', $va_detail_type_config['options'], null))) {
		$vb_show_compare = caGetOption('compare', $va_rep_display_info, false);
	}
	if ($vb_show_compare) {
	   $compare_icon = $o_media_display_config->get('compare_icon'); 
	   $vs_tool_bar .= "<a href='#' class='compare_link' aria-label='Compare' data-id='representation:{$vn_rep_id}'>{$compare_icon}</a>";
	}

	if(($ps_table == "ca_objects") && is_array($va_add_to_set_link_info) && sizeof($va_add_to_set_link_info)){
		$vs_tool_bar .= " <a href='#' class='setsButton' onclick='caMediaPanel.showPanel(\"".caNavUrl($po_request, '', $va_add_to_set_link_info['controller'], 'addItemForm', array('context' => $ps_context, (is_object($pt_subject) && $pt_subject->primaryKey()) ? $pt_subject->primaryKey() : "object_id" => $pn_subject_id))."\"); return false;' aria-label='".$va_add_to_set_link_info['link_text']."' title='".$va_add_to_set_link_info['link_text']."'>".$va_add_to_set_link_info['icon']."</a>\n";
	}
	if(caObjectsDisplayDownloadLink($po_request, $pn_subject_id, $pt_representation)){
		# -- get version to download configured in media_display.conf
		$vs_download_version = caGetAvailableDownloadVersions($po_request, $pt_representation->getMediaInfo('media', 'INPUT', 'MIMETYPE'), ['returnVersionForUser' => true]);
		
		if($vs_download_version){
			$download_icon = $o_media_display_config->get('download_icon');
			$vs_tool_bar .= caNavLink($po_request, $download_icon, 'dlButton', 'Detail', 'DownloadRepresentation', '', array('context' => $ps_context, 'representation_id' => $pt_representation->getPrimaryKey(), "id" => $pn_subject_id, "download" => 1, "version" => $vs_download_version), array("aria-label" => _t("Download")));
		}
	}
	$vs_tool_bar .= "</div><!-- end detailMediaToolbar -->\n";

	return $vs_tool_bar;
}
# ---------------------------------------
/**
 *
 */
function caGetAvailableDownloadVersions(RequestHTTP $request, ?string $mimetype, ?array $options=null) {
	$download_display_info = caGetMediaDisplayInfoForMimetype('download', $mimetype);
	
	$download_version = caGetOption(['download_version', 'display_version'], $download_display_info);
	$download_version_by_role = caGetOption('roles', $download_display_info);
		
	$available_versions = [];
	if($download_version) { $available_versions[] = $download_version; }
	
	if($request->isLoggedIn()) {
		$user_roles = $request->user->getUserRoles(['skipVars' => true]);
		foreach($user_roles as $role) {
			if(isset($download_version_by_role[$role['code']])) {
				$download_version = $download_version_by_role[$role['code']];
				$available_versions[] = $download_version;
				break;
			}
		}
	}
	
	if(caGetOption('returnVersionForUser', $options, false)) {
		return $download_version;
	}
	return $available_versions;
}
# ---------------------------------------
/**
 * Extract an IIIF service-style media identifier from the current request. First checks for a "identifier" parameter, which is
 * assumed to be an IIIF service-style media identifier (Ex. representation:114; attribute:29341). If 
 * that is not defined the numeric representation_id or value_id parameters are converted into representation and attribute IIIF identifiers respectively.
 *
 * @param RequestHTTP $po_request The current request
 * @return string IIIF service-style media identifier or null if no identifier is found
 */
function caGetMediaIdentifier($po_request) {		
	if (!($ps_identifier = $po_request->getParameter('identifier', pString))) {
		if($pn_representation_id = $po_request->getParameter('representation_id', pInteger)) { 
			$ps_identifier = "representation:{$pn_representation_id}";
		} elseif($pn_value_id = $po_request->getParameter('value_id', pInteger)) {
			$ps_identifier = "attribute:{$pn_value_id}";
		}
	}
	return $ps_identifier ? $ps_identifier : null;
}
# ---------------------------------------
/**
 * Parse IIIF service-style identifier and return instance for underlying media. Instance will be
 * either a ca_object_representations model or a ca_attribute_values model instance.
 *
 * @param string $identifier
 * @param array $options Options include:
 *		checkAccess = [TODO: IMPLEMENT THIS]
 * @return ca_object_representations|ca_attribute_values instance
 */
function caGetMediaForMediaIdentifier(string $identifier, array $options=null) {		
	list($type, $id) = explode(':', $identifier);

	switch(strtolower($type)) {
		case 'representation':
			$rep = new ca_object_representations($id);
			return ($rep->isLoaded()) ? $rep : null;
			break;
		case 'attribute':
			$val = new ca_attribute_values($id);
			return ($val->isLoaded()) ? $val : null;
			break;
	}
	return null;
}
# ---------------------------------------
/**
 *
 */
function caGetMediaAnnotationList($po_data, $pa_options=null) {
	$va_detail_config = caGetDetailConfig()->get($po_data->tableName());
	$ps_annotation_display_template 	= caGetOption('displayAnnotationTemplate', $pa_options, caGetOption('displayAnnotationTemplate', $va_detail_config['options'], '^ca_representation_annotations.preferred_labels.name'));
	$ps_display_type		 			= caGetOption('display', $pa_options, false);

	$va_annotation_list = [];
	$va_props = $po_data->getMediaInfo('media', 'original', 'PROPERTIES');
	if (
		is_array($va_annotations = $po_data->get('ca_representation_annotations.annotation_id', array('returnAsArray' => true))) 
		&& 
		sizeof($va_annotations)
		&&
		($qr_annotations = caMakeSearchResult('ca_representation_annotations', $va_annotations))
	) {
		while($qr_annotations->nextHit()) {
			if (!preg_match('!^TimeBased!', $qr_annotations->getAnnotationType())) { continue; }
			$vn_rep_id = $qr_annotations->get('ca_representation_annotations.representation_id');
			$va_annotation_list[] = "<a href='#' onclick='caUI.mediaPlayerManager.seek(\"caMediaOverlayTimebased_{$vn_rep_id}_{$ps_display_type}\", ".((float)$qr_annotations->getPropertyValue('startTimecode', true) - (float)$va_props['timecode_offset'])."); return false;'>".$qr_annotations->getWithTemplate($ps_annotation_display_template)."</a>";
		}
	}

	return $va_annotation_list;
}
# ---------------------------------------
/*
 * Render a single media item as a slide for use with a media viewer generated by caRepresentationViewer(). 
 * This is used by the media viewer to load media via Ajax as-needed.
 *
 * @param RequestHTTP $po_request The current request
 * @param string $ps_identifier IIIF style media identifier
 * @param BaseModel $pt_subject A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is show for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *		inline = generate media viewer for use embedded within a page, rather than a full-screen overlay. [Default is false]
 *		display = media_display.conf display version to use. [Default is 'detail']
 *		context = viewer context value to pass in toolbar. For Pawtucket details this is the detail name. [Default is null]
 *		captionTemplate = Display template for caption to include with image. Caption is evalulated related to the subject ($pt_subject) instance. [Default is null]
 *		displayAnnotations = Mode of display for annotations on representation. Valid values are: viewer (in viewer), div (in external div with class #detailAnnotations), none (no display) [Default is none]
 *		displayAnnotationTemplate = Template to use when formatting list of annotations [Default is the annotation title (^ca_representation_annotations.preferred_labels.name)]
 *
 * @return string Viewer HTML
 * @throws ApplicationException
 * @see caRepresentationViewer()
 */
function caGetMediaViewerHTML($po_request, $ps_identifier, $pt_subject, $pa_options=null) {
	if (!($va_identifier = caParseMediaIdentifier($ps_identifier))) {
		throw new ApplicationException(_t('Invalid identifier %1', $ps_identifier));
	}
	$pn_subject_id = $pt_subject->getPrimaryKey();
	$va_detail_config = caGetDetailConfig()->get($pt_subject->tableName());

	$ps_display_type 					= caGetOption('display', $pa_options, 'media_overlay');
	$pb_inline 							= (bool)caGetOption('inline', $pa_options, false);
	$ps_context 						= caGetOption('context', $pa_options, $po_request->getParameter('context', pString));
	$ps_display_annotations	 			= caGetOption('displayAnnotations', $pa_options, false);
	$ps_annotation_display_template 	= caGetOption('displayAnnotationTemplate', $pa_options, caGetOption('displayAnnotationTemplate', $va_detail_config['options'], '^ca_representation_annotations.preferred_labels.name'));
	$pb_hide_overlay_controls			= (bool)caGetOption('hideOverlayControls.', $pa_options, false);
	$pa_check_acccess 					= caGetOption('checkAccess', $pa_options, null);
	$pb_no_overlay						= (bool)caGetOption('noOverlay', $pa_options, false);
	$item_id							= (int)caGetOption('item_id', $pa_options, null);

	$vs_caption = $vs_tool_bar = '';

	switch($va_identifier['type']) {
		case 'representation':
			//
			// View object representation
			//
			$pn_representation_id = (int)$va_identifier['id'];
			$t_instance = new ca_object_representations($pn_representation_id);

			if ($pb_inline) {
				$vs_caption = ($vs_template = caGetOption('captionTemplate', $pa_options, caGetOption('captionTemplate', $va_display_info, null))) ? $t_instance->getWithTemplate($vs_template) : '';
			}
			if (!$t_instance->isReadable($po_request)) { 
				throw new ApplicationException(_t('Cannot view media'));
			}

			if (!($vs_mimetype = $t_instance->getMediaInfo('media', 'original', 'MIMETYPE'))) {
				$vs_mimetype = $t_instance->getMediaInfo('media', 'h264_hi', 'MIMETYPE');
			}
			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype))) {
				throw new ApplicationException(_t('Invalid viewer: %1/%2', $vs_mimetype, $ps_display_type));
			}

			$va_display_info = caGetMediaDisplayInfo($ps_display_type, $vs_mimetype);

			if ((($vn_use_universal_viewer_for_image_list_length = caGetOption('use_universal_viewer_for_image_list_length_at_least', $va_display_info, null))
			||
			($vn_use_mirador_for_image_list_length = caGetOption('use_mirador_for_image_list_length_at_least', $va_display_info, null)))
			) {
				$vn_image_count = $pt_subject->numberOfRepresentationsOfClass('image');
				$vn_rep_count = $pt_subject->getRepresentationCount();

				// Are there enough representations? Are all representations images? 
				if ($vn_image_count == $vn_rep_count) {
					if (!is_null($vn_use_universal_viewer_for_image_list_length) && ($vn_image_count >= $vn_use_universal_viewer_for_image_list_length)) {
						$va_display_info['viewer'] = $vs_viewer_name = 'UniversalViewer';
						$pb_hide_overlay_controls = true;
					} elseif(!is_null($vn_use_mirador_for_image_list_length) && ($vn_image_count >= $vn_use_mirador_for_image_list_length)) {
						$va_display_info['viewer'] = $vs_viewer_name = 'Mirador';
						$pb_hide_overlay_controls = true;
					}
				}
			}

			if(!$pn_subject_id) {
				if (is_array($va_subject_ids = $t_instance->get($pt_subject->tableName().'.'.$pt_subject->primaryKey(), array('returnAsArray' => true))) && sizeof($va_subject_ids)) {
					$pn_subject_id = array_shift($va_subject_ids);
				} else {
					throw new ApplicationException(_t('Invalid subject'));
					return;
				}
			}

			$vs_viewer = $vs_viewer_name::getViewerHTML(
				$po_request, 
				"representation:{$pn_representation_id}", 
				['t_instance' => $t_instance, 't_subject' => $pt_subject, 'display' => $va_display_info, 'display_type' => $ps_display_type],
				['viewerWrapper' => caGetOption('inline', $pa_options, false) ? 'viewerInline' : null, 'context' => $ps_context, 'hideOverlayControls' => $pb_hide_overlay_controls, 
				'noOverlay' => $pb_no_overlay, 'checkAccess' => $pa_check_acccess, 'item_id' => $item_id,
				'resultList' => caGetOption('resultList', $pa_options, null), 'showRepresentationViewerNextPreviousLinks' => (bool)caGetOption('showRepresentationViewerNextPreviousLinks', $pa_options, false)]
			);

			if ($pb_inline) {	
				$vs_tool_bar = caRepToolbar($po_request, $t_instance, $pt_subject, array('display' => $ps_display_type, 'context' => $ps_context, 'checkAccess' => $pa_check_acccess));
				$vs_viewer = "<div class='repViewerContCont'><div id='cont{$pn_representation_id}' class='repViewerCont'>{$vs_viewer}{$vs_tool_bar}{$vs_caption}</div></div>";
			}

			if (($ps_display_annotations) && (is_array($va_annotation_list = caGetMediaAnnotationList($t_instance, $pa_options)))) {
				$vs_viewer .= join("<br/>\n", $va_annotation_list);
			}
			return $vs_viewer;
			break;
		case 'attribute':
			//
			// View FT_MEDIA attribute media 
			//
			$pn_value_id = (int)$va_identifier['id'];
			$t_instance = new ca_attribute_values($pn_value_id);
			$t_instance->useBlobAsMediaField(true);
			$t_attr = new ca_attributes($t_instance->get('attribute_id'));


			$pt_subject = Datamodel::getInstanceByTableNum($t_attr->get('table_num'), true);
			$pt_subject->load($t_attr->get('row_id'));

			
			if (!$pt_subject->isReadable($po_request)) { 
				throw new ApplicationException(_t('Cannot view media'));
			}
			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('value_blob', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer: %1/%2', $ps_display_type, $vs_mimetype));
			}

			$vs_viewer = $vs_viewer_name::getViewerHTML(
				$po_request, 
				"attribute:{$pn_value_id}", 
				['t_instance' => $t_instance, 't_subject' => $pt_subject, 'display' => caGetMediaDisplayInfo($ps_display_type, $vs_mimetype), 'display_type' => $ps_display_type],
				['viewerWrapper' => caGetOption('inline', $pa_options, false) ? 'viewerInline' : null, 'context' => $ps_context, 'hideOverlayControls' => $pb_hide_overlay_controls, 'checkAccess' => $pa_check_acccess, 'noOverlay' => $pb_no_overlay]
			);

			if ($pb_inline) {
				$vs_tool_bar = caRepToolbar($po_request, $t_instance, $pt_subject, array('display' => $ps_display_type, 'context' => $ps_context, 'checkAccess' => $pa_check_acccess));
				$vs_viewer = "<div data-representation_id='{$pn_representation_id}' data-value_id='{$pn_value_id}' class='repViewerContCont'><div id='cont{$pn_representation_id}' class='repViewerCont'>{$vs_viewer}{$vs_tool_bar}{$vs_caption}</div></div>";
			}

			return $vs_viewer;
			break;
	}

	throw new ApplicationException(_t('Invalid identifier', $ps_identifier));
}
# ---------------------------------------
/*
 * Returns data callbacks from media viewers
 *
 * @param RequestHTTP $po_request The current request
 * @param string $ps_identifier IIIF-style media identifer
 * @param RepresentableBaseModel $pt_subject A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *			display = media_display.conf display version to use. [Default is 'detail']
 *
 * @return string Data for media viewer (JSON, HTML, etc.)
 * @throws ApplicationException
 */
function caGetMediaViewerData($po_request, $ps_identifier, $pt_subject, $pa_options=null) {
	$ps_identifier = urldecode($ps_identifier);
	if (!($va_identifier = caParseMediaIdentifier($ps_identifier))) {
		throw new ApplicationException(_t('Invalid identifier %1', $ps_identifier));
	}

	$ps_display_type = caGetOption('display', $pa_options, 'media_overlay');

	switch($va_identifier['type']) {
		case 'representation':
			$t_instance = new ca_object_representations($va_identifier['id']);

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('media', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			$va_display_info = caGetMediaDisplayInfo($ps_display_type, $vs_mimetype);
			if ((($vn_use_universal_viewer_for_image_list_length = caGetOption('use_universal_viewer_for_image_list_length_at_least', $va_display_info, null))
			||
			($vn_use_mirador_for_image_list_length = caGetOption('use_mirador_for_image_list_length_at_least', $va_display_info, null)))
			) {
				$vn_image_count = $pt_subject->numberOfRepresentationsOfClass('image');
				$vn_rep_count = $pt_subject->getRepresentationCount();

				// Are there enough representations? Are all representations images? 
				if ($vn_image_count == $vn_rep_count) {
					if (!is_null($vn_use_universal_viewer_for_image_list_length) && ($vn_image_count >= $vn_use_universal_viewer_for_image_list_length)) {
						$va_display_info['viewer'] = $vs_viewer_name = 'UniversalViewer';
					} elseif(!is_null($vn_use_mirador_for_image_list_length) && ($vn_image_count >= $vn_use_mirador_for_image_list_length)) {
						$va_display_info['viewer'] = $vs_viewer_name = 'Mirador';
					}
				}
			}

			return $vs_viewer_name::getViewerData($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => $va_display_info, 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
		case 'attribute':
			$t_instance = new ca_attribute_values($va_identifier['id']);
			$t_instance->useBlobAsMediaField(true);
			$t_attr = new ca_attributes($t_instance->get('attribute_id'));
			$pt_subject = Datamodel::getInstanceByTableNum($t_attr->get('table_num'), true);
			$pt_subject->load($t_attr->get('row_id'));

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('value_blob', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			return $vs_viewer_name::getViewerData($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => caGetMediaDisplayInfo($ps_display_type, $vs_mimetype), 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
	}

	throw new ApplicationException(_t('Invalid identifier', $ps_identifier));
}
# ---------------------------------------
/*
 * 
 *
 * @param RequestHTTP $po_request The current request
 * @param string $ps_identifier IIIF-style media identifer
 * @param RepresentableBaseModel $pt_subject A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *			display = media_display.conf display version to use. [Default is 'detail']
 *
 * @return string Data for media viewer (JSON, HTML, etc.)
 * @throws ApplicationException
 */
function caSearchMediaData($po_request, $ps_identifier, $pt_subject, $pa_options=null) {
	if (!($va_identifier = caParseMediaIdentifier($ps_identifier))) {
		throw new ApplicationException(_t('Invalid identifier %1', $ps_identifier));
	}

	$ps_display_type = caGetOption('display', $pa_options, 'media_overlay');

	switch($va_identifier['type']) {
		case 'representation':
			$t_instance = new ca_object_representations($va_identifier['id']);

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('media', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			$va_display_info = caGetMediaDisplayInfo($ps_display_type, $vs_mimetype);

			return $vs_viewer_name::searchViewerData($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => $va_display_info, 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
		case 'attribute':
			$t_instance = new ca_attribute_values($va_identifier['id']);
			$t_instance->useBlobAsMediaField(true);
			$t_attr = new ca_attributes($t_instance->get('attribute_id'));
			$pt_subject = Datamodel::getInstanceByTableNum($t_attr->get('table_num'), true);
			$pt_subject->load($t_attr->get('row_id'));

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('value_blob', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			return $vs_viewer_name::searchViewerData($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => caGetMediaDisplayInfo($ps_display_type, $vs_mimetype), 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
	}

	throw new ApplicationException(_t('Invalid identifier', $ps_identifier));
}
# ---------------------------------------
/*
 * 
 *
 * @param RequestHTTP $po_request The current request
 * @param string $ps_identifier IIIF-style media identifer
 * @param RepresentableBaseModel $pt_subject A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *			display = media_display.conf display version to use. [Default is 'detail']
 *
 * @return string Data for media viewer (JSON, HTML, etc.)
 * @throws ApplicationException
 */
function caMediaDataAutocomplete($po_request, $ps_identifier, $pt_subject, $pa_options=null) {
	if (!($va_identifier = caParseMediaIdentifier($ps_identifier))) {
		throw new ApplicationException(_t('Invalid identifier %1', $ps_identifier));
	}

	$ps_display_type = caGetOption('display', $pa_options, 'media_overlay');

	switch($va_identifier['type']) {
		case 'representation':
			$t_instance = new ca_object_representations($va_identifier['id']);

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('media', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			$va_display_info = caGetMediaDisplayInfo($ps_display_type, $vs_mimetype);

			return $vs_viewer_name::autocomplete($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => $va_display_info, 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
		case 'attribute':
			$t_instance = new ca_attribute_values($va_identifier['id']);
			$t_instance->useBlobAsMediaField(true);
			$t_attr = new ca_attributes($t_instance->get('attribute_id'));
			$pt_subject = Datamodel::getInstanceByTableNum($t_attr->get('table_num'), true);
			$pt_subject->load($t_attr->get('row_id'));

			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype = $t_instance->getMediaInfo('value_blob', 'original', 'MIMETYPE')))) {
				throw new ApplicationException(_t('Invalid viewer'));
			}

			return $vs_viewer_name::autocomplete($po_request, $ps_identifier, ['t_subject' => $pt_subject, 't_instance' => $t_instance, 'display' => caGetMediaDisplayInfo($ps_display_type, $vs_mimetype), 'display_type' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)]);
			break;
	}

	throw new ApplicationException(_t('Invalid identifier', $ps_identifier));
}
# ------------------------------------------------------------------
/**
 * Returns an array of representations rendered as viewable media.
 *
 * @param RequestHTTP $po_request The current request
 * @param SearchResult $po_data A ca_object_represention or RepresentableBaseModel search result to render slides for. A slide will be rendered for each representation.
 * @param RepresentableBaseModel $pt_subject A model instance loaded with the subject (the record the media is shown in the context of. Eg. if a representation is shown for an object this is an instance for that object record)
 * @param array $pa_options Options include:
 *			display = media_display.conf display version to use. [Default is 'detail']
 *		
 * @return array Array of HTML media viewers for representations referenced by $po_data
 * @throws ApplicationException
 */
function caRepresentationViewerHTMLBundles($po_request, $po_data, $pt_subject, $pa_options=null) {
	$ps_display_type 		= caGetOption('display', $pa_options, null);
	$pn_subject_id = $pt_subject->getPrimaryKey();
	
	$va_reps = [];
	while($po_data->nextHit()) {
		$va_representation_ids = $po_data->get('ca_object_representations.representation_id', ['returnAsArray' => true]);
		
		foreach($va_representation_ids as $vn_representation_id) {
			$t_instance = new ca_object_representations($vn_representation_id);

			if (!($vs_mimetype = $t_instance->getMediaInfo('media', 'original', 'MIMETYPE'))) {
				$versions = $t_instance->getMediaVersions('media');
				$version = array_pop($versions);
				$vs_mimetype = $t_instance->getMediaInfo('media', $version, 'MIMETYPE');
				$vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype);
			}
			if (!($vs_viewer_name = MediaViewerManager::getViewerForMimetype($ps_display_type, $vs_mimetype))) {
				//throw new ApplicationException(_t('Invalid viewer %1/%2', $ps_display_type, $vs_mimetype));
				continue;
			}

			$va_display_info = caGetMediaDisplayInfo($ps_display_type, $vs_mimetype);
			if ($pt_subject && ($vn_use_universal_viewer_for_image_list_length = caGetOption('use_universal_viewer_for_image_list_length_at_least', $va_display_info, null))) {
				$vn_image_count = $pt_subject->numberOfRepresentationsOfClass('image');
				$vn_rep_count = $pt_subject->getRepresentationCount();

				// Are there enough representations? Are all representations images? 
				if (($vn_image_count == $vn_rep_count) && ($vn_image_count >= $vn_use_universal_viewer_for_image_list_length)) {
					$va_display_info['viewer'] = $vs_viewer_name = 'UniversalViewer';
				}
			}

			if(!$pn_subject_id) {
				if (is_array($va_subject_ids = $t_instance->get($pt_subject->tableName().'.'.$pt_subject->primaryKey(), array('returnAsArray' => true))) && sizeof($va_subject_ids)) {
					$vn_subject_id = array_shift($va_subject_ids);
				} else {
					throw new ApplicationException(_t('Invalid id'));
					return;
				}
			}

			$vs_tool_bar = caGetOption('noToolBar', $pa_options, false) ? "" : caRepToolbar($po_request, $t_instance, $pt_subject, array('display' => $ps_display_type, 'context' => caGetOption('context', $pa_options, null)));

			$vs_caption = (isset($pa_options["captionTemplate"]) && $pa_options["captionTemplate"]) ? $po_data->getWithTemplate($pa_options["captionTemplate"]) : "";

			$va_reps[$vn_rep_id = $po_data->get('ca_object_representations.representation_id')] = "<div data-representation_id='{$vn_rep_id}' class='repViewerContCont'><div id='cont{$vn_rep_id}' class='repViewerCont'>".$vs_viewer_name::getViewerHTML(
				$po_request, 
				"representation:{$vn_representation_id}", 
				['t_instance' => $t_instance, 't_subject' => $pt_subject, 'display' => $va_display_info, 'display_type' => $ps_display_type],
				['viewerWrapper' => 'viewerInline', 'context' => caGetOption('context', $pa_options, null), 'checkAccess' => caGetOption('checkAccess', $pa_options, null)]
			).$vs_tool_bar.$vs_caption."</div></div>";

			//if (sizeof($va_reps) > 10) { break(2); }
		}
	}
	return $va_reps;
}
# ------------------------------------------------------------------
/**
 * Get Javascript code that generates Tooltips for a list of elements
 * @param array $pa_tooltips List of tooltips to set as selector=>text map
 * @param string $ps_class CSS class to use for tooltips
 *
 * @return string
 */
function caGetTooltipJS($pa_tooltips, $ps_class = 'tooltipFormat') {
	$vs_buf = "<script type='text/javascript'>\njQuery(document).ready(function() {\n";

	foreach($pa_tooltips as $vs_element_selector => $vs_tooltip_text) {
		$vs_buf .= "jQuery('{$vs_element_selector}').attr('title', '".preg_replace('![\n\r]{1}!', ' ', addslashes($vs_tooltip_text))."').tooltip({ tooltipClass: '{$ps_class}', show: 150, hide: 150});\n";
	}

	$vs_buf .= "});\n</script>\n";

	return $vs_buf;
}
# ------------------------------------------------------------------
/**
 * Get bundle preview for a relationship bundle
 *
 * @param array $pa_initial_values
 * @param string $ps_delimiter
 *
 * @return string
 */
function caGetBundlePreviewForRelationshipBundle($pa_initial_values, $ps_delimiter='; ') {
	if(!is_array($pa_initial_values) || sizeof($pa_initial_values) == 0) {
		return '""';
	}

	// it's very unlikely that the preview will fit more then 10 items
	if(sizeof($pa_initial_values) > 10) {
		$pa_initial_values = array_slice($pa_initial_values, 0, 10);
	}

	$va_previews = array();
	foreach($pa_initial_values as $va_item) {
		$va_previews[] = trim($va_item['_display'] ?? null);
	}

	return caEscapeForBundlePreview(join($ps_delimiter, $va_previews));
}
# ------------------------------------------------------------------
/**
 * Perform tag substitution on a view. 
 *
 * Views can contain tags in the form {{{tagname}}}. Some tags, such as "itemType" and "detailType" are defined by
 * the detail controller. More usefully, you can pull data from the item being detailed by using a valid "get" expression
 * as a tag (Eg. {{{ca_objects.idno}}}. Even more usefully for some, you can also use a valid bundle display template
 * (see http://docs.collectiveaccess.org/wiki/Bundle_Display_Templates) as a tag. The template will be evaluated in the 
 * context of the item being detailed.
 *
 * @param View $po_view
 * @param string $ps_template_path
 * @param BaseModel|SearchResult $pm_subject
 * @param array $pa_options
 * 			render = 
 *			checkAccess = 
 *			clearVars = 
 *			barcodes = 
 *
 * @return array|string
 */
function caDoTemplateTagSubstitution($po_view, $pm_subject, $ps_template_path, $pa_options=null) {
	$pa_access_values = caGetOption('checkAccess', $pa_options, null);
	$pb_barcodes = caGetOption('barcodes', $pa_options, false);

	if (caGetOption('clearVars', $pa_options, false)) { $po_view->clearViewTagsVars($ps_template_path); }

	$va_defined_vars = array_keys($po_view->getAllVars());		// get list defined vars (we don't want to copy over them)

	$va_tag_list = $po_view->getTagList($ps_template_path);		// get list of tags in view

	$va_barcode_files_to_delete = [];
	foreach($va_tag_list as $vs_tag) {
		if (in_array($vs_tag, $va_defined_vars)) { continue; }

		if ($pb_barcodes && ($vs_barcode_file = caParseBarcodeViewTag($vs_tag, $po_view, $pm_subject, $pa_options))) {
			$va_barcode_files_to_delete[] = $vs_barcode_file;
		} elseif ((strpos($vs_tag, "^") !== false) || (strpos($vs_tag, "<") !== false)) {
			$po_view->setVar($vs_tag, $pm_subject->getWithTemplate($vs_tag, array('autoConvertLineBreaks' => true, 'checkAccess' => $pa_access_values, 'doRefSubstitution' => true)));
		} elseif (strpos($vs_tag, ".") !== false) {
			$po_view->setVar($vs_tag, $pm_subject->get($vs_tag, array('checkAccess' => $pa_access_values, 'doRefSubstitution' => true)));
		} else {
			$po_view->setVar($vs_tag, "?{$vs_tag}");
		}
	}

	if (caGetOption('render', $pa_options, false)) {
		return $po_view->render($ps_template_path);
	}

	if ($pb_barcodes) {
		return $va_barcode_files_to_delete;
	}

	return true;
}
# ------------------------------------------------------------------
/**
 * Check if hierarchy browser drag-and-drop sorting is enabled for the current user in the current table for a given item.
 *
 * @param RequestHTTP $pt_request The current request
 * @param string $ps_table The table being browsed
 * @param int $pn_id The primary key for the parent of the hierarchy level being browsed. Some tables (notably ca_list_items) can have different enabled statuses for different items. If null then status is determined at the table level. [Default is null]
 *
 * @return bool
 */
function caDragAndDropSortingForHierarchyEnabled($pt_request, $ps_table, $pn_id=null) {
	$o_config = Configuration::load();

	if (!($t_instance = Datamodel::getInstanceByTableName($ps_table, true))) { return null; }

	if(!$pt_request->isLoggedIn() || (!$pt_request->user->canDoAction("can_edit_{$ps_table}") && (($vs_hier_table = $t_instance->getProperty('HIERARCHY_DEFINITION_TABLE')) ? !$pt_request->user->canDoAction("can_edit_{$vs_hier_table}") : false))) { return false; }
	if (!$t_instance->isHierarchical()) { return false; }
	if (!($vs_rank_fld = $t_instance->getProperty('RANK'))) { return false; }
	if (!is_null($pn_id) && !$t_instance->load($pn_id)) { return false; }

	$vs_def_table_name = $t_instance->getProperty('HIERARCHY_DEFINITION_TABLE');
	$vs_def_id_fld = $t_instance->getProperty('HIERARCHY_ID_FLD');

	if ($vs_def_table_name && ($t_def = Datamodel::getInstanceByTableName($vs_def_table_name, true)) && ($t_def->load($t_instance->get($vs_def_id_fld))) && ($t_def->hasField('default_sort')) && ((int)$t_def->get('default_sort') === __CA_LISTS_SORT_BY_RANK__)) {
		return true;
	} else {
		$va_sort_values = $o_config->getList("{$ps_table}_hierarchy_browser_sort_values");
		if ((sizeof($va_sort_values) >= 1) && ($va_sort_values[0] === "{$ps_table}.{$vs_rank_fld}")) { return true; }
	}
	return false;
}
# ------------------------------------------------------------------
/**
 * Return an array of statuses for drag-and-drop reordering by row_id for a given table. If the table does not support separate
 * drag-and-drop statuses per row then a boolean value is returned that applies to the entire table. 
 *
 * Currently, only ca_list_items supports per-row statuses.
 *
 * @param RequestHTTP $pt_request The current request
 * @param string $ps_table The table being browsed
 * @param int $pn_id The primary key for the parent of the hierarchy level being browsed. Some tables (notably ca_list_items) can have different enabled statuses for different items.
 *
 * @return mixed An array if the table supports per-row drag-and-drop reordering statuses, boolean values as would be returned by caDragAndDropSortingForHierarchyEnabled() otherwise.
 *
 * @seealso caDragAndDropSortingForHierarchyEnabled
 */
function caGetDragAndDropSortingAvailabilityMap($pt_request, $ps_table, $pn_id) {
	$o_config = Configuration::load();

	if ($ps_table == 'ca_list_items') {
		$t_list = new ca_lists();
		$va_list_of_lists = $t_list->getListOfLists();

		$va_map = [];
		foreach($va_list_of_lists as $vn_list_id => $va_lists_by_locale) {
			foreach($va_lists_by_locale as $vn_locale_id => $va_item) {
				$va_map[$va_item['root_id']] = caDragAndDropSortingForHierarchyEnabled($pt_request, 'ca_list_items', $va_item['root_id']);
			}
		}
		return $va_map;
	} else {
		return caDragAndDropSortingForHierarchyEnabled($pt_request, $ps_table, $pn_id);
	}
}
# ------------------------------------------------------------------
/**
 * Extract a value from an array of settings using the specified locale. If a value for the locale is
 * not available all other locales with the same language will be tried. For example, if en_US is specified
 * and there is no value then en_AU, en_GB, en_CA, etc. will be tried as well.
 *
 * @param array $ps_settings A settings block
 * @param string $ps_key The name of the setting
 * @param string $ps_locale A locale code (ex. "en_US") or language code (ex. "en")
 */
function caExtractSettingValueByLocale($pa_settings, $ps_key, $ps_locale) {
	if (isset($pa_settings[$ps_key])) {
		if(isset($pa_settings[$ps_key]) && !is_array($pa_settings[$ps_key])) { return $pa_settings[$ps_key]; }
		if (isset($pa_settings[$ps_key][$ps_locale]) && ($pa_settings[$ps_key][$ps_locale])) {
			return $pa_settings[$ps_key][$ps_locale];
		} elseif(is_array($va_locales_for_language = ca_locales::localesForLanguage($ps_locale, ['codesOnly' => true]))) {
			foreach($va_locales_for_language as $vs_locale) {
				if($pa_settings[$ps_key][$vs_locale] ?? null) { return $pa_settings[$ps_key][$vs_locale]; }
			}
		}
	}
	return null;
}
# ------------------------------------------------------------------
/**
 *
 *
 * @param RequestHTTP $po_request
 * @param string $ps_text
 * @param array $pa_options Options include:
 *      page = Page_id or path to evaluate media within. [Default is null]
 *		value_id = 
 *
 * @return string
 */
function caProcessReferenceTags($request, $text, $options=null) {
	$pm_page = caGetOption('page', $options, null);
	$idnos = [];

	$allowed_tags = ['b', 'i', 'em', 'strong', 'u', 'strike', 'img', 'video', 'audio', 'div', 'span']; // TODO: make configurable

	$text = html_entity_decode($text);
	$value_id = caGetOption('value_id', $options, null);

	if (!is_array($access_values = caGetUserAccessValues($request)) || !sizeof($access_values)) { $access_values = null; }

	$tags = [
		'object' => 'ca_objects', 'entity' => 'ca_entities', 'place' => 'ca_places',
		'occurrence' => 'ca_occurrences', 'collection' => 'ca_collections', 'loan' => 'ca_loans',
		'movement' => 'ca_movements', 'location' => 'ca_storage_locations', 'media' => 'ca_site_page_media', 'mediaRef' => 'ca_attributes'];
	
	// Old style (bbcode-like) tags
	foreach($tags as $ref_tag => $ref_type
	) {
		if (preg_match_all("!\[{$ref_tag} ([^\]]+)\]([^\[]+)\[/{$ref_tag}\]!", $text, $matches)) {
			foreach($matches[1] as $i => $attr_string) {
				if (sizeof($vals = caParseAttributes($attr_string, ['id', 'idno', 'class', 'version'])) > 0) {
					$vals['content'] = $matches[2][$i];
					$idnos[$ref_type][$matches[0][$i]] = array_filter($vals, function($v) { return !is_null($v); });
				}
			}
		}
		if (preg_match_all("!\[{$ref_tag} ([^\]]+)/\]!", $text, $matches)) {
			foreach($matches[1] as $i => $attr_string) {
				if (sizeof($vals = caParseAttributes($attr_string, ['id', 'idno', 'class', 'version'])) > 0) {
					$idnos[$ref_type][$matches[0][$i]] = array_filter($vals, function($v) { return !is_null($v); });
				}
			}
		}
	}
	
	// New style (html-like) tags 
	// Ex. <object idno="2024.001">^ca_objects.preferred_labels.name</object>
	$o_doc = str_get_dom($text);
	$group_count = 0;
	foreach($o_doc->children as $index => $node) {
		$tag = strtolower($node->tag);
		if(isset($tags[$tag])) {
			$table = $tags[$tag];
			if($t_instance = $table::findAsInstance(['idno' => $node->idno])) {
				$template = $node->html();
				$node->setOuterText($t_instance->getWithTemplate($template));
			}
		} else {
			switch($tag) {
				case 'mediagroup':
					$rep_ids = [];
					$group_count++;
					if($t_instance = caGetInstanceForValueID($value_id)) {							
						if(!($group = mb_strtolower($node->group))) { $group = "group_{$group_count}"; }
						
						$render = strtolower($node->render);
						$template = strtolower($node->getInnerText());
						$idnos = $node->idnos;
						
						if($group && ($qr = $t_instance->getRepresentationsAsSearchResult())) {
							$media_group = [];
							while($qr->nextHit()) {
								if(mb_strtolower($qr->get('ca_object_representations.media_group')) !== $group) { continue; }
								
								$media_item = [
									'representation_id' => $rep_id = $qr->get('ca_object_representations.representation_id'),
									'caption' => $qr->get('ca_object_representations.caption'),
								];
								if($template) {
									$media_item['display'] = $qr->getWithTemplate($template);
								}
								foreach($qr->getMediaVersions('media') as $v) {
									$media_item['urls'][$v] = $qr->getMediaUrl('media', $v);
									$media_item['tags'][$v] = $qr->getMediaTag('media', $v);
								}
								
								$media_group[] = $media_item;
							}
							$rep_ids = array_map(function($v) { return $v['representation_id']; }, $media_group);
						}	
						
						if($idnos) {
							$idnos = preg_split('![ ]*[;,]+[ ]*!', $idnos);
							if(sizeof($idnos = array_filter($idnos, 'strlen'))) {
								$idnos = array_map('trim', $idnos);
								
								$acc = [];
								if($qr = ca_objects::findAsSearchResult(['idno' => ['IN', $idnos]])) {
									while($qr->nextHit()) {
										if(!($rep_id = $qr->get('ca_object_representations.representation_id'))) { continue; }
										$rep_ids[] = $rep_id;
										$idno = $qr->get('ca_objects.idno');
										$media_item = [
											'representation_id' => $rep_id,
											'caption' => strip_tags($qr->get('ca_object_representations.caption'), $allowed_tags),
										];
										if($template) {
											$media_item['display'] = strip_tags($qr->getWithTemplate($template), $allowed_tags);
										}
										foreach($qr->getMediaVersions('media') as $v) {
											$media_item['urls'][$v] = $qr->getMediaUrl('media', $v);
											$media_item['tags'][$v] = $qr->getMediaTag('media', $v);
										}
										
										$acc[$idno] = $media_item;
									}
									foreach($idnos as $idno) {
										$media_group[] = $acc[$idno];
									}
								}
							}
						}
						
						if(sizeof($rep_ids)) {
							$view = new View($request, $request->getViewsDirectoryPath()."/bundles/");
							$view->setVar('representation_ids', $rep_ids);
							$view->setVar('subject', $t_instance);
							$view->setVar('representations', caMakeSearchResult('ca_object_representations', $rep_ids));
							$view->setVar('media_group', $media_group);
							$view->setVar('idnos', $idnos);
							$view->setVar('group', $group);
							$view->setVar('value_id', $value_id);
							$view->setVar('template', $template);
							$view->setVar('width', $node->width);
							$view->setVar('height', $node->height);
							$content = $view->render("mediagroups/{$render}_html.php");
							$node->setOuterText($content);
						} else {
							$node->setOuterText("<p>ERROR</p>");
						}
					} else {
						$node->setOuterText("<p>ERROR</p>");
					}
					break;
			}
		}
	}
	$text = str_replace(array("<~root~>", "</~root~>"), "", $o_doc->html());
	
	if (sizeof($idnos)) {
		foreach($idnos as $ref_type => $va_tags) {
			switch($ref_type) {
				case 'ca_attributes':
					foreach($va_tags as $tag => $va_tag) {
						$vn_value_id = (int)$va_tag['id'];

						$t_instance = ca_attributes::getRowInstanceForValueID($vn_value_id);
						if (!$t_instance->isReadable($request)) { continue; }
						if ($template = $va_tag['content']) {

							$t_attr = ca_attributes::getAttributeForValueID($vn_value_id);
							$text = str_replace($tag, caProcessTemplate($template, $t_attr->getAttributeValues(['returnAs' => 'array', 'version' => caGetOption('version', $va_tag, array_shift($t_attr->getMediaVersions('value_blob')))]), []), $text);
						} else {
							$t_val = new ca_attribute_values($vn_value_id);
							$text = str_replace($tag, $t_val->getMediaTag('value_blob', caGetOption('version', $va_tag, array_shift($t_val->getMediaVersions('value_blob')))), $text);
						}
					}
					break;
				case 'ca_site_page_media':
					foreach($idnos[$ref_type] as $tag => $va_l) {
						$params = ['idno' => $va_l['idno']];
						if ($pm_page && ((int)$pm_page > 0)) {
							$params['page_id'] = (int)$pm_page;
						} elseif (strlen($pm_page)) {
							$params['path'] = $pm_page;
						}
						$qr_m = ca_site_page_media::find($params, ['returnAs' => 'searchResult']);
						while ($qr_m->nextHit()) {
							if (is_array($access_values) && !in_array($qr_m->get('access'), $access_values)) { 
								$text = str_replace($tag, '', $text); // remove tag that cannot be resolved.
								continue;
							}

							if (!($caption = trim($qr_m->get('caption')))) { $caption = null; }
							if (!($title = trim($qr_m->get('title')))) { $title = null; }
							if (!($idno = trim($qr_m->get('idno')))) { $idno = null; }

							$alt_text = caGetOption(['caption', 'title', 'idno'], ['caption' => $caption, 'title' => $title, 'idno' => $idno], null);

							if ($template = $va_l['content']) {
								$template = str_replace("^title", $title, $template);
								$template = str_replace("^caption", $caption, $template);
								$template = str_replace("^idno", $idno, $template);
								$template = str_replace("^file", $qr_m->getMediaTag('media', caGetOption('version', $va_l, array_shift($qr_m->getMediaVersions('media'))), ['alt' => $alt_text]), $template);
								$text = str_replace($tag, $template, $text);
							} else {
								$text = str_replace($tag, $qr_m->getMediaTag('media', caGetOption('version', $va_l, array_shift($qr_m->getMediaVersions('media'))), ['alt' => $alt_text]), $text);
							}

							break;
						}
					}
					break;
				default:
					$va_map = call_user_func($ref_type.'::getIDsForIdnos', array_map(function($v) { return $v['idno']; }, $idnos[$ref_type]), ['forceToLowercase' => true, 'checkAccess' => $access_values]);

					$idnos[$ref_type] = array_map(function($v) use ($va_map) { $v['id'] = $va_map[strtolower($v['idno'])]; return $v; }, $va_tags);

					foreach($idnos[$ref_type] as $tag => $va_l) {
						$link_text = '';
						if (isset($va_l['id']) && $va_l['id']) {
							$x = caProcessTemplateForIDs($va_l['content'], $ref_type, [$va_l['id']]);
							switch(__CA_APP_TYPE__) {
								case 'PROVIDENCE':
									$link_text= caEditorLink($request, $va_l['content'], $va_l['class'], $ref_type, $va_l['id']);
									break;
								case 'PAWTUCKET':
									$link_text= caDetailLink($request, $x, $va_l['class'], $ref_type, $va_l['id']);
									break;
							}
						}
						
						$text = str_replace($tag, $link_text, $text);
					}
					break;
			}

		}
	}
	return $text;
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetLookupUrlsForTables($po_request=null, $pa_tables=null) {
	global $g_request;
	if (!$po_request) { $po_request = $g_request; }
	if (!$po_request) { return []; }
	if (!is_array($pa_tables) || !sizeof($pa_tables)) {
		$pa_tables = ['ca_objects', 'ca_entities', 'ca_places', 'ca_occurrences', 'ca_collections', 'ca_object_lots', 'ca_loans', 'ca_movements'];
	}


	$va_lookup_urls = [];
	foreach($pa_tables as $vs_table) {
		if (!caTableIsActive($vs_table)) { continue; }
		$va_urls = caJSONLookupServiceUrl($po_request, $vs_table);
		$va_lookup_urls[$vs_table] = ['singular' => Datamodel::getTableProperty($vs_table, 'NAME_SINGULAR'), 'plural' => Datamodel::getTableProperty($vs_table, 'NAME_PLURAL'), 'code' => strtolower(Datamodel::getTableProperty($vs_table, 'NAME_SINGULAR')), 'url' => $va_urls['search']];   
	}
	return $va_lookup_urls;
}
# ------------------------------------------------------------------
/**
 *
 */
function caConvertCurrentLocationCriteriaToBundleSettings() {
	$o_config = Configuration::load();
	$va_bundle_settings = array();
	$t_rel_type = new ca_relationship_types();

	$va_map = $o_config->getAssoc('current_location_criteria');
	if(!is_array($va_map)){
		$va_map = array();
	}

	foreach($va_map as $vs_table => $va_types) {
		$va_bundle_settings["{$vs_table}_showTypes"] = array();
		if(is_array($va_types)) {
			foreach($va_types as $vs_type => $va_config) {
				switch($vs_table) {
					case 'ca_storage_locations':
					case 'ca_objects_x_storage_locations':
						$va_bundle_settings["{$vs_table}_showRelationshipTypes"][] = $t_rel_type->getRelationshipTypeID('ca_objects_x_storage_locations', $vs_type);
						break;
					default:
						if(!is_array($va_config)) { break; }
						$va_bundle_settings["{$vs_table}_showTypes"][] = array_shift(caMakeTypeIDList($vs_table, array($vs_type)));
						$va_bundle_settings["{$vs_table}_{$vs_type}_dateElement"] = $va_config['date'];
						break;
				}
			}
		}
	}
	return $va_bundle_settings;
}
# ------------------------------------------------------------------
/**
 * Return currently set blank preferred label placeholder text. This text
 * is used to set labels that are saved with no value set.
 *
 * The returned value will be the placeholder as configured via the app.conf
 * "blank_label_text" option. If the option is not set the default value of
 * "[BLANK]" will be returned.
 *
 * @param mixed $table Table name (or number) blank label is to be applied to. If set 
 *						table-specific text is set with fallback to general "blank_label_text"
 *
 * @return string
 */
function caGetBlankLabelText($table=null) {
	if (MemoryCache::contains('blank_label_text_'.$table)) { return MemoryCache::fetch('blank_label_text_'.$table); }
	$config = Configuration::load();

	$table_orig = $table;
	$d = [];
	if (($table) && ($t = Datamodel::getInstance($table, true))) {
		if(is_a($t, 'BaseLabel')) {
			$table = $t->getSubjectTableName();
		}
		$d[] = $table.'_blank_label_text';
	}

	$d[] = 'blank_label_text';

	if ($label_text = $config->get($d)) {
		if(is_array($label_text)) { $label_text = join(' ', $label_text); }
		MemoryCache::save('blank_label_text_'.$table, $l = _t($label_text));
		if($table !== $table_orig) { MemoryCache::save('blank_label_text_'.$table_orig, $l); }
		return $l;
	}
	return $g_blank_label_text = _t('BLANK');
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetDisplayLabelForBundle($bundle) {
	$tmp = explode('.', $bundle);
	if (!($t = Datamodel::getInstance($tmp[0], true))) { return null; }

	return $t->getDisplayLabel($bundle);
}
# ------------------------------------------------------------------
/**
 * Returns concatenated first name and surname, with a default in case
 * both values are null.
 *
 * In case any of the names is missing, no additional (left or right)
 * whitespace is included.
 *
 * @param      $fname
 * @param      $lname
 * @param null $default
 *
 * @return mixed|string
 */
function caFormatPersonName($fname, $lname, $default=null){
	$names = [];
	$fname ? ( $names[] = $fname ) : null ;
	$lname ? ( $names[] = $lname ) : null ;

	if ($fname || $lname){
		return join( ' ', $names );
	}
	return _t($default);
}
# ------------------------------------------------------------------
/**
 * Strip special characters for filename prior to download
 *
 * @param string $filename
 * @param array $options No options are currently supported
 *
 * @return string 
 * @throws ApplicationException
 */
function caEscapeFilenameForDownload(string $filename, ?array $options=null) : string {
	$v = preg_replace("![\|;\<\>\(\)\$\`\~&\\\\]+!", "_", html_entity_decode($filename));
	if(preg_match('^\.+$', $filename)) {
		throw new ApplicationError(_t('Invalid filename'));
	}
	return $v;
}
# ------------------------------------------------------------------
/**
 * Generate name for downloaded representation media file based upon app.conf 
 * downloaded_file_naming directive.
 *
 * @param string $table Table name of primary record (Eg. ca_objects when downloaded representations related to an object).
 * @param array $data Media download data. Keys include:
 *		idno = identifer of primary record.
 *		index = index of download when multiple names are present. May be omitted if not applicable.
 *		version = version of media being downloaded.
 *		extension = file extension of media being downloaded.
 *		original_filename = original filename of media being downloaded.
 *		representation_id = Representation_id of media being downloaded.
 * @param array $options Options include:
 *		mode = Naming mode. Can be idno, idno_and_version, idno_and_rep_id_and_version, original_name. If not set defaults to value in <table>_downloaded_file_naming or _downloaded_file_naming app.conf directive.
 *
 * @return string File name
 */
function caGetRepresentationDownloadFileName(string $table, array $data, ?array $options=null) : string {
	$config = Configuration::load();
	if(isset($data['extension']) && (in_array(strtolower($data['extension']), ['m4v', 'm4a']))) { $data['extension'] = 'mp4'; }
	switch($mode = caGetOption('mode', $options, $config->get(["{$table}_downloaded_file_naming", 'downloaded_file_naming']))) {
		case 'idno':
			$filename = $data['idno'].(strlen($data['index']) ? '_'.$data['index'] : '').'.'.$data['extension'];
			break;
		case 'idno_and_version':
			$filename = $data['idno'].'_'.$data['version'].'_'.(strlen($data['index']) ? '_'.$data['index'] : '').'.'.$data['extension'];
			break;
		case 'idno_and_rep_id_and_version':
			$filename = $data['idno'].'_representation_'.$data['representation_id'].'_'.$data['version'].'.'.$data['extension'];
			break;
		case 'original_name':
		default:
			if (strpos($mode, "^") !== false) { // template
			   $filename = caProcessTemplateForIDs($mode, 'ca_object_representations', [$data['representation_id']]);
			   
			} elseif ($data['original_filename']) {
				$tmp = explode('.', $data['original_filename']);
				if (sizeof($tmp) > 1) { 
					if (strlen($ext = array_pop($tmp)) < 3) {
						$tmp[] = $ext;
					}
				}
				$filename = join('_', $tmp); 					
			} else {
				$filename = $data['idno'].'_representation_'.$data['representation_id'].'_'.$data['version'];
			}

			if (isset($va_file_names[$filename.'.'.$data['extension']])) {
				$filename.= "_".$data['index'];
			}

			if(!preg_match("!{$data['extension']}$!i", $filename)) {
				$filename .= '.'.$data['extension'];
			}
			break;
	} 

	return caEscapeFilenameForDownload($filename);
}
# ------------------------------------------------------------------
/**
 * Generate name for downloaded ZIP file containing multiple representation media files based upon app.conf 
 * downloaded_file_naming directive.
 *
 * @param string $table Table name of primary record (Eg. ca_objects when downloaded representations related to an object).
 * @param int $id
 * @param array $options Options include:
 *		extension = 
 *
 * @return string File name
 */
function caGetMediaDownloadArchiveName($table, $id, $options=null) {
	$config = Configuration::load();
	switch($mode = $config->get(["{$table}_downloaded_media_archive_file_naming", 'downloaded_media_archive_file_naming', "{$table}_downloaded_file_naming", 'downloaded_file_naming'])) {
		case 'idno':
			// Noop - fall through	
		default:
			if (strpos($mode, "^") === false) { // use default template
				$mode = "^{$table}.idno";
			}
			if (!($filename = caProcessTemplateForIDs($mode, $table, [$id]))) {
				$filename = 'export';
			}
			$ext = caGetOption('extension', $options, 'zip');

			if(!preg_match("!\.{$ext}$!i", $filename)) {
				$filename .= ".{$ext}";
			}
			break;
	} 
	return caEscapeFilenameForDownload($filename);
}
# ------------------------------------------------------------------
/**
 * Return logo for specified UI context
 *
 * @param string $content Content where logo will be used. Possible values are: "menuBar" (application menu bar); "login" (application login screen), "report" (report headers)
 *
 * @return string
 */
function caGetBrandingLogo(string $type, array $options=null) : ?string {
	if(!in_array($type, ['menuBar', 'login', 'report'], true)) { return null; }
	
	$abs = caGetOption('absolute', $options, false);
	
	global $g_request;
	$config = Configuration::load();
	
	if(is_array($branding = $config->getAssoc('branding')) && is_array($logo = caGetOption($type, $branding, null)) && !empty($logo['src'])) {
		return caHTMLImage(($abs ? __CA_BASE_DIR__ : __CA_URL_ROOT__).'/'.caGetOption('src', $logo), ['alt' => caGetOption('alt', $logo), 'class' => caGetOption('class', $logo), 'style' => caGetOption('style', $logo), 'id' => caGetOption('id', $logo), 'scaleCSSWidthTo' => caGetOption('width', $logo), 'scaleCSSHeightTo' => caGetOption('height', $logo)]);
	}
	
	if(!$g_request) { return null; }
	
	// Return header defined by old app.conf directive
	switch($type) {
		case 'menuBar':
			$img = $config->get('header_img');
			break;
		case 'login':
			$img = $config->get('login_img');
			break;
		case 'report':
			$img = $config->get('header_img');
			break;
		default:
			return null;
			
	}
	if ($img) {
		return caHTMLImage($g_request->getUrlPathForThemeFile("graphics/logos/{$img}"), ['alt' => _t("CollectiveAccess Logo"), 'class' => 'headerImg']);
	}
	return null;
}
# ------------------------------------------------------------------
/**
 * Return currently configured logo graphic for menu bar
 *
 * @return string
 */
function caGetMenuBarLogo() {
	return caGetBrandingLogo('menuBar');
}
# ------------------------------------------------------------------
/**
 * Return currently configured logo graphic for login screen
 *
 * @return string
 */
function caGetLoginLogo() {
	return caGetBrandingLogo('login');
}
# ------------------------------------------------------------------
/**
 * Return currently configured logo graphic for report headers
 *
 * @return string
 */
function caGetReportLogo() {
	return caGetBrandingLogo('report', ['absolute' => true]);
}
# ------------------------------------------------------------------
/**
 * Output sidecar data to browser.
 *
 * @param int $sidecar_id
 * @param ca_users $user
 *
 * @return void
 * 
 * @throws ApplicationException
 */
function caReturnMediaSidecarData(int $sidecar_id, ?ca_users $user=null) : void {
	if(!$t_sidecar = ca_object_representation_sidecars::find($sidecar_id, ['returnAs' => 'firstModelInstance'])) {
		throw new ApplicationException(_t('Invalid sidecar'));
	}
	$t_rep = ca_object_representations::find($t_sidecar->get('ca_object_representation_sidecars.representation_id'), ['returnAs' => 'firstModelInstance']);
	if(!$t_rep || ($user && !$t_rep->isReadable($user))) {
		throw new ApplicationException(_t('Access denied'));
	}
	
	$mimetype = $t_sidecar->get('mimetype');
	
	if(caMimetypeIsValid($mimetype, ['image/*', 'text/prs.wavefront-mtl', 'application/octet-stream'])) {
		AppController::getInstance()->removeAllPlugins();	// prevent header/footer from being rendered
		header("Content-type: {$mimetype}");
		
		$r = fopen($t_sidecar->getFilePath('sidecar_file'), 'r');
		fpassthru($r);
		fclose($r);
		return;
	} 
	throw new ApplicationException(_t('Invalid sidecar type'));
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetReferenceToExistingRepresentationMedia(ca_object_representations $t_rep, array $options=null) {
	$return_as_array = caGetOption('returnAsArray', $options, false);
	
	global $g_request;
	$rel = $rec_label = null;
	foreach(['ca_objects', 'ca_entities', 'ca_occurrences', 'ca_collections'] as $parent_table) {
		if($rel_list = $t_rep->getRelatedItems($parent_table, ['returnAs' => 'array'])) {
			$rel = array_shift($rel_list);
			$rec_label = $rel['label'].($rel['idno']? " (".$rel['idno'].")" : '');
			break;
		}
	}

	if($return_as_array) {
		if($rec_label) {
			return [
				'table' => $parent_table,
				'id' => $rel[Datamodel::primaryKey($parent_table)],
				'label' => $rel['label'],
				'idno' => $rel['idno']
			];
		} else {
			return [
				'table' => 'ca_object_representations',
				'id' => $t_rep->getPrimaryKey(),
				'label' => $t_rep->get('ca_object_representations.preferred_labels.name'),
				'idno' => $t_rep->get('ca_object_representations.idno')
			];
		}
	} else {
		$rec_label = $rec_label ? 
			_t('Media aleady exists in %1', ($g_request ? 
				caEditorLink($g_request, $rec_label, '', $parent_table, $rel[Datamodel::primaryKey($parent_table)])
				: $rec_label))
			:
			($g_request ? _t('Media aleady %1', caEditorLink($g_request, _t('exists'), '', 'ca_object_representations', $t_rep->getPrimaryKey())) : _t('Media already exists'))
		;
		return $rec_label;
	}
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetTextExcerpt(?string $content, array $search_terms, ?array $options=null) : ?string {
	require_once(__CA_LIB_DIR__.'/Parsers/ganon.php');
	
	$before = caGetOption('before', $options, 100);
	$after = caGetOption('after', $options, 100);
	$search_terms = array_map(function($v) {
		$t = preg_replace('![i]{0,1}\*!', '', $v);
		return $t;
	}, $search_terms);
	
	$excerpts = [];
	for($s=0; $s < sizeof($search_terms); $s++) {
		$n = sizeof($search_terms) - $s;
		while($n > 0) {
			$terms = array_map(function($v) {
				return preg_quote($v, '!')."[^\s]*";
			}, array_slice($search_terms, $s, $n));
			$regex = trim(join('[ ]+', $terms));
			if(preg_match("/(?<![A-Za-z0-9]){$regex}/iu", $content, $m)) {
				if($m[0]){
					$index = mb_strpos($content, $m[0]);
					$start = (($index-$before) > 0) ? ($index-$before) : 0;
					$length = $before + $after + mb_strlen($m[0]);
					
					$str = mb_substr($content, $start);
					if($o_doc = str_get_dom($str)) {
						$extext = '';
						foreach($o_doc->children as $i => $node) {
							switch($node->tag) {
								case '~text~':
									$t = $node->html();
									if(mb_strlen($extext.$t) > $length) {
										$t = mb_substr($t, 0, $length - mb_strlen($extext));
									}
									$extext .= $t;
									break;
								default:
									$extext .= $node->html();
									break;
							}
							
							if(mb_strlen($extext) >= $length) { break; }
						}
					} else {
						$extext = mb_substr(strip_tags($str), 0, $length);
					}
					
					$excerpts[] = "<p>... {$extext} ...</p>";
					$content = mb_substr($content, $start + $length);
				}
			}
			$n--;
		}
	}
	return join('', array_unique($excerpts));
}
# ------------------------------------------------------------------
/**
 *
 */
function caHighlightText($content, $highlight_words) {
	if(is_array($content)) { return $content; }

	if(!is_array($highlight_words)) { return $content; }	// use global directly, if possible, for performance
	
	$highlight_words = array_reduce($highlight_words, function($c, $v) {
		if(mb_substr($v, -1, 1) == '*') {
			$v = mb_substr($v, 0, mb_strlen($v) - 1);
			if($v[-1] == 'i') { $v = mb_substr($v, 0, mb_strlen($v) - 1); }
			array_push($c, preg_quote($v, '/').'[A-Za-z0-9]*');
		}
		if(!strlen($v)) { array_pop($c); return $c; }
		if(mb_substr($v, -1, 1) == 's') {
			array_push($c, (mb_substr($v, 0, mb_strlen($v) - 1)."'s"));
			array_push($c, (mb_substr($v, 0, mb_strlen($v) - 1)."’s"));
		}
		array_push($c, $v);
		return $c;
	}, []);
	if(!sizeof($highlight_words)) { return $content; }
	usort($highlight_words, function($a, $b) {
		return strlen($b) <=> strlen($a);
	});
	
	$content = preg_replace("/(?<![A-Za-z0-9])(".join('|', $highlight_words).")/i", "<span class=\"highlightText\">\\1</span>", $content);
	
	return $content;
}
# ------------------------------------------------------------------
/**
 *
 */
function caApplyFindViewUserRestrictions(ca_users $t_user, string $table, ?array $options=null) : array {
	$views = caGetFindViewList($table);
	if(!is_a($t_user, 'ca_users', )) { return $views; }
	if(!$t_user->isValidPreference("find_{$table}_available_result_views")) { return $views; }
	
	$allowed_views = $t_user->getPreference("find_{$table}_available_result_views");
	
	$type_id = caGetOption('type_id', $options, null);
	if(is_array($allowed_views[$table][$type_id] ?? null) && sizeof($allowed_views[$table][$type_id])) {
		foreach($views as $i => $v) {
			if(!in_array($v, $allowed_views[$table][$type_id], true)) {
				unset($views[$i]);
			}
		}
	}
	return $views;
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetFindViewList($table_name_or_num) : ?array {
	if(!($t_instance = Datamodel::getInstance($table_name_or_num, true))) { return null; }
	$table = $t_instance->tableName();
	
	switch($table) {
		case 'ca_objects':
			return [
				'list' => _t('list'),
				'full' => _t('full'),
				'thumbnail' => _t('thumbnails'),
			];
			break;
		case 'ca_object_representations':
			return [
				'list' => _t('list'),
				'thumbnail' => _t('thumbnails'),
			];
			break;
		default:
			return [
				'list' => _t('list')
			];
			break;
	}	
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetQuillToolbar(array $options=null) : ?array {
	$config = Configuration::load();
	
	$map = [
		'bold' => ['code' => 'bold'],
		'italic' => ['code' => 'italic'],
		'underline' => ['code' => 'underline'],
		'strike' => ['code' => 'strike'],
		'subscript' => ['code' => 'script', 'value' => 'sub'],
		'superscript' => ['code' => 'script', 'value' => 'super'],
		'header' => ['code' => 'header', 'value' => [false, 1, 2, 3, 4, 5, 6]],
		
		'clean' => ['code' => 'clean'],
		'removeformat' => ['code' => 'clean'],	// synonym for "clean"
		
		//'font' => ['code' => 'font', 'value' => []],	// Doesn't actually set font - sets Quill-specific class so let's not offer this
		//'fontsize' => ['code' => 'size', 'value' => []], / Doesn't actually set size - sets Quill-specific class so let's not offer this
		'textcolor' => ['code' => 'color', 'value' => []],
		//'background' => ['code' => 'background', 'value' => []], // Doesn't appear to render even thought Quill docs reference it
		
		'blockquote' => ['code' => 'blockquote'],
		'code' => ['code' => 'code', 'class' => 'ql-code-block'],
		'link' => ['code' => 'link'],
		'image' => ['code' => 'image'],
		'video' => ['code' => 'video'],
		'formula' => ['code' => 'formula'],
		'align' => ['code' => 'align', 'value' => []],
		
		'numberedlist' => ['code' => 'list', 'value' => 'ordered'],
		'bulletedlist' => ['code' => 'list', 'value' => 'bullet'],
		'checkList' => ['code' => 'list', 'value' => 'check'],
		'outdent' => ['code' => 'indent', 'value' => '+1'],
		'indent' => ['code' => 'indent', 'value' => '-1'],
		
		'direction' => ['code' => 'direction', 'value' => 'rtl'],
		//'undo' => ['code' => 'undo'],	// Doesn't appear to render even thought Quill docs reference it
		//'redo' => ['code' => 'redo'], // Doesn't appear to render even thought Quill docs reference it
	];
	
	$toolbar = $config->get(strtolower((caGetOption('type', $options, 'editor') )!== 'content') ? 'wysiwyg_editor_toolbar' : 'wysiwyg_content_editor_toolbar');
	if(!is_array($toolbar)) { return null; }
		
	$groups = [];
	foreach($toolbar as $group_name => $list) {
		$group = [];
		foreach($list as $item) {
			$item_case = strtolower($item);
			if(isset($map[$item_case])) {
				if(isset($map[$item_case]['value'])) {
					$group[] = [$map[$item_case]['code'] => $map[$item_case]['value']];
				} else {
					$group[] = $map[$item_case]['code'];
				}
			}
		}
		$groups[] = $group;
	}
	
	return $groups;
}
# ------------------------------------------------------------------
/**
 *
 */
function caGetCK5Toolbar(array $options=null) : ?array {
	$config = Configuration::load();
	
	$map = [
		'bold' => ['code' => 'bold'],
		'italic' => ['code' => 'italic'],
		'underline' => ['code' => 'underline'],
		'strike' => ['code' => 'strikethrough'],
		'subscript' => ['code' => 'subscript'],
		'superscript' => ['code' => 'superscript'],
		'code' => ['code' => 'code'],
		'header' => ['code' => 'heading'],
		
		'clean' => ['code' => 'removeFormat'],
		'removeformat' => ['code' => 'removeFormat'],	// synonym for "clean"
		
		'font' => ['code' => 'fontfamily'],	
		'fontsize' => ['code' => 'fontsize'],
		'textcolor' => ['code' => 'fontColor'],
		'background' => ['code' => 'fontBackgroundColor'],
		
		'blockquote' => ['code' => 'blockQuote'],
		'link' => ['code' => 'link'],
		'image' => ['code' => 'linkImage'],
		'video' => ['code' => 'mediaEmbed'],
		'formula' => ['code' => 'formula'],
		'align' => ['code' => 'alignment'],
		
		'numberedlist' => ['code' => 'numberedList'],
		'bulletedlist' => ['code' => 'bulletedList'],
		'checkList' => ['code' => 'todoList'],
		'outdent' => ['code' => 'outdent'],
		'indent' => ['code' => 'indent'],
		'source' => ['code' => 'sourceEditing'],
	
		'undo' => ['code' => 'undo'],	
		'redo' => ['code' => 'redo']
	];
	
	$toolbar = $config->get(strtolower((caGetOption('type', $options, 'editor') )!== 'content') ? 'wysiwyg_editor_toolbar' : 'wysiwyg_content_editor_toolbar');
	if(!is_array($toolbar)) { return null; }
		
	$groups = [];
	foreach($toolbar as $group_name => $list) {
		$group = [];
		foreach($list as $item) {
			$item_case = strtolower($item);
			if(isset($map[$item_case])) {
				if(isset($map[$item_case]['value'])) {
					$group[] = [$map[$item_case]['code'] => $map[$item_case]['value']];
				} else {
					$group[] = $map[$item_case]['code'];
				}
			}
		}
		$groups = array_merge($groups, $group);
	}
	return $groups;
}
# ------------------------------------------------------------------

