Source: src/js/models/LookupModel.js

define(["jquery", "jqueryui", "underscore", "backbone"], function (
  $,
  $ui,
  _,
  Backbone,
) {
  "use strict";

  /**
   * @class LookupModel
   * @classdesc A utility model that contains functions for looking up values
   * from various services
   * @classcategory Models
   * @extends Backbone.Model
   */
  var LookupModel = Backbone.Model.extend(
    /** @lends LookupModel.prototype */ {
      defaults: {
        concepts: {},
      },

      initialize: function () {},

      bioportalSearch: function (request, response, localValues, allValues) {
        // make sure we have something to lookup
        if (!MetacatUI.appModel.get("bioportalAPIKey")) {
          response(localValues);
          return;
        }

        var query =
          MetacatUI.appModel.get("bioportalSearchUrl") +
          "?q=" +
          request.term +
          "&apikey=" +
          MetacatUI.appModel.get("bioportalAPIKey") +
          "&ontologies=ECSO&pagesize=1000&suggest=true";
        var availableTags = [];
        $.get(query, function (data, textStatus, xhr) {
          _.each(data.collection, function (obj) {
            var choice = {};
            choice.label = obj["prefLabel"];
            var synonyms = obj["synonym"];
            if (synonyms) {
              choice.synonyms = [];
              _.each(synonyms, function (synonym) {
                choice.synonyms.push(synonym);
              });
            }
            choice.filterLabel = obj["prefLabel"];
            choice.value = obj["@id"];
            if (obj["definition"]) {
              choice.desc = obj["definition"][0];
            }

            // mark items that we know we have matches for
            if (allValues) {
              var matchingChoice = _.findWhere(allValues, {
                value: choice.value,
              });
              if (matchingChoice) {
                //choice.label = "*" + choice.label;
                choice.match = true;

                // remove it from the local value - why have two?
                if (localValues) {
                  localValues = _.reject(localValues, function (obj) {
                    return obj.value == matchingChoice.value;
                  });
                }
                //availableTags.push(choice);
              }
            }

            availableTags.push(choice);
          });

          // combine the lists if called that way
          if (localValues) {
            availableTags = localValues.concat(availableTags);
          }

          response(availableTags);
        });
      },

      bioportalExpand: function (term) {
        // make sure we have something to lookup
        if (!MetacatUI.appModel.get("bioportalAPIKey")) {
          response(null);
          return;
        }

        var terms = [];
        var countdown = 0;

        var query =
          MetacatUI.appModel.get("bioportalSearchUrl") +
          "?q=" +
          term +
          "&apikey=" +
          MetacatUI.appModel.get("bioportalAPIKey") +
          "&ontologies=ECSO&pagesize=1000&suggest=true";
        $.ajax({
          url: query,
          method: "GET",
          async: false, // we want to wait for the response!
          success: function (data, textStatus, xhr) {
            _.each(data.collection, function (obj) {
              // use the preferred label
              var prefLabel = obj["prefLabel"];
              terms.push(prefLabel);

              // add the synonyms
              var synonyms = obj["synonym"];
              if (synonyms) {
                _.each(synonyms, function (synonym) {
                  terms.push(synonym);
                });
              }
              // process the descendants
              var descendantsUrl = obj["links"]["descendants"];
              //if (false) {
              if (descendantsUrl && countdown > 0) {
                countdown--;

                $.ajax({
                  url:
                    descendantsUrl +
                    "?apikey=" +
                    MetacatUI.appModel.get("bioportalAPIKey"),
                  method: "GET",
                  async: false,
                  success: function (data, textStatus, xhr) {
                    _.each(data.collection, function (obj) {
                      var prefLabel = obj["prefLabel"];
                      var synonyms = obj["synonym"];
                      if (synonyms) {
                        _.each(synonyms, function (synonym) {
                          terms.push(synonym);
                        });
                      }
                    });
                  },
                });
              }
            });
          },
        });
        return terms;
      },

      bioportalGetConcepts: function (uri, callback) {
        var concepts = this.get("concepts")[uri];

        if (concepts) {
          callback(concepts);
          return;
        } else {
          concepts = [];
        }

        // make sure we have something to lookup
        if (!MetacatUI.appModel.get("bioportalAPIKey")) {
          return;
        }

        var query =
          MetacatUI.appModel.get("bioportalSearchUrl") +
          "?q=" +
          encodeURIComponent(uri) +
          "&apikey=" +
          MetacatUI.appModel.get("bioportalAPIKey") +
          "&ontologies=ECSO&pagesize=1000&suggest=true";
        var availableTags = [];
        var model = this;
        $.get(query, function (data, textStatus, xhr) {
          _.each(data.collection, function (obj) {
            var concept = {};
            concept.label = obj["prefLabel"];
            concept.value = obj["@id"];
            if (obj["definition"]) {
              concept.desc = obj["definition"][0];
            }
            // add the synonyms
            var synonyms = obj["synonym"];
            if (synonyms) {
              concept.synonyms = [];
              _.each(synonyms, function (synonym) {
                concept.synonyms.push(synonym);
              });
            }

            concepts.push(concept);
          });
          model.get("concepts")[uri] = concepts;

          callback(concepts);
        });
      },

      bioportalGetConceptsBatch: function (uris, callback) {
        // make sure we have something to lookup
        if (!MetacatUI.appModel.get("bioportalBatchUrl")) {
          return;
        }
        // prepare the request JSON
        var batchData = {};
        batchData["http://www.w3.org/2002/07/owl#Class"] = {};
        batchData["http://www.w3.org/2002/07/owl#Class"]["display"] =
          "prefLabel,synonym,definition";
        batchData["http://www.w3.org/2002/07/owl#Class"]["collection"] = [];
        _.each(uris, function (uri) {
          var item = {};
          item["class"] = uri;
          item["ontology"] = "http://data.bioontology.org/ontologies/ECSO";
          batchData["http://www.w3.org/2002/07/owl#Class"]["collection"].push(
            item,
          );
        });

        var url = MetacatUI.appModel.get("bioportalBatchUrl");
        var model = this;
        $.ajax(url, {
          method: "POST",
          //url: url,
          data: JSON.stringify(batchData),
          contentType: "application/json",
          headers: {
            Authorization:
              "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"),
          },
          error: function (e) {
            console.log(e);
          },
          success: function (data, textStatus, xhr) {
            _.each(data["http://www.w3.org/2002/07/owl#Class"], function (obj) {
              var concept = {};
              concept.label = obj["prefLabel"];
              concept.value = obj["@id"];
              if (obj["definition"]) {
                concept.desc = obj["definition"][0];
              }
              // add the synonyms
              var synonyms = obj["synonym"];
              if (synonyms) {
                concept.synonyms = [];
                _.each(synonyms, function (synonym) {
                  concept.synonyms.push(synonym);
                });
              }

              var conceptList = [];
              conceptList.push(concept);
              model.get("concepts")[concept.value] = conceptList;
            });

            callback.apply();
          },
        });
      },

      orcidGetConcepts: function (uri, callback) {
        var people = this.get("concepts")[uri];

        if (people) {
          callback(people);
          return;
        } else {
          people = [];
        }

        var query =
          MetacatUI.appModel.get("orcidBaseUrl") +
          uri.substring(uri.lastIndexOf("/"));
        var model = this;
        $.get(query, function (data, status, xhr) {
          // get the orcid info
          var profile = $(data).find("orcid-profile");

          _.each(profile, function (obj) {
            var choice = {};
            choice.label =
              $(obj).find("orcid-bio > personal-details > given-names").text() +
              " " +
              $(obj).find("orcid-bio > personal-details > family-name").text();
            choice.value = $(obj).find("orcid-identifier > uri").text();
            choice.desc = $(obj).find("orcid-bio > personal-details").text();
            people.push(choice);
          });

          model.get("concepts")[uri] = people;

          // callback with answers
          callback(people);
        });
      },

      /*
       * Supplies search results for ORCiDs to autocomplete UI elements
       */
      orcidSearch: function (request, response, more, ignore) {
        var people = [];

        if (!ignore) var ignore = [];

        var query = MetacatUI.appModel.get("orcidSearchUrl") + request.term;
        $.get(query, function (data, status, xhr) {
          // get the orcid info
          var profile = $(data).find("orcid-profile");

          _.each(profile, function (obj) {
            var choice = {};
            choice.value = $(obj).find("orcid-identifier > uri").text();

            if (_.contains(ignore, choice.value.toLowerCase())) return;

            choice.label =
              $(obj).find("orcid-bio > personal-details > given-names").text() +
              " " +
              $(obj).find("orcid-bio > personal-details > family-name").text();
            choice.desc = $(obj).find("orcid-bio > personal-details").text();
            people.push(choice);
          });

          // add more if called that way
          if (more) {
            people = more.concat(people);
          }

          // callback with answers
          response(people);
        });
      },

      /*
       * Gets the bio of a person given an ORCID Updates the given user model
       * with the bio info from ORCID
       */
      orcidGetBio: function (options) {
        if (!options || !options.userModel) return;

        var orcid = options.userModel.get("username"),
          onSuccess = options.success || function () {},
          onError = options.error || function () {};

        $.ajax({
          url: MetacatUI.appModel.get("orcidSearchUrl") + orcid,
          type: "GET",
          //accepts: "application/orcid+json",
          success: function (data, textStatus, xhr) {
            // get the orcid info
            var orcidNode = $(data).find("path:contains(" + orcid + ")"),
              profile = orcidNode.length
                ? $(orcidNode).parents("orcid-profile")
                : [];

            if (!profile.length) return;

            var fullName =
              $(profile)
                .find("orcid-bio > personal-details > given-names")
                .text() +
              " " +
              $(profile)
                .find("orcid-bio > personal-details > family-name")
                .text();
            options.userModel.set("fullName", fullName);

            onSuccess(data, textStatus, xhr);
          },
          error: function (xhr, textStatus, error) {
            onError(xhr, textStatus, error);
          },
        });
      },

      /**
       * Using the NSF Award Search API, get a list of grants that match the
       * given term, as long as the term is at least 3 characters long and
       * doesn't consist of only digits. Used to populate the autocomplete list
       * for the Funding fields in the metadata editor. For this method to work,
       * there must be a grantsUrl set in the MetacatUI.appModel.
       * @param {jQuery} request - The jQuery UI autocomplete request object
       * @param {function} response - The jQuery UI autocomplete response function
       * @see {@link https://www.research.gov/common/webapi/awardapisearch-v1.htm}
       */
      getGrantAutocomplete: function (
        request,
        response,
        beforeRequest,
        afterRequest,
      ) {
        // Handle errors in this function or in the findGrants function
        function handleError(error) {
          if (typeof afterRequest == "function") afterRequest();
          console.log("Error fetching awards from NSF: ", error);
          response([]);
        }

        try {
          let term = request.term;

          // Only search after 3 characters or more, and not just digits
          if (!term || term.length < 3 || /^\d+$/.test(term)) return;

          // If the beforeRequest function was passed, call it now
          if (typeof beforeRequest == "function") beforeRequest();

          // Search for grants
          this.findGrants(term)
            .then((awards) => {
              response(this.formatFundingForAutocomplete(awards));
            })
            .catch(handleError)
            .finally(() => {
              if (typeof afterRequest == "function") afterRequest();
            });
        } catch (error) {
          handleError(error);
        }
      },

      /**
       * Search the NSF Award Search API for grants that match the given term.
       * @param {string} term - The term to search for
       * @param {number} [offset=1] - The offset to use in the search. Defaults
       * to 1.
       * @returns {Promise} A promise that resolves to an array of awards in the
       * format {id: string, title: string}
       * @since 2.28.0
       */
      findGrants: function (term, offset = 1) {
        let awards = [];
        if (!term || term.length < 3) return awards;
        const grantsUrl = MetacatUI.appModel.get("grantsUrl");
        if (!grantsUrl) return awards;

        term = $.ui.autocomplete.escapeRegex(term);
        term = encodeURIComponent(term);
        const filterBy = "keyword";
        const url =
          `${grantsUrl}?${filterBy}=${term}` +
          `&printFields=title,id&offset=${offset}`;

        return fetch(url)
          .then((response) => {
            return response.json();
          })
          .then((data) => {
            if (!data || !data.response || !data.response.award) return awards;
            return data.response.award;
          })
          .catch((error) => {
            console.error("Error fetching data: ", error);
            return awards;
          });
      },

      /**
       * Format awards from the NSF Award Search API for use in the jQuery UI
       * autocomplete widget.
       * @param {Array} awards - An array of awards in the format
       * {id: string, title: string}
       * @returns {Array} An array of awards in the format
       * {value: string, label: string}
       * @since 2.28.0
       */
      formatFundingForAutocomplete: function (awards) {
        if (!awards || !awards.length) return [];
        return awards.map((award) => ({
          value: award.id,
          label: award.title,
        }));
      },

      getAccountsAutocomplete: function (request, response) {
        var searchTerm = $.ui.autocomplete.escapeRegex(request.term);

        //Only search after 2 characters or more
        if (searchTerm.length < 2) return;

        var url =
          MetacatUI.appModel.get("accountsUrl") + "?query=" + searchTerm;

        // Send the AJAX request as a JSONP data type since it will be
        // cross-origin
        var requestSettings = {
          url: url,
          success: function (data, textStatus, xhr) {
            if (!data) return [];

            //If an XML doc was not returned from the server, then try to parse
            //the response as XML
            if (!XMLDocument.prototype.isPrototypeOf(data)) {
              try {
                data = $.parseXML(data);
              } catch (e) {
                //If the parsing XML failed, exit now
                console.error(
                  "The accounts service did not return valid XML.",
                  e,
                );
                return;
              }
            }

            var list = [];

            _.each(
              $(data)
                .children(/.+subjectInfo/)
                .children(),
              function (accountNode, i) {
                var name = "";
                var type = "";

                if ($(accountNode).children("givenName").length) {
                  name =
                    $(accountNode).children("givenName").text() +
                    " " +
                    $(accountNode).children("familyName").text();
                  type = "person";
                } else {
                  name = $(accountNode).children("groupName").text();
                  type = "group";
                }

                if (!name) {
                  name = $(accountNode).children("subject").text();
                  type = "unknown";
                }

                list.push({
                  value: $(accountNode).children("subject").text(),
                  label:
                    name +
                    "  (" +
                    $(accountNode).children("subject").text() +
                    ")",
                  type: type,
                });
              },
            );

            var term = $.ui.autocomplete.escapeRegex(request.term),
              startsWithMatcher = new RegExp("^" + term, "i"),
              startsWith = $.grep(list, function (value) {
                return startsWithMatcher.test(
                  value.label || value.value || value,
                );
              }),
              containsMatcher = new RegExp(term, "i"),
              contains = $.grep(list, function (value) {
                return (
                  $.inArray(value, startsWith) < 0 &&
                  containsMatcher.test(value.label || value.value || value)
                );
              });

            response(startsWith.concat(contains));
          },
        };

        //Send the query
        $.ajax(requestSettings);
      },

      /**
       * Calls the monitor/status DataONE MN API and gets the size of the index
       * queue.
       * @param {function} [onSuccess]
       * @param {function} [onError]
       */
      getSizeOfIndexQueue: function (onSuccess, onError) {
        try {
          if (!MetacatUI.appModel.get("monitorStatusUrl")) {
            if (typeof onSuccess == "function") {
              onSuccess();
            } else {
              //Trigger a custom event for the size of the index queue.
              this.trigger("sizeOfQueue", -1);
            }

            return false;
          }

          var model = this;

          //Check if there is an indexing queue, because this model may still be
          //indexing
          var requestSettings = {
            url: MetacatUI.appModel.get("monitorStatusUrl"),
            type: "GET",
            error: function () {
              if (typeof onError == "function") {
                onError();
              }
            },
            success: function (data) {
              var sizeOfQueue = parseInt(
                $(data).find("status > index > sizeOfQueue").text(),
              );

              if (sizeOfQueue > 0 || sizeOfQueue == 0) {
                //Trigger a custom event for the size of the index queue.
                model.trigger("sizeOfQueue", sizeOfQueue);

                if (typeof onSuccess == "function") {
                  onSuccess(sizeOfQueue);
                }
              } else {
                if (typeof onError == "function") {
                  onError();
                }
              }
            },
          };

          $.ajax(
            _.extend(
              requestSettings,
              MetacatUI.appUserModel.createAjaxSettings(),
            ),
          );
        } catch (e) {
          console.error(e);

          if (typeof onError == "function") {
            onError();
          }
        }
      },
    },
  );
  return LookupModel;
});