<?PHP
#
#   FILE:  SavedSearch.php
#
#   NOTES:
#       - the "$SearchGroups" values used herein contain a multi-dimentional
#           array in the form of:
#               $Criteria["MAIN"]["SearchStrings"][<field names>] = <value>
#           for fields with a single value, and:
#               $Criteria[<field ID>]["SearchStrings"][<field name>][] = <value>
#           for fields with multiple values
#
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2011-2013 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
    function SavedSearch($SearchId, $SearchName = NULL, $UserId = NULL,
            $Frequency = NULL, $SearchGroups = 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."'");
            $this->Record = $this->DB->FetchRow();

            # 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("SavedSearches");

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

        # save search parameters if provided
        if ($SearchGroups) {  $this->SearchGroups($SearchGroups);  }
    }

    # get/set search parameters
    function SearchGroups($NewSearchGroups = NULL)
    {
        $Schema = new MetadataSchema();

        # if new search parameters were supplied
        if ($NewSearchGroups)
        {
            # remove existing entries for this search from the database
            $this->DB->Query("DELETE FROM SavedSearchTextParameters WHERE SearchId = ".$this->SearchId);
            $this->DB->Query("DELETE FROM SavedSearchIdParameters WHERE SearchId = ".$this->SearchId);

            # for each search group
            foreach ($NewSearchGroups as $GroupIndex => $Group)
            {
                # if group holds single parameters
                if ($GroupIndex == "MAIN")
                {
                    # for each field within group
                    foreach ($Group["SearchStrings"] as $FieldName => $Value)
                    {
                        # convert value array to single value (if necessary)
                        if (is_array($Value))
                        {
                            $ConvertedValue = "";
                            foreach ($Value as $SingleValue)
                            {
                                $ConvertedValue .= $SingleValue." ";
                            }
                            $Value = trim($ConvertedValue);
                        }

                        # add new text search parameter entry to database
                        if ($FieldName == "XXXKeywordXXX")
                        {
                            $FieldId = -101;
                        }
                        else
                        {
                            $Field = $Schema->GetFieldByName($FieldName);
                            $FieldId = $Field->Id();
                        }
                        $this->DB->Query("INSERT INTO SavedSearchTextParameters"
                                ." (SearchId, FieldId, SearchText) VALUES"
                                ." (".$this->SearchId.", ".$FieldId.", '".addslashes($Value)."')");
                    }
                }
                else
                {
                    # convert value(s) as appropriate for field type
                    $FieldId = ($GroupIndex[0] == "X")
                            ? substr($GroupIndex, 1)
                            : $GroupIndex;
                    $Field = $Schema->GetField($FieldId);
                    $FieldName = $Field->Name();
                    $Values = SavedSearch::TranslateValues($Field, $Group["SearchStrings"][$FieldName], "SearchGroup to Database");

                    # for each converted value
                    foreach ($Values as $Value)
                    {
                        # add new ID search parameter entry to database
                        $this->DB->Query("INSERT INTO SavedSearchIdParameters"
                                ." (SearchId, FieldId, SearchValueId) VALUES"
                                ." (".$this->SearchId.", ".$FieldId.", ".$Value.")");
                    }
                }
            }

            # save search parameters locally
            $this->SearchGroups = $NewSearchGroups;
        }
        else
        {
            # if search groups not already read in
            if (!isset($this->SearchGroups))
            {
                # for each text search parameter
                $SearchGroups = array();
                $this->DB->Query("SELECT * FROM SavedSearchTextParameters"
                        ." WHERE SearchId = ".$this->SearchId);
                while ($Record = $this->DB->FetchRow())
                {
                    # add parameter to search criteria
                    if ($Record["FieldId"] == -101)
                    {
                        $SearchGroups["MAIN"]["SearchStrings"]["XXXKeywordXXX"] =
                                $Record["SearchText"];
                    }
                    else
                    {
                        $Field = $Schema->GetField($Record["FieldId"]);
                        $SearchGroups["MAIN"]["SearchStrings"][$Field->Name()] =
                                $Record["SearchText"];
                    }
                }

                # for each value ID search parameter
                $this->DB->Query("SELECT * FROM SavedSearchIdParameters"
                        ." WHERE SearchId = ".$this->SearchId);
                while ($Record = $this->DB->FetchRow())
                {
                    # translate value based on field type
                    $FieldId = $Record["FieldId"];
                    if (!isset($Fields[$FieldId]))
                    {
                        $Fields[$FieldId] = $Schema->GetField($FieldId);
                    }
                    $Values = SavedSearch::TranslateValues($Fields[$FieldId],
                            $Record["SearchValueId"], "Database to SearchGroup");

                    # add parameter to search criteria
                    foreach ($Values as $Value)
                    {
                        $SearchGroups[$FieldId]["SearchStrings"]
                                [$Fields[$FieldId]->Name()][] = $Value;
                    }
                }

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

                # save search parameters locally
                $this->SearchGroups = $SearchGroups;
            }
        }

        # return search parameters to caller
        return $this->SearchGroups;
    }

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

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

    /**
     * Get/set user ID.
     * @param int $NewValue New user ID value.
     * @return Current user ID value.
     */
    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.
     */
    function Frequency($NewValue = DB_NOVALUE)
            {  return $this->UpdateValue("Frequency", $NewValue);  }

    # set date search was last run to current date/time
    function UpdateDateLastRun()
    {
        $this->DB->Query("UPDATE SavedSearches SET DateLastRun = NOW() WHERE SearchId = ".$this->SearchId);
    }

    # get/set date search was last run
    function DateLastRun($NewValue = DB_NOVALUE)
            {  return $this->UpdateValue("DateLastRun", $NewValue);  }

    /**
    * 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 "?").
    */
    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 "?").
    */
    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 = SavedSearch::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.
    */
    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.
     */
    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 = SavedSearch::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;
    }

    # set search groups from URL (GET method) parameters
    # (returns search group array)
    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 = SavedSearch::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 = SavedSearch::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.
     */
    function GetSearchGroupsAsTextDescription(
            $IncludeHtml = TRUE, $StartWithBreak = TRUE, $TruncateLongWordsTo = 0)
    {
        return self::TranslateSearchGroupsToTextDescription($this->SearchGroups(),
            $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.
     */
    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 = SavedSearch::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.
    */
    function GetSearchFieldNames()
    {
        return self::TranslateSearchGroupsToSearchFieldNames($this->SearchGroups());
    }

    /**
     * Extract list of fields to be searched from search group array.
     * @param array $SearchGroups Search group array.
     * @return Array of field names.
     */
    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.
    */
    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!)
    */
    function Delete()
    {
        $this->DB->Query("DELETE FROM SavedSearches"
                ." WHERE SearchId = ".intval($this->SearchId));
        $this->DB->Query("DELETE FROM SavedSearchTextParameters"
                ." WHERE SearchId = ".intval($this->SearchId));
        $this->DB->Query("DELETE FROM SavedSearchIdParameters"
                ." WHERE SearchId = ".intval($this->SearchId));
    }


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

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

    # utility function to convert between value representations
    # (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
    private function UpdateValue($FieldName, $NewValue)
    {
        return $this->DB->UpdateValue("SavedSearches", $FieldName, $NewValue,
               "SearchId = ".$this->SearchId, $this->Record);
    }

    # legacy methods for backward compatibility
    function GetSearchId() {  return $this->Id();  }

    /** @endcond */
}
