<?PHP
#
#   FILE:  Classification.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2002-2016 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Metadata type representing hierarchical ("Tree") controlled vocabulary values.
*/
class Classification extends Item
{
    # ---- PUBLIC INTERFACE --------------------------------------------------

    /** Parent value for classifications with no parent. */
    const NOPARENT = -1;

    /**
    * Add new classification to the hierarchy.
    * @param string $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.
    * @param int $FieldId MetadataField ID for new Classification.
    * @param int $ParentId ID of parent in hierachy of for new Classification.
    *       Use Classification::NOPARENT for new Classification with no parent
    *       (i.e. at top level of hierarchy).  (OPTIONAL - only required if
    *       full classification name is not supplied)
    */
    public static function Create($Name, $FieldId, $ParentId = NULL)
    {
        static $IdCache;

        # initialize state for creation
        self::$SegmentsCreated = 0;

        # if parent class supplied
        $DB = new Database();
        if ($ParentId !== NULL)
        {
            # error out if parent ID is invalid
            if ($ParentId != self::NOPARENT)
            {
                if ($DB->Query("SELECT COUNT(*) AS NumberFound"
                        ." FROM Classifications"
                        ." WHERE ClassificationId = ".intval($ParentId),
                        "NumberFound") < 1)
                {
                    throw new InvalidArgumentException("Invalid parent ID"
                            ." specified (".$ParentId.").");
                }
            }

            # error out if name already exists
            $Name = trim($Name);
            $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)
            {
                throw new Exception("Duplicate name specified for"
                        ." new classification (".$Name.").");
            }

            # determine full name and depth for new classification
            if ($ParentId == self::NOPARENT)
            {
                $NewName = $Name;
                $NewDepth = 0;
            }
            else
            {
                $DB->Query("SELECT ClassificationName, Depth"
                        ." FROM Classifications"
                        ." WHERE ClassificationId = ".intval($ParentId));
                $ParentInfo = $DB->FetchRow();
                $NewName = $ParentInfo["ClassificationName"]." -- ".$Name;
                $NewDepth = $ParentInfo["Depth"] + 1;
            }

            # add classification to database
            $InitialValues = array(
                    "FieldId" => $FieldId,
                    "ParentId" => $ParentId,
                    "SegmentName" => $Name,
                    "ResourceCount" => 0,
                    "Depth" => $NewDepth,
                    "ClassificationName" => $NewName);
            $NewItem = parent::CreateWithValues($InitialValues);
        }
        else
        {
            # parse classification name into separate segments
            $Segments = preg_split("/--/", $Name);

            # start out with top as parent
            $ParentId = self::NOPARENT;

            # 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 (self::$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 == self::NOPARENT)
                        {
                            $IdCache[$FieldId][$ParentId][$Segment] = $DB->Query(
                                    "SELECT ClassificationId FROM Classifications"
                                    ." WHERE ParentId = ".self::NOPARENT
                                    ." 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
                    $InitialValues = array(
                            "FieldId" => $FieldId,
                            "ParentId" => $ParentId,
                            "SegmentName" => $Segment,
                            "ResourceCount" => 0,
                            "Depth" => $CurrentDepth,
                            "ClassificationName" => $CurrentFullName);
                    $NewItem = parent::CreateWithValues($InitialValues);
                    $ClassId = $NewItem->Id();
                    $IdCache[$FieldId][$ParentId][$Segment] = $ClassId;

                    # track total number of new classification segments created
                    self::$SegmentsCreated++;
                }

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

        # return new classification to caller
        return new self($NewItem->Id());
    }

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

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

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

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

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

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

    /**
    * Get number of all resources (minus temporary ones) having this
    * classification assigned to them. This is only updated by
    * RecalcResourceCount() and Delete().
    * @return Count of all resources having this classification.
    */
    public function FullResourceCount()
    {
        return $this->ValueCache["FullResourceCount"];
    }

    /**
    * Get number of new segments (Classifications) generated when creating
    * a new Classification with a full name.
    */
    public static function SegmentsCreated()
    {
        return self::$SegmentsCreated;
    }

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

    /**
    * Get or set the segment name.
    * @param string $NewValue New segment name.  (OPTIONAL)
    * @return Segment name.
    */
    public function SegmentName($NewValue = DB_NOVALUE)
    {
        return $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 string $NewValue New link string.
    * @return Current link string.
    */
    public function LinkString($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("LinkString", $NewValue);
    }

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

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

    /**
    * Get or set the Qualifier associated with the Classification.
    * @param Qualifier $NewValue New Qualifier.
    * @return Associated Qualifier (object) or NULL if no qualifier.
    * @see Classification::QualifierId()
    */
    public 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.
    */
    public function RecalcDepthAndFullName()
    {
        # start with full classification name set to our segment name
        $FullClassName = $this->ValueCache["SegmentName"];

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

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

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

            # increment depth value
            $Depth++;

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

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

        # save new depth and full classification name
        $this->UpdateValue("Depth", $Depth);
        $this->UpdateValue("ClassificationName", $FullClassName);
    }

    /**
    * Update the LastAssigned timestamp for this classification.
    */
    public function UpdateLastAssigned()
    {
        $this->DB->Query("UPDATE Classifications SET LastAssigned=NOW() "
                         ."WHERE ClassificationId=".intval($this->Id));
    }

    /**
    * 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 array $IdsToSkip Classification IDs to skip during recalculation.  (OPTIONAL)
    * @return Array of IDs of the Classifications that were updated.
    */
    public 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 Resources.ResourceId > 0"
                    ." 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
            $this->UpdateValue("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->ValueCache["ParentId"] != self::NOPARENT)
            && (!$IdsToSkip || !in_array($this->ValueCache["ParentId"], $IdsToSkip)) )
        {
            $Class = new Classification($this->ValueCache["ParentId"]);
            $IdsUpdated = array_merge($IdsUpdated, $Class->RecalcResourceCount());
        }

        # retrieve new count of all resources directly associated with class
        $FullCount = $this->DB->Query("SELECT COUNT(*) AS ResourceCount"
                        ." FROM ResourceClassInts I, Resources R"
                        ." WHERE I.ClassificationId = ".intval($this->Id)
                        ." AND R.ResourceId > 0"
                        ." AND I.ResourceId = R.ResourceId",
                "ResourceCount");

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

        # save new full count
        $this->UpdateValue("FullResourceCount", $ResourceCount);

        # 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.
    */
    public 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.
    */
    public 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 array_unique($ChildList);
    }

    /**
    * Remove Classification (and accompanying associations) from database.
    * @param bool $DeleteParents Flag indicating whether to also delete
    *      Classification entries above this one in the hierarchy.  (OPTIONAL
    *      - defaults to FALSE)
    * @param bool $DeleteIfHasResources Flag indicating whether to delete the
    *      Classification if it still has Resources associated with it.
    *      (OPTIONAL - defaults to FALSE)
    * @param bool $DeleteIfHasChildren Flag indicating whether to delete the
    *      Classification if others have it as a parent.  (OPTIONAL - defaults
    *      to FALSE)
    * @return int Number of classifications deleted.
    */
    public function Delete($DeleteParents = FALSE,
            $DeleteIfHasResources = FALSE, $DeleteIfHasChildren = FALSE)
    {
        # if no resources or okay to delete with resources
        #         and no children or okay to delete with children
        $DeleteCount = 0;
        if (($DeleteIfHasResources || ($this->ResourceCount() == 0))
                && ($DeleteIfHasChildren || ($this->ChildCount() == 0)))
        {
            if ($this->ResourceCount() != 0)
            {
                $this->DB->Query("DELETE FROM ResourceClassInts"
                        ." WHERE ClassificationId = ".intval($this->Id));
                $this->RecalcResourceCount();
            }

            # delete this classification
            parent::Delete();
            $DeleteCount++;

            # delete parent classification (if requested)
            $ParentId = $this->ValueCache["ParentId"];
            if (($DeleteParents) && ($ParentId != self::NOPARENT))
            {
                $Parent = new Classification($ParentId);
                $DeleteCount += $Parent->Delete(
                        TRUE, $DeleteIfHasResources, $DeleteIfHasChildren);
            }
        }

        # return total number of classifications deleted to caller
        return $DeleteCount;
    }


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

    static private $SegmentsCreated;
}
