<?PHP

#
#   FILE:  SPT--ItemFactory.php
#
#   METHODS PROVIDED:
#       ItemFactory()
#           - constructor
#       Status()
#           - return current error status
#       GetLastTempItem()
#           - retrieve temp item based on user ID and last modified date
#                     (returns NULL if no temp item found for that ID)
#       CleanOutStaleTempItems()
#           - clear out any temp items more than one week old
#
#   NOTES:
#       - for a derived class to use the temp methods the item record in the
#             database must include "DateLastModified" and "LastModifiedById"
#             fields, and the item object must include a "Delete()" method
#
#   AUTHOR:  Edward Almasy
#
#   Part of the Collection Workflow Integration System
#   Copyright 2007 Edward Almasy and Internet Scout
#   http://scout.wisc.edu
#


class ItemFactory {

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

    # object constructor
    function ItemFactory($ItemClassName, $ItemTableName, $ItemIdFieldName, 
            $ItemNameFieldName = NULL, $FieldId = NULL, $OrderOpsAllowed = FALSE)
    {
        # save item access names
        $this->ItemClassName = $ItemClassName;
        $this->ItemTableName = $ItemTableName;
        $this->ItemIdFieldName = $ItemIdFieldName;
        $this->ItemNameFieldName = $ItemNameFieldName;

        # save field ID (if specified)
        if ($FieldId !== NULL) {  $this->FieldId = intval($FieldId);  }

        # save flag indicating whether item type allows ordering operations
        $this->OrderOpsAllowed = $OrderOpsAllowed;
        if ($OrderOpsAllowed)
        {  
            $this->OrderList = new DoublyLinkedItemList(
                    $ItemTableName, $ItemIdFieldName);
            $this->SetOrderOpsCondition(NULL);
        }
 
		# grab our own database handle
		$this->DB = new SPTDatabase();
        
        # assume everything will be okay
        $this->ErrorStatus = 0;
    }
    
    # return current error status
    function Status() {  return $this->ErrorStatus;  }
    
    # get ID of currently edited item
    function GetCurrentEditedItemId()
    {
        # if ID available in session variable
        global $Session;
        if ($EditedIds = $Session->Get($this->ItemClassName."EditedIds"))
        {
            # look up value in session variable
            $ItemId = $EditedIds[0];
        }
        else
        {
            # attempt to look up last temp item ID
            $ItemId = $this->GetLastTempItemId();

            # store it in session variable
            $EditedIds = array($ItemId);
            $Session->RegisterVariable($this->ItemClassName."EditedIds", $EditedIds);
        }

        # return ID (if any) to caller
        return $ItemId;
    }

    # set ID of currently edited item
    function SetCurrentEditedItemId($NewId)
    {
        # if edited ID array already stored for session
        global $Session;
        if ($EditedIds = $Session->Get($this->ItemClassName."EditedIds"))
        {
            # prepend new value to array
            array_unshift($EditedIds, $NewId);
        }
        else
        {
            # start with fresh array
            $EditedIds = array($NewId);
        }

        # save in session variable
        $Session->RegisterVariable($this->ItemClassName."EditedIds", $EditedIds);
    }

    # clear currently edited item ID
    function ClearCurrentEditedItemId()
    {
        # if edited item IDs available in a session variable
        global $Session;
        $SessionVarName = $this->ItemClassName."EditedIds";
        if ($EditedIds = $Session->Get($SessionVarName))
        {
            # remove current item from edited item ID array
            array_shift($EditedIds);

            # if no further edited items
            if (count($EditedIds) < 1)
            {
                # destroy session variable
                $Session->UnregisterVariable($SessionVarName);
            }
            else
            {
                # save new shorter edited item ID array to session variable
                $Session->RegisterVariable($SessionVarName, $EditedIds);
            }
        }
    }

    # clear currently edited item ID and item
    function ClearCurrentEditedItem()
    {
        # if current edited item is temp item
        $CurrentEditedItemId = $this->GetCurrentEditedItemId();
        if ($CurrentEditedItemId < 0)
        {
            # delete temp item from DB
            $this->DB->Query("DELETE FROM ".$this->ItemTableName
                             ." WHERE ".$this->ItemIdFieldName." = ".$CurrentEditedItemId);
        }
        
        # clear current edited item ID
        $this->ClearCurrentEditedItemId();
    }

    # clear out any temp items more than one week old
    function CleanOutStaleTempItems()
    {
        # load array of stale items
        $this->DB->Query("SELECT ".$this->ItemIdFieldName." FROM ".$this->ItemTableName
                   ." WHERE ".$this->ItemIdFieldName." < 0"
                   ." AND DateLastModified < DATE_SUB(NOW(), INTERVAL 7 DAY)");
        $ItemIds = $this->DB->FetchColumn($this->ItemIdFieldName);
        
        # delete stale items
        foreach ($ItemIds as $ItemId)
        {
            $Item = new $this->ItemClassName($ItemId);
            $Item->Delete();
        }
    }
    
    # retrieve most recent temp item ID based on user ID
    # (returns NULL if no temp item found for that user ID)
    function GetLastTempItemId()
    {
        # retrieve ID of most recently modified temp item for this user
        global $User;
        $ItemId = $this->DB->Query("SELECT ".$this->ItemIdFieldName." FROM ".$this->ItemTableName
                                 ." WHERE LastModifiedById = '".$User->Get("UserId")."'"
                                 ." AND ".$this->ItemIdFieldName." < 0"
                                 ." ORDER BY ".$this->ItemIdFieldName." ASC"
                                 ." LIMIT 1",
                                 $this->ItemIdFieldName);
        
        # return item to caller (or NULL if none found)
        return $ItemId;
    }
    
    # return next item ID
    function GetNextItemId()
    {
        # if no highest item ID found
        $HighestItemId = $this->GetHighestItemId();
        if ($HighestItemId <= 0)
        {
            # start with item ID 1
            $ItemId = 1;
        }
        else
        {
            # else use next ID available after highest
            $ItemId = $HighestItemId + 1;
        }

        # return next ID to caller
        return $ItemId;
    }

    # return highest item ID ($Condition should not include "WHERE")
    function GetHighestItemId($Condition = NULL, $IncludeTempItems = FALSE)
    {
        # if temp items are supposed to be included
        if ($IncludeTempItems)
        {
            # condition is only as supplied
            $ConditionString = ($Condition == NULL) ? "" : " WHERE ".$Condition;
        }
        else
        {
            # condition is non-negative IDs plus supplied condition
            $ConditionString = " WHERE ".$this->ItemIdFieldName." >= 0"
                       .(($Condition == NULL) ? "" : " AND ".$Condition);
        }

        # return highest item ID to caller
        return $this->DB->Query("SELECT ".$this->ItemIdFieldName
                                    ." FROM ".$this->ItemTableName
                                    .$ConditionString
                                    ." ORDER BY ".$this->ItemIdFieldName
                                    ." DESC LIMIT 1",
                                $this->ItemIdFieldName);
    }
    
    # return next temp item ID
    function GetNextTempItemId()
    {
        $LowestItemId = $this->DB->Query("SELECT ".$this->ItemIdFieldName
                                         ." FROM ".$this->ItemTableName
                                         ." ORDER BY ".$this->ItemIdFieldName
                                         ." ASC LIMIT 1", 
                                         $this->ItemIdFieldName);
        if ($LowestItemId > 0)
        {
            $ItemId = -1;
        }
        else
        {
            $ItemId = $LowestItemId - 1;
        }
        return $ItemId;
    }

    # return count of items
    function GetItemCount($Condition = NULL, $IncludeTempItems = FALSE)
    {
        # if condition was supplied
        if ($Condition != NULL)
        {
            # use condition
            $ConditionString = " WHERE ".$Condition;
        }
        else
        {
            # if field ID is available
            if (isset($this->FieldId))
            {
                # use condition for matching field ID
                $ConditionString = " WHERE FieldId = ".intval($this->FieldId);
            }
            else
            {
                # use no condition
                $ConditionString = "";
            }
        }

        # if temp items are to be excluded
        if (!$IncludeTempItems)
        {
            # if a condition was previously set
            if (strlen($ConditionString))
            {
                # add in condition to exclude temp items
                $ConditionString .= " AND (".$this->ItemIdFieldName." >= 0)";
            }
            else
            {
                # use condition to exclude temp items
                $ConditionString = " WHERE ".$this->ItemIdFieldName." >= 0";
            }
        }

        # retrieve item count
        $Count = $this->DB->Query("SELECT COUNT(*) AS RecordCount"
                                      ." FROM ".$this->ItemTableName
                                      .$ConditionString,
                                  "RecordCount");

        # return count to caller
        return $Count;
    }

    # return array of item IDs ($Condition should not include "WHERE")
    function GetItemIds($Condition = NULL, $IncludeTempItems = FALSE)
    {
        # if temp items are supposed to be included
        if ($IncludeTempItems)
        {
            # condition is only as supplied
            $ConditionString = ($Condition == NULL) ? "" : " WHERE ".$Condition;
        }
        else
        {
            # condition is non-negative IDs plus supplied condition
            $ConditionString = " WHERE ".$this->ItemIdFieldName." >= 0"
                       .(($Condition == NULL) ? "" : " AND ".$Condition);
        }

        # get item IDs
        $this->DB->Query("SELECT ".$this->ItemIdFieldName
                                      ." FROM ".$this->ItemTableName
                                      .$ConditionString);
        $ItemIds = $this->DB->FetchColumn($this->ItemIdFieldName);

        # return IDs to caller
        return $ItemIds;
    }

    # return latest modification date ($Condition should not include "WHERE")
    function GetLatestModificationDate($Condition = NULL)
    {
        # return modification date for item most recently changed
        $ConditionString = ($Condition == NULL) ? "" : " WHERE ".$Condition;
        return $this->DB->Query("SELECT MAX(DateLastModified) AS LastChangeDate"
                                    ." FROM ".$this->ItemTableName.$ConditionString,
                                "LastChangeDate");
    }

    # retrieve item by item ID
    function GetItem($ItemId)
    {
        return new $this->ItemClassName($ItemId);
    }
    
    # retrieve item by name
    function GetItemByName($Name, $IgnoreCase = FALSE)
    {
        # error out if this is an illegal operation for this item type
        if ($this->ItemNameFieldName == NULL)
        {
            exit("<br>SPT - ERROR: attempt to get item by name on item type"
                    ."(".$this->ItemClassName.") that has no name field specified<br>\n");
        }

        # query database for item ID
        $Comparison = $IgnoreCase
                ? "LOWER(".$this->ItemNameFieldName.") = '"
                        .addslashes(strtolower($Name))."'"
                : $this->ItemNameFieldName." = '" .addslashes($Name)."'";
        $ItemId = $this->DB->Query("SELECT ".$this->ItemIdFieldName
                                      ." FROM ".$this->ItemTableName
                                      ." WHERE ".$Comparison
                                            .(isset($this->FieldId)
                                                    ? " AND FieldId = ".$this->FieldId
                                                    : ""),
                                   $this->ItemIdFieldName);

        # if item ID was not found
        if ($ItemId === NULL)
        {
            # return NULL to caller
            $Item = NULL;
        }
        else
        {
            # generate new item object
            $Item = $this->GetItem($ItemId);
        }
        
        # return new object to caller
        return $Item;
    }

    # retrieve array of item names indexed by IDs and sorted by name
    function GetItemNames()
    {
        # error out if this is an illegal operation for this item type
        if ($this->ItemNameFieldName == NULL)
        {
            exit("<br>SPT - ERROR: attempt to get array of item names on item type"
                    ."(".$this->ItemClassName.") that has no name field specified<br>\n");
        }

        # query database for item names
        $this->DB->Query("SELECT ".$this->ItemIdFieldName
                                            .", ".$this->ItemNameFieldName
                                        ." FROM ".$this->ItemTableName
                                        .(isset($this->FieldId)
                                                ? " WHERE FieldId = ".$this->FieldId
                                                : "")
                                        ." ORDER BY ".$this->ItemNameFieldName);
        $Names = $this->DB->FetchColumn("Name", "Id");

        # return item names to caller
        return $Names;
    }

    # retrieve names of items matching search string (array index is IDs)
    # (NOTE:  IncludeVariants parameter is NOT YET SUPPORTED!)
    function SearchForItemNames($SearchString, $NumberOfResults = 100, 
            $IncludeVariants = FALSE, $UseBooleanMode = TRUE)
    {
        # error out if this is an illegal operation for this item type
        if ($this->ItemNameFieldName == NULL)
        {
            exit("<br>SPT - ERROR: attempt to search for item names on item type"
                    ."(".$this->ItemClassName.") that has no name field specified<br>\n");
        }

        # return no results if empty search string passed in
        if (!strlen(trim($SearchString))) {  return array();  }

        # construct SQL query
        $QueryString = "SELECT ".$this->ItemIdFieldName.",".$this->ItemNameFieldName
                ." FROM ".$this->ItemTableName." WHERE";
        if ($this->FieldId)
        {
            $QueryString .= " FieldId = ".$this->FieldId." AND";
        }
        if ($UseBooleanMode)
        {
            $SearchString = preg_replace("/[)\(><]+/", "", $SearchString);
            $Words = preg_split("/[\s]+/", trim($SearchString));
            $NewSearchString = "";
            $InQuotedString = FALSE;
            foreach ($Words as $Word)
            {
                if ($InQuotedString == FALSE) {  $NewSearchString .= "+";  }
                if (preg_match("/^\"/", $Word)) {  $InQuotedString = TRUE;  }
                if (preg_match("/\"$/", $Word)) {  $InQuotedString = FALSE;  }
                $NewSearchString .= $Word." ";
            }
            $QueryString .= " MATCH (".$this->ItemNameFieldName.")"
                    ." AGAINST ('".addslashes(trim($NewSearchString))."'"
                    ." IN BOOLEAN MODE)";
        }
        else
        {
            $QueryString .= " MATCH (".$this->ItemNameFieldName.")"
                    ." AGAINST ('".addslashes(trim($SearchString))."')";
        }
        $QueryString .= " LIMIT ".$NumberOfResults;

        # perform query and retrieve names and IDs of items found by query
        $DB = new SPTDatabase();
        $DB->Query($QueryString);
        $Names = $DB->FetchColumn($this->ItemNameFieldName, $this->ItemIdFieldName);

        # return names to caller
        return $Names;
    }

    /**
    * add items with specified names
    * @param ItemNames Array of item names.  Leading or trailing whitespace is automatically trimmed off of the names.
    * @param Qualifier Qualifier object to associate with items being added.
    * @return Number of items added.
    * @note Only items with new names will be added.
    * @note This method only works for item types where a new item can be created by calling the constructor with NULL, an item name, and a field ID (in that order) as parameters.
    */
    function AddItems($ItemNames, $Qualifier = NULL)
    {
        # for each supplied item name
        $ItemCount = 0;
        foreach ($ItemNames as $Name)
        {
            # if item does not exist with this name
            $Name = trim($Name);
            if ($this->GetItemByName($Name) === NULL)
            {
                # add item
                $NewItem = new $this->ItemClassName(NULL, $Name, $this->FieldId);
                $ItemCount++;

                # assign qualifier to item if supplied
                if ($Qualifier !== NULL)
                {
                    $NewItem->Qualifier($Qualifier);
                }
            }
        }

        # return count of items added to caller
        return $ItemCount;
    }

    # ---- order operations --------------------------------------------------

    # set SQL condition (added to WHERE clause) used to select items for ordering ops
    # (use NULL to clear any previous condition)
    function SetOrderOpsCondition($Condition)
    {
        # condition is non-negative IDs (non-temp items) plus supplied condition
        $NewCondition = $this->ItemIdFieldName." >= 0"
                   .(($Condition) ? " AND ".$Condition : "");
        $this->OrderList->SqlCondition($NewCondition);
    }

    # insert/move item to before specified item
    function InsertBefore($SourceItemOrItemId, $TargetItemOrItemId)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (InsertBefore()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # insert/move item
        $this->OrderList->InsertBefore($SourceItemOrItemId, $TargetItemOrItemId);
    }

    # insert/move item to after specified item
    function InsertAfter($SourceItemOrItemId, $TargetItemOrItemId)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (InsertAfter()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # insert/move item
        $this->OrderList->InsertAfter($SourceItemOrItemId, $TargetItemOrItemId);
    }

    # add/move item to beginning of list
    function Prepend($ItemOrItemId)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (Prepend()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # prepend item
        $this->OrderList->Prepend($ItemOrItemId);
    }

    # add/move item to end of list
    function Append($ItemOrItemId)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (Append()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # add/move item
        $this->OrderList->Append($ItemOrItemId);
    }

    # retrieve list of item IDs in order
    function GetItemIdsInOrder($AddStrayItemsToOrder = TRUE)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (GetItemIdsInOrder()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # retrieve list of IDs
        return $this->OrderList->GetIds($AddStrayItemsToOrder);
    }

    # remove item from existing order
    function RemoveItemFromOrder($ItemId)
    {
        # error out if ordering operations are not allowed for this item type
        if (!$this->OrderOpsAllowed)
        {
            exit("<br>SPT - ERROR: attempt to perform ordering operation"
                    ." (RemoveItemFromOrder()) on item type"
                    ."(".$this->ItemClassName.") that does not support ordering<br>\n");
        }

        # remove item
        $this->OrderList->Remove($ItemId);
    }


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

    var $ItemClassName;
    var $ItemTableName;
    var $ItemIdFieldName;
    var $ItemNameFieldName;
    var $DB;
    var $ErrorStatus;
    var $FieldId;
    var $OrderOpsAllowed;
    var $OrderList;
 
    # get/set ordering values
    function GetPreviousItemId($ItemId)
    {
        return $this->DB->Query("SELECT Previous".$this->ItemIdFieldName
                    ." FROM ".$this->ItemTableName
                    ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId), 
                "Previous".$this->ItemIdFieldName);
    }
    function GetNextItemIdInOrder($ItemId)
    {
        return $this->DB->Query("SELECT Next".$this->ItemIdFieldName
                    ." FROM ".$this->ItemTableName
                    ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId), 
                "Next".$this->ItemIdFieldName);
    }
    function SetPreviousItemId($ItemId, $NewValue)
    {
        $this->DB->Query("UPDATE ".$this->ItemTableName
                ." SET Previous".$this->ItemIdFieldName." = ".intval($NewValue)
                ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId));
    }
    function SetNextItemId($ItemId, $NewValue)
    {
        $this->DB->Query("UPDATE ".$this->ItemTableName
                ." SET Next".$this->ItemIdFieldName." = ".intval($NewValue)
                ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId));
    }
    function SetPreviousAndNextItemIds($ItemId, $NewPreviousId, $NewNextId)
    {
        $this->DB->Query("UPDATE ".$this->ItemTableName
                ." SET Previous".$this->ItemIdFieldName." = ".intval($NewPreviousId)
                        .", Next".$this->ItemIdFieldName." = ".intval($NewNextId)
                ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId));
    }
}


?>
