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
         */
        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;
    }
);