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

/**
*
*   Search Form Protocol
*
*   KEYWORD SEARCH
*   F_SearchString      text to search for
*
*   TEXT SEARCH ("N" is an arbitrary zero-based index)
*   F_SearchCatN        metadata field ID or "KEYWORD" for keyword search
*   F_SearchTextN       text to search for
*
*   SEARCH LIMITS ("N" is the metadtata field ID)
*   F_SearchLimitN      one or more values as defined below
*
*   SEARCH LIMIT VALUES (metadata field type and how it will be compared)
*   Controlled Name     controlled name IDs (all or any, depending on setting)
*   Flag                1 for TRUE and 0 for FALSE (equal to)
*   Number              minimum value for field (greater than or equal to)
*   Option              controlled name IDs (all or any, depending on setting)
*   User                user IDs (all or any, depending on setting)
*
*   PRESENTATION (corresponding GET variable names follow the descriptions)
*   F_ResultsPerPage    number of search results to display per page
*   F_ReverseSortOrder  1 to sort in reverse of default order for field (RS)
*   F_SavedSearchId     ID of saved search to run (ID)
*   F_SortField         metadata field ID or "R" for sort by relevance (SF)
*   F_StartingIndex     zero-based index into search results (SI)
*
*   NOTES
*   - Search parameters will be assembled into single parameter set with "AND"
*           logic at the top level.
*   - Fields that are not viewable by the current user will be omitted from
*           search parameters.
*   - If a saved search ID is specified, search parameters will be ignored.
*   - "Refine Search" link will include search parameters as generated by
*           SearchParameterSet::UrlParameterString()
*
*/

# ----- CONFIGURATION  -------------------------------------------------------

# possible types of metadata fields for search limits
$GLOBALS["G_MDFTypesForLimits"] =
        MetadataSchema::MDFTYPE_OPTION |
        MetadataSchema::MDFTYPE_USER |
        MetadataSchema::MDFTYPE_FLAG |
        MetadataSchema::MDFTYPE_NUMBER |
        MetadataSchema::MDFTYPE_TREE;

# default sort field
TransportControlsUI::DefaultSortField("R");

# default active tab
TransportControlsUI::DefaultActiveTab(MetadataSchema::SCHEMAID_DEFAULT);

# default presentation parameters
$H_DefaultResultsPerPage = $GLOBALS["G_SysConfig"]->DefaultRecordsPerPage();
$H_DefaultSortFieldId = $GLOBALS["G_SysConfig"]->DefaultSortField();
$H_DefaultSortFieldId = ($H_DefaultSortFieldId == 0)
        ? TransportControlsUI::DefaultSortField() : $H_DefaultSortFieldId;
$H_DefaultStartingIndex = 0;

# if a user is logged in and has a RecordsPerPage setting, that should
# override the system default
if ($GLOBALS["G_User"]->IsLoggedIn() &&
    !is_null($GLOBALS["G_User"]->Get("RecordsPerPage")))
{
    $H_DefaultResultsPerPage = $GLOBALS["G_User"]->Get("RecordsPerPage");
}

# ----- EXPORTED FUNCTIONS ---------------------------------------------------

/**
* Get possible values for sorting field list.
* @param int $SchemaId Schema to retrieve fields from.
* @return array Array of field names, with field IDs for the index.
*/
function GetPossibleSortFields($SchemaId)
{
    # start with dummy entry for relevance sort
    $SortFields = array("R" => "Relevance");

    # retrieve fields that are of types that could be used for sort
    $Schema = new MetadataSchema($SchemaId);
    $PossibleFields = $Schema->GetFields(
            MetadataSchema::MDFTYPE_TEXT |
            MetadataSchema::MDFTYPE_NUMBER |
            MetadataSchema::MDFTYPE_DATE |
            MetadataSchema::MDFTYPE_TIMESTAMP |
            MetadataSchema::MDFTYPE_URL );

    # for each field
    foreach ($PossibleFields as $Field)
    {
        # if field is indicated to be used for sorting and is visible to user
        if ($Field->IncludeInSortOptions()
                && $Field->UserCanView($GLOBALS["G_User"]))
        {
            # add field to list
            $SortFields[$Field->Id()] = $Field->GetDisplayName();
        }
    }

    # sort fields by name
    natsort($SortFields);

    # return list to caller
    return $SortFields;
}


# ----- LOCAL FUNCTIONS ------------------------------------------------------

/**
* Retrieve search parameters from form values.
* @param array $FormVars Form variable array (usually $_POST).
* @return object Parameters in SearchParameterSet object.
*/
function GetSearchParametersFromForm($FormVars)
{
    # start with empty set
    $Params = new SearchParameterSet();
    $Params->Logic("AND");

    # if there is a keyword ("quick") search value
    if (isset($FormVars["F_SearchString"]))
    {
        # if there was a search string supplied
        if (strlen(trim($FormVars["F_SearchString"])))
        {
            # add keyword string to search criteria
            $Params->AddParameter($FormVars["F_SearchString"]);
        }
    }

    # while there are search text fields left to examine
    $FormFieldIndex = 0;

    # track which fields were selected
    $SearchSelections = array();

    while (isset($FormVars["F_SearchCat".$FormFieldIndex]))
    {
        # retrieve metadata field type for box
        $FieldKey = $FormVars["F_SearchCat".$FormFieldIndex];

        $SearchSelections["F_SearchCat".$FormFieldIndex] = $FieldKey;

        # if value is available for box
        if (isset($FormVars["F_SearchText".$FormFieldIndex])
                && (strlen($FormVars["F_SearchText".$FormFieldIndex])))
        {
            # retrieve box value
            $Value = $FormVars["F_SearchText".$FormFieldIndex];

            # if this is a keyword search field
            if (strtoupper($FieldKey) == "KEYWORD")
            {
                # add keyword search for value
                $Params->AddParameter($Value);
            }
            else
            {
                $FieldIds = explode("-", $FieldKey);

                if (count($FieldIds)==1)
                {
                    $Params->AddParameter($Value, intval(current($FieldIds)) );
                }
                elseif (count($FieldIds)>1)
                {
                    $Subgroup = new SearchParameterSet();
                    $Subgroup->Logic("OR");

                    foreach ($FieldIds as $FieldId)
                    {
                        $Subgroup->AddParameter($Value, intval($FieldId));
                    }
                    $Params->AddSet($Subgroup);
                }
            }
        }

        # save search selections if user was logged in
        if (count($SearchSelections) &&
            $GLOBALS["G_User"]->IsLoggedIn())
        {
            $GLOBALS["G_User"]->Set(
                "SearchSelections", serialize($SearchSelections) );
        }

        # move to next search box
        $FormFieldIndex++;
    }

    # for each possible limit field
    foreach (MetadataSchema::GetAllSchemas() as $SchemaId => $Schema)
    {
        $Subgroups = array();
        $Fields = $Schema->GetFields($GLOBALS["G_MDFTypesForLimits"]);
        foreach ($Fields as $FieldId => $Field)
        {
            $FieldType = $Field->Type();
            $FieldName = $Field->Name();

            # if value is available for this field
            if (isset($FormVars["F_SearchLimit".$FieldId]))
            {
                # retrieve value and convert to an array if necessary
                $Values = $FormVars["F_SearchLimit".$FieldId];
                if  (!is_array($Values))
                {
                    $Values = array($Values);
                }

                # handle value based on field type
                switch ($Field->Type())
                {
                    case MetadataSchema::MDFTYPE_FLAG:
                        # add flag value to set for this field (if meaningful)
                        if ($Values[0] >= 0)
                        {
                            if (!isset($Subgroups[$FieldId]))
                            {
                                $Subgroups[$FieldId] = new SearchParameterSet();
                            }
                            $Subgroups[$FieldId]->AddParameter("=".$Values[0], $Field);
                        }
                        break;

                    case MetadataSchema::MDFTYPE_NUMBER:
                        # add numeric value to set for this field (if meaningful)
                        if ($Values[0] >= 0)
                        {
                            if (!isset($Subgroups[$FieldId]))
                            {
                                $Subgroups[$FieldId] = new SearchParameterSet();
                            }
                            $Subgroups[$FieldId]->AddParameter(">=".$Values[0], $Field);
                        }
                        break;

                    default:
                        # retrieve possible values for field
                        $PossibleValues = $Field->GetPossibleValues();

                        # for each value selected
                        $ValuesToAdd = array();
                        foreach ($Values as $VIndex)
                        {
                            # include value to be added to set
                            $Value = $PossibleValues[$VIndex];
                            $ValuesToAdd[] = "=".$Value;
                            if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
                            {
                                $ValuesToAdd[]= "^".$Value." -- ";
                            }
                        }

                        # if there were valid values found
                        if (count($ValuesToAdd))
                        {
                            # add values to set for this field
                            if (!isset($Subgroups[$FieldId]))
                            {
                                $Subgroups[$FieldId] = new SearchParameterSet();
                            }
                            $Subgroups[$FieldId]->AddParameter($ValuesToAdd, $Field);
                        }
                        break;
                }
            }
        }

        # if there were limit search parameters found
        if (count($Subgroups))
        {
            # for each field with limit search parameters
            foreach ($Subgroups as $FieldId => $Subgroup)
            {
                # set search logic for field subgroup
                $Subgroup->Logic($Fields[$FieldId]->SearchGroupLogic());

                # add field subgroup to search parameters
                $Params->AddSet($Subgroup);
            }
        }
    }

    # return search parameters to caller
    return $Params;
}

/**
* Get the sort order (ascending or descending) appropriate to the specified
* type of metadata field.
* @param int $FieldId ID of metadata field.
* @param bool $ReverseOrder TRUE to reverse normal order.
* @return bool TRUE to sort descending, or FALSE to sort ascending.
*/
function GetFieldSortOrder($FieldId, $ReverseOrder)
{
    # if sorting on the pseudo-field "Relevance"
    if ($FieldId == "R")
    {
        # sort order is descending
        $SortDescending = TRUE;
    }
    else
    {
        # if field is valid
        $Schema = new MetadataSchema();
        if ($Schema->ItemExists($FieldId))
        {
            # determine sort order based on field type
            $Field = $Schema->GetField($FieldId);
            switch ($Field->Type())
            {
                case MetadataSchema::MDFTYPE_DATE:
                case MetadataSchema::MDFTYPE_TIMESTAMP:
                    $SortDescending = TRUE;
                    break;

                default:
                    $SortDescending = FALSE;
                    break;
            }
        }
        else
        {
            # assume sort order is ascending
            $SortDescending = FALSE;
        }
    }

    # reverse sort order if requested
    if ($ReverseOrder)
    {
        $SortDescending = $SortDescending ? FALSE : TRUE;
    }

    # return order to caller
    return $SortDescending;
}


# ----- MAIN: LOAD PARAMETERS ------------------------------------------------

# load search parameters from form values
$H_SearchParams = GetSearchParametersFromForm($_POST);

$SearchParametersGottenFromForm = ($H_SearchParams->ParameterCount() > 0)
        ? TRUE : FALSE;

# retrieve results presentation parameters
$H_ResultsPerPage = $SearchParametersGottenFromForm ?
        GetArrayValue($_POST, "F_RecordsPerPage", $H_DefaultResultsPerPage) :
        GetArrayValue($_GET, "RP", $H_DefaultResultsPerPage);
$H_TransportUI = new TransportControlsUI(
        MetadataSchema::GetAllSchemaIds(), $H_ResultsPerPage);

if ($SearchParametersGottenFromForm &&
    isset($_POST["F_SortField"]))
{
    $H_TransportUI->SortField($_POST["F_SortField"]);
}

# determine sort directions
$SortFields = $H_TransportUI->SortField();
$ReverseSortFlags = $H_TransportUI->ReverseSortFlag();
foreach (MetadataSchema::GetAllSchemaIds() as $ItemType)
{
    $H_SortDescending[$ItemType] = GetFieldSortOrder(
            $SortFields[$ItemType], $ReverseSortFlags[$ItemType]);
}

# add any search parameters from URL
if (SearchParameterSet::IsLegacyUrl($_SERVER["REQUEST_URI"]))
{
    $H_SearchParams->SetFromLegacyUrl($_SERVER["REQUEST_URI"]);
}
else
{
    $H_SearchParams->UrlParameters($_GET);
}


# ----- MAIN: PERFORM ACTIONS ------------------------------------------------

# if user requested to save search
if (isset($_POST["Submit"]) && ($_POST["Submit"] == "Save"))
{
    # if we're editing an existing search
    if (isset($_POST["F_SavedSearchId"]))
    {
        # pull the search ID out of _POST
        $SearchId = intval($_POST["F_SavedSearchId"]);

        # check that the search exists
        $SSFactory = new SavedSearchFactory();
        if ($SSFactory->ItemExists($SearchId))
        {
            $SavedSearch = new SavedSearch($SearchId);

            # and if the current user owns the search, save their changes
            if ($SavedSearch->UserId() == $GLOBALS["G_User"]->Id())
            {
                $SavedSearch->SearchName(trim($_POST["F_SearchName"]));
                $SavedSearch->SearchParameters($H_SearchParams);

                $GLOBALS["AF"]->SetJumpToPage("SavedSearch");
                return;
            }
        }
    }
    else
    {
        # jump to new saved search page
        $GLOBALS["AF"]->SetJumpToPage("NewSavedSearch&"
                .$H_SearchParams->UrlParameterString());
        return;
    }
}

# if saved search specified and user is logged in
if (isset($_GET["ID"]) && $GLOBALS["G_User"]->IsLoggedIn())
{
    # if specified saved search exists
    $SSFactory = new SavedSearchFactory();
    if ($SSFactory->ItemExists($_GET["ID"]))
    {
        # if search is owned by current user
        $SavedSearch = new SavedSearch($_GET["ID"]);
        if ($SavedSearch->UserId() == $GLOBALS["G_User"]->Id())
        {
            # load saved search parameters
            $H_SearchParams = $SavedSearch->SearchParameters();
        }
        else
        {
            # load empty search parameter set and results
            $H_SearchParams = new SearchParameterSet();
            $H_SearchResults = array();
        }
    }
}
else
{
    # if some search parameters came from form values
    if ($SearchParametersGottenFromForm)
    {
        # construct new URL with all parameters
        $NewPageParameters = "SearchResults&"
                .$H_SearchParams->UrlParameterString()
                .$H_TransportUI->UrlParameterString(FALSE);

        # add on parameters as needed
        if ($H_ResultsPerPage != $H_DefaultResultsPerPage)
        {
            $NewPageParameters .= "&RP=".$H_ResultsPerPage;
        }

        # reload with new URL containing all parameters
        $GLOBALS["AF"]->SetJumpToPage($NewPageParameters);
        return;
    }
}

# if we have search parameters
if ($H_SearchParams->ParameterCount())
{
    # if we have keyword search parameters
    if (count($H_SearchParams->GetKeywordSearchStrings()))
    {
        # pull the existing keywords out of the search
        $OldKeywords = $H_SearchParams->GetKeywordSearchStrings();
        $H_SearchParams->RemoveParameter($OldKeywords);

        # signal the event allowing them to be modified
        $SignalResult = $GLOBALS["AF"]->SignalEvent(
            "EVENT_KEYWORD_SEARCH",
            array("Keywords" => $OldKeywords) );

        # and put the result back in to the search
        $H_SearchParams->AddParameter($SignalResult["Keywords"]);
    }

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

    $H_DisplayParams = SPTSearchEngine::ConvertToDisplayParameters(
        $H_SearchParams);

    # retrieve sort fields for search (convert relevance to NULL for search engine)
    $SortFields = array();
    foreach ($H_TransportUI->SortField() as $ItemType => $SortField)
    {
        $SortFields[$ItemType] = ($SortField == "R") ? NULL : $SortField;
    }

    # run search
    $SEngine = new SPTSearchEngine();
    $H_SearchResults = $SEngine->Search($H_SearchParams, 0, PHP_INT_MAX,
            $SortFields, $H_SortDescending, NULL);
    $H_SearchTime = $SEngine->SearchTime();

    # filter out any temporary or unviewable records
    foreach ($H_SearchResults as $SchemaId => $SchemaResults)
    {
        $RFactory = new ResourceFactory($SchemaId);
        $ViewableResourceIds = $RFactory->FilterNonViewableResources(
                array_keys($SchemaResults), $GLOBALS["G_User"]);
        $FlippedViewableResourceIds = array_flip($ViewableResourceIds);
        $TempSearchResults = array();
        foreach ($SchemaResults as $Id => $Score)
        {
            if (($Id >= 0) && isset($FlippedViewableResourceIds[$Id]))
            {
                $TempSearchResults[$Id] = $Score;
            }
        }
        $H_SearchResults[$SchemaId] = $TempSearchResults;
    }

    # inform transport control UI of search results
    $H_TransportUI->ItemCount($H_SearchResults);

    # signal search performed
    $SignalResult = $GLOBALS["AF"]->SignalEvent(
        "EVENT_SEARCH_RESULTS", array(
            "SearchResults" => $H_SearchResults));
    $H_SearchResults = $SignalResult["SearchResults"];

    $SignalResult = $GLOBALS["AF"]->SignalEvent(
        "EVENT_SEARCH_COMPLETE", array(
            "SearchParameters" => $H_SearchParams,
            "SearchResults" => $H_SearchResults));

    # if we had no results at all for any schema
    if (count($H_SearchResults) == 0)
    {
        # dummy up empty result set so results pages is not completely empty
        $H_SearchResults[MetadataSchema::SCHEMAID_DEFAULT] = array();
    }
    else
    {
        # determine active tab based on number of results, using score
        #       totals in cases where number of results are identical
        foreach ($H_SearchResults as $ResultType => $ResultScores)
        {
            if (!isset($HighestCount) || (count($ResultScores) > $HighestCount))
            {
                $HighestCount = count($ResultScores);
                $HighestCountType = $ResultType;
            }
            elseif (isset($HighestCount) && (count($ResultScores) == $HighestCount))
            {
                if (array_sum($ResultScores)
                        > array_sum($H_SearchResults[$HighestCountType]))
                {
                    $HighestCountType = $ResultType;
                }
            }
        }

        # set active tab if a likely candidate was found
        if (isset($HighestCountType))
        {
            $H_TransportUI->ActiveTab($HighestCountType);
        }
    }
}
else
{
    # load empty search parameter set and results
    $H_SearchParams = new SearchParameterSet();
    $H_DisplayParams = $H_SearchParams;
    $H_SearchResults = array();
    $H_SearchTime = 0;
}
