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

/**
 * Class to build metadata field ordering functionality on top of the foldering
 * functionality.
 */
class MetadataFieldOrder extends Folder
{

    /**
    * The default name given to the folders that are really metadata field
    * orders.
    */
    const DEFAULT_FOLDER_NAME = "FieldOrder";

    /**
    * Load an existing metadata field order.
    * @param int $Id The ID of the metadata field order to load.
    * @throws Exception if the ID is invalid or the order does not exist.
    */
    public function __construct($Id)
    {
        # being loading by calling the superclass method
        parent::Folder($Id);

        # create a class-wide database
        $this->Database = new Database();

        # query for order associations from the database
        $this->Database->Query("
            SELECT * FROM MetadataFieldOrders
            WHERE OrderId = '".addslashes($Id)."'");

        # the ID is invalid
        if ($this->Database->NumRowsSelected() < 1)
        {
            throw new Exception("Unknown metadata field order ID");
        }

        # fetch the data
        $Row = $this->Database->FetchRow();

        # set the values
        $this->SchemaId = $Row["SchemaId"];
        $this->OrderName = $Row["OrderName"];
    }

    /**
    * Get the ID of the metadata schema with which the metadata field order is
    * associated.
    * @return Returns the ID of the metadata schema with which the metadata
    *      field order is associated.
    */
    public function SchemaId()
    {
        return $this->SchemaId;
    }

    /**
    * Get the name of the metadata field order.
    * @return Returns the name of the metadata field order.
    */
    public function OrderName()
    {
        return $this->OrderName;
    }

    /**
    * Delete the metadata field order. This removes the association of the order
    * with any schemas and deletes the folder it uses. The object should not be
    * used after calling this method.
    */
    public function Delete()
    {
        # remove the order from the orders associated with schemas
        $this->Database->Query("
            DELETE FROM MetadataFieldOrders
            WHERE OrderId = '".addslashes($this->Id())."'");

        # remove the folder by calling the superclass method
        parent::Delete();
    }

    /**
    * Fix any issues found in case an unfound bug causes something to go awry.
    */
    public function MendIssues()
    {
        $Schema = new MetadataSchema($this->SchemaId);

        # get all the fields including disabled fields but excluding temp fields
        $Fields = $Schema->GetFields(NULL, NULL, TRUE);

        foreach ($Fields as $Field)
        {
            # add the field if it isn't already in the order
            if (!$this->ItemInOrder($Field))
            {
                $this->AppendItem($Field->Id(), "MetadataField");
            }
        }
    }

    /**
     * Transform the item IDs of the metadata field order object into objects.
     * @return array an array of metadata field order object items
     */
    public function GetItems()
    {
        $ItemIds = $this->GetItemIds();
        $Items = array();

        foreach ($ItemIds as $Info)
        {
            try
            {
                $Items[] = new $Info["Type"]($Info["ID"]);
            }

            # skip invalid fields
            catch (InvalidArgumentException $Exception)
            {
                continue;
            }
        }

        return $Items;
    }

    /**
     * Create a new metadata field group with the given name.
     * @param string $Name group name
     * @return MetadataFieldGroup new metadata field group
     */
    public function CreateGroup($Name)
    {
        $FolderFactory = new FolderFactory();

        # create the new group
        $Folder = $FolderFactory->CreateMixedFolder($Name);
        $Group = new MetadataFieldGroup($Folder->Id());

        # and add it to this ordering
        $this->AppendItem($Group->Id(), "MetadataFieldGroup");

        return $Group;
    }

    /**
     * Move the metadata fields out of the given metadata group to the metadata
     * field order and then delete it.
     * @param MetadataFieldGroup $Group metadata field group
     * @return void
     */
    public function DeleteGroup(MetadataFieldGroup $Group)
    {
        if ($this->ContainsItem($Group->Id(), "MetadataFieldGroup"))
        {
            $this->MoveFieldsToOrder($Group);
            $this->RemoveItem($Group->Id(), "MetadataFieldGroup");
        }
    }

    /**
     * Get all the fields in this metadata field ordering in order.
     * @return array array of MetadataField objects
     */
    public function GetFields()
    {
        $Fields = array();

        foreach ($this->GetItems() as $Item)
        {
            # add fields to the list
            if ($Item instanceof MetadataField)
            {
                $Fields[$Item->Id()] = $Item;
            }

            # add fields of groups to the list
            else if ($Item instanceof MetadataFieldGroup)
            {
                foreach ($Item->GetFields() as $Field)
                {
                    $Fields[$Field->Id()] = $Field;
                }
            }
        }

        return $Fields;
    }

    /**
     * Get all the groups in this metadata field ordering in order.
     * @return array array of MetadataFieldGroup objects
     */
    public function GetGroups()
    {
        $ItemIds = $this->GetItemIds();
        $GroupIds = array_filter($ItemIds, array($this, "GroupFilterCallback"));

        $Groups = array();

        # transform group info to group objects
        foreach ($GroupIds as $GroupId)
        {
            try
            {
              $Groups[$GroupId["ID"]] = new $GroupId["Type"]($GroupId["ID"]);
            } catch (Exception $Exception) {}
        }

        return $Groups;
    }

    /**
     * Move the given item up in the order.
     * @param MetadataField|MetadataFieldGroup $Item item
     * @param callback $Filter callback to filter out items before moving
     * @return void
     * @throws Exception if the item isn't a metadata field or metadata group
     * @throws Exception if the item isn't in the order
     * @throws Exception if a callback is given and it isn't callable
     */
    public function MoveItemUp($Item, $Filter=NULL)
    {
        # make sure the item is a field or group
        if (!$this->IsFieldOrGroup($Item))
        {
            throw new Exception("Item must be a field or group");
        }

        # make sure the item is in the order
        if (!$this->ItemInOrder($Item))
        {
            throw new Exception("Item must exist in the ordering");
        }

        # make sure the filter is callable if set
        if (!is_null($Filter) && !is_callable($Filter))
        {
            throw new Exception("Filter callback must be callable");
        }

        $ItemType = $this->GetItemType($Item);
        $Enclosure = $this->GetEnclosure($Item);
        $EnclosureType = $this->GetItemType($Enclosure);
        $Previous = $this->GetSiblingItem($Item, -1, $Filter);
        $PreviousId = $this->GetItemId($Previous);
        $PreviousType = $this->GetItemType($Previous);

        # determine if the item is at the top of the list
        $ItemAtTop = is_null($Previous);

        # determine if a field needs to be moved into a group
        $FieldToGroup = $ItemType == "MetadataField";
        $FieldToGroup = $FieldToGroup && $EnclosureType == "MetadataFieldOrder";
        $FieldToGroup = $FieldToGroup && $PreviousType == "MetadataFieldGroup";

        # determine if a field needs to be moved out of a group
        $FieldToOrder = $ItemType == "MetadataField";
        $FieldToOrder = $FieldToOrder && $EnclosureType == "MetadataFieldGroup";
        $FieldToOrder = $FieldToOrder && $ItemAtTop;

        # move a field into a group if necessary
        if ($FieldToGroup)
        {
            $this->MoveFieldToGroup($Previous, $Item, "append");
        }

        # or move a field from a group to the order if necessary
        else if ($FieldToOrder)
        {
            $this->MoveFieldToOrder($Enclosure, $Item, "before");
        }

        # otherwise just move the item up if not at the top of the list
        else if (!$ItemAtTop)
        {
            $this->MoveItemAfter($Item, $Previous);
        }
    }

    /**
     * Move the given item down in the order.
     * @param MetadataField|MetadataFieldGroup $Item
     * @param callback $Filter callback to filter out items before moving
     * @return void
     * @throws Exception if the item isn't a metadata field or metadata group
     * @throws Exception if the item isn't in the order
     * @throws Exception if a callback is given and it isn't callable
     */
    public function MoveItemDown($Item, $Filter=NULL)
    {
        # make sure the item is a field or group
        if (!$this->IsFieldOrGroup($Item))
        {
            throw new Exception("Item must be a field or group");
        }

        # make sure the item is in the order
        if (!$this->ItemInOrder($Item))
        {
            throw new Exception("Item must exist in the ordering");
        }

        # make sure the filter is callable if set
        if (!is_null($Filter) && !is_callable($Filter))
        {
            throw new Exception("Filter callback must be callable");
        }

        $ItemType = $this->GetItemType($Item);
        $Enclosure = $this->GetEnclosure($Item);
        $EnclosureType = $this->GetItemType($Enclosure);
        $Next = $this->GetSiblingItem($Item, 1, $Filter);
        $NextId = $this->GetItemId($Next);
        $NextType = $this->GetItemType($Next);

        # determine if the item is at the bottom of the list
        $ItemAtBottom = is_null($Next);

        # determine if a field needs to be moved into a group
        $FieldToGroup = $ItemType == "MetadataField";
        $FieldToGroup = $FieldToGroup && $EnclosureType == "MetadataFieldOrder";
        $FieldToGroup = $FieldToGroup && $NextType == "MetadataFieldGroup";

        # determine if a field needs to be moved out of a group
        $FieldToOrder = $ItemType == "MetadataField";
        $FieldToOrder = $FieldToOrder && $EnclosureType == "MetadataFieldGroup";
        $FieldToOrder = $FieldToOrder && $ItemAtBottom;

        # move a field into a group if necessary
        if ($FieldToGroup)
        {
            $this->MoveFieldToGroup($Next, $Item, "prepend");
        }

        # or move a field from a group to the order if necessary
        else if ($FieldToOrder)
        {
            $this->MoveFieldToOrder($Enclosure, $Item, "after");
        }

        # otherwise just move the item down if not at the bottom
        else if (!$ItemAtBottom)
        {
            $this->MoveItemAfter($Next, $Item);
        }
    }

    /**
     * Move the given item to the top of the order.
     * @param MetadataField|MetadataFieldGroup $Item the item to move
     * @throws Exception if the item isn't a metadata field or metadata group
     * @throws Exception if the item isn't in the order
     */
    public function MoveItemToTop($Item)
    {
        # make sure the item is either a field or group
        if (!$this->IsFieldOrGroup($Item))
        {
            throw new Exception("Item must be a either field or group");
        }

        # make sure the item is in the order
        if (!$this->ItemInOrder($Item))
        {
            throw new Exception("Item must exist in the ordering");
        }

        $OrderId = $this->GetItemId($this);
        $OrderType = $this->GetItemType($this);
        $ItemId = $this->GetItemId($Item);
        $ItemType = $this->GetItemType($Item);
        $ItemEnclosure = $this->GetEnclosure($Item);
        $ItemEnclosureId = $this->GetItemId($ItemEnclosure);
        $ItemEnclosureType = $this->GetItemType($ItemEnclosure);

        $SameEnclosureId = $OrderId == $ItemEnclosureId;
        $SameEnclosureType = $OrderType == $ItemEnclosureType;

        # remove the item from its enclosure if necessary
        if (!$SameEnclosureId || !$SameEnclosureType)
        {
            $ItemEnclosure->RemoveItem($ItemId, $ItemType);
        }

        # move the item to the top of the order
        $this->PrependItem($ItemId, $ItemType);
    }

    /**
     * Move the given item to the top of the order.
     * @param MetadataFieldGroup $Group the group within which to move the field
     * @param MetadataField $Field the field to move
     * @throws Exception if the group or field aren't in the order
     */
    public function MoveFieldToTopOfGroup(
        MetadataFieldGroup $Group,
        MetadataField $Field)
    {
        # make sure the items are in the order
        if (!$this->ItemInOrder($Group) || !$this->ItemInOrder($Field))
        {
            throw new Exception("Item must exist in the ordering");
        }

        $GroupId = $this->GetItemId($Group);
        $GroupType = $this->GetItemType($Group);
        $FieldId = $this->GetItemId($Field);
        $FieldType = $this->GetItemType($Field);
        $FieldEnclosure = $this->GetEnclosure($Field);
        $FieldEnclosureId = $this->GetItemId($FieldEnclosure);
        $FieldEnclosureType = $this->GetItemType($FieldEnclosure);

        $SameEnclosureId = $GroupId == $FieldEnclosureId;
        $SameEnclosureType = $GroupType == $FieldEnclosureType;

        # remove the item from its enclosure if necessary
        if (!$SameEnclosureId || !$SameEnclosureType)
        {
            $FieldEnclosure->RemoveItem($FieldId, $FieldType);
        }

        # move the item to the top of the group
        $Group->PrependItem($FieldId, $FieldType);
    }

    /**
     * Move the given item after the given target item.
     * @param MetadataField|MetadataFieldGroup $Target the item to move after
     * @param MetadataField|MetadataFieldGroup $Item the item to move
     * @return void
     * @throws Exception if the items aren't a metadata field or metadata group
     * @throws Exception if the items aren't in the order
     * @throws Exception if attempting to put a group into another one
     */
    public function MoveItemAfter($Target, $Item)
    {
        # make sure the items are either a field or group
        if (!$this->IsFieldOrGroup($Target) || !$this->IsFieldOrGroup($Item))
        {
            throw new Exception("Items must be a either field or group");
        }

        # make sure the items are in the order
        if (!$this->ItemInOrder($Target) || !$this->ItemInOrder($Item))
        {
            throw new Exception("Items must exist in the ordering");
        }

        $TargetId = $this->GetItemId($Target);
        $TargetType = $this->GetItemType($Target);
        $ItemId = $this->GetItemId($Item);
        $ItemType = $this->GetItemType($Item);
        $TargetEnclosure = $this->GetEnclosure($Target);
        $TargetEnclosureId = $this->GetItemId($TargetEnclosure);
        $TargetEnclosureType = $this->GetItemType($TargetEnclosure);
        $ItemEnclosure = $this->GetEnclosure($Item);
        $ItemEnclosureId = $this->GetItemId($ItemEnclosure);
        $ItemEnclosureType = $this->GetItemType($ItemEnclosure);

        $TargetInGroup = $TargetEnclosure instanceof MetadataFieldGroup;
        $ItemIsField = $Item instanceof MetadataField;

        # make sure only fields are placed in groups
        if ($TargetInGroup && !$ItemIsField)
        {
            throw new Exception("Only fields can go into field groups");
        }

        $SameEnclosureId = $TargetEnclosureId == $ItemEnclosureId;
        $SameEnclosureType = $TargetEnclosureType == $ItemEnclosureType;

        # move a field into a group if necessary
        if (!$SameEnclosureId || !$SameEnclosureType)
        {
            $ItemEnclosure->RemoveItem($ItemId, $ItemType);
        }

        # move the item after the target
        $TargetEnclosure->InsertItemAfter(
            $TargetId,
            $ItemId,
            $TargetType,
            $ItemType);
    }

    /**
     * Determine whether the given item is a member of this order.
     * @param MetadataField|MetadataFieldGroup $Item item
     * @return bool TRUE if the item belongs to the order or FALSE otherwise
     */
    public function ItemInOrder($Item)
    {
        # the item would have to be a field or group to be in the order
        if (!$this->IsFieldOrGroup($Item))
        {
            return FALSE;
        }

        $ItemId = $this->GetItemId($Item);
        $ItemType = $this->GetItemType($Item);

        # if the item is in the order, i.e., not in a group
        if ($this->ContainsItem($ItemId, $ItemType))
        {
            return TRUE;
        }

        # the item is in one of the groups, so search each one for it
        foreach ($this->GetGroups() as $Group)
        {
            if ($Group->ContainsItem($ItemId, $ItemType))
            {
                return TRUE;
            }
        }

        # the item was not found
        return FALSE;
    }

    /**
    * Create a new metadata field order, optionally specifying the order of the
    * fields. The array values for the order should be ordered field IDs. This
    * will overwrite any existing orders associated with the schema that have
    * the same name as the one given.
    * @param MetadataSchema $Schema Schema with which to associate the order.
    * @param string $Name Name for the metadata field order.
    * @param array $FieldOrder Optional ordered array of field IDs.
    * @return Returns a new MetadataFieldOrder object.
    */
    public static function Create(
        MetadataSchema $Schema,
        $Name,
        array $FieldOrder=array())
    {
        $ExistingOrders = self::GetOrdersForSchema($Schema);

        # remove existing orders with the same name
        if (array_key_exists($Name, $ExistingOrders))
        {
            $ExistingOrders[$Name]->Delete();
        }

        # create the folder
        $FolderFactory = new FolderFactory();
        $Folder = $FolderFactory->CreateMixedFolder(self::DEFAULT_FOLDER_NAME);

        # get all the fields including disabled fields but excluding temp fields
        $Fields = $Schema->GetFields(NULL, NULL, TRUE);

        # first, add each field from the given order
        foreach ($FieldOrder as $FieldId)
        {
            # skip invalid field IDs
            if (!array_key_exists($FieldId, $Fields))
            {
                continue;
            }

            # remove the field from the array of fields so that we'll know after
            # looping which fields weren't added
            unset($Fields[$FieldId]);

            # add the metadata field to the folder
            $Folder->AppendItem($FieldId, "MetadataField");
        }

        # finally, add any remaining fields that weren't removed in the loop
        # above
        foreach ($Fields as $FieldId => $Field)
        {
            $Folder->AppendItem($FieldId, "MetadataField");
        }

        $Database = new Database();

        # associate the order with the schema in the database
        $Database->Query("
            INSERT INTO MetadataFieldOrders
            SET SchemaId = '".addslashes($Schema->Id())."',
            OrderId = '".addslashes($Folder->Id())."',
            OrderName = '".addslashes($Name)."'");

        # reconstruct the folder as a metadata schema order object and return
        return new MetadataFieldOrder($Folder->Id());
    }

    /**
    * Get a metadata field order with a specific name for a given metadata
    * schema.
    * @param MetadataSchema $Schema Schema of which to get the order.
    * @param string $Name Name of the metadata field order to get.
    * @return Returns a MetadataFieldOrder or NULL if it doesn't exist.
    * @see GetOrderForSchemaId()
    */
    public static function GetOrderForSchema(MetadataSchema $Schema, $Name)
    {
        return self::GetOrderForSchemaId($Schema->Id(), $Name);
    }

    /**
    * Get a metadata field order with a specific name for a given metadata
    * schema ID.
    * @param int $SchemaId Schema ID of which to get the order.
    * @param string $Name Name of the metadata field order to get.
    * @return Returns a MetadataFieldOrder or NULL if it doesn't exist.
    * @see GetOrderForSchema()
    */
    public static function GetOrderForSchemaId($SchemaId, $Name)
    {
        $Orders = self::GetOrdersForSchemaId($SchemaId);

        # return NULL if the order doesn't exist
        if (!array_key_exists($Name, $Orders))
        {
            return NULL;
        }

        # return the order
        return $Orders[$Name];
    }

    /**
    * Get all of the orders associated with a schema.
    * @param MetadataSchema $Schema Schema of which to get the orders.
    * @return Returns an array of orders associated with the schema.
    * @see GetOrdersForSchemaId()
    */
    public static function GetOrdersForSchema(MetadataSchema $Schema)
    {
        return self::GetOrdersForSchemaId($Schema->Id());
    }

    /**
    * Get all of the orders associated with a schema ID.
    * @param int $SchemaId ID of the schema of which to get the orders.
    * @return Returns an array of orders associated with the schema.
    * @see GetOrdersForSchema()
    */
    public static function GetOrdersForSchemaId($SchemaId)
    {
        $Orders = array();
        $Database = new Database();

        # query the database for the orders associated with the schema
        $Database->Query("
            SELECT * FROM MetadataFieldOrders
            WHERE SchemaId = '".addslashes($SchemaId)."'");

        # loop through each found record
        foreach ($Database->FetchRows() as $Row)
        {
            try
            {
                # construct an object using the ID and add it to the array
                $Orders[$Row["OrderName"]] = new MetadataFieldOrder($Row["OrderId"]);
            }

            # remove invalid orders when encountered
            catch (Exception $Exception)
            {
                $Database->Query("
                    DELETE FROM MetadataFieldOrders
                    WHERE OrderId = '".addslashes($Row["OrderId"])."'");
            }
        }

        return $Orders;
    }

    /**
     * Determine if the given item is a metadata field or metadata field group.
     * @param mixed $Item item
     * @return TRUE if the item is a metadata field or group, FALSE otherwise
     */
    protected function IsFieldOrGroup($Item)
    {
        if ($Item instanceof MetadataField)
        {
            return TRUE;
        }

        if ($Item instanceof MetadataFieldGroup)
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
     * Get the ID of the given item.
     * @param MetadataField|MetadataFieldGroup|MetadataFieldOrder $Item item
     * @return int|null the ID of the item or NULL if the item is invalid
     */
    protected function GetItemId($Item)
    {
        return is_object($Item) ? $Item->Id() : NULL;
    }

    /**
     * Get the type of the given item.
     * @param MetadataField|MetadataFieldGroup|MetadataFieldOrder $Item item
     * @return string|null the type of the item or NULL if the item is invalid
     */
    protected function GetItemType($Item)
    {
        return is_object($Item) ? get_class($Item) : NULL;
    }

    /**
     * Callback for the filter to retrieve groups only from the metadata field
     * order.
     * @param array $Item array of item info, i.e., item ID and type
     * @return bool TRUE if the item is a group or FALSE otherwise
     */
    protected function GroupFilterCallback($Item)
    {
        return $Item["Type"] == "MetadataFieldGroup";
    }

    /**
     * Get the metadata field order or metadata field group that encloses the
     * given item.
     * @param MetadataField|MetadataFieldGroup $Item item
     * @return MetadataFieldGroup|MetadataFieldOrder|null the metadata field
     *   order or metadata field group that encloses the item, or NULL otherwise
     */
    protected function GetEnclosure($Item)
    {
        $ItemId = $this->GetItemId($Item);
        $ItemType = $this->GetItemType($Item);

        # the item is in the order, i.e., not in a group
        if ($this->ContainsItem($ItemId, $ItemType))
        {
            return $this;
        }

        # the item is in one of the groups, so search each one for it
        foreach ($this->GetGroups() as $Group)
        {
            if ($Group->ContainsItem($ItemId, $ItemType))
            {
                return $Group;
            }
        }

        # the item was not found
        return NULL;
    }

    /**
     * Get the item object of the item that is the given distance from the item.
     * @param MetadataField|MetadataFieldGroup $Item item
     * @param int $Offset distance from the item, negative values are allowed
     * @param callback $Filter callback to filter out items
     * @return array item info, i.e., item ID and type, or NULL if not found
     */
    protected function GetSiblingItem($Item, $Offset, $Filter=NULL)
    {
        $Id = $this->GetItemId($Item);
        $Type = $this->GetItemType($Item);
        $Sibling = NULL;

        # the sibling is in the order, i.e., not in a group
        if ($this->ContainsItem($Id, $Type))
        {
            return $this->FindSiblingItem($this, $Item, $Offset, $Filter);
        }

        # otherwise search for it in the groups
        foreach ($this->GetGroups() as $Group)
        {
            if ($Group->ContainsItem($Id, $Type))
            {
                try
                {
                    $Sibling = $this->FindSiblingItem(
                        $Group,
                        $Item,
                        $Offset,
                        $Filter);

                    if ($Sibling)
                    {
                        return $Sibling;
                    }
                } catch (Exception $Exception) {}

                break;
            }
        }

        return NULL;
    }

    /**
     * Attempt to find the item that is the given distance from the item within
     * the given enclosure.
     * @param MetadataFieldGroup|MetadataFieldOrder $Enclosure item enclosure
     * @param MetadataField|MetadataFieldGroup $Item item
     * @param int $Offset distance from the item, negative values are allowed
     * @param callback $Filter callback to filter out items
     * @return array item info, i.e., item ID and type, or NULL if not found
     */
    protected function FindSiblingItem($Enclosure, $Item, $Offset, $Filter=NULL)
    {
        $ItemIds = $Enclosure->GetItemIds();

        # filter items if necessary
        if (is_callable($Filter))
        {
            $ItemIds = array_filter($ItemIds, $Filter);

            # maintain continuous indices
            ksort($ItemIds);
            $ItemIds = array_values($ItemIds);
        }

        $Id = $this->GetItemId($Item);
        $Type = $this->GetItemType($Item);
        $Index = array_search(array("ID" => $Id, "Type" => $Type), $ItemIds);

        if (!is_null($Index) && array_key_exists($Index+$Offset, $ItemIds))
        {
            $SiblingInfo = $ItemIds[$Index+$Offset];
            return new $SiblingInfo["Type"]($SiblingInfo["ID"]);
        }

        return NULL;
    }

    /**
     * Move the field with the given ID to the group with the given ID,
     * optionally specifying the place where the should be placed.
     * @param MetadataFieldGroup $Group metadata field group
     * @param MetadataField $Field metadata field
     * @param string $Placement where to place the field ("prepend" or "append")
     */
    protected function MoveFieldToGroup(
        MetadataFieldGroup $Group,
        MetadataField $Field,
        $Placement)
    {
        # determine which action to use based on the placement value
        $Action = $Placement == "prepend" ? "PrependItem" : "AppendItem";

        $GroupId = $this->GetItemId($Group);
        $FieldId = $this->GetItemId($Field);

        $OrderHasGroup = $this->ContainsItem($GroupId, "MetadataFieldGroup");
        $OrderHasField = $this->ContainsItem($FieldId, "MetadataField");

        # make sure the field and group are in the order before editing
        if ($OrderHasGroup && $OrderHasField)
        {
            $this->RemoveItem($FieldId, "MetadataField");
            $Group->$Action($FieldId, "MetadataField");
        }
    }

    /**
     * Move the field with the given ID from the group with the given ID to the
     * order, optionally specifying where the field should be placed.
     * @param MetadataFieldGroup $Group metadata field group
     * @param MetadataField $Field metadata field
     * @param string $Placement where to place the field ("before" or "after")
     * @return void
     */
    protected function MoveFieldToOrder(
        MetadataFieldGroup $Group,
        MetadataField $Field,
        $Placement)
    {
        # determine which action to use based on the placement value
        $Action = $Placement == "before" ? "InsertItemBefore" : "InsertItemAfter";

        $GroupId = $this->GetItemId($Group);
        $FieldId = $this->GetItemId($Field);

        $OrderHasGroup = $this->ContainsItem($GroupId, "MetadataFieldGroup");
        $GroupHasField = $Group->ContainsItem($FieldId, "MetadataField");

        # make sure the field is in the group and the group is in the order
        if ($OrderHasGroup && $GroupHasField)
        {
            $Group->RemoveItem($FieldId, "MetadataField");
            $this->$Action(
                $GroupId,
                $FieldId,
                "MetadataFieldGroup",
                "MetadataField");
        }
    }

    /**
     * Move all the metadata fields out of the given metadata field group and
     * into the main order.
     * @param MetadataFieldGroup $Group metadata field group
     * @return void
     */
    protected function MoveFieldsToOrder(MetadataFieldGroup $Group)
    {
        $ItemIds = $Group->GetItemIds();
        $PreviousItemId = $Group->Id();
        $PreviousItemType = "MetadataFieldGroup";

        foreach ($ItemIds as $ItemInfo)
        {
            $ItemId = $ItemInfo["ID"];
            $ItemType = $ItemInfo["Type"];

            $this->InsertItemAfter(
                $PreviousItemId,
                $ItemId,
                $PreviousItemType,
                $ItemType);

            $PreviousItemId = $ItemId;
            $PreviousItemType = $ItemType;
        }
    }

    /**
    * Database object with which to query the database.
    */
    protected $Database;

    /**
    * The ID of the metadata schema this metadata field order is associated
    * with.
    */
    protected $SchemaId;

    /**
    * The name of the metadata field order.
    */
    protected $OrderName;

}
