<?PHP

#
#   FILE:  SPT--DoublyLinkedList.php
#
#   METHODS PROVIDED:
#       DoublyLinkedList()
#           - constructor
#       SomeMethod($SomeParameter, $AnotherParameter)
#           - short description of method
#
#   AUTHOR:  
#
#   Part of the Scout Portal Toolkit
#   Copyright 2007 Internet Scout
#   http://scout.wisc.edu
#

class DoublyLinkedItemList {

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

    # object constructor
    function DoublyLinkedItemList(
            $ItemTableName, $ItemIdFieldName, $SqlCondition = NULL)
    {
        # grab our own database handle
        $this->DB = new SPTDatabase();

        # save configuration
        $this->ItemIdFieldName = $ItemIdFieldName;
        $this->ItemTableName = $ItemTableName;
        $this->SqlCondition($SqlCondition);
    }

    # insert/move item to before specified item
    function InsertBefore($SourceItemOrItemId, $TargetItemOrItemId)
    {   
        # retrieve item IDs 
        $SourceItemId = is_object($SourceItemOrItemId)
                ? $SourceItemOrItemId->Id() : $SourceItemOrItemId;
        $TargetItemId = is_object($TargetItemOrItemId)
                ? $TargetItemOrItemId->Id() : $TargetItemOrItemId;
                
        # remove source item from current position if necessary
        $this->Remove($SourceItemId);
    
        # update IDs to link in new item
        $CurrentTargetItemPreviousId = $this->GetPreviousItemId($TargetItemId);
        $this->SetPreviousItemId($TargetItemId, $SourceItemId);
        if ($CurrentTargetItemPreviousId != -1)
        {
            $this->SetNextItemId($CurrentTargetItemPreviousId, $SourceItemId);
        }
        $this->SetPreviousAndNextItemIds($SourceItemId,
                $CurrentTargetItemPreviousId, $TargetItemId);
    }

    # insert/move item to after specified item
    function InsertAfter($SourceItemOrItemId, $TargetItemOrItemId)
    {
        # retrieve item IDs 
        $SourceItemId = is_object($SourceItemOrItemId)
                ? $SourceItemOrItemId->Id() : $SourceItemOrItemId;
        $TargetItemId = is_object($TargetItemOrTargetItemId)
                ? $TargetItemOrTargetItemId->Id() : $TargetItemOrTargetItemId;

        # remove source item from current position if necessary
        $this->Remove($SourceItemId);

        # update IDs to link in new item
        $CurrentTargetItemNextId = $this->GetNextItemIdInOrder($TargetItemId);
        $this->SetNextItemId($TargetItemId, $SourceItemId);
        if ($CurrentTargetItemNextId != -1)
        {
            $this->SetPreviousItemId($CurrentTargetItemNextId, $SourceItemId);
        }
        $this->SetPreviousAndNextItemIds($SourceItemId,
                $TargetItemId, $CurrentTargetItemNextId);
    }

    # add/move item to beginning of list
    function Prepend($ItemOrItemId)
    {
        # get item ID
        $ItemId = is_object($ItemOrItemId) ? $ItemOrItemId->Id() : $ItemOrItemId;

        # remove new item from current position if necessary
        $this->Remove($ItemId);

        # if there are items currently in list
        $ItemIds = $this->GetIds(FALSE);
        if (count($ItemIds))
        {
            # link last item to source item
            $FirstItemId = array_shift($ItemIds);
            $this->SetPreviousItemId($FirstItemId, $ItemId);
            $this->SetPreviousAndNextItemIds($ItemId, -1, $FirstItemId);
        }
        else
        {
            # add item to list as only item
            $this->SetPreviousAndNextItemIds($ItemId, -1, -1);
        }
    }

    # add/move item to end of list
    function Append($ItemOrItemId)
    {
        # get item ID
        $ItemId = is_object($ItemOrItemId) ? $ItemOrItemId->Id() : $ItemOrItemId;

        # remove item from current position if necessary
        $this->Remove($ItemId);

        # if there are items currently in list
        $ItemIds = $this->GetIds(FALSE);
        if (count($ItemIds))
        {
            # link last item to source item
            $LastItemId = array_pop($ItemIds);
            $this->SetNextItemId($LastItemId, $ItemId);
            $this->SetPreviousAndNextItemIds($ItemId, $LastItemId, -1);
        }
        else
        {
            # add item to list as only item
            $this->SetPreviousAndNextItemIds($ItemId, -1, -1);
        }
    }

    # retrieve list of item IDs in order
    function GetIds($AddStrayItemsToOrder = TRUE)
    {
        # retrieve ordering IDs
        $this->DB->Query("SELECT ".$this->ItemIdFieldName
                .", Previous".$this->ItemIdFieldName
                .", Next".$this->ItemIdFieldName
                ." FROM ".$this->ItemTableName
                .$this->GetCondition(TRUE)
                ." ORDER BY ".$this->ItemIdFieldName." ASC");
        $PreviousItemIds = array();
        $NextItemIds = array();
        while ($Record = $this->DB->FetchRow())
        {
            $ItemId = intval($Record[$this->ItemIdFieldName]);
            $PreviousItemIds[$ItemId] = 
                    intval($Record["Previous".$this->ItemIdFieldName]);
            $NextItemIds[$ItemId] = intval($Record["Next".$this->ItemIdFieldName]);
        }

        # pull unordered items out of list
        $StrayItemIds = array_keys($PreviousItemIds, -2);
        foreach ($StrayItemIds as $StrayItemId)
        {
            unset($PreviousItemIds[$StrayItemId]);
            unset($NextItemIds[$StrayItemId]);
        }

        # find first item
        $ItemId = array_search(-1, $PreviousItemIds);

        # if first item was found
        $ItemIds = array();
        if ($ItemId !== FALSE)
        {
            # traverse linked list to build list of item IDs
            do
            {
                $ItemIds[] = $ItemId;
                unset($PreviousItemIds[$ItemId]);
                if (isset($NextItemIds[$ItemId])) {  $ItemId = $NextItemIds[$ItemId];  }
            }
            while (isset($NextItemIds[$ItemId]) && ($ItemId != -1)
                    && !in_array($ItemId, $ItemIds));

            # add any items left over to stray items list
            $StrayItemIds = array_unique($StrayItemIds + array_keys($PreviousItemIds));
        }

        # add any stray items to end of list (if so configured)
        if ($AddStrayItemsToOrder)
        {
            foreach ($StrayItemIds as $StrayItemId)
            {
                $this->Append($StrayItemId);
                $ItemIds[] = $StrayItemId;
            }
        }

        # return list of item IDs to caller
        return $ItemIds;
    }

    # remove item from existing order
    function Remove($ItemId)
    {
        $CurrentItemPreviousId = $this->GetPreviousItemId($ItemId);
        $CurrentItemNextId = $this->GetNextItemIdInOrder($ItemId);
        if ($CurrentItemPreviousId >= 0)
        {
            $this->SetNextItemId(
                    $CurrentItemPreviousId, $CurrentItemNextId);
        }
        if ($CurrentItemNextId >= 0)
        {
            $this->SetPreviousItemId(
                    $CurrentItemNextId, $CurrentItemPreviousId);
        }
    }

    # set SQL condition for ordering operations
    # (use NULL to clear condition)
    function SqlCondition($NewCondition)
    {
        $this->Condition = $NewCondition;
    }


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

    var $DB;
    var $ItemIdFieldName;
    var $ItemTableName;
    var $Condition;

    # get/set ordering values
    function GetPreviousItemId($ItemId)
    {
        return $this->DB->Query("SELECT Previous".$this->ItemIdFieldName
                    ." FROM ".$this->ItemTableName
                    ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId)
                    .$this->GetCondition(),
                "Previous".$this->ItemIdFieldName);
    }
    function GetNextItemIdInOrder($ItemId)
    {
        return $this->DB->Query("SELECT Next".$this->ItemIdFieldName
                    ." FROM ".$this->ItemTableName
                    ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId)
                    .$this->GetCondition(),
                "Next".$this->ItemIdFieldName);
    }
    function SetPreviousItemId($ItemId, $NewValue)
    {
        $this->DB->Query("UPDATE ".$this->ItemTableName
                ." SET Previous".$this->ItemIdFieldName." = ".intval($NewValue)
                ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId)
                .$this->GetCondition());
    }
    function SetNextItemId($ItemId, $NewValue)
    {
        $this->DB->Query("UPDATE ".$this->ItemTableName
                ." SET Next".$this->ItemIdFieldName." = ".intval($NewValue)
                ." WHERE ".$this->ItemIdFieldName." = ".intval($ItemId)
                .$this->GetCondition());
    }
    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)
                .$this->GetCondition());
    }

    # return DB query condition (if any) with proper additional syntax
    function GetCondition($ThisIsOnlyCondition = FALSE)
    {
        return $this->Condition ? 
                ($ThisIsOnlyCondition ? " WHERE " : " AND ").$this->Condition 
                : "";
    }
}


?>
