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

define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
  $,
  _,
  Backbone,
  DataONEObject,
) {
  /**
   * @class EMLNumericDomain
   * @classdesc EMLNumericDomain represents the measurement scale of an interval
   * or ratio measurement scale attribute, and is an extension of
   * EMLMeasurementScale.
   * @classcategory Models/Metadata/EML211
   * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_measurementScale
   * @extends Backbone.Model
   */
  var EMLNumericDomain = Backbone.Model.extend(
    /** @lends EMLNumericDomain.prototype */ {
      type: "EMLNumericDomain",

      /* Attributes of an EMLNonNumericDomain object */
      defaults: function () {
        return {
          /* Attributes from EML, extends attributes from EMLMeasurementScale */
          measurementScale: null, // the required name of this measurement scale
          unit: null, // the required standard or custom unit definition
          precision: null, // the precision of the observed number
          numericDomain: {}, // a required numeric domain object or its reference
        };
      },

      /**
       * The map of lower case to camel case node names
       * needed to deal with parsing issues with $.parseHTML().
       * Use this until we can figure out issues with $.parseXML().
       */
      nodeNameMap: {
        standardunit: "standardUnit",
        customunit: "customUnit",
        numericdomain: "numericDomain",
        numbertype: "numberType",
      },

      /* Initialize an EMLNonNumericDomain object */
      initialize: function (attributes, options) {
        this.on("change:numericDomain", this.trickleUpChange);
      },

      /**
       * Parse the incoming measurementScale's XML elements
       */
      parse: function (attributes, options) {
        var $objectDOM;
        var measurementScale;
        var rootNodeName;

        if (attributes.objectDOM) {
          rootNodeName = $(attributes.objectDOM)[0].localName;
          $objectDOM = $(attributes.objectDOM);
        } else if (attributes.objectXML) {
          rootNodeName = $(attributes.objectXML)[0].localName;
          $objectDOM = $($(attributes.objectXML)[0]);
        } else {
          return {};
        }

        // do we have an appropriate measurementScale tree?
        var index = _.indexOf(
          ["measurementscale", "interval", "ratio"],
          rootNodeName,
        );
        if (index == -1) {
          throw new Error(
            "The measurement scale XML does not have a root " +
              "node of 'measurementScale', 'interval', or 'ratio'.",
          );
        }

        // If measurementScale is present, add it
        if (rootNodeName == "measurementscale") {
          attributes.measurementScale = $objectDOM
            .children()
            .first()[0].localName;
          $objectDOM = $objectDOM.children().first();
        } else {
          attributes.measurementScale = $objectDOM.localName;
        }

        // Add the unit
        var unitObject = {};
        var unit = $objectDOM.children("unit");
        var standardUnitNodes = unit.children("standardunit"),
          customUnitNodes = unit.children("customunit"),
          standardUnit,
          customUnit;

        if (standardUnitNodes.length) {
          standardUnit = standardUnitNodes.text();

          if (standardUnit) unitObject.standardUnit = standardUnit;
        } else if (customUnitNodes.length) {
          customUnit = customUnitNodes.text();

          if (customUnit) unitObject.customUnit = customUnit;
        }

        attributes.unit = unitObject;

        // Add the precision
        var precision = $objectDOM.children("precision").text();
        if (precision) {
          attributes.precision = precision;
        }

        // Add the numericDomain
        var numericDomainObject = {};
        var numericDomain = $objectDOM.children("numericdomain");
        var numberType;
        var boundsArray = [];
        var boundsObject;
        var bounds;
        var minimum;
        var maximum;
        var references;
        if (numericDomain) {
          // Add the XML id of the numeric domain
          if ($(numericDomain).attr("id")) {
            numericDomainObject.xmlID = $(numericDomain).attr("id");
          }

          // Add the numberType
          numberType = $(numericDomain).children("numbertype");

          if (numberType) {
            numericDomainObject.numberType = numberType.text();

            // Add optional bounds
            bounds = $(numericDomain).children("bounds");
            if (bounds.length) {
              _.each(bounds, function (bound) {
                boundsObject = {}; // initialize on each
                minimum = $(bound).children("minimum").text();
                maximum = $(bound).children("maximum").text();
                if (minimum && maximum) {
                  boundsObject.minimum = minimum;
                  boundsObject.maximum = maximum;
                } else if (minimum) {
                  boundsObject.minimum = minimum;
                } else if (maximum) {
                  boundsObject.maximum = maximum;
                }
                // If one of min or max is defined, add to the bounds array
                if (boundsObject.minimum || boundsObject.maximum) {
                  boundsArray.push(boundsObject);
                }
              });
            }
            numericDomainObject.bounds = boundsArray;
          } else {
            // Otherwise look for references
            references = $(numericDomain).children("references");
            if (references) {
              numericDomainObject.references = references.text();
            }
          }
          attributes.numericDomain = numericDomainObject;
        }
        attributes.objectDOM = $objectDOM[0];

        return attributes;
      },

      /* Serialize the model to XML */
      serialize: function () {
        var objectDOM = this.updateDOM();
        var xmlString = objectDOM.outerHTML;

        // Camel-case the XML
        xmlString = this.formatXML(xmlString);

        return xmlString;
      },

      /* Copy the original XML DOM and update it with new values from the model */
      updateDOM: function (objectDOM) {
        var nodeToInsertAfter;
        var type = this.get("measurementScale");
        if (typeof type === "undefined") {
          console.warn("Defaulting to an interval measurementScale.");
          type = "interval";
        }
        if (!objectDOM) {
          objectDOM = this.get("objectDOM");
        }
        var objectXML = this.get("objectXML");

        // If present, use the cached DOM
        if (objectDOM) {
          objectDOM = objectDOM.cloneNode(true);

          // otherwise, use the cached XML
        } else if (objectXML) {
          objectDOM = $(objectXML)[0].cloneNode(true);

          // This is new, create it
        } else {
          objectDOM = document.createElement(type);
        }

        // Update the unit
        var unit = this.get("unit");
        var unitNode;
        var unitTypeNode;
        if (unit) {
          // Remove any existing unit
          $(objectDOM).find("unit").remove();

          // Build a unit element, and populate a standard or custom child
          unitNode = document.createElement("unit");

          if (typeof unit.standardUnit !== "undefined") {
            unitTypeNode = document.createElement("standardUnit");
            $(unitTypeNode).text(unit.standardUnit);
          } else if (typeof unit.customUnit !== "undefined") {
            unitTypeNode = document.createElement("customUnit");
            $(unitTypeNode).text(unit.customUnit);
          } else {
            // Hmm, unit isn't an object?
            // Default to a standard unit
            unitTypeNode = document.createElement("standardUnit");
            if (typeof unit === "string") {
              $(unitTypeNode).text(unit);
              console.warn("EMLNumericDomain.unit should be an object.");
            } else {
              // We're really striking out. Default to dimensionless.
              $(unitTypeNode).text("dimensionless");
              console.warn(
                "Defaulting EMLNumericDomain.unit to dimensionless.",
              );
            }
          }
          $(unitNode).append(unitTypeNode);

          // Add the unit to the DOM
          nodeToInsertAfter = this.getEMLPosition(objectDOM, "unit");

          if (!nodeToInsertAfter) {
            $(objectDOM).prepend(unitNode);
          } else {
            $(nodeToInsertAfter).after(unitNode);
          }
        }

        // Update the precision
        if (this.get("precision")) {
          if ($(objectDOM).find("precision").length) {
            $(objectDOM).find("precision").text(this.get("precision"));
          } else {
            nodeToInsertAfter = this.getEMLPosition(objectDOM, "precision");

            if (!nodeToInsertAfter) {
              $(objectDOM).append(
                $(document.createElement("precision")).text(
                  this.get("precision"),
                )[0],
              );
            } else {
              $(nodeToInsertAfter).after(
                $(document.createElement("precision")).text(
                  this.get("precision"),
                )[0],
              );
            }
          }
        }

        // Update the numericDomain
        var numericDomain = this.get("numericDomain");
        var numericDomainNode = $(objectDOM).find("numericdomain")[0];
        var numberType;
        var numberTypeNode;
        var minBound;
        var maxBound;
        var boundsNode;
        var minBoundNode;
        var maxBoundNode;
        if (numericDomain) {
          var oldNumericDomainNode = $(numericDomainNode).clone();

          // Remove the existing numericDomainNode node
          if (typeof numericDomainNode !== "undefined") {
            numericDomainNode.remove();
          }

          // Build the new numericDomain node
          numericDomainNode = document.createElement("numericdomain");

          // Do we have numberType?
          if (typeof numericDomain.numberType !== "undefined") {
            numberTypeNode = document.createElement("numbertype");
            $(numberTypeNode).text(numericDomain.numberType);
            $(numericDomainNode).append(numberTypeNode);
          }

          // Do we have bounds?
          if (
            typeof numericDomain.bounds !== "undefined" &&
            numericDomain.bounds.length
          ) {
            _.each(numericDomain.bounds, function (bound) {
              minBound = bound.minimum;
              maxBound = bound.maximum;
              boundsNode = document.createElement("bounds");

              var hasBounds =
                typeof minBound !== "undefined" ||
                typeof maxBound !== "undefined";

              if (hasBounds) {
                // Populate the minimum element
                if (typeof minBound !== "undefined") {
                  minBoundNode = $(document.createElement("minimum"));
                  minBoundNode.text(minBound);

                  var existingExclusive = oldNumericDomainNode
                    .find("minimum")
                    .attr("exclusive");

                  if (!existingExclusive || existingExclusive === "false")
                    minBoundNode.attr("exclusive", "false");
                  else minBoundNode.attr("exclusive", "true");
                }

                // Populate the maximum element
                if (typeof maxBound !== "undefined") {
                  maxBoundNode = $(document.createElement("maximum"));
                  maxBoundNode.text(maxBound);

                  var existingExclusive = oldNumericDomainNode
                    .find("maximum")
                    .attr("exclusive");

                  if (!existingExclusive || existingExclusive === "false")
                    maxBoundNode.attr("exclusive", "false");
                  else maxBoundNode.attr("exclusive", "true");
                }

                $(boundsNode).append(minBoundNode, maxBoundNode);
                $(numericDomainNode).append(boundsNode);
              } else {
                // Do nothing. Content is missing, don't append the node
              }
            });
          } else {
            // Basically do nothing. Don't append the numericDomain element
            // TODO: handle numericDomain.references
          }
          nodeToInsertAfter = this.getEMLPosition(objectDOM, "numericDomain");

          if (!nodeToInsertAfter) {
            $(objectDOM).append(numericDomainNode);
          } else {
            $(nodeToInsertAfter).after(numericDomainNode);
          }
        }
        return objectDOM;
      },

      formatXML: function (xmlString) {
        return DataONEObject.prototype.formatXML.call(this, xmlString);
      },

      /**/
      getEMLPosition: function (objectDOM, nodeName) {
        // TODO: set the node order
        var nodeOrder = ["unit", "precision", "numericDomain"];

        var position = _.indexOf(nodeOrder, nodeName);

        // Append to the bottom if not found
        if (position == -1) {
          return $(objectDOM).children().last()[0];
        }

        // Otherwise, go through each node in the node list and find the
        // position where this node will be inserted after
        for (var i = position - 1; i >= 0; i--) {
          if ($(objectDOM).find(nodeOrder[i]).length) {
            return $(objectDOM).find(nodeOrder[i]).last()[0];
          }
        }
      },

      validate: function () {
        var errors = {};

        if (!this.get("unit")) errors.unit = "Choose a unit.";

        if (Object.keys(errors).length) return errors;
        else {
          this.trigger("valid");
          return;
        }
      },

      /*
       * Climbs up the model heirarchy until it finds the EML model
       *
       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
       */
      getParentEML: function () {
        var emlModel = this.get("parentModel"),
          tries = 0;

        while (emlModel.type !== "EML" && tries < 6) {
          emlModel = emlModel.get("parentModel");
          tries++;
        }

        if (emlModel && emlModel.type == "EML") return emlModel;
        else return false;
      },

      /* Let the top level package know of attribute changes from this object */
      trickleUpChange: function () {
        MetacatUI.rootDataPackage.packageModel.set("changed", true);
      },
    },
  );

  return EMLNumericDomain;
});