Source: src/js/models/metadata/eml211/EMLReferences.js

define(["jquery", "backbone", "common/EMLUtilities"], (
  $,
  Backbone,
  EMLUtilities,
) => {
  /**
   * @class EMLReferences
   * @classdesc A model for the EML <references> element.
   * See {@link https://eml.ecoinformatics.org/schema/eml-resource_xsd.html#ReferencesGroup}
   * @classcategory Models/Metadata/EML211
   * @constructs
   * @augments Backbone.Model
   * @since 2.34.0
   */
  const EMLReferences = Backbone.Model.extend(
    /** @lends EMLReferences.prototype */
    {
      /**
       * Default attributes for an EML <references> element.
       * @returns {object} - Default attributes for the model.
       * @property {string} references - The id of another element being
       * referenced.
       * @property {string | null} system - Optional system attribute for
       * external system scoping.
       * @property {string} objectDOM - The DOM representation of this
       * references element.
       * @property {string} modelType - The name of the backbone model that is
       * used to represent this the element that the reference points to. We
       * need this to be able to find the element in the EML document that
       * corresponds to this reference.
       * @property {Backbone.Model} parentModel - The parent EML model of this
       * references element, e.g. a EMLAttributeList.
       */
      defaults() {
        return {
          references: "",
          system: null,
          objectDOM: null,
          modelType: null,
          parentModel: null,
        };
      },

      // TODO... listen to change in xml id for linked model. Also if that model
      // is destroyed, this reference should be invalidated or removed....

      /**
       * Convert the references xml to model attributes.
       * @param {HTMLElement | jQuery} response - The XML response to parse.
       * @param {object} options - Options for parsing.
       * @param {Backbone.Model} options.parentModel - The parent model of
       * this references element, e.g. a EMLAttributeList.
       * @returns {object} - The parsed attributes.
       */
      parse(response, options = {}) {
        let references = response;
        if (!references) return {};

        // .get is a jQuery function
        if (typeof references.get === "function") {
          // if references is a jQuery object, get the first element
          references = references.get(0);
        }

        const system = references.getAttribute("system");
        const ref = references.textContent || references.innerHTML;

        return {
          references: ref || null,
          system: system || null,
          parentModel: options.parentModel || null,
          modelType: options.modelType || null,
          objectDOM: references,
        };
      },

      /**
       * Get the parent EML model of this references element.
       * @returns {EML} - The parent EML model.
       */
      getParentEML() {
        return EMLUtilities.getParentEML(this);
      },

      /**
       * Find the subtree in the EML document that corresponds to the references.
       * i.e. the element with the same id that the reference points to.
       * @returns {Backbone.Model | null} - The linked model or null if not found.
       */
      getLinkedModel() {
        const ref = this.get("references");
        const eml = this.getParentEML();
        const modelType = this.get("modelType");
        let found = null;

        if (!ref || !eml || !modelType) {
          // If no reference, parentEML, or modelType, return null
          return null;
        }

        switch (modelType) {
          // Check if the model is a DataONEObject
          case "EMLAttributeList": {
            const attributeLists = eml.get("entities").getAllAttributeLists();
            found = attributeLists.find((list) => list.get("xmlID") === ref);
            break;
          }
          // TODO: Add support for other models
          default:
            // If the model is not recognized, return null
            throw new Error(
              `Support for "${modelType}" is not implemented yet.`,
            );
        }

        return found || null;
      },

      /**
       * Get the linked DOM element in the EML document that corresponds to the
       * references. This is the element with the same id that the reference
       * points to.
       * @returns {jQuery | null} - The linked DOM element or null if not found.
       */
      getLinkedDOM() {
        const eml = this.getParentEML();
        if (!eml) return null;
        let emlDOM = eml.get("objectDOM");
        const ref = this.get("references");

        if (!ref || !emlDOM) return null;

        // if emlDOM is not a jQuery object, convert it to one
        emlDOM = $(emlDOM);

        // Find the element in the DOM with the matching ID
        const linkedElement = emlDOM.find(`[id="${ref}"]`);
        if (linkedElement.length) {
          // If found, return the linked element
          return linkedElement;
        }
        const linkedModel = this.getLinkedModel();
        if (linkedModel) {
          // If the model is found, return its DOM representation
          return linkedModel.get("objectDOM") || linkedModel.updateDOM();
        }
        // If not found, return null
        return null;
      },

      /**
       * Check if the references element is empty.
       * @returns {boolean} - True if the references element is empty, false
       * otherwise.
       */
      isEmpty() {
        // Even if there's a system attribute, if there is no reference, it's
        // empty
        return !this.get("references");
      },

      /**
       * Validate the attributes to ensure a valid <references> structure.
       * @returns {object} - An object containing validation errors, if any.
       */
      validate() {
        const errors = {};
        const ref = this.get("references");
        const system = this.get("system");

        if (this.isEmpty()) return false;

        if (!ref) {
          errors.references = "The ID of the referenced element is required.";
        }

        if (system && typeof system !== "string") {
          errors.system = "The 'system' attribute must be a string.";
        }

        // Check if there is either a node in the DOM with a matching ID
        // or a model in the EML model with a matching xmlID (since the refed
        // element may not be in the DOM yet)
        const linkedModel = this.getLinkedModel();
        const linkedDOM = this.getLinkedDOM();
        if (!linkedModel && !linkedDOM) {
          errors.references =
            "The referenced element must exist in the EML document.";
        }
        // Check if the linked model is valid
        if (linkedModel && !linkedModel.isValid()) {
          errors.references = "The referenced element is not valid.";
        }

        return errors;
      },

      /**
       * Serialize the model to an XML string.
       * @returns {string} - The XML representation of this <references> element.
       */
      updateDOM() {
        if (!this.get("references")) {
          const dom = this.get("objectDOM");
          if (dom) {
            dom.remove();
          }
          this.set("objectDOM", null);
          return null;
        }

        let dom = this.get("objectDOM");
        if (!dom) {
          dom = document.createElement("references");
        } else {
          dom = dom.cloneNode(true);
        }
        const ref = this.get("references");
        const system = this.get("system");

        dom.textContent = ref || "";
        if (system) {
          dom.setAttribute("system", system);
        } else {
          dom.removeAttribute("system");
        }
        this.set("objectDOM", dom);
        return dom;
      },
    },
  );

  return EMLReferences;
});