<?PHP

#
#   FILE:  Classification.php
#   AUTHOR:  Edward Almasy
#
#   Part of the Scout Portal Toolkit
#   Copyright 2002-2003 Internet Scout Project
#   http://scout.wisc.edu
#

/**
* Metadata type representing hierarchical ("Tree") controlled vocabulary values.
*/
class Classification {

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

    # error status codes
    const CLASSSTAT_OK = 0;
    const CLASSSTAT_INVALIDID = 1;
    const CLASSSTAT_INVALIDPARENTID = 2;
    const CLASSSTAT_DUPLICATENAME = 3;

    /**
    * Class constructor.  This can be used both to access an existing
    * classification or to add a new classification to a hierarchy.  For
    * existing classifications specify just the Classification ID.  For new
    * classifications, pass in NULL for the Classification ID, and specify
    * all of the other parameters.
    * @param ClassId ID of Classification.  Use NULL to create a new Classification.
    * @param Name Full name or segment name for new Classification.  Segment name can be used if a parent ID is also supplied, otherwise full name is assumed.  (OPTIONAL)
    * @param FieldId MetadataField ID for new Classification.  (OPTIONAL)
    * @param ParentId ID of parent in hierachy of for new Classification.  Use -1 for new Classification with no parent (i.e. at top level of hierarchy).  (OPTIONAL)
    */
    function Classification($ClassId, $Name = NULL, $FieldId = NULL, $ParentId = NULL)
    {
        static $IdCache;

        # assume everything will turn out okay
        $this->ErrorStatus = Classification::CLASSSTAT_OK;

        # create DB handle for our use
        $this->DB = new Database();
        $DB = $this->DB;

        # if class ID not given (indicating class must be created)
        if ($ClassId === NULL)
        {
            # if parent class supplied
            if ($ParentId !== NULL)
            {
                # if parent ID was invalid
                if (($ParentId != -1)
                    && ($DB->Query("SELECT COUNT(*) AS NumberFound"
                                   ." FROM Classifications"
                                   ." WHERE ClassificationId = ".intval($ParentId),
                                   "NumberFound") < 1))
                {
                    # set error code for bad parent ID
                    $this->ErrorStatus = Classification::CLASSSTAT_INVALIDPARENTID;
                }
                else
                {
                    # if name already exists
                    $Name = trim($Name);
                    if ($FieldId === NULL)
                    {
                        # If we know what field we're trying to add a classifcation for,
                        # Check just within that field
                        $Count = $DB->Query("SELECT COUNT(*) AS NumberFound FROM Classifications"
                                ." WHERE ParentId = ".intval($ParentId)
                                ." AND LOWER(SegmentName) = '"
                                        .addslashes(strtolower($Name))."'",
                                            "NumberFound");
                    }
                    else
                    {
                        # Otherwise, check all classifications for all fields
                        $Count = $DB->Query("SELECT COUNT(*) AS NumberFound FROM Classifications"
                                            ." WHERE ParentId = ".intval($ParentId)
                                            ." AND FieldId = ".intval($FieldId)
                                            ." AND LOWER(SegmentName) = '"
                                            .addslashes(strtolower($Name))."'",
                                            "NumberFound");
                    }

                    if ($Count > 0)
                    {
                        # set error code for duplicate class name
                        $this->ErrorStatus = Classification::CLASSSTAT_DUPLICATENAME;
                    }
                    else
                    {
                        # add class to database
                        $ParentId = intval($ParentId);
                        if ($ParentId == -1)
                        {
                            $NewName = $Name;
                            $NewDepth = 0;
                        }
                        else
                        {
                            $DB->Query("SELECT ClassificationName, Depth"
                                    ." FROM Classifications"
                                    ." WHERE ClassificationId = ".$ParentId);
                            $ParentInfo = $DB->FetchRow();
                            $NewName = $ParentInfo["ClassificationName"]." -- ".$Name;
                            $NewDepth = $ParentInfo["Depth"] + 1;
                        }
                        $DB->Query("INSERT INTO Classifications"
                                ." (FieldId, ParentId, SegmentName, ResourceCount,"
                                        ." Depth, ClassificationName) VALUES"
                                ." (".intval($FieldId).", ".$ParentId.","
                                        ." '".addslashes($Name)."', 0, "
                                        .$NewDepth.", '".addslashes($NewName)."')");

                        # retrieve ID of new class
                        $this->Id = $DB->LastInsertId("Classifications");
                    }
                }
            }
            else
            {
                # parse classification name into separate segments
                $Segments = preg_split("/--/", $Name);

                # start out with top as parent
                $ParentId = -1;

                # for each segment
                $CurrentDepth = -1;
                $CurrentFullName = "";
                foreach ($Segments as $Segment)
                {
                    # track segment depth and full classification name for use in adding new entries
                    $Segment = trim($Segment);
                    $CurrentDepth++;
                    $CurrentFullName .= (($CurrentFullName == "") ? "" : " -- ").$Segment;

                    # if we have added classifications
                    $Segment = addslashes($Segment);
                    if ($this->SegmentsCreated)
                    {
                        # we know that current segment will not be found
                        $ClassId = NULL;
                    }
                    else
                    {
                        # look up classification with current parent and segment name
                        if (!isset($IdCache[$FieldId][$ParentId][$Segment]))
                        {
                            if ($ParentId == -1)
                            {
                                $IdCache[$FieldId][$ParentId][$Segment] = $DB->Query(
                                        "SELECT ClassificationId FROM Classifications"
                                        ." WHERE ParentId = -1"
                                        ." AND SegmentName = '".addslashes($Segment)."'"
                                        ." AND FieldId = ".intval($FieldId),
                                        "ClassificationId");
                            }
                            else
                            {
                                $IdCache[$FieldId][$ParentId][$Segment] = $DB->Query(
                                        "SELECT ClassificationId FROM Classifications "
                                        ."WHERE ParentId = ".intval($ParentId)
                                        ." AND SegmentName = '".addslashes($Segment)."'",
                                        "ClassificationId");
                            }
                        }
                        $ClassId = $IdCache[$FieldId][$ParentId][$Segment];
                    }

                    # if classification not found
                    if ($ClassId === NULL)
                    {
                        # add new classification
                        $DB->Query("INSERT INTO Classifications "
                                 ."(FieldId, ParentId, SegmentName,"
                                        ." ClassificationName, Depth, ResourceCount) "
                                 ."VALUES (".intval($FieldId).", "
                                    .intval($ParentId).", "
                                    ."'".addslashes($Segment)."', "
                                    ."'".addslashes($CurrentFullName)."', "
                                    .intval($CurrentDepth).", 0)");
                        $ClassId = $DB->LastInsertId("Classifications");
                        $IdCache[$FieldId][$ParentId][$Segment] = $ClassId;

                        # track total number of new classification segments created
                        $this->SegmentsCreated++;
                    }

                    # set parent to created or found class
                    $PreviousParentId = $ParentId;
                    $ParentId = $ClassId;
                }

                # our class ID is the one that was last found
                $this->Id = $ClassId;
            }
        }
        else
        {
            # our class ID is the one that was supplied by caller
            $this->Id = intval($ClassId);
        }

        # if no error encountered
        if ($this->ErrorStatus == Classification::CLASSSTAT_OK)
        {
            # load in attributes from database
            $DB->Query("SELECT * FROM Classifications"
                       ." WHERE ClassificationId = ".intval($this->Id));
            $this->DBFields = $DB->NumRowsSelected()>0 ? $DB->FetchRow() : NULL ;

            # set error status if class info not loaded
            if ($this->DBFields === NULL ||
                $this->DBFields["ClassificationId"] != $this->Id)
            {
                $this->ErrorStatus = Classification::CLASSSTAT_INVALIDID;
            }
        }
    }

    /**
    * Returns success/failure code for last call (where applicable).
    * @return Status code.
    */
    function Status() {  return $this->ErrorStatus;  }

    /**
    * Get Classification ID.
    * @return Numerical Classification ID.
    */
    function Id()            {  return $this->Id;  }

    /**
    * Get full classification name (all segments).
    * @return Classification name.
    */
    function FullName()      {  return stripslashes($this->DBFields["ClassificationName"]);  }

    /**
    * Get name of classification segment.
    * @return Segment name.
    */
    function Name()          {  return $this->FullName();  }

    /**
    * Get variant name of classification, if any.
    * @return Variant name.
    */
    function VariantName()   {  return NULL;  }

    /**
    * Get depth of classification in hierarchy.  Top level is depth of 0.
    * @return Depth in hierarchy.
    */
    function Depth()         {  return $this->DBFields["Depth"];  }

    /**
    * Get number of Resources having this Classification assigned to them.
    * This is only updated by RecalcResourceCount() and Delete().
    * @return Count of resources having this Classification.
    */
    function ResourceCount() {  return $this->DBFields["ResourceCount"];  }

    /**
    * Get number of new segments (Classifications) generated when creating
    * a new Classification with a full name.
    */
    function SegmentsCreated() { return $this->SegmentsCreated; }

    /**
    * Get ID of parent Classification.  Returns -1 if no parent (i.e. Classification
    * is at top level of hierarchy).
    */
    function ParentId()      {  return $this->DBFields["ParentId"];  }

    /**
    * Get or set the segment name.
    * @param NewValue New segment name.  (OPTIONAL)
    * @return Segment name.
    */
    function SegmentName($NewValue = DB_NOVALUE) {
            return stripslashes($this->UpdateValue("SegmentName", $NewValue));  }

    /**
    * Get or set the stored link string for the Classification.  (This value is
    * not used, updated, or manipulated in any way by Classification, and is only
    * being stored as a UI optimization.)
    * @param NewValue New link string.
    * @return Current link string.
    */
    function LinkString($NewValue = DB_NOVALUE) {
            return stripslashes($this->UpdateValue("LinkString", $NewValue));  }

    /**
    * Get or set the Qualifier associated with the Classification by ID.
    * @param NewValue ID of new Qualifier.
    * @return ID of current Qualifier.
    * @see Qualifier()
    */
    function QualifierId($NewValue = DB_NOVALUE) {
            return $this->UpdateValue("QualifierId", $NewValue);  }

    /**
    * Get or set the ID of the MetadataField for the Classification.
    * @param NewValue ID of new MetadataField.
    * @return ID of currently associated MetadataField.
    */
    function FieldId($NewValue = DB_NOVALUE) {
            return $this->UpdateValue("FieldId", $NewValue);  }

    /**
    * Get or set the Qualifier associated with the Classification.
    * @param NewValue New Qualifier.
    * @return Associated Qualifier (object) or NULL if no qualifier.
    * @see QualifierId()
    */
    function Qualifier($NewValue = DB_NOVALUE)
    {
        # if new qualifier supplied
        if ($NewValue !== DB_NOVALUE)
        {
            # set new qualifier ID
            $this->QualifierId($NewValue->Id());

            # use new qualifier for return value
            $Qualifier = $NewValue;
        }
        else
        {
            # if qualifier is available
            if ($this->QualifierId() !== NULL)
            {
                # create qualifier object using stored ID
                $Qualifier = new Qualifier($this->QualifierId());
            }
            else
            {
                # return NULL to indicate no qualifier
                $Qualifier = NULL;
            }
        }

        # return qualifier to caller
        return $Qualifier;
    }

    /**
    * Rebuild classification full name and recalculate depth in hierarchy.
    * This is a DB-intensive and recursive function, and so should not be
    * called without some forethought.
    */
    function RecalcDepthAndFullName()
    {
        $DB = $this->DB;

        # start with full classification name set to our segment name
        $FullClassName = $this->DBFields["SegmentName"];

        # assume to begin with that we're at the top of the hierarchy
        $Depth = 0;

        # while parent available
        $ParentId = $this->DBFields["ParentId"];
        while ($ParentId != -1)
        {
            # retrieve classification information
            $DB->Query("SELECT SegmentName, ParentId "
                    ."FROM Classifications "
                    ."WHERE ClassificationId=".$ParentId);
            $Record = $DB->FetchRow();

            # prepend segment name to full classification name
            $FullClassName = stripslashes($Record["SegmentName"])
                    ." -- ".$FullClassName;

            # increment depth value
            $Depth++;

            # move to parent of current classification
            $ParentId = $Record["ParentId"];
        }

        # for each child
        $DB->Query("SELECT ClassificationId "
                ."FROM Classifications "
                ."WHERE ParentId=".intval($this->Id));
        while ($Record = $DB->FetchRow())
        {
            # perform depth and name recalc
            $Child = new Classification($Record["ClassificationId"]);
            $Child->RecalcDepthAndFullName();
        }

        # save new depth and full classification name
        $DB->Query("UPDATE Classifications SET "
                ."Depth=".intval($Depth).", "
                ."ClassificationName='".addslashes($FullClassName)."' "
                ."WHERE ClassificationId=".intval($this->Id));
        $this->DBFields["ClassificationName"] = $FullClassName;
        $this->DBFields["Depth"] = $Depth;
    }

    /**
    * Recalculate number of resources assigned to class and any parent classes.
    * This is a DB-intensive and recursive function, and so should not be
    * called without some forethought.
    * @param IdsToSkip Classification IDs to skip during recalculation.  (OPTIONAL)
    * @return Array of IDs of the Classifications that were updated.
    */
    function RecalcResourceCount($IdsToSkip = NULL)
    {
        $IdsUpdated = array();

        # if we don't have a skip list or we aren't in the skip list
        if (!$IdsToSkip || !in_array($this->Id, $IdsToSkip))
        {
            # retrieve new count of resources directly associated with class
            $this->DB->Query("SELECT COUNT(*) AS ResourceCount"
                    ." FROM ResourceClassInts, Resources"
                    ." WHERE ClassificationId=".intval($this->Id)
                    ." AND ResourceClassInts.ResourceId = Resources.ResourceId"
                    ." AND ReleaseFlag = 1");
            $Record = $this->DB->FetchRow();
            $ResourceCount = $Record["ResourceCount"];

            # add on resources associated with all children
            $ResourceCount += $this->DB->Query(
                    "SELECT SUM(ResourceCount) AS ResourceCountTotal "
                        ."FROM Classifications "
                        ."WHERE ParentId = ".intval($this->Id),
                    "ResourceCountTotal");

            # save new count to database
            $this->DB->Query("UPDATE Classifications SET "
                    ."ResourceCount=".$ResourceCount." "
                    ."WHERE ClassificationId=".intval($this->Id));

            # save new count to our local cache
            $this->DBFields["ResourceCount"] = $ResourceCount;

            # add our ID to list of IDs that have been recalculated
            $IdsUpdated[] = $this->Id;
        }

        # update resource count for our parent (if any)
        if (($this->DBFields["ParentId"] != -1)
            && (!$IdsToSkip || !in_array($this->DBFields["ParentId"], $IdsToSkip)) )
        {
            $Class = new Classification($this->DBFields["ParentId"]);
            if ($Class->Status() == Classification::CLASSSTAT_OK)
            {
                $IdsUpdated = array_merge($IdsUpdated, $Class->RecalcResourceCount());
            }
        }

        # return list of IDs of updated classifications to caller
        return $IdsUpdated;
    }

    /**
    * Get number of classifications that have this Classification as their direct parent.
    * @return Count of child Classifications.
    */
    function ChildCount()
    {
        # return count of classifications that have this one as parent
        return $this->DB->Query("SELECT COUNT(*) AS ClassCount "
                    ."FROM Classifications "
                    ."WHERE ParentId=".intval($this->Id),
                "ClassCount");
    }

    /**
    * Get list of IDs of Classifications that have this class as an "ancestor"
    * (parent, grandparent, great-grandparent, etc).
    * @return Array of child/grandchild/etc Classification IDs.
    */
    # this also returns grandchildren, great grandchildren, etc.
    function ChildList()
    {
        $ChildList = array();

        $this->DB->Query("SELECT ClassificationId  "
                    ."FROM Classifications "
                    ."WHERE ParentId=".intval($this->Id));

         while ($Entry = $this->DB->FetchRow())
         {
            $ChildList[] = $Entry["ClassificationId"];
            $Child = new Classification($Entry["ClassificationId"]);
            if($Child->ChildCount() > 0)
            {
                $GrandChildList = $Child->ChildList();
                $ChildList = array_merge($GrandChildList, $ChildList);
            }
         }
         return $ChildList;
    }

    /**
    * Remove Classification (and accompanying associations) from database.
    * @param DeleteParents Flag indicating whether to also delete Classification entries above this one in the hierarchy.  (OPTIONAL - defaults to FALSE)
    * @param DeleteIfHasResources Flag indicating whether to delete the Classification if it still has Resources associated with it.  (OPTIONAL - defaults to FALSE)
    * @param DeleteIfHasChildren Flag indicating whether to delete the Classification if others have it as a parent.  (OPTIONAL - defaults to FALSE)
    */
    function Delete($DeleteParents = FALSE,
            $DeleteIfHasResources = FALSE, $DeleteIfHasChildren = FALSE)
    {
        $DB = $this->DB;

        # if no resources or okay to delete with resources
        #         and no children or okay to delete with children
        if (($DeleteIfHasResources || ($this->ResourceCount() == 0))
                && ($DeleteIfHasChildren || ($this->ChildCount() == 0)))
        {
            $ParentId = $this->DBFields["ParentId"];

            if ($DeleteIfHasResources)
            {
                $DB->Query("DELETE FROM ResourceClassInts "
                        ."WHERE ClassificationId=".intval($this->Id));
                $this->RecalcResourceCount();
            }
            # delete this classification
            $DB->Query("DELETE FROM Classifications "
                    ."WHERE ClassificationId=".intval($this->Id));

            # delete parent classification (if requested)
            if (($DeleteParents) && ($this->DBFields["ParentId"] != -1))
            {
                $Parent = new Classification($this->DBFields["ParentId"]);
                $Parent->Delete(
                        TRUE, $DeleteIfHasResources, $DeleteIfHasChildren);
            }
        }
    }


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

    private $DB;
    private $DBFields;
    private $Id;
    private $ErrorStatus;
    private $SegmentsCreated;

    # convenience function to supply parameters to Database->UpdateValue()
    private function UpdateValue($FieldName, $NewValue)
    {
        return $this->DB->UpdateValue("Classifications", $FieldName, $NewValue,
                               "ClassificationId = ".intval($this->Id),
                               $this->DBFields, TRUE);
    }
}


?>
