<?PHP
#
#   FILE: EditMetadataField.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2012 Internet Scout Project
#   http://scout.wisc.edu/
#

# ----- 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->QualifierList();

    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)
{
    $Schema = new MetadataSchema();
    $Field = $Schema->GetField($Id);

    # make sure the field is valid
    if ($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)
{
    # 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;

    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",
        "ViewingPrivilege",
        "AuthoringPrivilege",
        "EditingPrivilege",
        "ViewingUserValue",
        "AuthoringUserValue",
        "EditingUserValue");

    # 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;

    $FormData = array();

    # start with the basic fields
    foreach (GetFieldDefaults($Field->Type()) as $Key => $Value)
    {
        $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;

    # 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)
    {
        $Schema = new MetadataSchema();

        if (count($Schema->GetFields(MetadataSchema::MDFTYPE_TREE)) == 1)
        {
            $FormData["IsLastTreeField"] = TRUE;
        }
    }

    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 = new MetadataSchema();
    $FormData = array();

    # get the basic fields first
    foreach (GetFieldDefaults($Type) as $Key => $Value)
    {
        $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"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "NumAjaxResults" => array("ValidateNumeric", $Input, "NumAjaxResults", "Maximum Number of Search Results", 10),
            "UseForOaiSets" => array("ValidateBoolean", $Input, "UseForOaiSets", "Use for OAI Sets"),
            "TreeBrowsingPrivilege" => array("ValidatePrivilege", $Input, "TreeBrowsingPrivilege", "Privilege Required for Browsing", TRUE));
    }

    # 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"),
            "SearchWeight" => array("ValidateNumeric", $Input, "SearchWeight", "Search Weight", 1, 20),
            "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"),
            "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));
    }

    # final block of fields that apply to every metadata field type
    $Validation += array(
        "Viewable" => array("ValidateBoolean", $Input, "Viewable", "Viewable"),
        "ViewingPrivilege" => array("ValidatePrivilege", $Input, "ViewingPrivilege", "Privilege Required for Viewing", TRUE),
        "AuthoringPrivilege" => array("ValidatePrivilege", $Input, "AuthoringPrivilege", "Privilege Required for Authoring", TRUE),
        "EditingPrivilege" => array("ValidatePrivilege", $Input, "EditingPrivilege", "Privilege Required for Editing", TRUE),
        "ViewingUserIsValue" => array("ValidateUserIsValue", $Input, "ViewingUserIsValue", "Privilege Required for Viewing"),
        "AuthoringUserIsValue" => array("ValidateUserIsValue", $Input, "AuthoringUserIsValue", "Privilege Required for Authoring"),
        "EditingUserIsValue" => array("ValidateUserIsValue", $Input, "EditingUserIsValue", "Privilege Required for Editing"),
        "ViewingUserValue" => array("ValidateUserValue", $Input, "ViewingUserValue", "Privilege Required for Viewing"),
        "AuthoringUserValue" => array("ValidateUserValue", $Input, "AuthoringUserValue", "Privilege Required for Authoring"),
        "EditingUserValue" => array("ValidateUserValue", $Input, "EditingUserValue", "Privilege Required for Editing"));

    # 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.";
    }

    $Schema = new MetadataSchema();

    # 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 = new MetadataSchema();

    # 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 a "User is value of" 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 ValidateUserIsValue(array $Input, $Key, $Name)
{
    $ValidValues = array(
        MetadataField::USERISVALUE_OR,
        MetadataField::USERISVALUE_UNSET,
        MetadataField::USERISVALUE_AND);
    $Value = GetArrayValue($Input, $Key);

    if (!in_array($Value, $ValidValues))
    {
        return "The \"user is value of\" setting for the <i>" . $Name
             . "</i> field is invalid.";
    }
}

/**
 * Validate the "User value" 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 ValidateUserValue(array $Input, $Key, $Name)
{
    $Schema = new MetadataSchema();
    $UserField = $Schema->GetField(GetArrayValue($Input, $Key));

    if ($UserField->Status() != MetadataSchema::MDFSTAT_OK)
    {
        return "The \"user is value of\" field for the <i>" . $Name . "</i>"
             . " field must be set to an user field.";
    }

    if ($UserField->Type() != MetadataSchema::MDFTYPE_USER)
    {
        return "The \"user is value of\" field for the <i>" . $Name . "</i>"
             . " field must be set to an user field.";
    }
}

/**
 * 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;
        }
    }

    # save the fields
    foreach ($Input as $Key => $Value)
    {
        $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();
    $FieldId = $Field->Id();

    # re-assign browsing field ID if that is what is being deleted
    if ($SysConfig->BrowsingFieldId() == $FieldId)
    {
        $Schema = new MetadataSchema();
        $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]);
    }

    return $ValidInput;
}

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

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

$H_FixedDefaults = MetadataField::$FixedDefaults;
$H_TypeBasedDefaults = MetadataField::$TypeBasedDefaults;
$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 ? "Add" : "Edit") . " 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("DBEditor");
    return;
}

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

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

# add a new field
else 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 = new MetadataSchema();
        $Field = $Schema->AddField(NULL, $Type);

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

        # go back to the field list
        $AF->SetJumpToPage("DBEditor");
        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("DBEditor");
        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)
    {
        DeleteField($Field);
        $AF->SetJumpToPage("DBEditor");
    }

    # 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();
    $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();
}
