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

class SavedSearch
{

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

    # search frequency mnemonics
    const SEARCHFREQ_NEVER =      0;
    const SEARCHFREQ_HOURLY =     1;
    const SEARCHFREQ_DAILY =      2;
    const SEARCHFREQ_WEEKLY =     3;
    const SEARCHFREQ_BIWEEKLY =   4;
    const SEARCHFREQ_MONTHLY =    5;
    const SEARCHFREQ_QUARTERLY =  6;
    const SEARCHFREQ_YEARLY =     7;

    # object constructor
    /**
    * Object constructor.
    * @param int|null $SearchId Saved search ID or NULL for new search
    * @param string|null $SearchName Updated search name (OPTIONAL)
    * @param int|null $UserId User who owns this search (OPTIONAL)
    * @param int|null $Frequency Search mailing frequency (OPTIONAL)
    * @param mixed $SearchParameters SearchParameterSet describing this search (OPTIONAL)
    */
    public function __construct($SearchId, $SearchName = NULL, $UserId = NULL,
            $Frequency = NULL, $SearchParameters = NULL)
    {
        # get our own database handle
        $this->DB = new Database();

        # if search ID was provided
        if ($SearchId !== NULL)
        {
            # save search ID
            $this->SearchId = intval($SearchId);

            # initialize our local copies of data
            $this->DB->Query(
                "SELECT * FROM SavedSearches "
                ."WHERE SearchId = ".$this->SearchId);

            if ($this->DB->NumRowsSelected() == 0)
            {
                throw new Exception ("Specified SearchId does not exist");
            }

            $this->Record = $this->DB->FetchRow();

            # get our Search Parameters
            $this->SearchParameters = new SearchParameterSet(
                $this->Record["SearchData"]);

            # remove now redundant 'Data' from our record
            unset ($this->Record["SearchData"]);

            # update search details where provided
            if ($SearchName)
            {
                $this->SearchName($SearchName);
            }

            if ($UserId)
            {
                $this->UserId($UserId);
            }

            if ($Frequency)
            {
                $this->Frequency($Frequency);
            }
        }
        else
        {
            # add new saved search to database
            $this->DB->Query("INSERT INTO SavedSearches"
                    ." (SearchName, UserId, Frequency) VALUES ("
                    ."'".addslashes($SearchName)."', "
                    .intval($UserId).", "
                    .intval($Frequency).")");

            # retrieve and save ID of new search locally
            $this->SearchId = $this->DB->LastInsertId();

            # save frequency and user ID locally
            $this->Record["SearchName"] = $SearchName;
            $this->Record["UserId"] = $UserId;
            $this->Record["Frequency"] = $Frequency;

            if (!is_null($SearchParameters) && is_array($SearchParameters))
            {
                $Params = new SearchParameterSet();
                $Params->SetFromLegacyArray($SearchParameters);
                $SearchParameters = $Params;
            }

            $EndUser = new CWUser($this->UserId());

            $RFactory = new ResourceFactory();

            # signal event to allow modification of search parameters
            $SignalResult = $GLOBALS["AF"]->SignalEvent(
            "EVENT_FIELDED_SEARCH", array(
                "SearchParameters" => $SearchParameters,
                "User" => $EndUser,
                "SavedSearch" => $this));
            $this->SearchParameters($SignalResult["SearchParameters"]);

            # perform search
            $SearchEngine = new SPTSearchEngine();
            $SearchResults = $SearchEngine->GroupedSearch(
                $SearchParameters, 0, PHP_INT_MAX);

            $NewItemIds = array_keys($SearchResults);

            #Only allow resources the user can view
            $NewItemIds = $RFactory->FilterNonViewableResources($NewItemIds, $EndUser);

            # if search results were found
            if (count($NewItemIds))
            {
                $this->SaveLastMatches($NewItemIds);
            }
        }
    }

    /**
    * Get/set search parameters from legacy array.  This function is
    * for backward compatibility only and should not be used in new
    * code.
    * @param array|null $NewSearchGroups Updated legacy array
    * @return current search params as legacy array
    * @deprecated
    */
    public function SearchGroups($NewSearchGroups = NULL)
    {
        if (!is_null($NewSearchGroups))
        {
            $Params = new SearchParameterSet();
            $Params->SetFromLegacyArray($NewSearchGroups);
            $this->SearchParameters($Params);
        }

        # return search parameters to caller
        return $this->SearchParameters()->GetAsLegacyArray();
    }

    /**
    * Get/set search parameters.
    * @param SearchParameterSet|null $NewParams Updated search parameters
    * @return current search parameters
    */
    public function SearchParameters($NewParams = NULL)
    {
        if (!is_null($NewParams))
        {
            if ($NewParams instanceof SearchParameterSet)
            {

                $Data = $NewParams->Data();

                $this->DB->Query(
                    "UPDATE SavedSearches SET SearchData = '". addslashes($Data)."'"
                    ." WHERE SearchId = ".$this->SearchId);

                $this->SearchParameters = new SearchParameterSet($Data);
            }
            else
            {
                throw new Exception("NewParams must be a SearchParameterSet");
            }
        }

        return clone $this->SearchParameters;
    }

    /**
    * Get/set name of search.
    * @param string $NewValue New name of search value.
    * @return Current name of search value.
    */
    public function SearchName($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("SearchName", $NewValue);
    }

    /**
    * Get ID of search.
    * @return Search ID.
    */
    public function Id()
    {
        return $this->SearchId;
    }

    /**
    * Get/set user ID.
    * @param int $NewValue New user ID value.
    * @return Current user ID value.
    */
    public function UserId($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("UserId", $NewValue);
    }

    /**
    * Get/set search frequency.
    * @param int $NewValue New search frequency value.
    * @return Current search frequency value.
    */
    public function Frequency($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("Frequency", $NewValue);
    }


    /**
    * Update date this search was last run.
    */
    public function UpdateDateLastRun()
    {
        $this->DB->Query(
            "UPDATE SavedSearches SET DateLastRun = NOW() "
                ."WHERE SearchId = ".$this->SearchId);
    }

    /**
    * Get/set the date this search was last run.
    * @param mixed $NewValue Updated value (OPTIONAL
    * @return current value
    */
    public function DateLastRun($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("DateLastRun", $NewValue);
    }

    /**
    * Save array of last matches.
    * @param array $ArrayofMatchingIds Matching Ids for a current search.
    */
    public function SaveLastMatches($ArrayofMatchingIds)
    {
        $NewValue = implode(",", $ArrayofMatchingIds);
        $this->UpdateValue("LastMatchingIds", $NewValue);
    }

    /**
    * Return array of most recently matched ResourceIds for a search
    * @return Array of Resource Ids for most recent run of a search
    */
    public function LastMatches()
    {
        return explode(",", $this->DB->Query(
            "SELECT LastMatchingIds FROM SavedSearches "
                ."WHERE SearchId = ".$this->SearchId, "LastMatchingIds"));
    }
    /**
    * Get search groups as URL parameters
    * (e.g. something like F2=madison&F4=american+history&G22=17-41).
    * @return String containing URL parameters (no leading "?").
    */
    public function GetSearchGroupsAsUrlParameters()
    {
        return self::TranslateSearchGroupsToUrlParameters($this->SearchGroups());
    }

    /**
    * Translate search group array into URL parameters
    * (e.g. something like F2=madison&F4=american+history&G22=17-41).
    * A search group array looks something like this:
    * @code
    * $SearchGroups = array(
    *       "MAIN" => array(
    *               "SearchStrings" => array(
    *                       "XXXKeywordXXX" => "some words for keyword search",
    *                       "Title" => "some words we are looking for in titles",
    *                       ),
    *               "Logic" => SearchEngine::LOGIC_AND,
    *               ),
    *       "23" => array(
    *               "SearchStrings" => array(
    *                       "Resource Type" => array(
    *                               "=Event",
    *                               "=Image",
    *                               ),
    *                       ),
    *               "Logic" => SearchEngine::LOGIC_OR,
    *               ),
    *       "25" => array(
    *               "SearchStrings" => array(
    *                       "Audience" => array(
    *                               "=Grades 10-12",
    *                               ),
    *                       ),
    *               "Logic" => SearchEngine::LOGIC_OR,
    *               ),
    *       );
    * @endcode
    * where "23" and "25" are the field IDs and "Resource Type" and "Audience"
    * are the corresponding field names.
    * @param array $SearchGroups Search group array.
    * @return String containing URL parameters (no leading "?").
    */
    public static function TranslateSearchGroupsToUrlParameters($SearchGroups)
    {
        # assume that no parameters will be found
        $UrlPortion = "";

        # for each group in parameters
        $Schema = new MetadataSchema();
        foreach ($SearchGroups as $GroupIndex => $Group)
        {
            # if group holds single parameters
            if ($GroupIndex == "MAIN")
            {
                # for each field within group
                foreach ($Group["SearchStrings"] as $FieldName => $Value)
                {
                    # add segment to URL for this field
                    if ($FieldName == "XXXKeywordXXX")
                    {
                        $FieldId = "K";
                    }
                    else
                    {
                        $Field = $Schema->GetFieldByName($FieldName);
                        $FieldId = $Field->Id();
                    }
                    if (is_array($Value))
                    {
                        $UrlPortion .= "&F".$FieldId."=";
                        $ValueString = "";
                        foreach ($Value as $SingleValue)
                        {
                            $ValueString .= $SingleValue." ";
                        }
                        $UrlPortion .= urlencode(trim($ValueString));
                    }
                    else
                    {
                        $UrlPortion .= "&F".$FieldId."=".urlencode($Value);
                    }
                }
            }
            else
            {
                # convert value based on field type
                $FieldId = ($GroupIndex[0] == "X")
                        ? substr($GroupIndex, 1)
                        : $GroupIndex;
                $Field = $Schema->GetField($FieldId);
                $FieldName = $Field->Name();
                $Values = self::TranslateValues($Field,
                        $Group["SearchStrings"][$FieldName],
                        "SearchGroup to Database");

                # add values to URL
                $FirstValue = TRUE;
                foreach ($Values as $Value)
                {
                    if ($FirstValue)
                    {
                        $FirstValue = FALSE;
                        $UrlPortion .= "&G".$FieldId."=".$Value;
                    }
                    else
                    {
                        $UrlPortion .= "-".$Value;
                    }
                }
            }
        }

        # trim off any leading "&"
        if (strlen($UrlPortion)) {  $UrlPortion = substr($UrlPortion, 1);  }

        # return URL portion to caller
        return $UrlPortion;
    }

    /**
    * Get search groups as an URL parameter array.
    * @return Array with strings like "F4" ("F" or "G" plus field ID) for the
    *       index and * "american+history" (search parameter) for the values.
    */
    public function GetSearchGroupsAsUrlParameterArray()
    {
        return self::TranslateSearchGroupsToUrlParameters($this->SearchGroups());
    }

    /**
     * Translate a search group array to an URL parameter array.
     * @param array $SearchGroups Search group array to translate.
     * @return Array with strings like "F4" ("F" or "G" plus field ID) for the
     *       index and * "american+history" (search parameter) for the values.
     */
    public static function TranslateSearchGroupsToUrlParameterArray($SearchGroups)
    {
        # assume that no parameters will be found
        $UrlPortion = array();

        # for each group in parameters
        $Schema = new MetadataSchema();
        foreach ($SearchGroups as $GroupIndex => $Group)
        {
            # if group holds single parameters
            if ($GroupIndex == "MAIN")
            {
                # for each field within group
                foreach ($Group["SearchStrings"] as $FieldName => $Value)
                {
                    # add segment to URL for this field
                    if ($FieldName == "XXXKeywordXXX")
                    {
                        $FieldId = "K";
                    }
                    else
                    {
                        $Field = $Schema->GetFieldByName($FieldName);
                        $FieldId = $Field->Id();
                    }
                    if (is_array($Value))
                    {
                        $ValueString = "";
                        foreach ($Value as $SingleValue)
                        {
                            $ValueString .= $SingleValue." ";
                        }

                        $UrlPortion["F".$FieldId] = urlencode(trim($ValueString));
                    }
                    else
                    {
                        $UrlPortion["F".$FieldId] = urlencode($Value);
                    }
                }
            }
            else
            {
                # convert value based on field type
                $FieldId = ($GroupIndex[0] == "X")
                        ? substr($GroupIndex, 1)
                        : $GroupIndex;
                $Field = $Schema->GetField($FieldId);
                $FieldName = $Field->Name();
                $Values = self::TranslateValues($Field,
                        $Group["SearchStrings"][$FieldName],
                        "SearchGroup to Database");
                $LeadChar = ($Group["Logic"] == SearchEngine::LOGIC_AND)
                        ? "H" : "G";

                # add values to URL
                $FirstValue = TRUE;
                foreach ($Values as $Value)
                {
                    if ($FirstValue)
                    {
                        $FirstValue = FALSE;
                        $UrlPortion[$LeadChar.$FieldId] = $Value;
                    }
                    else
                    {
                        $UrlPortion[$LeadChar.$FieldId] .= "-".$Value;
                    }
                }
            }
        }

        # return URL portion to caller
        return $UrlPortion;
    }

    /**
    * Translate URL parameters to legacy search group array.
    * @param array $GetVars Get variables (as from $_GET)
    * @return array Legacy search group array
    */
    public static function TranslateUrlParametersToSearchGroups($GetVars)
    {
        # if URL segment was passed in instead of GET var array
        if (is_string($GetVars))
        {
            $GetVars = ParseQueryString($GetVars);
        }

        # start with empty list of parameters
        $SearchGroups = array();

        $Schema = new MetadataSchema();
        $AllFields = $Schema->GetFields(NULL, NULL, TRUE);

        foreach ($AllFields as $Field)
        {
            $FieldId = $Field->Id();
            $FieldName = $Field->Name();

            # if URL included literal value for this field
            if (isset($GetVars["F".$FieldId]))
            {
                # retrieve value and add to search parameters
                $SearchGroups["MAIN"]["SearchStrings"][$FieldName] =
                        $GetVars["F".$FieldId];
            }

            # if URL included group value for this field
            if (isset($GetVars["G".$FieldId]))
            {
                # retrieve and parse out values
                $Values = explode("-", $GetVars["G".$FieldId]);

                # translate values
                $Values = self::TranslateValues($Field, $Values,
                        "Database to SearchGroup");

                # add values to searchgroups
                $SearchGroups[$FieldId]["SearchStrings"][$FieldName] = $Values;
            }

            # if URL included group value for this field
            if (isset($GetVars["H".$FieldId]))
            {
                # retrieve and parse out values
                $Values = explode("-", $GetVars["H".$FieldId]);

                # translate values
                $Values = self::TranslateValues($Field, $Values,
                        "Database to SearchGroup");

                # add values to searchgroups
                $SearchGroups["X".$FieldId]["SearchStrings"][$FieldName] = $Values;
            }
        }

        # if keyword pseudo-field was included in URL
        if (isset($GetVars["FK"]))
        {
            # retrieve value and add to search parameters
            $SearchGroups["MAIN"]["SearchStrings"]["XXXKeywordXXX"] = $GetVars["FK"];
        }

        # set search logic
        foreach ($SearchGroups as $GroupIndex => $Group)
        {
            $SearchGroups[$GroupIndex]["Logic"] = ($GroupIndex == "MAIN")
                    ? SearchEngine::LOGIC_AND
                    : (($GroupIndex[0] == "X")
                            ? SearchEngine::LOGIC_AND : SearchEngine::LOGIC_OR);
        }

        # return parameters to caller
        return $SearchGroups;
    }

    /**
     * Get multi-line string describing search criteria.
     * @param bool $IncludeHtml Whether to include HTML tags for formatting.  (OPTIONAL,
     *       defaults to TRUE)
     * @param bool $StartWithBreak Whether to start string with BR tag.  (OPTIONAL,
     *       defaults to TRUE)
     * @param int $TruncateLongWordsTo Number of characters to truncate long words to
     *       (use 0 for no truncation).  (OPTIONAL, defaults to 0)
     * @return String containing text describing search criteria.
     */
    public function GetSearchGroupsAsTextDescription(
            $IncludeHtml = TRUE, $StartWithBreak = TRUE, $TruncateLongWordsTo = 0)
    {
        return $this->SearchParameters->TextDescription(
            $IncludeHtml, $StartWithBreak, $TruncateLongWordsTo);
    }

    /**
     * Translate search group array into  multi-line string describing search criteria.
     * @param array $SearchGroups Search group array.
     * @param bool $IncludeHtml Whether to include HTML tags for formatting.  (OPTIONAL,
     *       defaults to TRUE)
     * @param bool $StartWithBreak Whether to start string with BR tag.  (OPTIONAL,
     *       defaults to TRUE)
     * @param int $TruncateLongWordsTo Number of characters to truncate long words to
     *       (use 0 for no truncation).  (OPTIONAL, defaults to 0)
     * @return String containing text describing search criteria.
     */
    public static function TranslateSearchGroupsToTextDescription($SearchGroups,
            $IncludeHtml = TRUE, $StartWithBreak = TRUE, $TruncateLongWordsTo = 0)
    {
        $Schema = new MetadataSchema();

        # start with empty description
        $Descrip = "";

        # set characters used to indicate literal strings
        $LiteralStart = $IncludeHtml ? "<i>" : "\"";
        $LiteralEnd = $IncludeHtml ? "</i>" : "\"";
        $LiteralBreak = $IncludeHtml ? "<br>\n" : "\n";

        # if this is a simple keyword search
        if (isset($SearchGroups["MAIN"]["SearchStrings"]["XXXKeywordXXX"])
            && (count($SearchGroups) == 1)
            && (count($SearchGroups["MAIN"]["SearchStrings"]) == 1))
        {
            # just use the search string
            $Descrip .= $LiteralStart;
            $Descrip .= defaulthtmlentities(
                $SearchGroups["MAIN"]["SearchStrings"]["XXXKeywordXXX"]);
            $Descrip .= $LiteralEnd . $LiteralBreak;
        }
        else
        {
            # start description on a new line (if requested)
            if ($StartWithBreak)
            {
                $Descrip .= $LiteralBreak;
            }

            # define list of phrases used to represent logical operators
            $WordsForOperators = array(
                    "=" => "is",
                    ">" => "is greater than",
                    "<" => "is less than",
                    ">=" => "is at least",
                    "<=" => "is no more than",
                    "!" => "is not",
                    );

            # for each search group
            foreach ($SearchGroups as $GroupIndex => $Group)
            {
                # if group is main
                if ($GroupIndex == "MAIN")
                {
                    # for each field in group
                    foreach ($Group["SearchStrings"] as $FieldName => $Value)
                    {
                        # determine wording based on operator
                        preg_match("/^[=><!]+/", $Value, $Matches);
                        if (count($Matches) && isset($WordsForOperators[$Matches[0]]))
                        {
                            $Value = preg_replace("/^[=><!]+/", "", $Value);
                            $Wording = $WordsForOperators[$Matches[0]];
                        }
                        else
                        {
                            $Wording = "contains";
                        }

                        # if field is psuedo-field
                        if ($FieldName == "XXXKeywordXXX")
                        {
                            # add criteria for psuedo-field
                            $Descrip .= "Keyword ".$Wording." "
                                    .$LiteralStart.htmlspecialchars($Value)
                                    .$LiteralEnd.$LiteralBreak;
                        }
                        else
                        {
                            # if field is valid
                            $Field = $Schema->GetFieldByName($FieldName);
                            if ($Field !== NULL)
                            {
                                # add criteria for field
                                $Descrip .= $Field->GetDisplayName()." ".$Wording." "
                                        .$LiteralStart.htmlspecialchars($Value)
                                        .$LiteralEnd.$LiteralBreak;
                            }
                        }
                    }
                }
                else
                {
                    # for each field in group
                    $LogicTerm = ($Group["Logic"] == SearchEngine::LOGIC_AND)
                            ? "and " : "or ";
                    foreach ($Group["SearchStrings"] as $FieldName => $Values)
                    {
                        # translate values
                        $Values = self::TranslateValues(
                                $FieldName, $Values, "SearchGroup to Display");

                        # for each value
                        $FirstValue = TRUE;
                        foreach ($Values as $Value)
                        {
                            # determine wording based on operator
                            preg_match("/^[=><!]+/", $Value, $Matches);
                            $Operator = $Matches[0];
                            $Wording = $WordsForOperators[$Operator];

                            # strip off operator
                            $Value = preg_replace("/^[=><!]+/", "", $Value);

                            # add text to description
                            if ($FirstValue)
                            {
                                $Descrip .= $FieldName." ".$Wording." "
                                        .$LiteralStart.htmlspecialchars($Value)
                                        .$LiteralEnd.$LiteralBreak;
                                $FirstValue = FALSE;
                            }
                            else
                            {
                                $Descrip .= ($IncludeHtml ?
                                            "&nbsp;&nbsp;&nbsp;&nbsp;" : "    ")
                                        .$LogicTerm.$Wording." ".$LiteralStart
                                        .htmlspecialchars($Value).$LiteralEnd
                                        .$LiteralBreak;
                            }
                        }
                    }
                }
            }
        }

        # if caller requested that long words be truncated
        if ($TruncateLongWordsTo > 4)
        {
            # break description into words
            $Words = explode(" ", $Descrip);

            # for each word
            $NewDescrip = "";
            foreach ($Words as $Word)
            {
                # if word is longer than specified length
                if (strlen(strip_tags($Word)) > $TruncateLongWordsTo)
                {
                    # truncate word and add ellipsis
                    $Word = NeatlyTruncateString($Word, $TruncateLongWordsTo - 3);
                }

                # add word to new description
                $NewDescrip .= " ".$Word;
            }

            # set description to new description
            $Descrip = $NewDescrip;
        }

        # return description to caller
        return $Descrip;
    }

    /**
    * Get list of fields to be searched
    * @return Array of field names.
    */
    public function GetSearchFieldNames()
    {
        return $this->SearchParameters->GetFields();
    }

    /**
    * Extract list of fields to be searched from search group array.
    * @param array $SearchGroups Search group array.
    * @return Array of field names.
    */
    public static function TranslateSearchGroupsToSearchFieldNames($SearchGroups)
    {
        # start out assuming no fields are being searched
        $FieldNames = array();

        # for each search group defined
        foreach ($SearchGroups as $GroupIndex => $Group)
        {
            # for each field in group
            foreach ($Group["SearchStrings"] as $FieldName => $Values)
            {
                # add field name to list of fields being searched
                $FieldNames[] = $FieldName;
            }
        }

        # return list of fields being searched to caller
        return $FieldNames;
    }

    /**
    * Get array of possible search frequency descriptions.
    * Frequencies may be excluded from list by supplying them as arguments.
    * @return Array of search frequency descriptions indexed by SEARCHFREQ constants.
    */
    public static function GetSearchFrequencyList()
    {
        # define list with descriptions
        $FreqDescr = array(
                self::SEARCHFREQ_NEVER     => "Never",
                self::SEARCHFREQ_HOURLY    => "Hourly",
                self::SEARCHFREQ_DAILY     => "Daily",
                self::SEARCHFREQ_WEEKLY    => "Weekly",
                self::SEARCHFREQ_BIWEEKLY  => "Bi-Weekly",
                self::SEARCHFREQ_MONTHLY   => "Monthly",
                self::SEARCHFREQ_QUARTERLY => "Quarterly",
                self::SEARCHFREQ_YEARLY    => "Yearly",
                );

        # for each argument passed in
        $Args = func_get_args();
        foreach ($Args as $Arg)
        {
            # remove value from list
            $FreqDescr = array_diff_key($FreqDescr, array($Arg => ""));
        }

        # return list to caller
        return $FreqDescr;
    }

    /**
    * Delete saved search.  (NOTE: Object is no longer usable after this call!)
    */
    public function Delete()
    {
        $this->DB->Query("DELETE FROM SavedSearches"
                ." WHERE SearchId = ".intval($this->SearchId));
    }


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

    private $SearchId;
    private $Record;
    private $SearchGroups;

    /**
    * Utility function to convert between value representations.
    * @param mixed $FieldOrFieldName Field to translate
    * @param mixed $Values Values to translate
    * @param string $TranslationType Desired translation
    * @return array translated value
    * (method accepts a value or array and always return an array)
    * (this is needed because values are represented differently:
    *                                 FLAG    USER    OPTION
    *     in DB / in URL / in forms   0/1     123     456
    *     used in SearchGroups        0/1     jdoe    cname
    *     displayed to user           On/Off  jdoe    cname
    * where "123" and "456" are option or controlled name IDs)
    */
    private static function TranslateValues($FieldOrFieldName, $Values, $TranslationType)
    {
        # start out assuming we won't find any values to translate
        $ReturnValues = array();

        # convert field name to field object if necessary
        if (is_object($FieldOrFieldName))
        {
            $Field = $FieldOrFieldName;
        }
        else
        {
            static $Schema;
            if (!isset($Schema)) {  $Schema = new MetadataSchema();  }
            $Field = $Schema->GetFieldByName($FieldOrFieldName);
        }

        # if incoming value is not an array
        if (!is_array($Values))
        {
            # convert incoming value to an array
            $Values = array($Values);
        }

        # for each incoming value
        foreach ($Values as $Value)
        {
            switch ($TranslationType)
            {
                case "SearchGroup to Display":
                    # if field is Flag field
                    if ($Field->Type() == MetadataSchema::MDFTYPE_FLAG)
                    {
                        # translate value to true/false label and add leading operator
                        $ReturnValues[] = ($Value == "=1") ?
                            "=".$Field->FlagOnLabel() : "=".$Field->FlagOffLabel();
                    }
                    elseif ($Field->Name() == "Cumulative Rating")
                    {
                        # translate numeric value to stars
                        $StarStrings = array(
                                "20" => "*",
                                "40" => "**",
                                "60" => "***",
                                "80" => "****",
                                "100" => "*****",
                                );
                        preg_match("/[0-9]+$/", $Value, $Matches);
                        $Number = $Matches[0];
                        preg_match("/^[=><!]+/", $Value, $Matches);
                        $Operator = $Matches[0];
                        $ReturnValues[] = $Operator.$StarStrings[$Number];
                    }
                    else
                    {
                        # use value as is
                        $ReturnValues[] = $Value;
                    }
                    break;

                case "SearchGroup to Database":
                    # strip off leading operator on value
                    $Value = preg_replace("/^[=><!]+/", "", $Value);

                    # look up index for value
                    if ($Field->Type() & (MetadataSchema::MDFTYPE_FLAG |
                                          MetadataSchema::MDFTYPE_NUMBER))
                    {
                        # (for flag or number fields the value index is already
                        # what is used in SearchGroups)
                        if ($Value >= 0)
                        {
                            $ReturnValues[] = $Value;
                        }
                    }
                    elseif ($Field->Type() == MetadataSchema::MDFTYPE_USER)
                    {
                        # (for user fields the value index is the user ID)
                        $User = new CWUser(strval($Value));
                        if ($User)
                        {
                            $ReturnValues[] = $User->Id();
                        }
                    }
                    elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION)
                    {
                        if (!isset($PossibleFieldValues))
                        {
                            $PossibleFieldValues = $Field->GetPossibleValues();
                        }
                        $NewValue = array_search($Value, $PossibleFieldValues);
                        if ($NewValue !== FALSE)
                        {
                            $ReturnValues[] = $NewValue;
                        }
                    }
                    else
                    {
                        $NewValue = $Field->GetIdForValue($Value);
                        if ($NewValue !== NULL)
                        {
                            $ReturnValues[] = $NewValue;
                        }
                    }
                    break;

                case "Database to SearchGroup":
                    # look up value for index
                    if ($Field->Type() == MetadataSchema::MDFTYPE_FLAG)
                    {
                        # (for flag fields the value index (0 or 1) is already
                        # what is used in Database)
                        if ($Value >= 0)
                        {
                            $ReturnValues[] = "=".$Value;
                        }
                    }
                    elseif ($Field->Type() == MetadataSchema::MDFTYPE_NUMBER)
                    {
                        # (for flag fields the value index (0 or 1) is already
                        #  what is used in Database)

                        if ($Value >= 0)
                        {
                            $ReturnValues[] = ">=".$Value;
                        }
                    }
                    elseif ($Field->Type() == MetadataSchema::MDFTYPE_USER)
                    {
                        $User = new CWUser(intval($Value));
                        if ($User)
                        {
                            $ReturnValues[] = "=".$User->Get("UserName");
                        }
                    }
                    elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION)
                    {
                        if (!isset($PossibleFieldValues))
                        {
                            $PossibleFieldValues = $Field->GetPossibleValues();
                        }

                        if (isset($PossibleFieldValues[$Value]))
                        {
                            $ReturnValues[] = "=".$PossibleFieldValues[$Value];
                        }
                    }
                    else
                    {
                        $NewValue = $Field->GetValueForId($Value);
                        if ($NewValue !== NULL)
                        {
                            $ReturnValues[] = "=".$NewValue;
                        }
                    }
                    break;
            }
        }

        # return array of translated values to caller
        return $ReturnValues;
    }

    /** @cond */

    # utility function for updating values in database
    /**
    * Update a value in the database.
    * @param string $FieldName Field to update
    * @param string $NewValue New value for field
    * @return updated value
    */
    private function UpdateValue($FieldName, $NewValue)
    {
        return $this->DB->UpdateValue("SavedSearches", $FieldName, $NewValue,
               "SearchId = ".$this->SearchId, $this->Record);
    }

    /**
    * Get search id.
    * @returns SearchId
    * @deprecated
    */
    public function GetSearchId()
    {
        return $this->Id();
    }

    /** @endcond */
}
