<?PHP
#
#   FILE: EditMetadataField.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2012-2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

# ----- EXPORTED FUNCTIONS ---------------------------------------------------

/**
 * Transform an array of field type enumerations to an array of human-readable
 * strings.
 * @param array $Types array of field type enumerations
 * @return array array of field human-readable strings
 */
function MakeTypesHumanReadable(array $Types)
{
    $Strings = MetadataField::$FieldTypeHumanEnums;

    foreach ($Types as $Constant => $Type)
    {
        $Types[$Constant] = GetArrayValue($Strings, $Constant, "Unknown");
    }

    return $Types;
}

/**
 * Get the list of all available qualifiers.
 * @return array list of all available qualifiers
 */
function GetQualifierList()
{
    $QualifierFactory = new QualifierFactory();
    $Qualifiers = $QualifierFactory->GetItemNames();

    return $Qualifiers;
}

/**
 * Get the first error from the given list of errors.
 * @param array $Errors list of errors
 * @return string|null the first error or NULL if none exist
 */
function GetFirstError(array $Errors)
{
    reset($Errors);
    return current($Errors) !== FALSE ? current($Errors) : NULL;
}

# ----- LOCAL FUNCTIONS ------------------------------------------------------

/**
 * Load the field with the given field ID, checking that the field is valid.
 * @param int $Id metadata field ID
 * @return MetadataField|null metadata field object if successful, else NULL
 */
function LoadMetadataField($Id)
{
    # the schema ID is unnecessary here
    $Schema = new MetadataSchema();
    $Field = $Schema->GetField($Id);

    # make sure the field is valid
    if (is_null($Field) || $Field->Status() !== MetadataSchema::MDFSTAT_OK)
    {
        return NULL;
    }

    return $Field;
}

/**
 * Load the qualifier with the given ID, checking that it is valid.
 * @param int $Id qualifier ID
 * @return Qualifier|null qualifier object if successful, else NULL
 */
function LoadQualifier($Id)
{
    # make sure the ID looks valid before attempting to load the qualifier
    if (!$Id || $Id == "--")
    {
        return NULL;
    }

    $Qualifier = new Qualifier($Id);

    # make sure the qualifier is valid
    if ($Qualifier->Status() != Qualifier::STATUS_OK)
    {
        return NULL;
    }

    return $Qualifier;
}

/**
 * Get the metadata field defaults for metadata fields with the given type.
 * @param int $Type field type to assume, text is used if invalid
 * @return array metadata field defaults
 */
function GetFieldDefaults($Type)
{
    $Schema = $GLOBALS["H_Schema"];

    # get the type-based defaults to use, defaulting to text ones if necessary
    $TypeBasedDefaults = GetArrayValue(
        MetadataField::$TypeBasedDefaults,
        $Type,
        MetadataField::$TypeBasedDefaults[MetadataSchema::MDFTYPE_TEXT]);

    # merge the lists
    $Defaults = array_merge(MetadataField::$FixedDefaults, $TypeBasedDefaults);

    # not a field per se because it requires additional actions
    $Defaults["AssociatedQualifierList"] = array();
    $Defaults["Type"] = MetadataSchema::MDFTYPE_TEXT;
    $Defaults["Name"] = NULL;

    # privileges
    $Defaults["ViewingPrivilegesLogic"] = $Schema->ViewingPrivileges()->AllRequired() ? "AND" : "OR";
    $Defaults["ViewingPrivileges"] = $Schema->ViewingPrivileges();
    $Defaults["AuthoringPrivilegesLogic"] = $Schema->AuthoringPrivileges()->AllRequired() ? "AND" : "OR";
    $Defaults["AuthoringPrivileges"] = $Schema->AuthoringPrivileges();
    $Defaults["EditingPrivilegesLogic"] = $Schema->EditingPrivileges()->AllRequired() ? "AND" : "OR";
    $Defaults["EditingPrivileges"] = $Schema->EditingPrivileges();
    $Defaults["PreviewingPrivilegesLogic"] = $Schema->ViewingPrivileges()->AllRequired() ? "AND" : "OR";
    $Defaults["PreviewingPrivileges"] = $Schema->ViewingPrivileges();

    return $Defaults;
}

/**
 * Extract the user-provided field values from the given array of values.
 * Removes the "F_" prefix from the keys of the values.
 * @param array $Values array of values, a subset of which are the field values
 * @return array extracted user-provided field values
 */
function ExtractFieldInput(array $Values)
{
    $Input = array();

    # extract form values
    foreach ($Values as $Key => $Value)
    {
        if (substr($Key, 0, 2) == "F_")
        {
            $Input[substr($Key, 2)] = $Value;
        }
    }

    # pick the most appropriate value for the default value field
    if (array_key_exists("DefaultValue", $Input))
    {
        $Type = GetArrayValue($Input, "Type");
        $DefaultValues = GetArrayValue($Input, "DefaultValue", array());

        # numeric default value
        if ($Type == MetadataSchema::MDFTYPE_NUMBER)
        {
            $Input["DefaultValue"] = GetArrayValue($DefaultValues, "Numeric");
        }

        # flag default value
        else if ($Type == MetadataSchema::MDFTYPE_FLAG)
        {
            $Input["DefaultValue"] = GetArrayValue($DefaultValues, "Flag");
        }

        # point default value
        else if ($Type == MetadataSchema::MDFTYPE_POINT)
        {
            $Input["DefaultValue"] = array(
                "X" => GetArrayValue($DefaultValues, "Point1"),
                "Y" => GetArrayValue($DefaultValues, "Point2"));
        }

        # everything else
        else
        {
            $Input["DefaultValue"] = GetArrayValue($DefaultValues, "Generic");
        }
    }

    $MayContainDoubleHyphen = array(
        "DefaultQualifier",
        "TreeBrowsingPrivilege",
        "ImagePreviewPrivilege");

    # transform values that might not be set to NULL
    foreach ($MayContainDoubleHyphen as $Key)
    {
        if (array_key_exists($Key, $Input))
        {
            if (IsFieldValueBlank($Input[$Key]) || $Input[$Key] == "--")
            {
                $Input[$Key] = NULL;
            }
        }
    }

    # special handling for the user privilege restrictions field
    if (array_key_exists("UserPrivilegeRestrictions", $Input))
    {
        $UserPrivilegeRestrictions = GetArrayValue($Input, "UserPrivilegeRestrictions");

        # transform input that's not an array to a blank value
        if (!is_array($UserPrivilegeRestrictions))
        {
            $Input["UserPrivilegeRestrictions"] = array();
        }
    }

    return $Input;
}

/**
 * Get the form data using the given field as a source.
 * @param MetadataField $Field metadata field
 * @return array form data
 */
function GetFormDataFromField(MetadataField $Field)
{
    global $G_PluginManager;

    $Schema = $GLOBALS["H_Schema"];

    $FormData = array();

    $DeprecatedMethods = array(
        "ViewingPrivilege", "EditingPrivilege", "AuthoringPrivilege", "TreeBrowsingPrivilege");

    # start with the basic fields
    foreach (GetFieldDefaults($Field->Type()) as $Key => $Value)
    {
        if (method_exists("MetadataField", $Key) &&
            !in_array($Key, $DeprecatedMethods) )
        {
            $FormData[$Key] = $Field->$Key();
        }
    }

    # determine if the field can be populated
    $CanPopulate = $Field->Type() == MetadataSchema::MDFTYPE_TREE;
    $CanPopulate = $CanPopulate || $Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME;
    $CanPopulate = $CanPopulate || $Field->Type() == MetadataSchema::MDFTYPE_OPTION;

    # various other information
    $FormData["Id"] = $Field->Id();
    $FormData["Type"] = $Field->Type();
    $FormData["Name"] = $Field->Name();
    $FormData["AllowedTypes"] = $Field->GetAllowedConversionTypes();
    $FormData["TypeAsName"] = $Field->TypeAsName();
    $FormData["AssociatedQualifierList"] = array_keys($Field->AssociatedQualifierList());
    $FormData["MappedName"] = MetadataSchema::FieldToStdNameMapping($Field->Id());
    $FormData["Owner"] = $Field->Owner();
    $FormData["HasOwner"] = !!$Field->Owner();
    $FormData["FieldExists"] = TRUE;
    $FormData["IsStandard"] = !is_null(MetadataSchema::FieldToStdNameMapping($Field->Id()));
    $FormData["CanPopulate"] = $CanPopulate;
    $FormData["CanExport"] = $CanPopulate;

    # privileges
    $FormData["ViewingPrivileges"] = $Field->ViewingPrivileges();
    $FormData["AuthoringPrivileges"] = $Field->AuthoringPrivileges();
    $FormData["EditingPrivileges"] = $Field->EditingPrivileges();
    $FormData["PreviewingPrivileges"] = $Field->PreviewingPrivileges();

    # sane defaults that might get changed below
    $FormData["HasInactiveOwner"] = FALSE;
    $FormData["IsLastTreeField"] = FALSE;

    # whether or not the field has an inactive owner
    if ($Field->Owner())
    {
        $FormData["HasInactiveOwner"] = !in_array(
            $Field->Owner(),
            $G_PluginManager->GetActivePluginList());
    }

    # whether or not the field is the last tree field
    if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
    {
        if (count($Schema->GetFields(MetadataSchema::MDFTYPE_TREE)) == 1)
        {
            $FormData["IsLastTreeField"] = TRUE;
        }
    }

    global $G_SysConfig;
    if ($G_SysConfig->ResourceRatingsEnabled() &&
            $Field->Name() == "Cumulative Rating")
        $FormData["IsTogglable"] = FALSE;
    else
        $FormData["IsTogglable"] = !$FormData["IsStandard"] &&
            (!$FormData["HasOwner"] || !$FormData["HasInactiveOwner"]);

    return $FormData;
}

/**
 * Get the form data using the metadata field defaults.
 * @param int $Type field type to assume (optional, defaults to text)
 * @param array $Override values to override the defaults and must be valid
 * @return array form data
 */
function GetFormDataFromDefaults($Type=MetadataSchema::MDFTYPE_TEXT, array $Override=array())
{
    $Schema = $GLOBALS["H_Schema"];
    $FormData = array();

    # get the basic fields first
    foreach (GetFieldDefaults($Type) as $Key => $Value)
    {
        if (method_exists("MetadataField", $Key))
        {
            $FormData[$Key] = GetArrayValue($Override, $Key, $Value);
        }
    }

    $AssociatedQualifierList = GetArrayValue(
        $Override,
        "AssociatedQualifierList",
        $FormData["AssociatedQualifierList"]);

    # various other information
    $FormData["Id"] = NULL;
    $FormData["Type"] = $Type;
    $FormData["Name"] = GetArrayValue($Override, "Name");
    $FormData["AllowedTypes"] = $Schema->GetAllowedFieldTypes();
    $FormData["TypeAsName"] = MetadataField::$FieldTypeHumanEnums[$Type];
    $FormData["AssociatedQualifierList"] = $AssociatedQualifierList;
    $FormData["MappedName"] = NULL;
    $FormData["Owner"] = NULL;
    $FormData["HasOwner"] = FALSE;
    $FormData["HasInactiveOwner"] = FALSE;
    $FormData["FieldExists"] = FALSE;
    $FormData["IsStandard"] = FALSE;
    $FormData["IsLastTreeField"] = FALSE;
    $FormData["CanPopulate"] = FALSE;
    $FormData["CanExport"] = FALSE;

    return $FormData;
}

/**
 * Validate the user-provided field values. If the type given is not
 * valid, type-based checks will not be performed.
 * @param array $Input user-provided field values
 * @param int $Type metadata field type
 * @return array an array of errors (field name => error message)
 */
function ValidateInput(array $Input, $Type)
{
    $Errors = array();

    # the first block of fields that apply to every metadata field type
    $Validation = array(
        "Type" => array("ValidateType", $Input),
        "Name" => array("ValidateName", $Input),
        "Label" => array("ValidateLabel", $Input),
        "Description" => array("ValidateNonempty", $Input, "Description", "Definition"),
        "Enabled" => array("ValidateBoolean", $Input, "Enabled", "Enabled"),
        "Editable" => array("ValidateBoolean", $Input, "Editable", "Editable"));

    # validation for text fields
    if ($Type == MetadataSchema::MDFTYPE_TEXT)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInSortOptions" => array("ValidateBoolean", $Input, "IncludeInSortOptions", "Include In Sort Options"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "TextFieldSize" => array("ValidateNumeric", $Input, "TextFieldSize", "Field Size", 1),
            "MaxLength" => array("ValidateNumeric", $Input, "MaxLength", "Max Length", 1),
            "AllowHTML" => array("ValidateBoolean", $Input, "AllowHTML", "Allow HTML"));
    }

    # validation for paragraph fields
    else if ($Type == MetadataSchema::MDFTYPE_PARAGRAPH)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "ParagraphRows" => array("ValidateNumeric", $Input, "ParagraphRows", "Paragraph Rows", 1),
            "ParagraphCols" => array("ValidateNumeric", $Input, "ParagraphCols", "Paragraph Columns", 1),
            "AllowHTML" => array("ValidateBoolean", $Input, "AllowHTML", "Allow HTML"),
            "UseWysiwygEditor" => array("ValidateBoolean", $Input, "UseWysiwygEditor", "Use WYSIWYG Editor"));
    }

    # validation for number fields
    else if ($Type == MetadataSchema::MDFTYPE_NUMBER)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInSortOptions" => array("ValidateBoolean", $Input, "IncludeInSortOptions", "Include In Sort Options"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "TextFieldSize" => array("ValidateNumeric", $Input, "TextFieldSize", "Field Size", 1),
            "MinValue" => array("ValidateNumeric", $Input, "MinValue", "Minimum Value"),
            "MaxValue" => array("ValidateNumeric", $Input, "MaxValue", "Maximum Value"),
            "DefaultValue" => array("ValidateNumeric", $Input, "DefaultValue", "Default Value", NULL, NULL, FALSE));

        # these fields need to be validated for the rating since they aren't
        # saved
        if (GetArrayValue($Input, "Name") == "Cumulative Rating")
        {
            unset($Validation["TextFieldSize"]);
            unset($Validation["DefaultValue"]);
            unset($Validation["MinValue"]);
            unset($Validation["MaxValue"]);
        }
    }

    # validation for date fields
    else if ($Type == MetadataSchema::MDFTYPE_DATE)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInSortOptions" => array("ValidateBoolean", $Input, "IncludeInSortOptions", "Include In Sort Options"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "TextFieldSize" => array("ValidateNumeric", $Input, "TextFieldSize", "Field Size", 1));
    }

    # validation for timestamp fields
    else if ($Type == MetadataSchema::MDFTYPE_TIMESTAMP)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInSortOptions" => array("ValidateBoolean", $Input, "IncludeInSortOptions", "Include In Sort Options"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "UpdateMethod" => array("ValidateUpdateMethod", $Input));

        $SkipUpdateMethod = array(
            "Date Of Record Creation",
            "Date Last Modified",
            "Date Of Record Release");

        # disregard the update method for these fields
        if (in_array(GetArrayValue($Input, "Name"), $SkipUpdateMethod))
        {
            unset($Validation["UpdateMethod"]);
        }
    }

    # validation for flag fields
    else if ($Type == MetadataSchema::MDFTYPE_FLAG)
    {
        $Validation += array(
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "FlagOnLabel" => array("ValidateNonempty", $Input, "FlagOnLabel", "Flag On Label"),
            "FlagOffLabel" => array("ValidateNonempty", $Input, "FlagOffLabel", "Flag Off Label"),
            "DefaultValue" => array("ValidateBoolean", $Input, "DefaultValue", "Default Value"));
    }

    # validation for tree fields
    else if ($Type == MetadataSchema::MDFTYPE_TREE)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInFacetedSearch" => array("ValidateBoolean", $Input, "IncludeInFacetedSearch", "Include In Faceted Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "OptionListThreshold" => array("ValidateOptionListThreshold", $Input),
            "AjaxThreshold" => array("ValidateAjaxThreshold", $Input),
            "NumAjaxResults" => array("ValidateNumeric", $Input, "NumAjaxResults", "Maximum Number of Search Results", 10),
            "UseForOaiSets" => array("ValidateBoolean", $Input, "UseForOaiSets", "Use for OAI Sets"),
            "DisplayAsListForAdvancedSearch" => array("ValidateBoolean", $Input, "DisplayAsLIstForAdvancedSearch", "Display As List For Advanced Search"),
        );
    }

    # validation for controlled name fields
    else if ($Type == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInFacetedSearch" => array("ValidateBoolean", $Input, "IncludeInFacetedSearch", "Include In Faceted Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "OptionListThreshold" => array("ValidateOptionListThreshold", $Input),
            "AjaxThreshold" => array("ValidateAjaxThreshold", $Input),
            "NumAjaxResults" => array("ValidateNumeric", $Input, "NumAjaxResults", "Maximum Number of Search Results", 10),
            "UseForOaiSets" => array("ValidateBoolean", $Input, "UseForOaiSets", "Use for OAI Sets"));
    }

    # validation for option fields
    else if ($Type == MetadataSchema::MDFTYPE_OPTION)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInFacetedSearch" => array("ValidateBoolean", $Input, "IncludeInFacetedSearch", "Include In Faceted Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "UseForOaiSets" => array("ValidateBoolean", $Input, "UseForOaiSets", "Use for OAI Sets"),
            "AllowMultiple" => array("ValidateBoolean", $Input, "AllowMultiple", "Allow Multiple"));
    }

    # validation for user fields
    else if ($Type == MetadataSchema::MDFTYPE_USER)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "UserPrivilegeRestrictions" => array("ValidateUserPrivilegeRestrictions", $Input));
    }

    # validation for image fields
    else if ($Type == MetadataSchema::MDFTYPE_IMAGE)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "DefaultAltText" => array("ValidateNonempty", $Input, "DefaultAltText", "Default Alt Text"),
            "MaxHeight" => array("ValidateNumeric", $Input, "MaxHeight", "Maximum  Height", 1),
            "MaxWidth" => array("ValidateNumeric", $Input, "MaxWidth", "Maximum  Width", 1),
            "MaxPreviewHeight" => array("ValidateNumeric", $Input, "MaxPreviewHeight", "Maximum Preview Height", 1),
            "MaxPreviewWidth" => array("ValidateNumeric", $Input, "MaxPreviewWidth", "Maximum Preview Width", 1),
            "MaxThumbnailHeight" => array("ValidateNumeric", $Input, "MaxThumbnailHeight", "Maximum Thumbnail Height", 1),
            "MaxThumbnailWidth" => array("ValidateNumeric", $Input, "MaxThumbnailWidth", "Maximum Thumbnail Width", 1),
            "ImagePreviewPrivilege" => array("ValidatePrivilege", $Input, "ImagePreviewPrivilege", "Privilege Required to View Full Image", TRUE));
    }

    # validation for file fields
    else if ($Type == MetadataSchema::MDFTYPE_FILE)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20));
    }

    # validation for URL fields
    else if ($Type == MetadataSchema::MDFTYPE_URL)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "IncludeInKeywordSearch" => array("ValidateBoolean", $Input, "IncludeInKeywordSearch", "Include In Keyword Search"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "IncludeInSortOptions" => array("ValidateBoolean", $Input, "IncludeInSortOptions", "Include In Sort Options"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "TextFieldSize" => array("ValidateNumeric", $Input, "TextFieldSize", "Field Size", 1),
            "MaxLength" => array("ValidateNumeric", $Input, "MaxLength", "Max Length", 1));
    }

    # validation for point fields
    else if ($Type == MetadataSchema::MDFTYPE_POINT)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "UsesQualifiers" => array("ValidateBoolean", $Input, "UsesQualifiers", "Uses Qualifiers"),
            "HasItemLevelQualifiers" => array("ValidateBoolean", $Input, "HasItemLevelQualifiers", "Has Item Level Qualifiers"),
            "AssociatedQualifierList" => array("ValidateAssociatedQualifierList", $Input),
            "DefaultQualifier" => array("ValidateDefaultQualifier", $Input),
            "ShowQualifiers" => array("ValidateBoolean", $Input, "ShowQualifiers", "Show Qualifiers"),
            "TextFieldSize" => array("ValidateNumeric", $Input, "TextFieldSize", "Field Size", 1),
            "PointPrecision" => array("ValidateNumeric", $Input, "PointPrecision", "Point Precision", 1),
            "PointDecimalDigits" => array("ValidateNumeric", $Input, "PointDecimalDigits", "Decimal Digits in Point", 0),
            "DefaultValue" => array("ValidatePointDefaultValue", $Input));
    }

    # validation for reference fields
    else if ($Type == MetadataSchema::MDFTYPE_REFERENCE)
    {
        $Validation += array(
            "Optional" => array("ValidateBoolean", $Input, "Optional", "Optional"),
            "IncludeInAdvancedSearch" => array("ValidateBoolean", $Input, "IncludeInAdvancedSearch", "Include In Advanced Search"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "NumAjaxResults" => array("ValidateNumeric", $Input, "NumAjaxResults", "Maximum Number of Search Results", 10));
    }

    # final block of fields that apply to every metadata field type
    $Validation += array(
        "ViewingPrivilegesLogic" => array("ValidateLogic", $Input, "ViewingPrivilegesLogic", "Top-Level Logic for Privilege Required for Viewing"),
        "ViewingPrivileges" => array("ValidatePrivileges", $Input, "ViewingPrivileges", "Privilege Required for Viewing", TRUE),
        "AuthoringPrivilegesLogic" => array("ValidateLogic", $Input, "AuthoringPrivilegesLogic", "Top-Level Logic for Privilege Required for Authoring"),
        "AuthoringPrivileges" => array("ValidatePrivileges", $Input, "AuthoringPrivileges", "Privilege Required for Authoring", TRUE),
        "EditingPrivilegesLogic" => array("ValidateLogic", $Input, "EditingPrivilegesLogic", "Top-Level Logic for Privilege Required for Editing"),
        "EditingPrivileges" => array("ValidatePrivileges", $Input, "EditingPrivileges", "Privilege Required for Editing", TRUE));

    # perform validation
    foreach ($Validation as $Name => $Callback)
    {
        $Errors[$Name] = call_user_func_array(array_shift($Callback), $Callback);
    }

    # remove fields that don't have an error message and reset indices
    return array_merge(array_filter($Errors));
}

/**
 * Determine if the given value is blank or empty if an array.
 * @param mixed $Value value to check for blankness
 * @return bool TRUE if the value is blank or FALSE otherwise
 */
function IsFieldValueBlank($Value)
{
    if (is_array($Value))
    {
        return !count($Value);
    }

    return !strlen(trim($Value));
}

/**
 * Validate the user-provided field values.
 * @param array $Input user-provided field values
 * @param int $Type metadata field type to assume
 * @return array an array of invalid fields and valid input
 */
function ValidateInputForField(array $Input, MetadataField $Field)
{
    $Type = GetArrayValue($Input, "Type");

    # do generic validation first
    $Errors = ValidateInput($Input, $Type);

    # do custom checking of the type and name given the field
    $Errors["Type"] = ValidateTypeForField($Input, $Field);
    $Errors["Name"] = ValidateNameForField($Input, $Field);

    # remove fields that don't have an error message and reset indices
    return array_merge(array_filter($Errors));
}

/**
 * Validate the field type.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateType(array $Input)
{
    $Type = GetArrayValue($Input, "Type");

    # the field type field is required
    if (IsFieldValueBlank($Type))
    {
        return "The <i>Type</i> field is required.";
    }

    # the field type must be valid
    if (!isset(MetadataField::$FieldTypeDBEnums[$Type]))
    {
        return "The <i>Type</i> field is invalid.";
    }
}

/**
 * Validate the field type within the context of the given field.
 * @param array $Input user-provided input
 * @param MetadataField $Field metadata field
 * @return null|string NULL on success or message on invalid input
 */
function ValidateTypeForField(array $Input, MetadataField $Field)
{
    $Type = GetArrayValue($Input, "Type");

    # use the checks from ValidateType()
    $TypeError = ValidateType($Input);

    # if there's an error already, just return it
    if (strlen($TypeError))
    {
        return $TypeError;
    }

    $AllowedConversionTypes = $Field->GetAllowedConversionTypes();

    # the (conversion) type must be okay
    if ($Type != $Field->Type() && !isset($AllowedConversionTypes[$Type]))
    {
        # get the allowed conversion types as a human-readable string
        $HumanField = new HumanMetadataField($Field);
        $TypeString = $HumanField->GetAllowedConversionTypes();

        return "The field must be one of the following types: ".$TypeString.".";
    }
}

/**
 * Validate the field name.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateName(array $Input)
{
    $Name = GetArrayValue($Input, "Name");

    # the name field is required
    if (IsFieldValueBlank($Name))
    {
        return "The <i>Name</i> field is required.";
    }

    # there are restrictions on what can be put in a name
    if (preg_match("/[^a-zA-Z0-9 \(\)]+/", $Name))
    {
        return "The <i>Name</i> field can only contain letters, numbers, spaces, and parentheses.";
    }

    $NormalizedName = preg_replace("/[^a-z0-9]/i", "", strtolower($Name));

    # can't use reserved words
    if ($NormalizedName == "resourceid" || $NormalizedName == "schemaid")
    {
        return "The <i>Name</i> field can't be a form of <i>Resource ID</i> or <i>Schema ID</i>.";
    }

    $Schema = $GLOBALS["H_Schema"];

    # names must be unique when trailing space is removed
    if ($Schema->FieldExists(trim($Name)))
    {
        return "The <i>Name</i> field value given is already in use.";
    }
}

/**
 * Validate the field name in the context of the given field.
 * @param array $Input user-provided input
 * @param MetadataField $Field metadata field
 * @return null|string NULL on success or message on invalid input
 */
function ValidateNameForField(array $Input, MetadataField $Field)
{
    $Name = GetArrayValue($Input, "Name");

    # the field name field is required
    if (IsFieldValueBlank($Name))
    {
        return "The <i>Name</i> field is required.";
    }

    # there are restrictions on what can be put in a name
    if (preg_match("/[^a-zA-Z0-9 \(\)]+/", $Name))
    {
        return "The <i>Name</i> field can only contain letters, numbers, spaces, and parentheses.";
    }

    $Schema = $GLOBALS["H_Schema"];

    # names must be unique when trailing space is removed
    if ($Field->Name() != trim($Name) && $Schema->FieldExists(trim($Name)))
    {
        return "The <i>Name</i> field value given is already in use.";
    }
}

/**
 * Validate the field label.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateLabel(array $Input)
{
    $Label = GetArrayValue($Input, "Label");

    # there are restrictions on what can be put in a label
    if (preg_match("/[^a-zA-Z0-9 ]+/", $Label))
    {
        return "The <i>Display Name</i> field can only contain letters, numbers, and spaces.";
    }
}

/**
 * Validate the default qualifier.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateDefaultQualifier(array $Input)
{
    $DefaultQualifier = GetArrayValue($Input, "DefaultQualifier");

    # it's okay if the default qualifier isn't set
    if (IsFieldValueBlank($DefaultQualifier))
    {
        return;
    }

    $Qualifier = LoadQualifier($DefaultQualifier);

    # make sure the qualifier exists
    if (!($Qualifier instanceof Qualifier))
    {
        return "The <i>Default Qualifier</i> field must be a valid qualifier.";
    }

    $AssociatedQualifierList = GetArrayValue($Input, "AssociatedQualifierList");

    # the default qualifier needs to be in the associated qualifier list
    if (!in_array($DefaultQualifier, $AssociatedQualifierList))
    {
        return "The <i>Default Qualifier</i> field be set to a field that is"
            ." selected in the <i>Allowed Qualifiers</i> field.";
    }
}

/**
 * Validate the user privilege restrictions.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateUserPrivilegeRestrictions(array $Input)
{
    $PrivilegeFactory = new PrivilegeFactory();
    $UserPrivilegeRestrictions = GetArrayValue($Input, "UserPrivilegeRestrictions", array());

    # check each restriction
    foreach ($UserPrivilegeRestrictions as $Restriction)
    {
        if (!$PrivilegeFactory->PrivilegeValueExists($Restriction))
        {
            return "The privileges given for the <i>Restrict to Users With One"
                  ." of the Following</i> field are invalid.";
        }
    }
}

/**
 * Validate the update method.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateUpdateMethod(array $Input)
{
    $ValidValues = array(
        MetadataField::UPDATEMETHOD_NOAUTOUPDATE,
        MetadataField::UPDATEMETHOD_ONRECORDCREATE,
        MetadataField::UPDATEMETHOD_BUTTON,
        MetadataField::UPDATEMETHOD_ONRECORDEDIT,
        MetadataField::UPDATEMETHOD_ONRECORDCHANGE);
    $UpdateMethod = GetArrayValue($Input, "UpdateMethod");

    if (!in_array($UpdateMethod, $ValidValues))
    {
        return "The <i>Update Method</i> field is invalid.";
    }
}

/**
 * Validate the default value field for points.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidatePointDefaultValue(array $Input)
{
    $DefaultValue = GetArrayValue($Input, "DefaultValue");
    $DefaultValue1 = GetArrayValue($DefaultValue, "X");
    $DefaultValue2 = GetArrayValue($DefaultValue, "Y");

    if (strlen($DefaultValue1) || strlen($DefaultValue2))
    {
        if (!is_numeric($DefaultValue1))
        {
            return "The values for the <i>Default Value</i> field must be numbers.";
        }

        if (!is_numeric($DefaultValue2))
        {
            return "The values for the <i>Default Value</i> field must be numbers.";
        }
    }
}

/**
 * Validate the associated qualifier list.
 * @param array $Input user-provided input
 * @return null|string NULL on success or message on invalid input
 */
function ValidateAssociatedQualifierList(array $Input)
{
    $AssociatedQualifierList = GetArrayValue($Input, "AssociatedQualifierList");

    # it's okay if there are none selected
    if (IsFieldValueBlank($AssociatedQualifierList))
    {
        return;
    }

    $QualifierList = GetQualifierList();

    # all the qualifiers must be valid
    if (count(array_diff($AssociatedQualifierList, array_keys($QualifierList))))
    {
        return "The <i>Allowed Qualifiers</i> field is invalid.";
    }
}

/**
 * Validate a boolean value.
 * @param array $Input user-provided input
 * @param string $Key field value key
 * @param string $Name field name displayed to the user
 * @return null|string NULL on success or message on invalid input
 */
function ValidateBoolean(array $Input, $Key, $Name)
{
    $Value = GetArrayValue($Input, $Key);

    if ($Value != 1 && $Value != 0)
    {
        return "The <i>" . $Name . "</i> field must be a yes or no value.";
    }
}

/**
 * Validate a numeric value.
 * @param array $Input user-provided input
 * @param string $Key field value key
 * @param string $Name field name displayed to the user
 * @param int|null $Lower lowest value the number can be (optional)
 * @param int|null $Upper highest value the number can be (optional)
 * @param bool $Required whether or not a value is required (optional)
 * @return null|string NULL on success or message on invalid input
 */
function ValidateNumeric(array $Input, $Key, $Name, $Lower=NULL, $Upper=NULL, $Required=TRUE)
{
    $MaxAllowedValue = 100000000;
    $Value = GetArrayValue($Input, $Key);

    # the field is not required and is blank
    if (!$Required && IsFieldValueBlank($Value))
    {
        return;
    }

    # must be a number
    if (!is_numeric($Value))
    {
        return "The <i>" . $Name . "</i> field must be a number.";
    }

    # must be greater than or equal to the lower bound, if specified
    if (is_numeric($Lower) && $Value < $Lower)
    {
        $Lower = number_format($Lower);
        return "The <i>" . $Name . "</i> field must be at least ".$Lower.".";
    }

    # must be less than or equal to the lower bound, if specified
    if (is_numeric($Upper) && $Value > $Upper)
    {
        $Upper = number_format($Upper);
        return "The <i>" . $Name . "</i> field must be no more than ".$Upper.".";
    }

    # must be less than a certain value because of technical limitations
    if ($Value > $MaxAllowedValue)
    {
        $MaxAllowedValue = number_format($MaxAllowedValue);
        return "The <i>" . $Name . "</i> field must be no more than "
              .$MaxAllowedValue.". This is for technical reasons.";
    }
}

/**
 * Validate a nonempty value.
 * @param array $Input user-provided input
 * @param string $Key field value key
 * @param string $Name field name displayed to the user
 * @return null|string NULL on success or message on invalid input
 */
function ValidateNonempty(array $Input, $Key, $Name)
{
    $Value = GetArrayValue($Input, $Key);

    if (!strlen($Value))
    {
        return "The <i>" . $Name . "</i> field is required.";
    }
}

/**
 * Validate a privilege value.
 * @param array $Input user-provided input
 * @param string $Key field value key
 * @param string $Name field name displayed to the user
 * @return null|string NULL on success or message on invalid input
 */
function ValidatePrivilege(array $Input, $Key, $Name, $NotSetOk=FALSE)
{
    $Value = GetArrayValue($Input, $Key);

    # if it's okay for the field not to be set
    if ($NotSetOk && IsFieldValueBlank($Value))
    {
        return;
    }

    $PrivilegeFactory = new PrivilegeFactory();

    if (!$PrivilegeFactory->PrivilegeValueExists($Value))
    {
        return "The privilege given for the <i>" . $Name . "</i> field is invalid.";
    }
}

/**
* Validate the values for a privilege set.
* @param array $Input User-provided input.
* @param string $Key Field value key.
* @param string $Name Field name displayed to the user.
* @return Returns NULL on success or message on invalid input.
*/
function ValidatePrivileges(array $Input, $Key, $Name, $NotSetOk=FALSE)
{
    $Value = GetArrayValue($Input, $Key);

    # if it's okay for the field not to be set
    if ($NotSetOk && IsFieldValueBlank($Value))
    {
        return;
    }

    try
    {
        # if there's an error, this will throw an exception
        GetPrivilegeSetFromFormData($Value);
    }

    catch (Exception $Exception)
    {
        return "The values given for the <i>" . $Name . "</i> field are "
            ."invalid. " . $Exception->getMessage();
    }
}

/**
* Validate the values for the logic for a privilege set.
* @param array $Input User-provided input.
* @param string $Key Field value key.
* @param string $Name Field name displayed to the user.
* @return Returns NULL on success or message on invalid input.
*/
function ValidateLogic(array $Input, $Key, $Name)
{
    $Value = GetArrayValue($Input, $Key);

    if ($Value != "AND" && $Value != "OR")
    {
        return "The logic given for the <i>" . $Name . "</i> is invalid.";
    }
}

/**
* Validate the option list threshold value.
* @param array $Input User-provided input.
* @return Returns NULL on success or message on invalid input.
*/
function ValidateOptionListThreshold(array $Input)
{
    $MaxAllowedValue = 100000000;
    $OptionListThreshold = GetArrayValue($Input, "OptionListThreshold");
    $AjaxThreshold = GetArrayValue($Input, "AjaxThreshold");

    # the field is required
    if (IsFieldValueBlank($OptionListThreshold))
    {
        return "The <i>Option List Threshold</i> field is required.";
    }

    # must be a number
    if (!is_numeric($OptionListThreshold))
    {
        return "The <i>Option List Threshold</i> field must be a number.";
    }

    # the field must be less than or equal to the AJAX threshold
    if ($OptionListThreshold > $AjaxThreshold)
    {
        return "The <i>Option List Threshold</i> field must be less than or "
              ."equal to the <i>AJAX Threshold</i> field.";
    }

    # must be less than a certain value because of technical limitations
    if ($OptionListThreshold > $MaxAllowedValue)
    {
        $MaxAllowedValue = number_format($MaxAllowedValue);
        return "The <i>Option List Threshold</i> field must be no more than "
              .$MaxAllowedValue.". This is for technical reasons.";
    }
}

/**
* Validate the AJAX threshold value.
* @param array $Input User-provided input.
* @return Returns NULL on success or message on invalid input.
*/
function ValidateAjaxThreshold(array $Input)
{
    $MaxAllowedValue = 100000000;
    $AjaxThreshold = GetArrayValue($Input, "AjaxThreshold");
    $OptionListThreshold = GetArrayValue($Input, "OptionListThreshold");

    # the field is required
    if (IsFieldValueBlank($AjaxThreshold))
    {
        return "The <i>AJAX Threshold</i> field is required.";
    }

    # must be a number
    if (!is_numeric($AjaxThreshold))
    {
        return "The <i>AJAX Threshold</i> field must be a number.";
    }

    # the field must be less than or equal to the AJAX threshold
    if ($AjaxThreshold < $OptionListThreshold)
    {
        return "The <i>AJAX Threshold</i> field must be greater than or "
              ."equal to the <i>Option List Threshold</i> field.";
    }

    # must be less than a certain value because of technical limitations
    if ($AjaxThreshold > $MaxAllowedValue)
    {
        $MaxAllowedValue = number_format($MaxAllowedValue);
        return "The <i>AJAX Threshold</i> field must be no more than "
              .$MaxAllowedValue.". This is for technical reasons.";
    }
}

/**
 * Save the given input to the given metadata field. The input must be validated
 * at this point.
 * @param MetadataField $Field metadata field
 * @param array $Input user input
 * @return void
 */
function SaveInput(MetadataField $Field, array $Input)
{
    if (array_key_exists("Name", $Input))
    {
        $Input["Name"] = trim($Input["Name"]);
    }

    $SkipUpdateMethod = array(
        "Date Of Record Creation",
        "Date Last Modified",
        "Date Of Record Release");

    # disregard the update method for these fields
    if (in_array($Field->Name(), $SkipUpdateMethod))
    {
        unset($Input["UpdateMethod"]);
    }

    $AssociatedQualifierList = $Field->AssociatedQualifierList();

    # remove association with any current qualifiers
    foreach ($AssociatedQualifierList as $Id => $Name)
    {
        $Field->UnassociateWithQualifier($Id);
    }

    $AssociateWith = GetArrayValue($Input, "AssociatedQualifierList", array());

    # associated with any given qualifiers
    foreach ($AssociateWith as $Id)
    {
        $Field->AssociateWithQualifier($Id);
    }

    # some fields can't be edited for the cumulative rating field
    if ($Field->Name() == "Cumulative Rating")
    {
        unset($Input["TextFieldSize"]);
        unset($Input["DefaultValue"]);
        unset($Input["MinValue"]);
        unset($Input["MaxValue"]);
    }

    # flag used to signal if images should be resized
    $ResizeImages = FALSE;

    # determine if images will need to be resized after saving the values
    foreach (array("", "Preview", "Thumbnail") as $Type)
    {
        # get the current and new dimensions
        $CurrentWidth = $Field->{"Max".$Type."Width"}();
        $CurrentHeight = $Field->{"Max".$Type."Height"}();
        $NewWidth = GetArrayValue($Input, "Max".$Type."Width");
        $NewHeight = GetArrayValue($Input, "Max".$Type."Height");

        # check if the width has changed
        if (is_numeric($NewWidth) && $NewWidth > 0 && $CurrentWidth != $NewWidth)
        {
            $ResizeImages = TRUE;
            break;
        }

        # check if the height has changed
        if (is_numeric($NewHeight) && $NewHeight > 0 && $CurrentHeight != $NewHeight)
        {
            $ResizeImages = TRUE;
            break;
        }
    }

    # handle the privileges separately
    foreach (array("View", "Author", "Edit") as $PrivilegeType)
    {
        $PrivilegeTypeName = $PrivilegeType . "ingPrivileges";

        # the privilege type is valid and it has a valid logic value
        if (isset($Input[$PrivilegeTypeName]))
        {
            # save the privileges
            $Field->{$PrivilegeTypeName}($Input[$PrivilegeTypeName]);
        }

        # remove the privilege-related values so they aren't "saved" below
        unset($Input[$PrivilegeTypeName]);
        unset($Input[$PrivilegeTypeName."Logic"]);
    }

    # these currently aren't being used
    unset($Input["PreviewingPrivilegesLogic"], $Input["PreviewingPrivileges"]);

    $DeprecatedFunctions = array(
        "ViewingPrivilege", "EditingPrivilege", "AuthoringPrivilege","TreeBrowsingPrivilege" );

    # save the fields
    foreach ($Input as $Key => $Value)
    {
        if (!in_array($Key, $DeprecatedFunctions))
            $Field->$Key($Value === "" ? NULL : $Value);
    }

    if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
    {
        $DefaultQualifier = $Field->DefaultQualifier();

        if ($Field->UsesQualifiers() && $DefaultQualifier)
        {
            # percolate the default qualifier for tree fields
            SetQualifierIdForClassifications($Field, $DefaultQualifier);
        }
    }

    if ($Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
    {
        $DefaultQualifier = $Field->DefaultQualifier();

        if ($Field->UsesQualifiers() && $DefaultQualifier)
        {
            # set the default qualifier for controlled name fields
            SetQualifierIdForControlledNames($Field, $DefaultQualifier);
        }
    }

    # flagged to resize images
    if ($ResizeImages)
    {
        ResizeImages($Field);
    }
}

/**
 * Set the default qualifier for the tree field at all levels if not already set
 * to some other value.
 * @param MetadataField $Field tree metadata field
 * @param int $DefaultQualifier default qualifier ID
 * @return void
 */
function SetQualifierIdForClassifications(MetadataField $Field, $DefaultQualifier)
{
    $Factory = new ClassificationFactory();
    $Ids = $Factory->GetItemIds("FieldId = ".$Field->Id());

    foreach ($Ids as $Id)
    {
        $Classification = new Classification($Id);
        $OldQualifier = LoadQualifier($Classification->QualifierId());

        # if there is no old qualifier or the qualifier name is blank, set
        # the default qualifier ID to the provided value
        if (is_null($OldQualifier) || !$OldQualifier->Name())
        {
            $Classification->QualifierId($DefaultQualifier);
        }
    }
}

/**
 * Set the default qualifier for the controlled name field if not already set to
 * some other value.
 * @param MetadataField $Field controlled name metadata field
 * @param int $DefaultQualifier default qualifier ID
 * @return void
 */
function SetQualifierIdForControlledNames(MetadataField $Field, $DefaultQualifier)
{
    $Factory = new ControlledNameFactory();
    $Ids = $Factory->GetItemIds("FieldId = ".$Field->Id());

    foreach ($Ids as $Id)
    {
        $ControlledName = new ControlledName($Id);
        $OldQualifier = LoadQualifier($ControlledName->QualifierId());

        # if there is no old qualifier or the qualifier name is blank, set
        # the default qualifier ID to the provided value
        if (is_null($OldQualifier) || !$OldQualifier->Name())
        {
            $ControlledName->QualifierId($DefaultQualifier);
        }
    }
}

/**
 * Resize the images associated with the given metadata field after one or more
 * max dimension values have changed.
 * @param MetadataField $Field metadata field
 * @return void
 */
function ResizeImages(MetadataField $Field)
{
    global $AF;

    # make sure the field is an image field
    if ($Field->Type() != MetadataSchema::MDFTYPE_IMAGE)
    {
        return;
    }

    $Database = new Database();
    $DBFieldName = $Field->DBFieldName();
    $MaxWidth = intval($Field->MaxWidth());
    $MaxHeight = intval($Field->MaxHeight());
    $MaxPreviewWidth = intval($Field->MaxPreviewWidth());
    $MaxPreviewHeight = intval($Field->MaxPreviewHeight());
    $MaxThumbnailWidth = intval($Field->MaxThumbnailWidth());
    $MaxThumbnailHeight = intval($Field->MaxThumbnailHeight());

    $Database->Query("
        SELECT I.*
        FROM Images I, Resources R
        WHERE R.".$DBFieldName." IS NOT NULL
        AND I.ImageId = R.".$DBFieldName);

    while (FALSE !== ($Data = $Database->FetchRow()))
    {
        $Image = new SPTImage($Data["ImageId"]);

        # skip invalid images
        if ($Image->Status() != AI_OKAY)
        {
            continue;
        }

        $AF->QueueUniqueTask(
            array($Image, "Resize"),
            array($MaxWidth, $MaxHeight,
                  $MaxPreviewWidth, $MaxPreviewHeight,
                  $MaxThumbnailWidth, $MaxThumbnailHeight),
            ApplicationFramework::PRIORITY_BACKGROUND,
            "Resize an image and its preview and thumbnail images after a change
            to its associated metadata field.");
    }
}

/**
 * Delete the given field, reassigning the default browsing field and removing
 * OAI field mappings as necessary.
 * @param MetadataField $Field metadata field
 * @return void
 */
function DeleteField(MetadataField $Field)
{
    global $SysConfig;

    $Schema = new MetadataSchema($Field->SchemaId());
    $FieldId = $Field->Id();

    # re-assign browsing field ID if that is what is being deleted and the
    # schema is the default one
    if ($SysConfig->BrowsingFieldId() == $FieldId
        && $Schema->Id() == MetadataSchema::SCHEMAID_DEFAULT)
    {
        $TreeFields = $Schema->GetFields(MetadataSchema::MDFTYPE_TREE);

        # remove the field to be deleted from the list
        unset($TreeFields[$FieldId]);

        # make sure at least one tree field exists for browsing
        if (!count($TreeFields))
        {
            return;
        }

        # reassign the browsing field
        $TreeField = array_shift($TreeFields);
        $SysConfig->BrowsingFieldId($TreeField->Id());
    }

    # drop the field
    $Schema->DropField($FieldId);

    # remove from OAIFieldMappings too
    $Database = new Database();
    $Database->Query("
        DELETE FROM OAIFieldMappings
        WHERE SPTFieldId = " . addslashes($FieldId));
}

/**
 * Get the input from the list of user input that is valid, based on which ones
 * are known to be invalid.
 * @param array $Input user input
 * @param array $InvalidInput invalid user input fields
 * @return array valid user input fields
 */
function GetValidInput(array $Input, array $InvalidInput)
{
    $ValidInput = array();
    $Defaults = GetFieldDefaults(GetArrayValue($Input, "Type"));
    $ValidFields = array_keys(array_diff_key($Defaults, array_flip($InvalidInput)));

    foreach ($ValidFields as $Key)
    {
        $ValidInput[$Key] = GetArrayValue($Input, $Key, $Defaults[$Key]);
    }

    # handle the privileges separately
    foreach (array("View", "Author", "Edit") as $PrivilegeType)
    {
        $PrivilegeTypeName = $PrivilegeType . "ingPrivileges";

        # the privilege type is valid and it has a valid logic value
        if (in_array($PrivilegeTypeName, $ValidFields)
            && in_array($PrivilegeTypeName."Logic", $ValidFields))
        {
            # create the privilege set from the input if available
            if (isset($Input[$PrivilegeTypeName]))
            {
                $PrivilegeSet = GetPrivilegeSetFromFormData($Input[$PrivilegeTypeName]);
                $PrivilegeSet->AllRequired($Input[$PrivilegeTypeName."Logic"] == "AND");
            }

            # create an empty privilege set otherwise
            else
            {
                $PrivilegeSet = new PrivilegeSet();
            }

            # pass the privilege set on
            $ValidInput[$PrivilegeTypeName] = $PrivilegeSet;
        }
    }

    return $ValidInput;
}

/**
* Get the page with necessary parameters when returning to the DBEditor page.
* @param mixed $Value MetadataSchema object, MetadataField object, or a schema
*      ID.
* @return Returns the page to return to.
*/
function GetReturnToPage($Value)
{
    $Suffix = "";

    # get the suffix from a metadata schema if not using the default schema
    if ($Value instanceof MetadataSchema)
    {
        if ($Value->Id() != MetadataSchema::SCHEMAID_DEFAULT)
        {
            $Suffix = "&SC=" . urlencode($Value->Id());
        }
    }

    # get the suffix from a metadata field if not using the default schema
    else if ($Value instanceof MetadataField)
    {
        if ($Value->SchemaId() != MetadataSchema::SCHEMAID_DEFAULT)
        {
            $Suffix = "&SC=" . urlencode($Value->SchemaId());
        }
    }

    # use the value directly if not using the default schema
    else if (!is_null($Value) && $Value != MetadataSchema::SCHEMAID_DEFAULT)
    {
        $Suffix = "&SC=" . urlencode($Value);
    }

    return "DBEditor" . $Suffix;
}

/**
* Get the metadata schema that is being edited.
* @param MetadataSchema $SchemaToUse Metadata schema that is edited for setting.
* @return Returns the metadata schema that is being edited.
*/
function GetMetadataSchema(MetadataSchema $SchemaToUse=NULL)
{
    return $GLOBALS["H_Schema"];
}

/**
* Get the list of privileges.
* @return Returns an array of all privileges.
*/
function GetPrivileges()
{
    static $Privileges;

    if (!isset($Privileges))
    {
        $PrivilegeFactory = new PrivilegeFactory();
        $Privileges = $PrivilegeFactory->GetPrivileges();
    }

    return $Privileges;
}

/**
* Get the list of all metadata fields supported in the privileges associated
* with metadata schemas.
* @return Returns an array of supported metadata fields.
*/
function GetSupportedFields()
{
    static $SupportedFields;

    if (!isset($SupportedFields))
    {
        $Schema = GetMetadataSchema();
        $SupportedFieldTypesInOrder = array(
            MetadataSchema::MDFTYPE_USER,
            MetadataSchema::MDFTYPE_FLAG,
            MetadataSchema::MDFTYPE_OPTION,
            MetadataSchema::MDFTYPE_NUMBER);

        $SupportedFields = array();

        foreach ($SupportedFieldTypesInOrder as $Type)
        {
            $SupportedFields += $Schema->GetFields($Type);
        }
    }

    return $SupportedFields;
}

/**
* Get the list of supported operators.
* @return Returns an array of the supported operators.
*/
function GetSupportedOperators()
{
    static $Operators = array("==", "!=", "<", ">");

    return $Operators;
}

/**
* Get the list of option values allowed for privilege sets
* @return array( Field Name => array(OptionId => OptionValue))
*/
function GetOptionValuesForPrivset()
{
    static $OptionValues;

    if (!isset($OptionValues))
    {
        $OptionValues = array();
        $Schema = GetMetadataSchema();

        foreach ($Schema->GetFields(MetadataSchema::MDFTYPE_OPTION) as $Field)
        {
            $OptionValues[$Field->Name()] = $Field->GetPossibleValues();
        }
    }

    return $OptionValues;
}

/**
* Construct a new privilege set from the given array of form data.
* @param array $FormData An array of form data. This variable will be modified
*       by reference.
* @return Returns a PrivilegeSet object.
* @throws Exception if invalid data is given.
*/
function GetPrivilegeSetFromFormData(array &$FormData)
{
    $NewPrivilegeSet = new PrivilegeSet();
    $Privileges = GetPrivileges();
    $SupportedFields = GetSupportedFields();
    $SupportedOperators = GetSupportedOperators();

    while (count($FormData))
    {
        # extract the form fields
        $SubjectField = array_shift($FormData);
        $OperatorField = array_shift($FormData);
        $ValueSelectField = array_shift($FormData);
        $ValueInputField = array_shift($FormData);

        # privilege condition
        if ($SubjectField == "current_user")
        {
            # invalid privilege ID
            if (!isset($Privileges[$ValueSelectField]) || is_null($ValueSelectField))
            {
                throw new Exception("Invalid privilege (".$ValueSelectField.")");
            }

            $NewPrivilegeSet->AddPrivilege($ValueSelectField);
        }

        # resource exists condition
        else if ($SubjectField == "have_resource")
        {
             $NewPrivilegeSet->AddCondition(
                 PrivilegeSet::HAVE_RESOURCE,
                     ($OperatorField == "==") ? TRUE : FALSE, "==" );
        }
        # metadata field condition
        else if (is_numeric($SubjectField))
        {
            # invalid field ID
            if (!isset($SupportedFields[$SubjectField]) || is_null($SubjectField))
            {
                throw new Exception("Invalid or unsupported field (".$SubjectField.")");
            }

            # invalid operator
            if (!in_array($OperatorField, $SupportedOperators))
            {
                throw new Exception("Invalid or unsupported operator (".$OperatorField.")");
            }

            $MetadataField = $SupportedFields[$SubjectField];

            switch ($MetadataField->Type())
            {
                case MetadataSchema::MDFTYPE_USER:
                    $Value = NULL;
                    break;
                case MetadataSchema::MDFTYPE_FLAG:
                    $Value = 1;
                    break;
                case MetadataSchema::MDFTYPE_TIMESTAMP:
                case MetadataSchema::MDFTYPE_DATE:
                case MetadataSchema::MDFTYPE_NUMBER:
                    $Value = $ValueInputField;
                    break;
                case MetadataSchema::MDFTYPE_OPTION:
                    # strip the "C" prefix used to distinguish controlled names from priv flags
                    $Value = intval( substr( $ValueSelectField, 1));
                    break;
                default:
                    $Value = NULL;
                    break;
            }

            $NewPrivilegeSet->AddCondition($MetadataField, $Value, $OperatorField);
        }

        # entering a nested privilege set
        else if ($SubjectField == "set_entry")
        {
            # the logic is invalid
            if ($ValueSelectField != "AND" && $ValueSelectField != "OR")
            {
                throw new Exception("Invalid privilege set logic (".$ValueSelectField.")");
            }

            $NestedPrivilegeSet = GetPrivilegeSetFromFormData($FormData);

            # only add the nested privilege set if it's not empty. use 1
            # because the logic is in the privilege info array
            if (count($NestedPrivilegeSet->GetPrivilegeInfo()) > 1)
            {
                $NestedPrivilegeSet->AllRequired($ValueSelectField == "AND");
                $NewPrivilegeSet->AddSet($NestedPrivilegeSet);
            }
        }

        # exiting a privilege set
        else if ($SubjectField == "set_exit")
        {
            break;
        }

        # unknown condition type
        else
        {
            throw new Exception("Unknown condition type");
        }
    }

    return $NewPrivilegeSet;
}

# ----- MAIN -----------------------------------------------------------------

global $H_Schema;
global $H_FieldData;
global $H_Errors;
global $H_FixedDefaults;
global $H_TypeBasedDefaults;

$H_FixedDefaults = MetadataField::$FixedDefaults;
$H_TypeBasedDefaults = MetadataField::$TypeBasedDefaults;
$SchemaId = GetArrayValue($_GET, "SC", GetArrayValue($_POST, "F_SchemaId"));
$FieldIdGiven = GetArrayValue($_GET, "Id", FALSE);
$Field = $FieldIdGiven ? LoadMetadataField(GetArrayValue($_GET, "Id")) : NULL;
$Operation = GetArrayValue($_POST, "Submit");
$Input = ExtractFieldInput($_POST);
$FieldRequiredFor = array(
    "Update Field", "Enable", "Disable", "Delete Field", "Populate Field...");

PageTitle(($FieldIdGiven ? "Edit" : "Add") . " Metadata Field");

if (!CheckAuthorization(PRIV_SYSADMIN, PRIV_COLLECTIONADMIN)) {  return;  }

# field ID given, but it's invalid. go back to the list of fields
if ($FieldIdGiven && !($Field instanceof MetadataField))
{
    $AF->SetJumpToPage(GetReturnToPage($SchemaId));
    return;
}

# no field ID given, but one is required. go back to the list of fields
if (!$FieldIdGiven && in_array($Operation, $FieldRequiredFor))
{
    $AF->SetJumpToPage(GetReturnToPage($SchemaId));
    return;
}

# cancel editing
if ($Operation == "Cancel")
{
    $AF->SetJumpToPage(GetReturnToPage($SchemaId));
    return;
}

# no schema ID given
if (is_null($SchemaId))
{
    # use the schema ID for the field if posible
    if ($Field instanceof MetadataField)
    {
        $SchemaId = $Field->SchemaId();
    }

    # otherwise use the default schema ID
    else
    {
        $SchemaId = MetadataSchema::SCHEMAID_DEFAULT;
    }
}

# construct the schema object
$H_Schema = new MetadataSchema($SchemaId);

# add a new field
if ($Operation == "Add Field")
{
    # validate the user input
    $Type = GetArrayValue($Input, "Type");
    $Errors = ValidateInput($Input, $Type);
    $ValidInput = GetValidInput($Input, array_keys($Errors));

    # at least one field value is invalid
    if (count($Errors))
    {
        # load the data so the form can be displayed and the issues resolved
        $H_Errors = $Errors;
        $H_FieldData = GetFormDataFromDefaults($Type, $ValidInput);
    }

    else
    {
        # create a new field
        $Schema = $H_Schema;
        $Field = $Schema->AddField("XTEMPFIELDNAMEX", $Type);

        # save the input and make the field permanent
        SaveInput($Field, $ValidInput);
        $Field->IsTempItem(FALSE);

        # go back to the field list
        $AF->SetJumpToPage(GetReturnToPage($H_Schema));
        return;
    }
}

# update an existing field (field checks are above)
else if ($Operation == "Update Field")
{
    # validate the user input
    $Errors = ValidateInputForField($Input, $Field);
    $ValidInput = GetValidInput($Input, array_keys($Errors));

    # save the valid fields
    SaveInput($Field, $ValidInput);

    # if there were no issues, just go back to the field list
    if (!count($Errors))
    {
        $AF->SetJumpToPage(GetReturnToPage($Field));
        return;
    }

    # otherwise load the data so the form can be displayed and the issues
    # resolved
    $H_Errors = $Errors;
    $H_FieldData = GetFormDataFromField($Field);
}

# delete a field (field checks are above)
else if ($Operation == "Delete Field")
{
    $Confirmation = GetArrayValue($_POST, "F_Confirmation", FALSE);

    # delete if confirmed
    if ($Confirmation)
    {
        $AF->SetJumpToPage(GetReturnToPage($Field));
        DeleteField($Field);
        return;
    }

    # otherwise request the user to confirm
    else
    {
        # save valid user input in case the delete button was pressed accidentally
        $Input = ExtractFieldInput($_POST);
        $Errors = ValidateInputForField($Input, $Field);
        SaveInput($Field, GetValidInput($Input, array_keys($Errors)));

        $AF->SetJumpToPage("ConfirmDeleteMetadataField&Id=".urlencode($Field->Id()));
    }
}

# populate a tree field (field checks are above)
else if ($Operation == "Populate Field...")
{
    # save valid user input
    $Input = ExtractFieldInput($_POST);
    $Errors = ValidateInputForField($Input, $Field);
    SaveInput($Field, GetValidInput($Input, array_keys($Errors)));

    $AF->SetJumpToPage("PopulateField&ID=".urlencode($Field->Id()));
}

# enable a field through AJAX, so no redirect (field checks are above)
else if ($Operation == "Enable")
{
    $Field->Enabled(TRUE);
    $AF->SuppressHTMLOutput();
    return;
}

# disable a field through AJAX, so no redirect (field checks are above)
else if ($Operation == "Disable")
{
    $Field->Enabled(FALSE);
    $AF->SuppressHTMLOutput();
    return;
}

# the form was not submitted and a field ID was given. setup for editing the
# field
else if ($FieldIdGiven)
{
    $H_Errors = array();
    if (isset($_GET["ERR"]))
    {
        $H_Errors []= $_GET["ERR"];
    }
    $H_FieldData = GetFormDataFromField($Field);
}

# the form was not submitted and a field ID was not given. setup for adding a
# new field
else
{
    $H_Errors = array();
    $H_FieldData = GetFormDataFromDefaults();
}
