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

/**
* Factory for Resource objects.
*/
class ResourceFactory extends ItemFactory {

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

    /**
    * Class constructor.
    * @param int $SchemaId ID of schema to load resources for.  (OPTIONAL,
    *       defaults to SCHEMAID_DEFAULT)
    */
    function ResourceFactory($SchemaId = MetadataSchema::SCHEMAID_DEFAULT)
    {
        # save schema ID
        $this->SchemaId = $SchemaId;

        # set up item factory base class
        $this->ItemFactory("Resource", "Resources", "ResourceId", NULL, FALSE,
                "SchemaId = ".intval($this->SchemaId));
    }

    /**
    * Duplicate the specified resource and return to caller.
    * @param int $ResourceId ID of resource to duplicate.
    * @return object New Resource object.
    */
    function DuplicateResource($ResourceId)
    {
        # create new target resource
        $DstResource = Resource::Create($this->SchemaId);

        # load up resource to duplicate
        $SrcResource = new Resource($ResourceId);

        # if resource to duplicate was found
        if ($SrcResource->Status() > 0)
        {
            # for each metadata field
            $Schema = new MetadataSchema($this->SchemaId);
            $Fields = $Schema->GetFields();
            foreach ($Fields as $Field)
            {
                # skip the Cumulative Rating and Date Last Modified fields
                if ($Field->Name() != "Cumulative Rating" &&
                    $Field->Name() != "Date Last Modified")
                {
                    $NewValue = $SrcResource->GetByField($Field, TRUE);

                    # clear default value from destination resource that is
                    # set when creating a new resource
                    $DstResource->ClearByField($Field);

                    # copy value from source resource to destination resource
                    $DstResource->SetByField($Field, $NewValue);
                }
            }
        }

        # return new resource to caller
        return $DstResource;
    }

    /**
    * Clear or change specific qualifier for all resources.
    * @param mixed Qualifier ID or object to clear or change.
    * @param mixed New Qualifier ID or object.  (OPTIONAL, defaults
    *       to NULL, which will clear old qualifier)
    */
    function ClearQualifier($ObjectOrId, $NewObjectOrId = NULL)
    {
        # sanitize qualifier ID or retrieve from object
        $QualifierId = is_object($ObjectOrId)
                ?  $ObjectOrId->Id() : intval($ObjectOrId);

        # if new qualifier passed in
        if ($NewObjectOrId !== NULL)
        {
            # sanitize qualifier ID to change to or retrieve it from object
            $NewQualifierIdVal = is_object($NewObjectOrId)
                    ?  $NewObjectOrId->Id() : intval($NewObjectOrId);
        }
        else
        {
            # qualifier should be cleared
            $NewQualifierIdVal = "NULL";
        }

        # for each metadata field
        $Schema = new MetadataSchema($this->SchemaId);
        $Fields = $Schema->GetFields();
        foreach ($Fields as $Field)
        {
            # if field uses qualifiers and uses item-level qualifiers
            $QualColName = $Field->DBFieldName()."Qualifier";
            if ($Field->UsesQualifiers()
                && $Field->HasItemLevelQualifiers()
                && $this->DB->FieldExists("Resources", $QualColName))
            {
                # set all occurrences to new qualifier value
                $this->DB->Query("UPDATE Resources"
                           ." SET ".$QualColName." = ".$NewQualifierIdVal.""
                           ." WHERE ".$QualColName." = '".$QualifierId."'"
                           ." AND SchemaId = ".intval($this->SchemaId));
            }
        }

        # clear or change qualifier association with controlled names
        # (NOTE: this should probably be done in a controlled name factory object)
        $this->DB->Query("UPDATE ControlledNames"
                   ." SET QualifierId = ".$NewQualifierIdVal
                   ." WHERE QualifierId = '".$QualifierId."'");

        # clear or change qualifier association with classifications
        # (NOTE: this should probably be done in a classification factory object)
        $this->DB->Query("UPDATE Classifications"
                   ." SET QualifierId = ".$NewQualifierIdVal
                   ." WHERE QualifierId = '".$QualifierId."'");
    }

    /**
    * Return number of resources that have ratings.
    * @return int Resource count.
    */
    function GetRatedResourceCount()
    {
        return $this->DB->Query(
                "SELECT COUNT(DISTINCT ResourceId) AS ResourceCount"
                        ." FROM ResourceRatings",
                "ResourceCount");
    }

    /**
    * Return number of users who have rated resources.
    * @return int User count.
    */
    function GetRatedResourceUserCount()
    {
        return $this->DB->Query(
                "SELECT COUNT(DISTINCT UserId) AS UserCount"
                        ." FROM ResourceRatings",
                "UserCount");
    }

    /**
    * Get resources sorted by descending Date of Record Release, with Date of
    * Record Creation as the secondary sort criteria..
    * @param int $Count Maximum number of resources to return.
    * @param int $Offset Starting offset of segment to return (0=beginning).
    * @param int $MaxDaysToGoBack Maximum number of days to go back for
    *       resources, according to Date of Record Release.
    * @return array Array of Resource objects.
    */
    function GetRecentlyReleasedResources($Count = 10, $Offset = 0, $MaxDaysToGoBack = 90)
    {
        # assume that no resources will be found
        $Resources = array();

        # calculate cutoff date for resources
        $CutoffDate = date("Y-m-d H:i:s", strtotime($MaxDaysToGoBack." days ago"));

        # query for resource IDs
        $this->DB->Query("SELECT ResourceId FROM Resources WHERE"
                ." DateOfRecordRelease > '".$CutoffDate."'"
                ." AND ReleaseFlag = 1"
                ." AND ResourceId >= 0"
                ." AND SchemaId = ".intval($this->SchemaId)
                ." ORDER BY DateOfRecordRelease DESC, DateOfRecordCreation DESC"
                ." LIMIT ".intval($Offset).", ".intval($Count));
        $ResourceIds = $this->DB->FetchColumn("ResourceId");

        # for each resource ID found
        foreach ($ResourceIds as $ResourceId)
        {
            # load resource and add to list of found resources
            $Resources[$ResourceId] = new Resource($ResourceId);
        }

        # return found resources to caller
        return $Resources;
    }

    /**
    * Get resource IDs sorted by specified field.  Only IDs for resources
    * with non-empty non-null values for the specified field are returned.
    * @param string $FieldName Name of field.
    * @param bool $Ascending If TRUE, sort is ascending, otherwise sort is descending.
    * @param int $Limit Number of IDs to retrieve.  (OPTIONAL)
    * @return array Resource IDs.
    */
    function GetResourceIdsSortedBy($FieldName, $Ascending = TRUE, $Limit = NULL)
    {
        # assume no resources will be found
        $ResourceIds = array();

        # get field
        $Schema = new MetadataSchema($this->SchemaId);
        $Field = $Schema->GetFieldByName($FieldName);

        # if field was found
        if ($Field != NULL)
        {
            # construct query based on field type
            switch ($Field->Type())
            {
                case MetadataSchema::MDFTYPE_TEXT:
                case MetadataSchema::MDFTYPE_PARAGRAPH:
                case MetadataSchema::MDFTYPE_URL:
                    $Count = $this->DB->Query("SELECT COUNT(*) AS ResourceCount"
                            ." FROM Resources WHERE "
                            .$Field->DBFieldName()." IS NOT NULL"
                            ." AND LENGTH(LTRIM(RTRIM(".$Field->DBFieldName()."))) > 0"
                            ." AND SchemaId = ".intval($this->SchemaId),
                            "ResourceCount");
                    if ($Count > 1)
                    {
                        $Query = "SELECT ResourceId FROM Resources"
                                ." WHERE SchemaId = ".intval($this->SchemaId)
                                ." ORDER BY ".$Field->DBFieldName()
                                .($Ascending ? " ASC" : " DESC");
                    }
                    break;

                case MetadataSchema::MDFTYPE_NUMBER:
                case MetadataSchema::MDFTYPE_TIMESTAMP:
                    $Count = $this->DB->Query("SELECT COUNT(*) AS ResourceCount"
                            ." FROM Resources WHERE "
                            .$Field->DBFieldName()." IS NOT NULL"
                            ." AND SchemaId = ".intval($this->SchemaId),
                            "ResourceCount");
                    if ($Count > 1)
                    {
                        $Query = "SELECT ResourceId FROM Resources"
                                ." WHERE SchemaId = ".intval($this->SchemaId)
                                ." ORDER BY ".$Field->DBFieldName()
                                .($Ascending ? " ASC" : " DESC");
                    }
                    break;

                case MetadataSchema::MDFTYPE_DATE:
                    $Count = $this->DB->Query("SELECT COUNT(*) AS ResourceCount"
                            ." FROM Resources WHERE "
                            .$Field->DBFieldName()."Begin IS NOT NULL"
                            ." AND SchemaId = ".intval($this->SchemaId),
                            "ResourceCount");
                    if ($Count > 1)
                    {
                        $Query = "SELECT ResourceId FROM Resources"
                                ." WHERE SchemaId = ".intval($this->SchemaId)
                                ." ORDER BY ".$Field->DBFieldName()."Begin"
                                .($Ascending ? " ASC" : " DESC");
                    }
                    break;
            }

            # if appropriate query was found
            if (isset($Query))
            {
                # if limited number of results were requested
                if ($Limit !== NULL)
                {
                    # add limit to query
                    $Query .= " LIMIT ".intval($Limit);
                }

                # perform query and retrieve resource IDs
                $this->DB->Query($Query);
                $ResourceIds = $this->DB->FetchColumn("ResourceId");
            }
        }

        # return resource IDs to caller
        return $ResourceIds;
    }

    /**
    * Get date/time of when last a resource was modified.
    * @param bool $OnlyReleasedResources If TRUE, only resources with the
    *       Release Flag set are considered.
    * @return int Unix timestamp or NULL if no last modification time found.
    */
    function GetTimestampOfLastResourceModification($OnlyReleasedResources = TRUE)
    {
        $LastChangeDate = $this->DB->Query(
                "SELECT MAX(DateLastModified) AS LastChangeDate"
                        ." FROM Resources"
                        ." WHERE SchemaId = ".intval($this->SchemaId)
                        .($OnlyReleasedResources ? " AND ReleaseFlag = 1" : ""),
                "LastChangeDate");
        return ($LastChangeDate ? strtotime($LastChangeDate) : NULL);
    }

    /**
    * Get possible field names for resources.
    * @return array Array of metadata field names with metadata field IDs
    *       for the index.
    */
    function GetPossibleFieldNames()
    {
        # retrieve field names from schema
        $FieldNames = array();
        $Schema = new MetadataSchema($this->SchemaId);
        $Fields = $Schema->GetFields();
        foreach ($Fields as $Field)
        {
            $FieldNames[$Field->Id()] = $Field->Name();
        }

        # return field names to caller
        return $FieldNames;
    }

    /**
    * Find resources with values that match those specified.  (Only works
    * for Text, Paragraph, Number, Timestamp, Date, Flag, and Url fields.)
    * @param array $ValuesToMatch Array with metadata field IDs for the index
    *       and string values to search for for the values.
    * @return Array of Resource objects with resource IDs for index.
    */
    function GetMatchingResources($ValuesToMatch)
    {
        # start out assuming we won't find any resources
        $Resources = array();

        # for each value
        $Schema = new MetadataSchema($this->SchemaId);
        $Fields = $Schema->GetFields(
                MetadataSchema::MDFTYPE_TEXT |
                MetadataSchema::MDFTYPE_PARAGRAPH |
                MetadataSchema::MDFTYPE_NUMBER |
                MetadataSchema::MDFTYPE_DATE |
                MetadataSchema::MDFTYPE_TIMESTAMP |
                MetadataSchema::MDFTYPE_FLAG |
                MetadataSchema::MDFTYPE_URL);
        $LinkingTerm = "";
        $Condition = "";
        foreach ($ValuesToMatch as $FieldId => $Value)
        {
            # if field can be used for comparison
            if (isset($Fields[$FieldId]))
            {
                # add comparison to condition
                $Condition .= $LinkingTerm.$Fields[$FieldId]->DBFieldName()
                        ." = '".addslashes($Value)."'";
                $LinkingTerm = " AND ";
            }
        }

        # if there were valid conditions
        if (strlen($Condition))
        {
            # build query statment
            $Query = "SELECT ResourceId FROM Resources WHERE ".$Condition
                    ." AND SchemaId = ".intval($this->SchemaId);

            # execute query to retrieve matching resource IDs
            $this->DB->Query($Query);
            $ResourceIds = $this->DB->FetchColumn("ResourceId");

            # retrieve resource objects
            foreach ($ResourceIds as $Id)
            {
                $Resources[$Id] = new Resource($Id);
            }
        }

        # return any resources found to caller
        return $Resources;
    }

    # Functions for keeping per-field resource counts updated:
    /**
    * Return the number of resources having a given value set for a
    * specified ControlledName field, restricted to a "CountType"
    * (e.g., All vs Released resources).
    * @param int $FieldId Desired field ID number
    * @param string $Value Field value to look for
    * @param string $CountType (default All) specifies which resources
    * should be counted.  By default, options are "All" and
    * "Released", but this can be extended in subclasses.
    * @return int the number of associated resources.
    */
    function GetResourceCount($FieldId, $Value, $CountType="All")
    {
        if ($FieldId<0)
            return NULL;

        $Schema = new MetadataSchema($this->SchemaId);
        $Field = $Schema->GetField($FieldId);
        if ($Field === NULL)
            return NULL;

        if ($this->ResourceCount === NULL)
        {
            $this->DB->Query(
                "SELECT FieldId, ClassName, CountType, Count FROM ResourceCounts");

            while ($Row = $this->DB->FetchRow())
            {
                $R_FieldId = $Row["FieldId"];
                $R_ClassName = $Row["ClassName"];
                $R_CountType = $Row["CountType"];
                $R_Count = $Row["Count"];

                $this->ResourceCount[$R_FieldId][$R_ClassName][$R_CountType] = $R_Count;
            }
        }

        if ($Field->Type() === MetadataSchema::MDFTYPE_OPTION
                || $Field->Type() === MetadataSchema::MDFTYPE_CONTROLLEDNAME)
        {
            return isset($this->ResourceCount[$FieldId][$Value][$CountType]) ?
                $this->ResourceCount[$FieldId][$Value][$CountType] :
                0 ;
        }
        else
        {
            return NULL;
        }
    }

    /**
    * Get the total number of released resources in the collection
    * @return int The total number of released resources.
    */
    public function GetReleasedResourceTotal()
    {
        return $this->DB->Query("
                SELECT COUNT(*) AS ResourceTotal
                FROM Resources
                WHERE ResourceId > 0
                AND ReleaseFlag = 1
                AND SchemaId = ".intval($this->SchemaId),
                "ResourceTotal");
    }

    /**
    * Get the total number of resources in the collection, even if they are
    * not released.
    * @return int The total number of resources.
    */
    public function GetResourceTotal()
    {
        return $this->DB->Query("
                SELECT COUNT(*) AS ResourceTotal
                FROM Resources
                WHERE ResourceId > 0
                AND SchemaId = ".intval($this->SchemaId),
                "ResourceTotal");
    }

    /**
    * Add a task to the queue which will update the resource counts
    * for ControlledNames.  We need to divest ourselves of large
    * instance variables before queueing the update, to avoid creating
    * gigantic rows in the Task Queue table.  This function should be called
    * after a resource is edited and when the search database is rebuilt.
    */
    function QueueResourceCountUpdate()
    {
        global $AF;

        # be sure that we're not a gigantic object when the task is queued
        $TmpResourceCount = $this->ResourceCount;
        $this->ResourceCount = NULL;

        $AF->QueueUniqueTask(
            array($this,"UpdateResourceCountCallback"), array());
        $this->ResourceCount = $TmpResourceCount;
    }

    /**
    * Update the stored counts of resources per controlled name,
    * looking at the private var $ResourceCountConditions to determine
    * what differnt kinds of counts should be available (by default,
    * "All" and "Released"), but this can be extended in a subclass.
    */
    function UpdateResourceCountCallback()
    {
        $DB = new Database();

        $DB->Query(
            "CREATE TABLE ResourceCountsNew (FieldId INT, ClassName TEXT, CountType TEXT, Count INT);");

        $Start = microtime(TRUE);

        foreach ($this->ResourceCountConditions as $CountType => $CountCondition)
        {
            $DB->Query(
                "INSERT INTO ResourceCountsNew "
                ."SELECT FieldId, ControlledName AS ClassName,"
                ."'".$CountType."' AS CountType, Count(ResourceId) AS Count "
                ."FROM (SELECT * FROM ResourceNameInts WHERE ResourceId IN "
                        ."(SELECT ResourceId FROM Resources "
                                ." WHERE SchemaId = ".intval($this->SchemaId)
                                .(($CountCondition!==NULL)
                                        ?" AND ".$CountCondition:"").")) AS T0 "
                ."JOIN ControlledNames USING(ControlledNameId) GROUP BY ControlledNameId;" );
        }

        $Stop = microtime(TRUE);

        $DB->Query(
            "INSERT INTO ResourceCountsNew VALUES (-1, '__LAST_UPDATED__', '', UNIX_TIMESTAMP()); ");
        $DB->Query(
            "INSERT INTO ResourceCountsNew VALUES (-2, '__UPDATE_RUNTIME__','',".($Stop-$Start).");");
        $DB->Query(
            "RENAME TABLE ResourceCounts TO ResourceCountsOld, ResourceCountsNew TO ResourceCounts; ");
        $DB->Query(
            "DROP TABLE ResourceCountsOld; ");
    }

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

    private $ResourceCount = NULL;
    private $ResourceCountConditions = array("All" => NULL, "Released" => "ReleaseFlag=1");
    private $SchemaId;
}
