/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.xmlui.aspect.discovery;

import org.apache.cocoon.caching.CacheableProcessingComponent;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.util.HashUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.excalibur.source.SourceValidity;
import org.apache.log4j.Logger;
import org.dspace.app.util.MetadataExposure;
import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
import org.dspace.app.xmlui.utils.DSpaceValidity;
import org.dspace.app.xmlui.utils.HandleUtil;
import org.dspace.app.xmlui.utils.UIException;
import org.dspace.app.xmlui.wing.Message;
import org.dspace.app.xmlui.wing.WingException;
import org.dspace.app.xmlui.wing.element.*;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.*;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.core.Constants;
import org.dspace.core.LogManager;
import org.dspace.discovery.*;
import org.dspace.discovery.configuration.DiscoveryConfiguration;
import org.dspace.discovery.configuration.DiscoveryHitHighlightFieldConfiguration;
import org.dspace.discovery.configuration.DiscoverySortConfiguration;
import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration;
import org.dspace.handle.HandleManager;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.*;
import java.util.List;

/**
 * This is an abstract search page. It is a collection of search methods that
 * are common between diffrent search implementation. An implementer must
 * implement at least three methods: addBody(), getQuery(), and generateURL().
 * <p/>
 * See the implementors SimpleSearch.
 *
 * @author Kevin Van de Velde (kevin at atmire dot com)
 * @author Mark Diggory (markd at atmire dot com)
 * @author Ben Bosman (ben at atmire dot com)
 */
public abstract class AbstractSearch extends AbstractDSpaceTransformer implements CacheableProcessingComponent{

    private static final Logger log = Logger.getLogger(AbstractSearch.class);

    /**
     * Language strings
     */
    private static final Message T_head1_community =
            message("xmlui.Discovery.AbstractSearch.head1_community");

    private static final Message T_head1_collection =
            message("xmlui.Discovery.AbstractSearch.head1_collection");

    private static final Message T_head1_none =
            message("xmlui.Discovery.AbstractSearch.head1_none");

    private static final Message T_no_results =
            message("xmlui.ArtifactBrowser.AbstractSearch.no_results");

    private static final Message T_all_of_dspace =
            message("xmlui.ArtifactBrowser.AbstractSearch.all_of_dspace");

    private static final Message T_sort_by_relevance =
            message("xmlui.Discovery.AbstractSearch.sort_by.relevance");

    private static final Message T_sort_by = message("xmlui.Discovery.AbstractSearch.sort_by.head");

    private static final Message T_rpp = message("xmlui.Discovery.AbstractSearch.rpp");
    private static final Message T_result_head_3 = message("xmlui.Discovery.AbstractSearch.head3");
    private static final Message T_result_head_2 = message("xmlui.Discovery.AbstractSearch.head2");

    /**
     * Cached query results
     */
    protected DiscoverResult queryResults;

    /**
     * Cached query arguments
     */
    protected DiscoverQuery queryArgs;

    /**
     * The options for results per page
     */
    private static final int[] RESULTS_PER_PAGE_PROGRESSION = {5, 10, 20, 40, 60, 80, 100};

    /**
     * Cached validity object
     */
    private SourceValidity validity;

    /**
     * Generate the unique caching key.
     * This key must be unique inside the space of this component.
     */
    public Serializable getKey() {
        try {
            String key = "";

            // Page Parameter
            key += "-" + getParameterPage();
            key += "-" + getParameterRpp();
            key += "-" + getParameterSortBy();
            key += "-" + getParameterOrder();
            key += "-" + getParameterEtAl();

            // What scope the search is at
            DSpaceObject scope = getScope();
            if (scope != null)
            {
                key += "-" + scope.getHandle();
            }

            // The actual search query.
            key += "-" + getQuery();

            return HashUtil.hash(key);
        } catch (RuntimeException re) {
            throw re;
        } catch (Exception e) {
            // Ignore all errors and just don't cache.
            return "0";
        }
    }

    /**
     * Generate the cache validity object.
     * <p/>
     * This validity object should never "over cache" because it will
     * perform the search, and serialize the results using the
     * DSpaceValidity object.
     */
    public SourceValidity getValidity() {
        if (this.validity == null) {
            try {
                DSpaceValidity validity = new DSpaceValidity();

                DSpaceObject scope = getScope();
                validity.add(scope);

                performSearch(scope);

                List<DSpaceObject> results = this.queryResults.getDspaceObjects();

                if (results != null) {
                    validity.add("total:"+this.queryResults.getTotalSearchResults());
                    validity.add("start:"+this.queryResults.getStart());
                    validity.add("size:" + results.size());

                    for (DSpaceObject dso : results) {
                        validity.add(dso);
                    }
                }

                Map<String, List<DiscoverResult.FacetResult>> facetResults = this.queryResults.getFacetResults();
                for(String facetField : facetResults.keySet()){
                    List<DiscoverResult.FacetResult> facetValues = facetResults.get(facetField);
                    for (DiscoverResult.FacetResult facetResult : facetValues)
                    {
                        validity.add(facetField + facetResult.getAsFilterQuery() + facetResult.getCount());
                    }
                }

                this.validity = validity.complete();
            } catch (RuntimeException re) {
                throw re;
            }
            catch (Exception e) {
                this.validity = null;
            }

            // add log message that we are viewing the item
            // done here, as the serialization may not occur if the cache is valid
            logSearch();
        }
        return this.validity;
    }


    /**
     * Build the resulting search DRI document.
     */
    public abstract void addBody(Body body) throws SAXException, WingException,
            UIException, SQLException, IOException, AuthorizeException;

    /**
     * Build the main form that should be the only form that the user interface requires
     * This form will be used for all discovery queries, filters, ....
     * At the moment however this form is only used to track search result hits
     * @param searchDiv the division to add the form to
     */
    protected void buildMainForm(Division searchDiv) throws WingException, SQLException {
        Request request = ObjectModelHelper.getRequest(objectModel);

        //We set our action to context path, since the eventual action will depend on which url we click on
        Division mainForm = searchDiv.addInteractiveDivision("main-form", getBasicUrl(), Division.METHOD_POST, "");

        String query = getQuery();
        mainForm.addHidden("query").setValue(query);

        Map<String, String[]> fqs = getParameterFilterQueries();
        if (fqs != null)
        {
            for (String parameter : fqs.keySet())
            {
                String[] values = fqs.get(parameter);
                if(values != null)
                {
                    for (String value : values)
                    {
                        mainForm.addHidden(parameter).setValue(value);
                    }
                }
            }
        }

        mainForm.addHidden("rpp").setValue(getParameterRpp());
        Hidden sort_by = mainForm.addHidden("sort_by");
        if(!StringUtils.isBlank(request.getParameter("sort_by")))
        {
            sort_by.setValue(request.getParameter("sort_by"));
        }else{
            sort_by.setValue("score");
        }

        Hidden order = mainForm.addHidden("order");
        if(getParameterOrder() != null)
        {
            order.setValue(request.getParameter("order"));
        }else{
            DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
            DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(dso);
            order.setValue(discoveryConfiguration.getSearchSortConfiguration().getDefaultSortOrder().toString());
        }
        if(!StringUtils.isBlank(request.getParameter("page")))
        {
            mainForm.addHidden("page").setValue(request.getParameter("page"));
        }
        //Optional redirect url !
        mainForm.addHidden("redirectUrl");
    }

    protected abstract String getBasicUrl() throws SQLException;

    /**
     * Attach a division to the given search division named "search-results"
     * which contains results for this search query.
     *
     * @param search The search division to contain the search-results division.
     */
    protected void buildSearchResultsDivision(Division search)
            throws IOException, SQLException, WingException, SearchServiceException {

        try {
            if (queryResults == null) {

                DSpaceObject scope = getScope();
                this.performSearch(scope);
            }
        }
        catch (RuntimeException e) {
            log.error(e.getMessage(), e);
            queryResults = null;
        }
        catch (Exception e) {
            log.error(e.getMessage(), e);
            queryResults = null;
        }

        Division results = search.addDivision("search-results", "primary");
        buildSearchControls(results);


        DSpaceObject searchScope = getScope();

        int displayedResults;
        long totalResults;
        float searchTime;

        if(queryResults != null && 0 < queryResults.getTotalSearchResults())
        {
            displayedResults = queryResults.getDspaceObjects().size();
            totalResults = queryResults.getTotalSearchResults();
            searchTime = ((float) queryResults.getSearchTime() / 1000) % 60;

            if (searchScope instanceof Community)
            {
                Community community = (Community) searchScope;
                String communityName = community.getMetadata("name");
                results.setHead(T_head1_community.parameterize(displayedResults, totalResults, communityName, searchTime));
            } else if (searchScope instanceof Collection){
                Collection collection = (Collection) searchScope;
                String collectionName = collection.getMetadata("name");
                results.setHead(T_head1_collection.parameterize(displayedResults, totalResults, collectionName, searchTime));
            } else {
                results.setHead(T_head1_none.parameterize(displayedResults, totalResults, searchTime));
            }
        }

        if (queryResults != null && 0 < queryResults.getDspaceObjects().size())
        {

            // Pagination variables.
            int itemsTotal = (int) queryResults.getTotalSearchResults();
            int firstItemIndex = (int) this.queryResults.getStart() + 1;
            int lastItemIndex = (int) this.queryResults.getStart() + queryResults.getDspaceObjects().size();

            //if (itemsTotal < lastItemIndex)
            //    lastItemIndex = itemsTotal;
            int currentPage = this.queryResults.getStart() / this.queryResults.getMaxResults() + 1;
            int pagesTotal = (int) ((this.queryResults.getTotalSearchResults() - 1) / this.queryResults.getMaxResults()) + 1;
            Map<String, String> parameters = new HashMap<String, String>();
            parameters.put("page", "{pageNum}");
            String pageURLMask = generateURL(parameters);
            Map<String, String[]> filterQueryParams = getParameterFilterQueries();
            if(filterQueryParams != null)
            {
                StringBuilder maskBuilder = new StringBuilder(pageURLMask);
                for (String filterQueryParam : filterQueryParams.keySet())
                {
                    String[] filterQueryValues = filterQueryParams.get(filterQueryParam);
                    if(filterQueryValues != null)
                    {
                        for (String filterQueryValue : filterQueryValues)
                        {
                            maskBuilder.append("&").append(filterQueryParam).append("=").append(filterQueryValue);
                        }
                    }
                }

                pageURLMask = maskBuilder.toString();
            }

            results.setMaskedPagination(itemsTotal, firstItemIndex,
                    lastItemIndex, currentPage, pagesTotal, pageURLMask);

            // Look for any communities or collections in the mix
            org.dspace.app.xmlui.wing.element.List dspaceObjectsList = null;

            // Put in palce top level search result list
            dspaceObjectsList = results.addList("search-results-repository",
                    org.dspace.app.xmlui.wing.element.List.TYPE_DSO_LIST, "repository-search-results");

            List<DSpaceObject> commCollList = new ArrayList<DSpaceObject>();
            List<Item> itemList = new ArrayList<Item>();
            for (DSpaceObject resultDso : queryResults.getDspaceObjects())
            {
                if(resultDso.getType() == Constants.COMMUNITY || resultDso.getType() == Constants.COLLECTION)
                {
                    commCollList.add(resultDso);
                }else
                if(resultDso.getType() == Constants.ITEM)
                {
                    itemList.add((Item) resultDso);
                }
            }

            if(CollectionUtils.isNotEmpty(commCollList))
            {
                org.dspace.app.xmlui.wing.element.List commCollWingList = dspaceObjectsList.addList("comm-coll-result-list");
                commCollWingList.setHead(T_result_head_2);
                for (DSpaceObject dso : commCollList)
                {
                    DiscoverResult.DSpaceObjectHighlightResult highlightedResults = queryResults.getHighlightedResults(dso);
                    if(dso.getType() == Constants.COMMUNITY)
                    {
                        //Render our community !
                        org.dspace.app.xmlui.wing.element.List communityMetadata = commCollWingList.addList(dso.getHandle() + ":community");

                        renderCommunity((Community) dso, highlightedResults, communityMetadata);
                    }else
                    if(dso.getType() == Constants.COLLECTION)
                    {
                        //Render our collection !
                        org.dspace.app.xmlui.wing.element.List collectionMetadata = commCollWingList.addList(dso.getHandle() + ":collection");

                        renderCollection((Collection) dso, highlightedResults, collectionMetadata);
                    }
                }
            }

            if(CollectionUtils.isNotEmpty(itemList))
            {
                org.dspace.app.xmlui.wing.element.List itemWingList = dspaceObjectsList.addList("item-result-list");
                if(CollectionUtils.isNotEmpty(commCollList))
                {
                    itemWingList.setHead(T_result_head_3);

                }
                for (Item resultDso : itemList)
                {
                    DiscoverResult.DSpaceObjectHighlightResult highlightedResults = queryResults.getHighlightedResults(resultDso);
                    renderItem(itemWingList, resultDso, highlightedResults);
                }
            }

        } else {
            results.addPara(T_no_results);
        }
        //}// Empty query
    }

    /**
     * Render the given item, all metadata is added to the given list, which metadata will be rendered where depends on the xsl
     * @param dspaceObjectsList a list of DSpace objects
     * @param item the DSpace item to be rendered
     * @param highlightedResults the highlighted results
     * @throws WingException
     * @throws SQLException Database failure in services this calls
     */
    protected void renderItem(org.dspace.app.xmlui.wing.element.List dspaceObjectsList, Item item, DiscoverResult.DSpaceObjectHighlightResult highlightedResults) throws WingException, SQLException {
        org.dspace.app.xmlui.wing.element.List itemList = dspaceObjectsList.addList(item.getHandle() + ":item");

        MetadataField[] metadataFields = MetadataField.findAll(context);
        for (MetadataField metadataField : metadataFields)
        {
            //Retrieve the schema for this field
            String schema = MetadataSchema.find(context, metadataField.getSchemaID()).getName();
            //Check if our field isn't hidden
            if (!MetadataExposure.isHidden(context, schema, metadataField.getElement(), metadataField.getQualifier()))
            {
                //Check if our metadata field is highlighted
                StringBuilder metadataKey = new StringBuilder();
                metadataKey.append(schema).append(".").append(metadataField.getElement());
                if (metadataField.getQualifier() != null)
                {
                    metadataKey.append(".").append(metadataField.getQualifier());
                }

                StringBuilder itemName = new StringBuilder();
                itemName.append(item.getHandle()).append(":").append(metadataKey.toString());


                DCValue[] itemMetadata = item.getMetadata(schema, metadataField.getElement(), metadataField.getQualifier(), Item.ANY);
                if(!ArrayUtils.isEmpty(itemMetadata))
                {
                    org.dspace.app.xmlui.wing.element.List metadataFieldList = itemList.addList(itemName.toString());
                    for (DCValue metadataValue : itemMetadata)
                    {
                        String value = metadataValue.value;
                        addMetadataField(highlightedResults, metadataKey.toString(), metadataFieldList, value);
                    }
                }
            }
        }

        //Check our highlighted results, we may need to add non metadata (like our full text)
        if(highlightedResults != null)
        {
            //Also add the full text snippet (if available !)
            List<String> fullSnippets = highlightedResults.getHighlightResults("fulltext");
            if(CollectionUtils.isNotEmpty(fullSnippets))
            {
                StringBuilder itemName = new StringBuilder();
                itemName.append(item.getHandle()).append(":").append("fulltext");

                org.dspace.app.xmlui.wing.element.List fullTextFieldList = itemList.addList(itemName.toString());

                for (String snippet : fullSnippets)
                {
                    addMetadataField(fullTextFieldList, snippet);
                }
            }
        }
    }

    /**
     * Render the given collection, all collection metadata is added to the list
     * @param collection the collection to be rendered
     * @param highlightedResults the highlighted results
     * @throws WingException
     */
    protected void renderCollection(Collection collection, DiscoverResult.DSpaceObjectHighlightResult highlightedResults, org.dspace.app.xmlui.wing.element.List collectionMetadata) throws WingException {

        String description = collection.getMetadata("introductory_text");
        String description_abstract = collection.getMetadata("short_description");
        String description_table = collection.getMetadata("side_bar_text");
        String identifier_uri = "http://hdl.handle.net/" + collection.getHandle();
        String provenance = collection.getMetadata("provenance_description");
        String rights = collection.getMetadata("copyright_text");
        String rights_license = collection.getMetadata("license");
        String title = collection.getMetadata("name");

        if(StringUtils.isNotBlank(description))
        {
            addMetadataField(highlightedResults, "dc.description", collectionMetadata.addList(collection.getHandle() + ":dc.description"), description);
        }
        if(StringUtils.isNotBlank(description_abstract))
        {
            addMetadataField(highlightedResults, "dc.description.abstract", collectionMetadata.addList(collection.getHandle() + ":dc.description.abstract"), description_abstract);
        }
        if(StringUtils.isNotBlank(description_table))
        {
            addMetadataField(highlightedResults, "dc.description.tableofcontents", collectionMetadata.addList(collection.getHandle() + ":dc.description.tableofcontents"), description_table);
        }
        if(StringUtils.isNotBlank(identifier_uri))
        {
            addMetadataField(highlightedResults, "dc.identifier.uri", collectionMetadata.addList(collection.getHandle() + ":dc.identifier.uri"), identifier_uri);
        }
        if(StringUtils.isNotBlank(provenance))
        {
            addMetadataField(highlightedResults, "dc.provenance", collectionMetadata.addList(collection.getHandle() + ":dc.provenance"), provenance);
        }
        if(StringUtils.isNotBlank(rights))
        {
            addMetadataField(highlightedResults, "dc.rights", collectionMetadata.addList(collection.getHandle() + ":dc.rights"), rights);
        }
        if(StringUtils.isNotBlank(rights_license))
        {
            addMetadataField(highlightedResults, "dc.rights.license", collectionMetadata.addList(collection.getHandle() + ":dc.rights.license"), rights_license);
        }
        if(StringUtils.isNotBlank(title))
        {
            addMetadataField(highlightedResults, "dc.title", collectionMetadata.addList(collection.getHandle() + ":dc.title"), title);
        }
    }

    /**
     * Render the given collection, all collection metadata is added to the list
     * @param community the community to be rendered
     * @param highlightedResults the highlighted results
     * @throws WingException
     */

    protected void renderCommunity(Community community, DiscoverResult.DSpaceObjectHighlightResult highlightedResults, org.dspace.app.xmlui.wing.element.List communityMetadata) throws WingException {
        String description = community.getMetadata("introductory_text");
        String description_abstract = community.getMetadata("short_description");
        String description_table = community.getMetadata("side_bar_text");
        String identifier_uri = "http://hdl.handle.net/" + community.getHandle();
        String rights = community.getMetadata("copyright_text");
        String title = community.getMetadata("name");

        if(StringUtils.isNotBlank(description))
        {
            addMetadataField(highlightedResults, "dc.description", communityMetadata.addList(community.getHandle() + ":dc.description"), description);
        }
        if(StringUtils.isNotBlank(description_abstract))
        {
            addMetadataField(highlightedResults, "dc.description.abstract", communityMetadata.addList(community.getHandle() + ":dc.description.abstract"), description_abstract);
        }
        if(StringUtils.isNotBlank(description_table))
        {
            addMetadataField(highlightedResults, "dc.description.tableofcontents", communityMetadata.addList(community.getHandle() + ":dc.description.tableofcontents"), description_table);
        }
        if(StringUtils.isNotBlank(identifier_uri))
        {
            addMetadataField(highlightedResults, "dc.identifier.uri", communityMetadata.addList(community.getHandle() + ":dc.identifier.uri"), identifier_uri);
        }
        if(StringUtils.isNotBlank(rights))
        {
            addMetadataField(highlightedResults, "dc.rights", communityMetadata.addList(community.getHandle() + ":dc.rights"), rights);
        }
        if(StringUtils.isNotBlank(title))
        {
            addMetadataField(highlightedResults, "dc.title", communityMetadata.addList(community.getHandle() + ":dc.title"), title);
        }
    }

    /**
     * Add the current value to the wing list,
     * @param highlightedResults the highlighted results
     * @param metadataKey the metadata key {schema}.{element}.{qualifier}
     * @param metadataFieldList the wing list we need to add the metadata value to
     * @param value the metadata value
     * @throws WingException
     */
    protected void addMetadataField(DiscoverResult.DSpaceObjectHighlightResult highlightedResults, String metadataKey, org.dspace.app.xmlui.wing.element.List metadataFieldList, String value) throws WingException {
        if(value == null){
            //In the unlikely event that the value is null, do not attempt to render this
            return;
        }

        if(highlightedResults != null && highlightedResults.getHighlightResults(metadataKey) != null)
        {
            //Loop over all our highlighted results
            for (String highlight : highlightedResults.getHighlightResults(metadataKey))
            {
                //If our non highlighted value matches our original one, ensure that the highlighted one is used
                DiscoverHitHighlightingField highlightConfig = queryArgs.getHitHighlightingField(metadataKey);
                //We might also have it configured for ALL !
                if(highlightConfig == null)
                {
                    highlightConfig = queryArgs.getHitHighlightingField("*");
                }
                switch (highlightConfig.getMaxChars())
                {
                    case DiscoverHitHighlightingField.UNLIMITED_FRAGMENT_LENGTH:
                        //Exact match required
                        //\r is not indexed in solr & will cause issues
                        if(highlight.replaceAll("</?em>", "").equals(value.replace("\r", "")))
                        {
                            value = highlight;
                        }
                        break;
                    default:
                        //Partial match allowed, only render the highlighted part
                        if(value.contains(highlight.replaceAll("</?em>", "")))
                        {
                            value = highlight;
                        }
                        break;
                }

            }
        }
        addMetadataField(metadataFieldList, value);
    }

    /**
     * Add our metadata value, this value will might contain the highlight ("<em></em>") tags, these will be removed & rendered as highlight wing fields.
     * @param metadataFieldList the metadata list we need to add the value to
     * @param value the metadata value to be rendered
     * @throws WingException
     */
    protected void addMetadataField(org.dspace.app.xmlui.wing.element.List metadataFieldList, String value) throws WingException {
        //We need to put everything in <em> tags in a highlight !
        org.dspace.app.xmlui.wing.element.Item metadataItem = metadataFieldList.addItem();
        while(value.contains("<em>") && value.contains("</em>"))
        {
            if(0 < value.indexOf("<em>"))
            {
                //Add everything before the <em> !
                metadataItem.addContent(value.substring(0, value.indexOf("<em>")));
            }
            metadataItem.addHighlight("highlight").addContent(StringUtils.substringBetween(value, "<em>", "</em>"));

            value = StringUtils.substringAfter(value, "</em>");

        }
        if(0 < value.length())
        {
            metadataItem.addContent(value);
        }
    }

    /**
     * Add options to the search scope field. This field determines in what
     * communities or collections to search for the query.
     * <p/>
     * The scope list will depend upon the current search scope. There are three
     * cases:
     * <p/>
     * No current scope: All top level communities are listed.
     * <p/>
     * The current scope is a community: All collections contained within the
     * community are listed.
     * <p/>
     * The current scope is a collection: All parent communities are listed.
     *
     * @param scope The current scope field.
     */
    protected void buildScopeList(Select scope) throws SQLException,
            WingException {

        DSpaceObject scopeDSO = getScope();
        if (scopeDSO == null) {
            // No scope, display all root level communities
            scope.addOption("/", T_all_of_dspace);
            scope.setOptionSelected("/");
            for (Community community : Community.findAllTop(context)) {
                scope.addOption(community.getHandle(), community.getMetadata("name"));
            }
        } else if (scopeDSO instanceof Community) {
            // The scope is a community, display all collections contained
            // within
            Community community = (Community) scopeDSO;
            scope.addOption("/", T_all_of_dspace);
            scope.addOption(community.getHandle(), community.getMetadata("name"));
            scope.setOptionSelected(community.getHandle());

            for (Collection collection : community.getCollections()) {
                scope.addOption(collection.getHandle(), collection.getMetadata("name"));
            }
        } else if (scopeDSO instanceof Collection) {
            // The scope is a collection, display all parent collections.
            Collection collection = (Collection) scopeDSO;
            scope.addOption("/", T_all_of_dspace);
            scope.addOption(collection.getHandle(), collection.getMetadata("name"));
            scope.setOptionSelected(collection.getHandle());

            Community[] communities = collection.getCommunities()[0]
                    .getAllParents();
            for (Community community : communities) {
                scope.addOption(community.getHandle(), community.getMetadata("name"));
            }
        }
    }

    /**
     * Query DSpace for a list of all items / collections / or communities that
     * match the given search query.
     *
     *
     * @param scope the dspace object parent
     */
    public void performSearch(DSpaceObject scope) throws UIException, SearchServiceException {

        if (queryResults != null)
        {
            return;
        }
        

        String query = getQuery();

        //DSpaceObject scope = getScope();

        int page = getParameterPage();

        List<String> filterQueries = new ArrayList<String>();

        String[] fqs = getFilterQueries();

        if (fqs != null)
        {
            filterQueries.addAll(Arrays.asList(fqs));
        }


        this.queryArgs = new DiscoverQuery();

        //Add the configured default filter queries
        DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(scope);
        List<String> defaultFilterQueries = discoveryConfiguration.getDefaultFilterQueries();
        queryArgs.addFilterQueries(defaultFilterQueries.toArray(new String[defaultFilterQueries.size()]));

        if (filterQueries.size() > 0) {
            queryArgs.addFilterQueries(filterQueries.toArray(new String[filterQueries.size()]));
        }


        queryArgs.setMaxResults(getParameterRpp());

        String sortBy = ObjectModelHelper.getRequest(objectModel).getParameter("sort_by");
        DiscoverySortConfiguration searchSortConfiguration = discoveryConfiguration.getSearchSortConfiguration();
        if(sortBy == null){
            //Attempt to find the default one, if none found we use SCORE
            sortBy = "score";
            if(searchSortConfiguration != null){
                for (DiscoverySortFieldConfiguration sortFieldConfiguration : searchSortConfiguration.getSortFields()) {
                    if(sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())){
                        sortBy = SearchUtils.getSearchService().toSortFieldIndex(sortFieldConfiguration.getMetadataField(), sortFieldConfiguration.getType());
                    }
                }
            }
        }
        String sortOrder = ObjectModelHelper.getRequest(objectModel).getParameter("order");
        if(sortOrder == null && searchSortConfiguration != null){
            sortOrder = searchSortConfiguration.getDefaultSortOrder().toString();
        }

        if (sortOrder == null || sortOrder.equalsIgnoreCase("DESC"))
        {
            queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.desc);
        }
        else
        {
            queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.asc);
        }


        String groupBy = ObjectModelHelper.getRequest(objectModel).getParameter("group_by");


        // Enable groupBy collapsing if designated
        if (groupBy != null && !groupBy.equalsIgnoreCase("none")) {
            /** Construct a Collapse Field Query */
            queryArgs.addProperty("collapse.field", groupBy);
            queryArgs.addProperty("collapse.threshold", "1");
            queryArgs.addProperty("collapse.includeCollapsedDocs.fl", "handle");
            queryArgs.addProperty("collapse.facet", "before");

            //queryArgs.a  type:Article^2

            // TODO: This is a hack to get Publications (Articles) to always be at the top of Groups.
            // TODO: I think the can be more transparently done in the solr solrconfig.xml with DISMAX and boosting
            /** sort in groups to get publications to top */
            queryArgs.setSortField("dc.type", DiscoverQuery.SORT_ORDER.asc);

        }


        queryArgs.setQuery(query != null && !query.trim().equals("") ? query : null);

        if (page > 1)
        {
            queryArgs.setStart((page - 1) * queryArgs.getMaxResults());
        }
        else
        {
            queryArgs.setStart(0);
        }

        if(discoveryConfiguration.getHitHighlightingConfiguration() != null)
        {
            List<DiscoveryHitHighlightFieldConfiguration> metadataFields = discoveryConfiguration.getHitHighlightingConfiguration().getMetadataFields();
            for (DiscoveryHitHighlightFieldConfiguration fieldConfiguration : metadataFields)
            {
                queryArgs.addHitHighlightingField(new DiscoverHitHighlightingField(fieldConfiguration.getField(), fieldConfiguration.getMaxSize(), fieldConfiguration.getSnippets()));
            }
        }

        this.queryResults = SearchUtils.getSearchService().search(context, scope, queryArgs);
    }



    /**
     * Returns a list of the filter queries for use in rendering pages, creating page more urls, ....
     * @return an array containing the filter queries
     */
    protected Map<String, String[]> getParameterFilterQueries()
    {
        try {
            Map<String, String[]> result = new HashMap<String, String[]>();
            result.put("fq", ObjectModelHelper.getRequest(objectModel).getParameterValues("fq"));
            return result;
        }
        catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns all the filter queries for use by solr
     *  This method returns more expanded filter queries then the getParameterFilterQueries
     * @return an array containing the filter queries
     */
    protected String[] getFilterQueries() {
        try {
            return ObjectModelHelper.getRequest(objectModel).getParameterValues("fq");
        }
        catch (Exception e) {
            return null;
        }
    }

    protected String[] getFacetsList() {
        try {
            return ObjectModelHelper.getRequest(objectModel).getParameterValues("fl");
        }
        catch (Exception e) {
            return null;
        }
    }

    protected int getParameterPage() {
        try {
            return Integer.parseInt(ObjectModelHelper.getRequest(objectModel).getParameter("page"));
        }
        catch (Exception e) {
            return 1;
        }
    }

    protected int getParameterRpp() {
        try {
            return Integer.parseInt(ObjectModelHelper.getRequest(objectModel).getParameter("rpp"));
        }
        catch (Exception e) {
            return 10;
        }
    }

    protected String getParameterSortBy() {
        String s = ObjectModelHelper.getRequest(objectModel).getParameter("sort_by");
        return s != null ? s : null;
    }

    protected String getParameterGroup() {
        String s = ObjectModelHelper.getRequest(objectModel).getParameter("group_by");
        return s != null ? s : "none";
    }

    protected String getParameterOrder() {
        return ObjectModelHelper.getRequest(objectModel).getParameter("order");
    }

    protected String getParameterScope() {
        return ObjectModelHelper.getRequest(objectModel).getParameter("scope");
    }

    protected int getParameterEtAl() {
        try {
            return Integer.parseInt(ObjectModelHelper.getRequest(objectModel).getParameter("etal"));
        }
        catch (Exception e) {
            return 0;
        }
    }

    /**
     * Determine if the scope of the search should fixed or is changeable by the
     * user.
     * <p/>
     * The search scope when performed by url, i.e. they are at the url handle/xxxx/xx/search
     * then it is fixed. However at the global level the search is variable.
     *
     * @return true if the scope is variable, false otherwise.
     */
    protected boolean variableScope() throws SQLException {
        return (HandleUtil.obtainHandle(objectModel) == null);
    }

    /**
     * Extract the query string. Under most implementations this will be derived
     * from the url parameters.
     *
     * @return The query string.
     */
    protected abstract String getQuery() throws UIException;

    /**
     * Generate a url to the given search implementation with the associated
     * parameters included.
     *
     * @param parameters
     * @return The post URL
     */
    protected abstract String generateURL(Map<String, String> parameters)
            throws UIException;


    /**
     * Recycle
     */
    public void recycle() {
        this.queryArgs = null;
        this.queryResults = null;
        this.validity = null;
        super.recycle();
    }


    protected void buildSearchControls(Division div)
            throws WingException, SQLException {


        DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
        DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(dso);

        Division searchControlsGear = div.addDivision("masked-page-control").addDivision("search-controls-gear", "controls-gear-wrapper");


        /**
         * Add sort by options, the gear will be rendered by a combination fo javascript & css
         */
        String currentSort = getParameterSortBy();
        org.dspace.app.xmlui.wing.element.List sortList = searchControlsGear.addList("sort-options", org.dspace.app.xmlui.wing.element.List.TYPE_SIMPLE, "gear-selection");
        sortList.addItem("sort-head", "gear-head first").addContent(T_sort_by);
        DiscoverySortConfiguration searchSortConfiguration = discoveryConfiguration.getSearchSortConfiguration();

        org.dspace.app.xmlui.wing.element.List sortOptions = sortList.addList("sort-selections");
        boolean selected = ("score".equals(currentSort) || (currentSort == null && searchSortConfiguration.getDefaultSort() == null));
        sortOptions.addItem("relevance", "gear-option" + (selected ? " gear-option-selected" : "")).addXref("sort_by=score&order=" + searchSortConfiguration.getDefaultSortOrder(), T_sort_by_relevance);

        if(searchSortConfiguration.getSortFields() != null)
        {
            for (DiscoverySortFieldConfiguration sortFieldConfiguration : searchSortConfiguration.getSortFields())
            {
                String sortField = SearchUtils.getSearchService().toSortFieldIndex(sortFieldConfiguration.getMetadataField(), sortFieldConfiguration.getType());

                boolean selectedAsc = ((sortField.equals(currentSort) && "asc".equals(getParameterOrder())) || (sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())) && DiscoverySortConfiguration.SORT_ORDER.asc.equals(searchSortConfiguration.getDefaultSortOrder()));
                boolean selectedDesc= ((sortField.equals(currentSort) && "desc".equals(getParameterOrder())) || (sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())) && DiscoverySortConfiguration.SORT_ORDER.desc.equals(searchSortConfiguration.getDefaultSortOrder()));
                String sortFieldParam = "sort_by=" + sortField + "&order=";
                sortOptions.addItem(sortField, "gear-option" + (selectedAsc ? " gear-option-selected" : "")).addXref(sortFieldParam + "asc", message("xmlui.Discovery.AbstractSearch.sort_by." + sortField + "_asc"));
                sortOptions.addItem(sortField, "gear-option" + (selectedDesc ? " gear-option-selected" : "")).addXref(sortFieldParam + "desc", message("xmlui.Discovery.AbstractSearch.sort_by." + sortField + "_desc"));
            }
        }

        //Add the rows per page
        sortList.addItem("rpp-head", "gear-head").addContent(T_rpp);
        org.dspace.app.xmlui.wing.element.List rppOptions = sortList.addList("rpp-selections");
        for (int i : RESULTS_PER_PAGE_PROGRESSION)
        {
            rppOptions.addItem("rpp-" + i, "gear-option" + (i == getParameterRpp() ? " gear-option-selected" : "")).addXref("rpp=" + i, Integer.toString(i));
        }
    }

    /**
     * Determine the current scope. This may be derived from the current url
     * handle if present or the scope parameter is given. If no scope is
     * specified then null is returned.
     *
     * @return The current scope.
     */
    protected DSpaceObject getScope() throws SQLException {
        Request request = ObjectModelHelper.getRequest(objectModel);
        String scopeString = request.getParameter("scope");

        // Are we in a community or collection?
        DSpaceObject dso;
        if (scopeString == null || "".equals(scopeString))
        {
            // get the search scope from the url handle
            dso = HandleUtil.obtainHandle(objectModel);
        }
        else
        {
            // Get the search scope from the location parameter
            dso = HandleManager.resolveToObject(context, scopeString);
        }

        return dso;
    }

    protected void logSearch() {
        int countCommunities = 0;
        int countCollections = 0;
        int countItems = 0;

        /**
         * TODO: Maybe we can create a default "type" facet for this
         * will give results for Items, Communities and Collection types
         * benefits... no iteration over results at all to sum types
         * leaves it upto solr...

         for (Object type : queryResults.getHitTypes())
         {
         if (type instanceof Integer)
         {
         switch (((Integer)type).intValue())
         {
         case Constants.ITEM:       countItems++;        break;
         case Constants.COLLECTION: countCollections++;  break;
         case Constants.COMMUNITY:  countCommunities++;  break;
         }
         }
         }
         */
        String logInfo = "";

        try {
            DSpaceObject dsoScope = getScope();

            if (dsoScope instanceof Collection) {
                logInfo = "collection_id=" + dsoScope.getID() + ",";
            } else if (dsoScope instanceof Community) {
                logInfo = "community_id=" + dsoScope.getID() + ",";
            }
        }
        catch (SQLException sqle) {
            // Ignore, as we are only trying to get the scope to add detail to the log message
        }

        log.info(LogManager.getHeader(context, "search", logInfo + "query=\""
                + (queryArgs == null ? "" : queryArgs.getQuery()) + "\",results=(" + countCommunities + ","
                + countCollections + "," + countItems + ")"));
    }
}
