# Source: src/js/collections/Filters.js

    define([
      "jquery", "underscore", "backbone",
      "models/filters/Filter", "models/filters/BooleanFilter", "models/filters/ChoiceFilter",
      "models/filters/DateFilter", "models/filters/NumericFilter", "models/filters/ToggleFilter",
    ],
      function (
        $, _, Backbone,
        Filter, BooleanFilter, ChoiceFilter,
        DateFilter, NumericFilter, ToggleFilter,
      ) {
        "use strict";

        /**
         * @class Filters
         * @classdesc A collection of Filter models that represents a full search
         * @classcategory Collections
         * @name Filters
         * @extends Backbone.Collection
        * @constructor
         */
        var Filters = Backbone.Collection.extend(
              /** @lends Filters.prototype */{

            /**
            * If the search results must always match one of the ids in the id filters,
            * then the id filters will be added to the query with an AND operator.
            * @type {boolean}
            */
            mustMatchIds: false,

            /**
            * Function executed whenever a new Filters collection is created.
            * @param {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup[]} models -
            * Array of filter or filter group models to add to this creation
            * @param {Object} [options] -
            * @property {boolean} isUIFilterType - Set to true to indicate that these filters
            * or filterGroups are part of a UIFilterGroup (aka custom Portal search filter).
            * Otherwise, it's assumed that this model is in a Collection model definition.
            * @property {XMLElement} objectDOM -  A FilterGroupType or UIFilterGroupType XML
            * element from a portal or collection document. If provided, the XML will be
            * parsed and the Filters models extracted
            * @property {boolean} catalogSearch  - If set to true, a catalog search phrase
            * will be appended to the search query that limits the results to un-obsoleted
            * metadata.
            */
            initialize: function (models, options) {
              try {
                if (typeof options === "undefined") {
                  var options = {};
                }
                if (options && options.objectDOM) {
                  // Models are automatically added to the collection by the parse function.
                  var isUIFilterType = options.isUIFilterType == true ? true : false
                  this.parse(options.objectDOM, isUIFilterType);
                }
                if (options.catalogSearch) {
                  this.catalogSearchQuery = this.createCatalogSearchQuery();
                }
              } catch (error) {
                console.log("Error initializing a Filters collection. Error details: " + error);
              }
            },

            /**
            * Creates the type of Filter Model based on the given filter type. This
            * function is typically not called directly. It is used by Backbone.js when adding
            * a new model to the collection.
            * @param {object} attrs - A literal object that contains the attributes to pass to the model
            * @property {string} attrs.filterType - The type of Filter to create
            * @property {XMLElement} attrs.objectDOM - The Filter XML
            * @param {object} options - A literal object of additional options to pass to the model
            * @returns {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup}
            */
            model: function (attrs, options) {

              // Get the model type
              var type = ""
              // If no filterType was specified, but an objectDOM exists (from parsing a
              // Collection or Portal document), get the filter type from the objectDOM
              // node name
              if (!attrs.filterType && attrs.objectDOM) {
                type = attrs.objectDOM.nodeName;
              } else if (attrs.filterType) {
                type = attrs.filterType;
              } else if (attrs.nodeName){
                type = attrs.nodeName
              }
              // Ignoring the case of the type allows using either the
              // filter type (e.g. BooleanFilter) or the nodeName value
              // (e.g. "booleanFilter")
              type = type.toLowerCase();

              switch (type) {
                case "booleanfilter":
                  return new BooleanFilter(attrs, options);

                case "datefilter":
                  return new DateFilter(attrs, options);

                case "numericfilter":
                  return new NumericFilter(attrs, options);

                case "filtergroup":
                  // We must initialize a Filter Group using the inline require syntax to
                  // avoid the problem of circular dependencies. Filters requires Filter
                  // Groups, and Filter Groups require Filters. For more info, see
                  // https://requirejs.org/docs/api.html#circular
                  var FilterGroup = require('models/filters/FilterGroup');
                  var newFilterGroup = new FilterGroup(attrs, options)
                  return newFilterGroup;

                case "choicefilter":
                  return new ChoiceFilter(attrs, options);

                case "togglefilter":
                  return new ToggleFilter(attrs, options);

                default:
                  return new Filter(attrs, options);
              }

            },

            /**
             * Parses a <filterGroup> or <definition> element from a collection or portal
             * document and sets the resulting models on this collection.
             *
             *  @param {XMLElement} objectDOM - A FilterGroupType or UIFilterGroupType XML
             *  element from a portal or collection document
             *  @param {boolean} isUIFilterType - Set to true to indicate that these filters
             *  or filterGroups are part of a UIFilterGroup (aka custom Portal search filter).
             *  Otherwise, it's assumed that the filters are part of a Collection model
             *  definition.
             *  @return {JSON} The result of the parsed XML, in JSON.
            */
            parse: function (objectDOM, isUIFilterType) {

              var filters = this;

              $(objectDOM).children().each(function (i, filterNode) {
                filters.add({
                  objectDOM: filterNode,
                  isUIFilterType: isUIFilterType == true ? true : false
                })
              });

              return filters.toJSON();
            },

            /**
             * Builds the query string to send to the query engine. Iterates over each filter
             * in the collection and adds to the query string.
             *
             * @param {string} [operator=AND] The operator to use to combine multiple filters in this filter group. Must be AND or OR.
             * @return {string} The query string to send to Solr
             */
            getQuery: function (operator = "AND") {

              // The complete query string that eventually gets returned
              var completeQuery = ""

              // Ensure that the operator is AND or OR so that the query string will be valid.
              // Default to AND.
              if (typeof operator !== "string") {
                var operator = "AND";
              }
              operator = operator.toUpperCase();
              if(!["AND", "OR"].includes(operator)){
                operator = "AND"
              }

              // Adds URI encoded spaces to either side of a string
              var padString = function(string){ return "%20" + string + "%20" }

              // Get the list of filters that use id fields since these are used differently.
              var idFilters = this.getIdFilters();
              // Get the remaining filters that don't contain any ID fields
              var mainFilters = this.getNonIdFilters();

              // Create the grouped query for the id filters
              var idFilterQuery = this.getGroupQuery(idFilters, "OR");
              // Make a query for all of the filters that do not contain ID fields
              var mainQuery = this.getGroupQuery(mainFilters, operator);

              // First add the query string built from the non-ID filters
              completeQuery += mainQuery;

              // Then add the Data Catalog filters if Filters was initialized with the
              // catalogSearch = true option. Filters that are used in the data catalog are
              // treated specially
              if(this.catalogSearchQuery && this.catalogSearchQuery.length){
                // If there are other filters besides the catalog filters, AND the catalog
                // filters to the end of the query for the other filters, regardless of which
                // operator this function uses to combine other filters.
                if (completeQuery && completeQuery.trim().length) {
                  completeQuery += padString("AND");
                }
                completeQuery += this.catalogSearchQuery
              }

              // Finally, add the ID filters to the very end of the query. This is done so
              // that the query string is constructed with these filters "OR"ed into the
              // query. For example, a query might be to look for datasets by a certain
              // scientist OR with the given id. If those filters were ANDed together, the
              // search would essentially ignore the creator filter and only return the
              // dataset with the matching id.
              if(idFilterQuery && idFilterQuery.trim().length){
                if (completeQuery && completeQuery.trim().length) {
                  // If the search results must always match one of the ids in the id filters,
                  // then add the id filters to the query with the AND operator. This flag
                  // is set on this Collection. Otherwise, use the OR operator
                  var idOperator = this.mustMatchIds ? padString("AND") : padString("OR");
                  completeQuery = "(" + completeQuery + ")" + idOperator + idFilterQuery;
                } else {
                  // If the query is ONLY made of id filters, then the id filter query is the
                  // complete query
                  completeQuery += idFilterQuery
                }
              }

              // Return the completed query
              return completeQuery;

            },

            /**
             * Searches the Filter models in this collection and returns any that have at
             * least one field that matches any of the ID query fields, such as by id, seriesId, or the isPartOf relationship.
             * @returns {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup[]}
             * Returns an array of filter models that include at least one ID field
             */
            getIdFilters: function(){
              try {
                return this.filter(function (filterModel) {
                  if(typeof filterModel.isIdFilter == "undefined"){
                    return false
                  }
                  return filterModel.isIdFilter()
                });
              } catch (error) {
                console.log("Error trying to find ID Filters, error details: " + error);
              }
            },

            /**
             * Searches the Filter models in this collection and returns all have no fields
             * matching any of the ID query fields.
             * @returns {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup[]}
             * Returns an array of filter models that do not include any ID fields
             */
            getNonIdFilters: function(){
              try {
                return this.difference(this.getIdFilters());
              } catch (error) {
                console.log("Error trying to find non-ID Filters, error details: " + error);
              }
            },

            /**
            * Get a query string for a group of Filters.
            * The Filters will be ANDed together, unless a different operator is given.
            * @param {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup[]} filterModels - The Filters to turn into a query string
            * @param {string} [operator="AND"] - The operator to use between filter models
            * @return {string} The query string
            */
            getGroupQuery: function (filterModels, operator="AND") {

              try {
                if(!filterModels || !filterModels.length || !this.getNonEmptyFilters(filterModels)){
                  return ""
                }
                //Start an array to contain the query fragments
                var groupQueryFragments = [];

                //For each Filter in this group, get the query string
                _.each(filterModels, function (filterModel) {
                  // Get the Solr query string from this model. Pass on the group operator so
                  // that we can detect whether this filter query needs a positive clause in
                  // case it has exclude set to true.
                  var filterQuery = filterModel.getQuery(operator);
                  //Add the filter query string to the overall array
                  if (filterQuery && filterQuery.length > 0) {
                    groupQueryFragments.push(filterQuery);
                  }
                }, this);

                //Join this group's query fragments with an OR operator
                if (groupQueryFragments.length) {
                  var queryString = groupQueryFragments.join("%20" + operator + "%20");
                  if(groupQueryFragments.length > 1){
                    queryString = "(" + queryString + ")"
                  }
                  return queryString
                }
                //Otherwise, return an empty string
                else {
                  return "";
                }
              } catch (error) {
                console.log("Error creating a group query, returning a blank string. " +
                  " Error details: " + error);
                return ""
              }

            },

            /**
             * Given a Solr field name, determines if that field is set as a filter option
             */
            filterIsAvailable: function (field) {

              var matchingFilter = this.find(function (filterModel) {
                return _.contains(filterModel.fields, field);
              });

              if (matchingFilter) {
                return true;
              } else {
                return false;
              }
            },

            /*
             * Returns an array of filter models in this collection that have a value set
             *
             * @return {Array} - an array of filter models in this collection that have a value set
             */
            getCurrentFilters: function () {
              var currentFilters = new Array();

              this.each(function (filterModel) {
                //If the filter model has values set differently than the default AND it is
                // not an invisible filter, then add it to the current filters array
                if (!filterModel.get("isInvisible") &&
                  ((Array.isArray(filterModel.get("values")) && filterModel.get("values").length &&
                    _.difference(filterModel.get("values"), filterModel.defaults().values).length) ||
                    (!Array.isArray(filterModel.get("values")) && filterModel.get("values") !== filterModel.defaults().values))
                ) {
                  currentFilters.push(filterModel);
                }
              });

              return currentFilters;
            },

            /*
             * Clear the values of all geohash-related models in the collection
             */
            resetGeohash: function () {
              //Find all the filters in this collection that are related to geohashes
              this.each(function (filterModel) {
                if (!filterModel.get("isInvisible") &&
                  (filterModel.type == "SpatialFilter" ||
                    _.intersection(filterModel.fields, ["geohashes", "geohashLevel", "geohashGroups"]).length)) {
                  filterModel.resetValue();
                }
              });
            },

            /**
             * Create a partial query string that's required for catalog searches
             * @returns {string} - Returns the query string fragment for a catalog search
             */
            createCatalogSearchQuery: function(){
              var catalogFilters = new Filters([
                {
                  fields: ["obsoletedBy"],
                  values: ["*"],
                  exclude: true
                },
                {
                  fields: ["formatType"],
                  values: ["METADATA"],
                  matchSubstring: false
                }]);
              var query = catalogFilters.getGroupQuery(catalogFilters.models, "AND");
              return query
            },

            /**
            * Creates and adds a Filter to this collection that filters datasets
            * to only those that the logged-in user has permission to change permission of.
            */
            addOwnershipFilter: function () {

              if (MetacatUI.appUserModel.get("loggedIn")) {
                //Filter datasets by their ownership
                this.add({
                  fields: ["rightsHolder", "changePermission"],
                  values: MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
                  operator: "OR",
                  fieldsOperator: "OR",
                  matchSubstring: false,
                  exclude: false
                });
              }

            },

            /**
            * Creates and adds a Filter to this collection that filters datasets
            * to only those that the logged-in user has permission to write to.
            */
            addWritePermissionFilter: function () {

              if (MetacatUI.appUserModel.get("loggedIn")) {
                //Filter datasets by their ownership
                this.add({
                  fields: ["rightsHolder", "writePermission", "changePermission"],
                  values: MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
                  operator: "OR",
                  fieldsOperator: "OR",
                  matchSubstring: false,
                  exclude: false
                });
              }

            },

            /**
            * Removes Filter models from this collection if they match the given field
            * @param {string} field - The field whose matching filters that should be removed from this collection
            */
            removeFiltersByField: function (field) {

              var toRemove = [];

              this.each(function (filter) {
                if (filter.get && filter.get("fields").includes(field)) {
                  toRemove.push(filter);
                }
              });

              this.remove(toRemove);

            },

            /**
             * Remove filters from the collection that are
             * lacking fields, values, and in the case of a numeric filter,
             * a min and max value.
             * @param {boolean} [recursive=false] - Set to true to also remove empty filters
             * from within any and all nested filterGroups.
             */
            removeEmptyFilters: function(recursive = false){

              try {
                var toRemove = this.difference(this.getNonEmptyFilters());
                this.remove(toRemove);

                if (recursive){
                  var nestedGroups = this.filter(function (filterModel) {
                    return filterModel.type == "FilterGroup" });

                  if(nestedGroups){
                    nestedGroups.forEach(function(filterGroupModel){
                      filterGroupModel.get("filters").removeEmptyFilters(true)
                    });
                  }
                }

              } catch (e) {
                console.log("Failed to remove empty Filter models from the Filters collection, error message: " + e);
              }

            },

            /**
             * getNonEmptyFilters - Returns the array of filters that are not empty
             * @return {Filter|BooleanFilter|ChoiceFilter|DateFilter|NumericFilter|ToggleFilter|FilterGroup[]}
             * returns an array of Filter or FilterGroup models that are not empty
             */
            getNonEmptyFilters: function(){
              try {
                return this.filter(function(filterModel){
                  return !filterModel.isEmpty();
                });
              } catch (e) {
                console.log("Failed to remove empty Filter models from the Filters collection, error message: " + e);
              }
            },

            /**
             * Remove a Filter from the Filters collection silently, and
             * replace it with a new model.
             *
             * @param  {Filter} model    The model to replace
             * @param  {object} newAttrs Attributes for the replacement model. Use the filterType attribute to replace with a different type of Filter.
             * @return {Filter}          Returns the replacement Filter model, which is already part of the Filters collection.
             */
            replaceModel: function (model, newAttrs) {
              try {
                var index = this.indexOf(model),
                  oldModelId = model.cid;

                this.remove(oldModelId, { silent: true });

                var newModel = this.add(
                  newAttrs,
                  { at: index }
                );

                return newModel;
              } catch (e) {
                console.log("Failed to replace a Filter model in a Filters collection, " + e);
              }
            },

            /**
             * visibleIndexOf - Get the index of a given model, excluding any
             * filters that are marked as invisible.
             *
             * @param  {Filter|BooleanFilter|NumericFilter|DateFilter} model The filter model for which to get the visible index
             * @return {number} An integer representing the filter model's position in the list of visible filters.
             */
            visibleIndexOf: function(model){
              try {
                // Don't count invisible filters in the index we display to the user
                var visibleFilters = this.filter(function(filterModel){
                  var isInvisible = filterModel.get("isInvisible");
                  return typeof isInvisible == "undefined" || isInvisible === false
                });
                return _.indexOf(visibleFilters, model);
              } catch (e) {
                console.log("Failed to get the index of a Filter within the collection of visible Filters, error message: " + e);
              }
            },

            /*
            hasGeohashFilter: function() {

                var currentFilters = this.getCurrentFilters();
                var geohashFilter = _.find(currentFilters, function(filterModel){
                    return (_.intersection(filterModel.get("fields"),
                        ["geohashes", "geohash"]).length > 0);
                });

                if(geohashFilter) {
                    return true;
                } else {
                    return false;
                }
            }
            */
          });
        return Filters;
      });