# Source: src/js/views/queryBuilder/QueryBuilderView.js

    define(["jquery",
        "underscore",
        "backbone",
        "collections/Filters",
        "collections/queryFields/QueryFields",
        "views/searchSelect/SearchableSelectView",
        "views/queryBuilder/QueryRuleView",
        "text!templates/queryBuilder/queryBuilder.html"
      ],
      function($, _, Backbone, Filters, QueryFields, SearchableSelect, QueryRule, Template) {

        /**
         * @class QueryBuilderView
         * @classdesc A view that provides a UI for users to construct a complex search
         * through the DataONE Solr index
         * @classcategory Views/QueryBuilder
         * @screenshot views/QueryBuilderView.png
         * @extends Backbone.View
         * @constructor
         * @since 2.14.0
         */
        var QueryBuilderView = Backbone.View.extend(
          /** @lends QueryBuilderView.prototype */
          {

            /**
            * The type of View this is
            * @type {string}
            */
            type: "QueryBuilderView",

            /**
             * The HTML class names for this view element
             * @type {string}
             */
            className: "query-builder",

            /**
             * A JQuery selector for the element in the template that will contain the query
             * rules
             * @type {string}
             */
            rulesContainerSelector: ".rules-container",

            /**
             * An ID for the element in the template that a user should click to add a new
             * rule. A unique ID will be appended to this ID, and the ID will be added to the
             * template.
             * @type {string}
             * @since 2.17.0
             */
            addRuleButtonID: "add-rule-",

            /**
             * An ID for the element in the template that a user should click to add a new
             * rule group. A unique ID will be appended to this ID, and the ID will be added
             * to the template.
             * @type {string}
             * @since 2.17.0
             */
            addRuleGroupButtonID: "add-rule-group-",

            /**
             * A JQuery selector for the element in the template that will contain the input
             * allowing a user to switch the exclude attribute from "include" to "exclude"
             * (i.e. to switch between exclude:false and exclude:true in the filterGroup
             * model.)
             * @type {string}
             * @since 2.17.0
             */
            excludeInputSelector: ".exclude-input",

            /**
             * A JQuery selector for the element in the template that will contain the input
             * allowing a user to switch the operator from "all" to "any" (i.e. to switch
             * between operator:"AND" and exclude:"OR" in the filterGroup model.)
             * @type {string}
             * @since 2.17.0
             */
            operatorInputSelector: ".operator-input",

            /**
             * The maximum number of levels nested Rule Groups (i.e. nested FilterGroup
             * models) that a user is permitted to *build* in the Query Builder. If a
             * Portal/Collection document is loaded into the Query Builder that has more than
             * the maximum allowable nested levels, those levels will still be displayed. This
             * only prevents the "Add Rule Group" button from being shown.
             * @type {number}
             * @since 2.17.0
             */
            nestedLevelsAllowed: 1,

            /**
             * An array of hex color codes used to help distinguish between different rules
             * @type {string[]}
             */
            ruleColorPalette: ["#44AA99", "#137733", "#c9a538", "#CC6677", "#882355",
              "#AA4499","#332288"
            ],

            /**
             * Query fields to exclude in the metadata field selector of each Query Rule. This
             * is a list of field names that exist in the query service index (i.e. Solr), but
             * which should be hidden in the Query Builder
             * @type {string[]}
             */
            excludeFields: [],

            /**
             * Query fields to exclude in the metadata field selector for any Query Rules that
             * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of
             * field names that exist in the query service index (i.e. Solr), but which should
             * be hidden in nested Query Builders
             * @type {string[]}
             */
            nestedExcludeFields: [],

            /**
             * Query fields that do not exist in the query service index, but which we would
             * like to show as options in the Query Builder field input.
             *
             * @type {SpecialField[]}
             *
             * @since 2.15.0
             */
            specialFields: [],

            /**
            * A Filters collection that stores filters to be edited with this Query Builder,
            * e.g. the definitionFilters in a Collection or Portal model. If a filterGroup is
            * set, then collection doesn't necessarily need to be set, as the Filters
            * collection from within the FilterGroup model will automatically be set on view.
            * @type {Filters}
            */
            collection: null,

            /**
            * The FilterGroup model that stores the filters, the exclude attribute, and the
            * group operator to be edited with this Query Builder. This does not need to be
            * set; just a Filters collection can be set on the view instead, but then there
            * will be no input to switch between the include & exclude and any & all, since
            * these are the exclude and operator attributes on the filterGroup model.
            * @type {FilterGroup}
            * @since 2.17.0
            */
            filterGroup: null,

            /**
             * The primary HTML template for this view
             * @type {Underscore.template}
             */
            template: _.template(Template),

            /**
             * events - A function that specifies a set of DOM events that will be bound to
             * methods on your View through Backbone.delegateEvents.
             * @see {@link https://backbonejs.org/#View-events}
             *
             * @return {Object}  The events hash
             */
            events: function(){
              try {
                var events = {};
                var addRuleAction = "click #" + this.addRuleButtonID + this.cid;
                events[addRuleAction] = "addQueryRule"
                var addRuleGroupAction = "click #" + this.addRuleGroupButtonID + this.cid;
                events[addRuleGroupAction] = "addQueryRuleGroup"
                return events
              } catch (e) {
                console.error("Failed to specify events for  the Query Builder View," +
                  " error message: " + e);
              }
            },

            /**
             * The list of QueryRuleViews that are contained within this queryBuilder
             * @type {QueryRuleView[]}
             */
            rules: [],

            /**
             * Creates a new QueryBuilderView
             * @param {Object} options - A literal object with options to pass to the view
             */
            initialize: function(options) {

              try {

                // Get all the options and apply them to this view
                if (typeof options == "object") {
                  var optionKeys = Object.keys(options);
                  _.each(optionKeys, function(key, i) {
                    this[key] = options[key];
                  }, this);
                }

                // If neither a Filters collection nor a FilterGroup model is provided in the
                // options for this view, then create a new FilterGroup model and set it on
                // the view.
                if(!this.collection && !this.filterGroup){
                  this.filterGroup = new FilterGroup()
                }

                // If there is a FilterGroup model set, but no Filters collection, then use
                // the Filters from within the FilterGroup model as the Filters collection.
                if(!this.collection && this.filterGroup){
                  this.collection = this.filterGroup.get("filters")
                }

              } catch (e) {
                console.error(
                  "Failed to initialize the Query Builder view, error message:", e
                );
              }
            },

            /**
             * render - Render the view
             *
             * @return {QueryBuilder}  Returns the view
             */
            render: function() {

              try {

                // Ensure the query fields are cached for the Query Field Select View and the
                // Query Rule View
                if (
                  typeof MetacatUI.queryFields === "undefined" ||
                  MetacatUI.queryFields.length === 0
                ) {
                  MetacatUI.queryFields = new QueryFields();
                  this.listenToOnce(MetacatUI.queryFields, "sync", this.render)
                  MetacatUI.queryFields.fetch();
                  return
                }

                // Insert the template into the view
                this.$el.html(this.template({
                  addRuleButtonID: this.addRuleButtonID + this.cid,
                  addRuleGroupButtonID: this.addRuleGroupButtonID + this.cid,
                }));

                // Nested Query Builders are used to display nested filterGroup models.
                // They need to be styled slightly different from the parent Query Builder.
                if(this.parentRule){
                  this.$el.addClass("nested")
                }

                // Remove the rule group button ID if no more nested Query Builders are
                // allowed.
                if(
                  typeof this.nestedLevelsAllowed == "number" &&
                  this.nestedLevelsAllowed < 1
                ){
                  this.$el.find("#" + this.addRuleGroupButtonID + this.cid).remove()
                };

                // Save the rules container element to the view before we add any nested
                // QueryBuilders (nested FilterGroups), since their rules container uses the
                // same selector.
                this.rulesContainer = this.$el.find(this.rulesContainerSelector);

                // If there is a FilterGroup model set on this view (not just a Filters
                // collection) then render the inputs that allow a user to edit the "exclude"
                // and "operator" attributes
                if(this.filterGroup){
                  this.renderExcludeOperatorInputs();
                }

                // Add a row for each rule that exists already in the model
                if(
                  this.collection && this.collection.models &&
                  this.collection.models.length
                ){
                  this.collection.models.forEach(function(model){
                    this.addQueryRule(model)
                  }, this);
                }
                // Render a new Query Rule at the end
                this.addQueryRule();

                return this;

              } catch (e) {
                console.error("Failed to render a Query Builder view, error message: ", e);
              }
            },

            /**
             * Insert two inputs: one that allows the user to edit the "exclude" attribute in
             * the FilterGroup model by selecting either "include" or "exclude"; and a second
             * that allows the user to edit the "operator" attribute in the FilterGroup model
             * by selecting between "all" and "any".
             * @since 2.17.0
             */
            renderExcludeOperatorInputs: function(){

              try {

                if(!this.filterGroup){
                  console.log("A filterGroup model is required to edit the exclude and " +
                    "operator attributes in a Query Builder View.");
                  return
                }

                // Select the elements in the template where the two inputs should be inserted
                var excludeContainer = this.$el.find(this.excludeInputSelector);
                var operatorContainer = this.$el.find(this.operatorInputSelector);
                // Create the exclude input
                var excludeInput = new SearchableSelect({
                  options: [
                    { label: "Include",
                      value: "false",
                      description: "Include all datasets with metadata that matches the rules" +
                        " that are set below."
                    },
                    { label: "Exclude",
                      value: "true",
                      description: "Match any dataset except those with metadata that match" +
                        " the rules that are set below"
                    }
                  ],
                  allowMulti: false,
                  allowAdditions: false,
                  inputLabel: "",
                  selected: [this.filterGroup.get("exclude").toString()],
                  clearable: false,
                });
                // Create the operator input
                var operatorInput = new SearchableSelect({
                  options: [
                    { label: "all",
                      value: "AND",
                      description: "For a dataset to match, it must have metadata that " +
                        "matches every rule set below."
                    },
                    { label: "any",
                      value: "OR",
                      description: "For a dataset to match, its metadata only needs to " +
                      "match one of the rules set below."
                    }
                  ],
                  allowMulti: false,
                  allowAdditions: false,
                  inputLabel: "",
                  selected: [this.filterGroup.get("operator")],
                  clearable: false,
                })
                // Update the FilterGroup model when the user changes the operator or exclude
                // options. newValues will always be an Array, but since these inputs don't
                // allow multiple selections (allowMulti: false), then there will only ever be
                // one value.
                this.stopListening(excludeInput)
                this.listenTo(excludeInput, "changeSelection", function(newValues){
                  // Convert the string (necessary to be used as a value in SearchableSelect)
                  // to a boolean. It should be "true" or "false".
                  var newExclude = newValues[0] == "true";
                  this.filterGroup.set("exclude", newExclude);
                });
                this.stopListening(operatorInput)
                this.listenTo(operatorInput, "changeSelection", function(newValues){
                  this.filterGroup.set("operator", newValues[0]);
                });
                // Render the inputs and insert them into the view. Replace the default text
                // within the containers otherwise.
                excludeContainer.html(excludeInput.render().el);
                operatorContainer.html(operatorInput.render().el);
              } catch (error) {
                console.log("There was a problem rendering the exclude and operator " +
                "inputs in a QueryBuilderView, error details: " + error);
              }
            },

            /**
             * Appends a new row (Query Rule View) to the end of the Query Builder
             *
             * @param {Filter|FilterGroup} filterModel The Filter model or FilterGroup model
             * for which to create a rule. If none is provided, then a Filter group model
             * will be created and added to the collection.
             */
            addQueryRule: function(filterModel){
              try {

                // Ensure that the object passed to this function is a filter. When the "add
                // rule" button is clicked, the Event object is passed to this function
                // instead. If no filter model is provided, assume that this is a new rule
                if(!filterModel || (filterModel && !/filter/i.test(filterModel.type))){
                  filterModel = this.collection.add({
                    nodeName: "filter",
                    operator: "OR",
                    fieldsOperator: "OR"
                  });
                }

                // Don't show invisible rules
                if(filterModel.get("isInvisible")){
                  return
                }

                // insert QueryRuleView
                var rule = new QueryRule({
                  model: filterModel,
                  ruleColorPalette: this.ruleColorPalette,
                  excludeFields: this.excludeFields,
                  nestedExcludeFields: this.nestedExcludeFields,
                  specialFields: this.specialFields,
                  parentRule: this.parentRule,
                  nestedLevelsAllowed: this.nestedLevelsAllowed,
                });

                // Insert and render the rule
                this.rulesContainer.append(rule.el);
                rule.render();
                // Add the rule to the list of rule sub-views
                // TODO: is this really needed? are they removed when rule removed?
                this.rules.push(rule);

              } catch (e) {
                console.error("Error adding a Query Rule, error message:", e);
              }
            },

            /**
             * Exactly the same as {@link QueryBuilderView#addQueryRule}, except that if no
             * model is provided to this function, then a FilterGroup model will be created
             * instead of a Filter model.
             * @param  {FilterGroup} filterGroupModel
             */
            addQueryRuleGroup: function(filterGroupModel){
              try {
                // Ensure that the object passed to this function is a filter. When the "add
                // rule" button is clicked, the Event object is passed to this function
                // instead. If no filter model is provided, assume that this is a new rule
                if(!filterGroupModel || (filterGroupModel && filterGroupModel.type != "FilterGroup")){
                  filterGroupModel = this.collection.add({
                    filterType: "FilterGroup",
                  });
                };
                this.addQueryRule(filterGroupModel)
              } catch (error) {
                console.log("Error adding a Query Rule Group in a Query Builder View. " +
                "Error details: " + error);
              }
            },

          });
          return QueryBuilderView;
      });