<?PHP

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

    /**
     * @const string DISPLAY_FOLDER_NAME name given to the root display folder
     */
    const DISPLAY_FOLDER_NAME = "FieldDisplayFolder";

    /**
     * @const string EDIT_FOLDER_NAME name given to the root edit folder
     */
    const EDIT_FOLDER_NAME = "FieldEditFolder";

    /**
     * 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)
        {
            $Items[] = new $Info["Type"]($Info["ID"]);
        }

        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 MetadataField|MetadataFieldGroup $Item the item 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;
    }

    /**
     * Extract the metadata field display and edit folder IDs from the given
     * system configuration.
     * @param SystemConfiguration $SysConfig system configuration
     * @return void
     */
    public static function SetConfiguration(SystemConfiguration $SysConfig)
    {
        $DisplayFolderId = $SysConfig->FieldDisplayFolder();
        $EditFolderId = $SysConfig->FieldEditFolder();

        # create the display folder and update the system configuration if
        # necessary
        if (!$DisplayFolderId)
        {
            $Order = self::CreateOrderingObject(
                self::DISPLAY_FOLDER_NAME,
                MetadataSchema::MDFORDER_DISPLAY);
            $DisplayFolderId = $Order->Id();
            $SysConfig->FieldDisplayFolder($DisplayFolderId);
        }

        # create the edit folder and update the system configuration if
        # necessary
        if (!$EditFolderId)
        {
            $Order = self::CreateOrderingObject(
                self::EDIT_FOLDER_NAME,
                MetadataSchema::MDFORDER_EDITING);
            $EditFolderId = $Order->Id();
            $SysConfig->FieldEditFolder($EditFolderId);
        }

        self::$DisplayFolderId = $DisplayFolderId;
        self::$EditFolderId = $EditFolderId;
    }

    /**
     * Get the metadata field display order object.
     * @return MetadataFieldOrder metadata field display order object
     * @throws Exception if the ordering configuration is invalid or not set
     */
    public static function GetDisplayOrderObject()
    {
        self::CheckOrderingConfiguration();

        return new MetadataFieldOrder(self::$DisplayFolderId);
    }

    /**
     * Get the metadata field edit order object.
     * @return MetadataFieldOrder metadata field edit order object
     * @throws Exception if the ordering configuration is invalid or not set
     */
    public static function GetEditOrderObject()
    {
        self::CheckOrderingConfiguration();

        return new MetadataFieldOrder(self::$EditFolderId);
    }

    /**
     * Fix any issues found in case an unfound bug causes something to go awry.
     * @return void
     */
    public static function MendIssues()
    {
        try
        {
            $DisplayOrder = self::GetDisplayOrderObject();
            $EditOrder = self::GetEditOrderObject();
            $Schema = new MetadataSchema();
            $Fields = $Schema->GetFields(NULL, NULL, TRUE);

            foreach ($Fields as $Field)
            {
                if (!$DisplayOrder->ItemInOrder($Field))
                {
                    $DisplayOrder->AppendItem($Field->Id(), "MetadataField");
                }

                if (!$EditOrder->ItemInOrder($Field))
                {
                    $EditOrder->AppendItem($Field->Id(), "MetadataField");
                }
            }
        } catch (Exception $Exception){}
    }

    /**
     * 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 int $GroupId metadata field group ID
     * @param int $FieldId metadata field ID
     * @param string $Placement where to place the field ("prepend" or "append")
     * @return void
     */
    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 int $GroupId metadata field group ID
     * @param int $FieldId metadata field ID
     * @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;
        }
    }

    /**
     * Check the ordering configuration, throwing an exception if it's invalid
     * or not set.
     * @return void
     * @throws Exception if the ordering configuration is invalid or not set
     */
    protected static function CheckOrderingConfiguration()
    {
        if (!self::$DisplayFolderId || !self::$EditFolderId)
        {
            throw new Exception("Metadata field ordering configuration not set");
        }
    }

    /**
     * Create an ordering object and its folder with the given folder name.
     * @param string $NewFolderName name to use when creating a folder
     * @param int $Type ordering type from MetadataSchema::MDFORDER_...
     * @return MetadataFieldOrdering field ordering object
     */
    protected static function CreateOrderingObject($NewFolderName, $Type)
    {
        # create the folder
        $FolderFactory = new FolderFactory();
        $Folder = $FolderFactory->CreateMixedFolder($NewFolderName);

        # try to get the existing order from the MetadataFields table, which
        # won't be available for newer sites
        $RowsForUpgrade = self::GetRowsForUpgrade($Type);

        # an upgrade should be performed instead of using the defaults
        if (count($RowsForUpgrade))
        {
            # add each field to the folder
            foreach ($RowsForUpgrade as $Row)
            {
                $Folder->AppendItem($Row["FieldId"], "MetadataField");
            }
        }

        # otherwise use the defaults
        else
        {
            # used later as a variable variable to get the ordering
            $DefaultOrder = $Type == MetadataSchema::MDFORDER_DISPLAY
                ?  "DefaultDisplayOrder" : "DefaultEditOrder";

            # add each field to the folder
            foreach (self::$$DefaultOrder as $Index => $FieldId)
            {
                $Folder->AppendItem($FieldId, "MetadataField");
            }
        }

        # wrap the folder in a MetadataFieldOrder object
        return new MetadataFieldOrder($Folder->Id());
    }

    /**
     * Get rows for upgrading purposes. This should only be run if an upgrade
     * should be performed.
     * @param int $Type ordering type from MetadataSchema::MDFORDER_...
     * @return array rows of field IDs in the correct order for the type
     * @see MetadataFieldOrdering::ShouldPerformOrderingUpgrade()
     */
    protected static function GetRowsForUpgrade($Type)
    {
        $Database = new Database();

        # temporarily suppress errors
        $Setting = Database::DisplayQueryErrors();
        Database::DisplayQueryErrors(FALSE);

        # see if the old columns exist
        $Handle = $Database->Query("
            SELECT EditingOrderPosition
            FROM MetadataFields
            LIMIT 1");

        # the columns do not exist so an upgrade cannot be performed
        if ($Handle === FALSE)
        {
            return array();
        }

        # determine which column to use for ordering
        $Column = $Type == MetadataSchema::MDFORDER_EDITING
            ? "DisplayOrderPosition" : "EditingOrderPosition";

        # query for the fields in their proper order
        $Database->Query("
            SELECT FieldId
            FROM MetadataFields
            WHERE FieldId > 0
            ORDER BY ".$Column." ASC");

        # restore the earlier error setting
        Database::DisplayQueryErrors($Setting);

        # return the resulting rows
        return $Database->FetchRows();
    }

    /**
     * @var int $DisplayFolderId ID of the metadata field display folder
     */
    protected static $DisplayFolderId;

    /**
     * @var int $EditFolderId ID of the metadata field edit folder
     */
    protected static $EditFolderId;

    /**
     * @var array $DefaultDisplayOrder the default display order for fields,
     *   where the key is the position and the value is the field ID.
     */
    protected static $DefaultDisplayOrder = array(
        0 => 42,
        1 => 41,
        2 => 43,
        3 => 44,
        4 => 46,
        5 => 45,
        6 => 40,
        7 => 39,
        8 => 34,
        9 => 33,
        10 => 37,
        11 => 36,
        12 => 38,
        13 => 35,
        14 => 47,
        15 => 48,
        16 => 57,
        17 => 56,
        18 => 58,
        19 => 59,
        20 => 61,
        21 => 60,
        22 => 55,
        23 => 54,
        24 => 50,
        25 => 49,
        26 => 51,
        27 => 52,
        28 => 53,
        29 => 32,
        30 => 31,
        31 => 1,
        32 => 2,
        33 => 4,
        34 => 20,
        35 => 21,
        36 => 19,
        37 => 3,
        38 => 27,
        39 => 11,
        41 => 23,
        42 => 26,
        43 => 25,
        44 => 24,
        45 => 9,
        46 => 8,
        47 => 7,
        48 => 6,
        49 => 22,
        50 => 10,
        51 => 28,
        52 => 5,
        53 => 12,
        54 => 62,
        55 => 13,
        56 => 14,
        57 => 16,
        58 => 17,
        59 => 30,
        60 => 29,
        61 => 18,
        62 => 63,
        63 => 64,
        64 => 65,
        65 => 66);

    /**
     * @var array $DefaultEditOrder the default editing order for fields, where
     *   the key is the position and the value is the field ID.
     */
    protected static $DefaultEditOrder = array(
        0 => 42,
        1 => 41,
        2 => 43,
        3 => 44,
        4 => 46,
        5 => 45,
        6 => 40,
        7 => 39,
        8 => 34,
        9 => 33,
        10 => 37,
        11 => 36,
        12 => 38,
        13 => 35,
        14 => 47,
        15 => 48,
        16 => 57,
        17 => 56,
        18 => 58,
        19 => 59,
        20 => 61,
        21 => 60,
        22 => 55,
        23 => 54,
        24 => 50,
        25 => 49,
        26 => 51,
        27 => 52,
        28 => 53,
        29 => 32,
        30 => 31,
        31 => 1,
        32 => 2,
        33 => 4,
        34 => 20,
        36 => 21,
        37 => 19,
        38 => 3,
        39 => 27,
        40 => 11,
        41 => 23,
        42 => 26,
        43 => 25,
        44 => 24,
        45 => 9,
        46 => 8,
        47 => 30,
        48 => 7,
        49 => 6,
        50 => 29,
        51 => 22,
        52 => 10,
        53 => 28,
        54 => 5,
        55 => 12,
        56 => 62,
        57 => 13,
        58 => 18,
        59 => 14,
        60 => 16,
        61 => 17,
        62 => 63,
        63 => 64,
        64 => 65,
        65 => 66);

}
