# Source: src/js/views/searchSelect/QueryFieldSelectView.js

    define([
        "jquery",
        "underscore",
        "backbone",
        "views/searchSelect/SearchableSelectView",
        "collections/queryFields/QueryFields"
      ],
      function($, _, Backbone, SearchableSelect, QueryFields) {

        /**
         * @class QueryFieldSelectView
         * @classdesc A select interface that allows the user to search for and
         * select metadata field(s).
         * @classcategory Views/SearchSelect
         * @extends SearchableSelect
         * @constructor
         * @since 2.14.0
         * @screenshot views/searchSelect/QueryFieldSelectView.png
         */
        var QueryFieldSelectView = SearchableSelect.extend(
          /** @lends QueryFieldSelectView.prototype */
          {
            /**
             * The type of View this is
             * @type {string}
             */
            type: "QueryFieldSelect",

            /**
             * className - the class names for this view element
             *
             * @type {string}
             */
            className: SearchableSelect.prototype.className + " query-field-select",

            /**
             * Text to show in the input field before any value has been entered
             * @type {string}
             */
            placeholderText: "Search for or select a field",

            /**
             * Label for the input element
             * @type {string}
             */
            inputLabel: "Select one or more metadata fields to query",

            /**
             * @see SearchableSelectView#submenuStyle
             * @default "accordion"
             */
            submenuStyle: "accordion",

            /**
             * A list of query fields names to exclude from the list of options
             * @type {string[]}
             */
            excludeFields: [],

            /**
             * An additional field object contains the properties an additional query field to
             * add that are required to render it correctly. An additional query field is one
             * that does not actually exist in the query service index.
             *
             * @typedef {Object} AdditionalField
             *
             * @property {string} name - A unique ID to represent this field. It must not
             * match the name of any other query fields.
             * @property {string[]} fields - The list of real query fields that this
             * abstracted field will represent. It must exactly match the names of the query
             * fields that actually exist.
             * @property {string} label - A user-facing label to display.
             * @property {string} description - A description for this field.
             * @property {string} category - The name of the category under which to place
             * this field. It must match one of the category names for an existing query
             * field.
             *
             * @since 2.15.0
             */

            /**
             * A list of additional fields which are not retrieved from the query service
             * index, but which should be added to the list of options. This can be used to
             * add abstracted fields which are a combination of multiple query fields, or to
             * add a duplicate field that has a different label.
             *
             * @type {AdditionalField[]}
             * @since 2.15.0
             */
            addFields: [],

            /**
             * A list of query fields names to display at the top of the menu, above
             * all other category headers
             * @type {string[]}
             */
            commonFields: ["text", "documents-special-field"],

            /**
             * The names of categories that should have items sorted alphabetically. Names
             * must exactly match those in the
             * {@link QueryField#categoriesMap Query Field model}
             * @type {string[]}
             * @since 2.15.0
             */
            categoriesToAlphabetize: ["General"],

            /**
             * Whether or not to exclude fields which are not searchable. Set to
             * false to keep query fields that are not searchable in the returned list
             * @type {boolean}
             */
            excludeNonSearchable: true,

            /**
             * Creates a new QueryFieldSelectView
             * @param {Object} options - A literal object with options to pass to the view
             */
            initialize: function(options){
              try {
                // Ensure the query fields are cached
                if ( typeof MetacatUI.queryFields === "undefined" ) {
                  MetacatUI.queryFields = new QueryFields();
                  MetacatUI.queryFields.fetch();
                }
                SearchableSelect.prototype.initialize.call(this, options);
              } catch (e) {
                console.log("Failed to initialize a Query Field Select View, error message: " + e);
              }
            },

            /**
             * postRender - Updates the view once the dropdown UI has loaded. Processes the
             * QueryFields given the options passed to this view, then updates the menu and
             * selection. Processing the fields takes some time, which is why we allow the
             * view to render before starting that process. This prevents slowing down the
             * rendering of parent views.
             */
            postRender : function(){

              try {
                var view = this;
                _.defer(function(){
                  view.processFields();
                  view.updateMenu();
                  // With the new menu in place, show the pre-selected values. Silent is set
                  // to true so that it doesn't trigger an update of the model. Defer to make
                  // sure the menu elements are attached.
                  _.defer(function() {
                    view.changeSelection(view.selected, true);
                  });
                  SearchableSelect.prototype.postRender.call(view);
                })
              }
              catch (error) {
                console.log( 'Post-render failed in a QueryFieldSelectView.' +
                  ' Error details: ' + error );
              }
            },

            /**
             * Retrieves the queryFields collection if not already fetched, then organizes the
             * fields based on the options passed to this view.
             * @since 2.17.0
             */
            processFields : function(){
              try {
                var view = this;

                // 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
                ) {
                  if (typeof MetacatUI.queryFields === "undefined") {
                    MetacatUI.queryFields = new QueryFields();
                  }
                  this.listenToOnce(MetacatUI.queryFields, "sync", this.processFields)
                  MetacatUI.queryFields.fetch();
                  return
                }

                // Convert the queryFields collection to an object formatted for the
                // SearchableSelect view.
                var fieldsJSON = MetacatUI.queryFields.toJSON();

                // Process & add additional fields set on this view (these are fields not
                // retrieved from the query service API)
                if (this.addFields && Array.isArray(this.addFields)) {
                  // For each added field, find the icon and category order from the already
                  // existing fields with the same category.
                  this.addFields = _.map(this.addFields, function (field) {
                    if (field.category) {
                      var categoryInfo = _.findWhere(fieldsJSON, { category: field.category });
                      ["icon", "categoryOrder"].forEach(function (prop) {
                        if (!field[prop]) {
                          field[prop] = categoryInfo[prop]
                        }
                      })
                    }
                    return field
                  }, this);
                  // Add the additional fields to the array of fields fetched from the
                  // query service API
                  fieldsJSON = fieldsJSON.concat(this.addFields);
                }

                // Move common fields to the top of the menu, outside of any
                // category headers, so that they are easy to find
                if (this.commonFields && Array.isArray(this.commonFields)) {
                  this.commonFields.forEach(function (commonFieldName) {
                    var i = _.findIndex(fieldsJSON, { name: commonFieldName });
                    if (i > 0) {
                      // If the category name is an empty string, no header will
                      // be created in the menu
                      fieldsJSON[i].category = ""
                      // The min categoryOrder in the QueryFields collection is 1
                      fieldsJSON[i].categoryOrder = 0
                      fieldsJSON[i].icon = "star"
                    }
                  });
                }

                // Filter out non-searchable fields (if option is true),
                // and fields that should be excluded
                var processedFields = _(fieldsJSON)
                  .chain()
                  .sortBy("categoryOrder")
                  .filter(
                    function (filter) {
                      if (this.excludeNonSearchable) {
                        if (["false", false].includes(filter.searchable)) {
                          return false
                        }
                      }
                      if (this.excludeFields && this.excludeFields.length) {
                        if (this.excludeFields.includes(filter.name)) {
                          return false
                        }
                      }
                      return true
                    }, this
                  )
                  .map(view.fieldToOption)
                  .groupBy("categoryOrder")
                  .value();

                // Rename the grouped categories
                for (const [key, value] of Object.entries(processedFields)) {
                  processedFields[value[0].category] = value;
                  delete processedFields[key];
                }

                // Sort items alphabetically for the specified categories
                if (this.categoriesToAlphabetize && this.categoriesToAlphabetize.length) {
                  this.categoriesToAlphabetize.forEach(function (categoryName) {
                    // Sort by category label
                    processedFields[categoryName].sort(function (a, b) {
                      // Ignore upper and lowercase
                      var nameA = a.label.toUpperCase();
                      var nameB = b.label.toUpperCase();
                      if (nameA < nameB)
                        return -1;
                      if (nameA > nameB)
                        return 1;
                      return 0;
                    });
                  })
                }

                // Set the formatted fields on the view
                this.options = processedFields;
              }
              catch (error) {
                console.log( 'There was an error organizing the Fields in a QueryFieldSelectView' +
                  ' Error details: ' + error );
              }
            },

            /**
             * fieldToOption - Converts an object that represents a QueryField model in the
             * format specified by the SearchableSelectView.options
             *
             * @param  {object} field An object with properties corresponding to a QueryField
             * model
             * @return {object}       An object in the format specified by
             * SearchableSelectView.options
             */
            fieldToOption: function(field) {
              return {
                label: field.label ? field.label : field.name,
                value: field.name,
                description: field.friendlyDescription ? field.friendlyDescription : field.description,
                icon: field.icon,
                category: field.category,
                categoryOrder: field.categoryOrder,
                type: field.type
              };
            },

            /**
             * addTooltip - Add a tooltip to a given element using the description in the
             * options object that's set on the view. This overwrites the prototype addTooltip
             * function so that we can use popovers with more details for query select fields.
             *
             * @param  {HTMLElement} element The HTML element a tooltip should be added
             * @param  {string} position how to position the tooltip - top | bottom | left |
             * right
             * @return {jQuery} The element with a tooltip wrapped by jQuery
             */
            addTooltip: function(element, position = "bottom"){

              if(!element){
                return
              }

              // Find the description in the options object, using the data-value
              // attribute set in the template. The data-value attribute is either
              // the label, or the value, depending on if a value is provided.
              var valueOrLabel = $(element).data("value"),
                  opt = _.chain(this.options)
                              .values()
                              .flatten()
                              .find(function(option){
                                return option.label == valueOrLabel || option.value == valueOrLabel
                              })
                              .value();

              var label = opt.label,
                  value = opt.value,
                  type = opt.type,
                  description = (opt.description ? opt.description : "");

              // For added fields, the value set on the options.value element is just a
              // unique identifier. The values that should be used to build a query are saved
              // in the addFields array set on this view.
              if(this.addFields && Array.isArray(this.addFields)){
                var specialField = _.findWhere(this.addFields, { name: valueOrLabel });
                if(specialField){
                  value = specialField.fields;
                  type = [];
                  specialField.fields.forEach(function(fieldName){
                    var realField = MetacatUI.queryFields.findWhere({
                      name: fieldName
                    });
                    if(realField){
                      type.push(realField.get("type"))
                    } else {
                      type.push("special field")
                    }
                  }, this);
                  type = type.join(", ");
                }
              }

              var contentEl = $(document.createElement("div")),
                  titleEl = $("<div>" + label + "</div>"),
                  valueEl = $("<code class='pull-right'>" + value + "</code>"),
                  typeEl = $("<span class='muted pull-right'><b>Type: " + type + "</b></span>"),
                  descriptionEl = $("<p>" + description + "</p>");

                titleEl.append(valueEl);
                contentEl.append(descriptionEl, typeEl)

              $(element).popover({
                title: titleEl,
                content: contentEl,
                html: true,
                trigger: "hover",
                placement: position,
                container: "body",
                delay: {
                  show: 1100,
                  hide: 50
                }
              })
              .on("show.bs.popover",
                function(){
                  var $el = $(this);
                  // Allow time for the popup to be added to the DOM
                  setTimeout(function () {
                    // Then add some css rules, and a special class to identify
                    // these popups if they need to be removed.
                    $el.data('popover').$tip
                      .css({
                        "maxWidth": "400px",
                        "pointerEvents" : "none"
                      })
                      .addClass("search-select-tooltip");
                  }, 10);
              });

              return $(element)

            },

            /**
             * isValidOption - Checks if a value is one of the values given in view.options
             *
             * @param  {string} value The value to check
             * @return {boolean}      returns true if the value is one of the values given in view.options
             */
            isValidOption: function(value){

              try {
                var view = this;

                // First check if the value is one of the fields that's excluded.
                if(view.excludeFields.includes(value)){
                  // If it is, then add it to the list of options
                  var newField = MetacatUI.queryFields.findWhere({
                    name: value
                  });
                  if(newField){
                    newField = view.fieldToOption(newField.toJSON());
                  }
                  view.options[newField.category].push(newField);
                  view.updateMenu();
                  // Make sure the new menu is attached before updating the selections
                  setTimeout(function () {
                    // If the selected value has been removed, re-add it.
                    if(!view.selected.includes(value)){
                      view.selected.push(value)
                    }
                    view.changeSelection(view.selected, silent = true);
                  }, 25);
                  return true
                } else {
                  var isValid = SearchableSelect.prototype.isValidOption.call(view, value);
                  return isValid
                }
              } catch (e) {
                console.log("Failed to check if option is valid in a Query Field Select View, error message: " + e);
              }

            },

          });
          return QueryFieldSelectView;
      });