<?PHP

#
#   FILE:  MetadataSchema.php
#
#   Copyright 2002-2010 Edward Almasy and Internet Scout
#   http://scout.wisc.edu
#

class MetadataSchema extends ItemFactory {

    # ---- PUBLIC INTERFACE --------------------------------------------------

    # types of field ordering
    const MDFORDER_DISPLAY =  1;
    const MDFORDER_EDITING =  2;
    const MDFORDER_ALPHABETICAL =  3;
    
    # metadata field types 
# (must parallel MetadataFields.FieldType declaration in install/CreateTables.sql 
#        and MetadataField::$FieldTypeDBEnums declaration below)
    const MDFTYPE_TEXT =            1;
    const MDFTYPE_PARAGRAPH =       2;
    const MDFTYPE_NUMBER =          4;
    const MDFTYPE_DATE =            8;
    const MDFTYPE_TIMESTAMP =       16;
    const MDFTYPE_FLAG =            32;
    const MDFTYPE_TREE =            64;
    const MDFTYPE_CONTROLLEDNAME =  128;
    const MDFTYPE_OPTION =          256;
    const MDFTYPE_USER =            512;
    const MDFTYPE_IMAGE =           1024;
    const MDFTYPE_FILE =            2048;
    const MDFTYPE_URL =             4096;
    const MDFTYPE_POINT =           8192;
    
    # error status codes
    const MDFSTAT_OK =                 1;
    const MDFSTAT_DUPLICATENAME =      2;
    const MDFSTAT_DUPLICATEDBCOLUMN =  4;
    const MDFSTAT_ILLEGALNAME =        8;
    const MDFSTAT_FIELDDOESNOTEXIST =  16;

    # object constructor
    function MetadataSchema()
    {
        # set up item factory base class
        $this->ItemFactory(
                "MetadataField", "MetadataFields", "FieldId", "FieldName");
        
        # start with field info caching enabled
        $this->CachingOn = TRUE;
    }

    # turn internal caching of field info on or off
    function CacheData($NewValue)
    {
        $this->CachingOn = $NewValue;
    }

    # add new metadata field
    function AddField($FieldName, $FieldType, $Optional = TRUE, $DefaultValue = NULL)
    {
        # create new field
        $Field = new MetadataField(NULL, $FieldName, $FieldType, $Optional, $DefaultValue);

        # save error code if create failed and return NULL
        if ($Field->Status() != MetadataSchema::MDFSTAT_OK)
        {
            $this->ErrorStatus = $Field->Status();
            $Field = NULL;
        }

        # return new field to caller
        return $Field;
    }

    # delete metadata field
    function DropField($FieldId)
    {
        $Field = new MetadataField($FieldId);
        $Field->Drop();
    }

    # retrieve field by ID
    function GetField($FieldId)
    {
        static $Fields;

        # if caching is off or field is already loaded
        if (($this->CachingOn != TRUE) || !isset($Fields[$FieldId]))
        {
            # retrieve field
            $Fields[$FieldId] = new MetadataField($FieldId);
        }

        # return field to caller
        return $Fields[$FieldId];
    }

    /**
    * Retrieve metadata field by name.
    * @param FieldName Field name.
    * @param IgnoreCase If TRUE, case is ignore when matching field names.
    * @return Requested MetadataField or NULL if no field found with specified name.
    */
    function GetFieldByName($FieldName, $IgnoreCase = FALSE)
    {
        $FieldId = $this->GetFieldIdByName($FieldName, $IgnoreCase);
        return ($FieldId === NULL) ? NULL : $this->GetField($FieldId);
    }

    /**
    * Retrieve metadata field ID by name.
    * @param FieldName Field name.
    * @param IgnoreCase If TRUE, case is ignore when matching field names.
    * @return ID of requested MetadataField or FALSE if no field found with
    *       specified name.
    */
    function GetFieldIdByName($FieldName, $IgnoreCase = FALSE)
    {
        static $FieldIdsByName;

        # if caching is off or field ID is already loaded
        if (($this->CachingOn != TRUE) || !isset($FieldIdsByName[$FieldName]))
        {
            # retrieve field ID from DB
            $Condition = $IgnoreCase
                    ? "WHERE LOWER(FieldName) = '".addslashes(strtolower($FieldName))."'"
                    : "WHERE FieldName = '".addslashes($FieldName)."'";
            $FieldIdsByName[$FieldName] = $this->DB->Query(
                    "SELECT FieldId FROM MetadataFields ".$Condition, "FieldId");
        }

        return $FieldIdsByName[$FieldName];
    }

    # check whether field with specified name exists
    function FieldExists($FieldName) {  return $this->NameIsInUse($FieldName);  }

    # retrieve array of fields
    function GetFields($FieldTypes = NULL, $OrderType = NULL, 
            $IncludeDisabledFields = FALSE, $IncludeTempFields = FALSE)
    {
        # create empty array to pass back
        $Fields = array();

        # for each field type in database
        if ($IncludeTempFields && $IncludeDisabledFields)
        {
            $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields");
        }
        else
        {
            if ($IncludeTempFields)
            {
                $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE Enabled != 0");
            }
            elseif ($IncludeDisabledFields)
            {
                $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE FieldId >= 0");
            }
            else
            {
                $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE FieldId >= 0 AND Enabled != 0");
            }
        }
        while ($Record = $this->DB->FetchRow())
        {
            # if no specific type requested or if field is of requested type
            if (($FieldTypes == NULL)
                || (MetadataField::$FieldTypePHPEnums[$Record["FieldType"]] & $FieldTypes))
            {
                # create field object and add to array to be passed back
                $Fields[$Record["FieldId"]] = new MetadataField($Record["FieldId"]);
            }
        }

        # if field sorting requested
        if ($OrderType !== NULL)
        {
            # sort field array by requested order type
            $this->FieldCompareType = $OrderType;
            $this->FieldOrderError = FALSE;
            uasort($Fields, array($this, "CompareFieldOrder"));

            # if field order error detected
            if ($this->FieldOrderError)
            {
                # repair (reset) field order
                $OrderIndex = 1;
                foreach ($Fields as $Field)
                {
                    $Field->OrderPosition($OrderType, $OrderIndex);
                    $OrderIndex++;
                }
            }
        }

        # return array of field objects to caller
        return $Fields;
    }

    # callback function for sorting fields
    function CompareFieldOrder($FieldA, $FieldB)
    {
        if ($this->FieldCompareType == MetadataSchema::MDFORDER_ALPHABETICAL)
        {
            return ($FieldA->Name() < $FieldB->Name()) ? -1 : 1;
        }
        else
        {
            if ($FieldA->OrderPosition($this->FieldCompareType) 
                    == $FieldB->OrderPosition($this->FieldCompareType))
            {
                $this->FieldOrderError = TRUE;
                return 0;
            }
            else
            {
                return ($FieldA->OrderPosition($this->FieldCompareType) 
                        < $FieldB->OrderPosition($this->FieldCompareType)) ? -1 : 1;
            }
        }
    }

    function GetFieldNames($FieldTypes = NULL, $OrderType = NULL,
                            $IncludeDisabledFields = FALSE, $IncludeTempFields = FALSE)
    {
        global $DB;

        $FieldNames=array();
        $Fields = $this->GetFields($FieldTypes, $OrderType, $IncludeDisabledFields, $IncludeTempFields);

        foreach($Fields as $Field)
        {
            $DB->Query("SELECT FieldName FROM MetadataFields WHERE FieldId=".$Field->Id());
            $FieldNames[ $Field->Id() ] = $DB->FetchField("FieldName");
        }

        return $FieldNames;
    }

    /**
    * Retrieve fields of specified type as HTML option list with field names
    * as labels and field IDs as value attributes.  The first element on the list
    * will have a label of "--" and an ID of -1 to indicate no field selected.
    * @param OptionListName Value of option list "name" attribute.
    * @param FieldTypes Types of fields to return.  (OPTIONAL - use NULL for all types)
    * @param SelectedFieldId ID of currently-selected field.  (OPTIONAL)
    * @param IncludeNullOption Whether to include "no selection" (-1) option.  
    *       (OPTIONAL - defaults to TRUE)
    * @return HTML for option list.
    */
    function GetFieldsAsOptionList($OptionListName, $FieldTypes = NULL, 
            $SelectedFieldId = NULL, $IncludeNullOption = TRUE)
    {
        # retrieve requested fields
        $FieldNames = $this->GetFieldNames($FieldTypes);

        # begin HTML option list
        $Html = "<select name=\"".$OptionListName."\">\n";
        if ($IncludeNullOption)
        {
            $Html .= "<option value=\"-1\">--</option>\n";
        }

        # for each metadata field
        foreach ($FieldNames as $Id => $Name)
        {
            # add entry for field to option list
            $Html .= "<option value=\"".$Id."\"";
            if ($Id == $SelectedFieldId) {  $Html .= " selected";  }
            $Html .= ">".htmlspecialchars($Name)."</option>\n";
        }

        # end HTML option list
        $Html .= "</select>\n";

        # return constructed HTML to caller
        return $Html;
    }

    # retrieve array of field types (enumerated type => field name)
    function GetFieldTypes()
    {
        return MetadataField::$FieldTypeDBEnums;
    }

    # retrieve array of field types that user can create (enumerated type => field name)
    function GetAllowedFieldTypes()
    {
        return MetadataField::$FieldTypeDBAllowedEnums;
    }

    # remove all metadata field associations for a given qualifier
    function RemoveQualifierAssociations($QualifierIdOrObject)
    {
        # sanitize qualifier ID or grab it from object
        $QualifierIdOrObject = is_object($QualifierIdOrObject)
                ? $QualifierIdOrObject->Id() : intval($QualifierIdOrObject);

        # delete intersection records from database
        $this->DB->Query("DELETE FROM FieldQualifierInts WHERE QualifierId = "
                         .$QualifierIdOrObject);
    }

    # return whether qualifier is in use by metadata field
    function QualifierIsInUse($QualifierIdOrObject)
    {
        # sanitize qualifier ID or grab it from object
        $QualifierIdOrObject = is_object($QualifierIdOrObject)
                ? $QualifierIdOrObject->Id() : intval($QualifierIdOrObject);

        # determine whether any fields use qualifier as default
        $DefaultCount = $this->DB->Query("SELECT COUNT(*) AS RecordCount FROM MetadataFields"
                                         ." WHERE DefaultQualifier = ".$QualifierIdOrObject,
                                         "RecordCount");

        # determine whether any fields are associated with qualifier
        $AssociationCount = $this->DB->Query("SELECT COUNT(*) AS RecordCount FROM FieldQualifierInts"
                                         ." WHERE QualifierId = ".$QualifierIdOrObject,
                                         "RecordCount");

        # report whether qualifier is in use based on defaults and associations
        return (($DefaultCount + $AssociationCount) > 0) ? TRUE : FALSE;
    }

    # move fields up or down in field order
    function MoveUpInOrder($FieldIdOrObj, $OrderType)
    {
        $this->MoveFieldInOrder($FieldIdOrObj, $OrderType, FALSE);
    }
    function MoveDownInOrder($FieldIdOrObj, $OrderType)
    {
        $this->MoveFieldInOrder($FieldIdOrObj, $OrderType, TRUE);
    }

    # return highest field ID currently in use
    function GetHighestFieldId() {  return $this->GetHighestItemId();  }

    /**
    * Get/set mapping of standard field name to specific field.
    * @param MappedName Standard field name.
    * @param FieldId ID of field to map to. (OPTIONAL)
    * @return ID of field to which standard field name is mapped or NULL if
    *   specified standard field name is not currently mapped.
    */
    static function StdNameToFieldMapping($MappedName, $FieldId = NULL)
    {
        if ($FieldId !== NULL)
        {
            self::$FieldMappings[$MappedName] = $FieldId;
        }
        return isset(self::$FieldMappings[$MappedName]) 
                ? self::$FieldMappings[$MappedName] : NULL;
    }

    /**
    * Get mapping of field ID to standard field name.
    * @param FieldId Field ID.
    * @return Standard field name to which specified field is mapped, or
    *   NULL if field is not currently mapped.
    */
    static function FieldToStdNameMapping($FieldId)
    {
        foreach (self::$FieldMappings as $MappedName => $MappedFieldId)
        {
            if ($MappedFieldId == $FieldId)
            {
                return $MappedName;
            }
        }
        return NULL;
    }

    /**
    * Get field by standard field name.
    * @param MappedName Standard field name.
    * @return MetadataField to which standard field name is mapped or NULL if
    *   specified standard field name is not currently mapped or mapped field
    *   does not exist.
    */
    function GetFieldByMappedName($MappedName)
    {
        return ($this->StdNameToFieldMapping($MappedName) == NULL) ? NULL
                : $this->GetField($this->StdNameToFieldMapping($MappedName));
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $FieldCompareType;
    private $FieldOrderError;
    private $CachingOn;
    private static $FieldMappings;

    private function MoveFieldInOrder($FieldIdOrObj, $OrderType, $MoveFieldDown)
    {
        # grab field ID
        $FieldId = is_object($FieldIdOrObj) ? $Field->Id() : $FieldIdOrObj;

        # retrieve array of fields
        $Fields = $this->GetFields(NULL, $OrderType);

        # reverse array of fields if we are moving field down
        if ($MoveFieldDown)
        {
            $Fields = array_reverse($Fields);
        }

        # for each field in order
        $PreviousField = NULL;
        foreach ($Fields as $Field)
        {
            # if field is the field to be moved
            if ($Field->Id() == $FieldId)
            {
                # if we have a previous field
                if ($PreviousField !== NULL)
                {
                    # swap field with previous field according to order type
                    $TempVal = $Field->OrderPosition($OrderType);
                    $Field->OrderPosition($OrderType, $PreviousField->OrderPosition($OrderType));
                    $PreviousField->OrderPosition($OrderType, $TempVal);
                }
            }

            # save field for next iteration
            $PreviousField = $Field;
        }
    }
}

?>
