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

define(["jquery", "underscore", "backbone",
        "models/DataONEObject"],
    function($, _, Backbone, DataONEObject) {

        /**
         * @class EMLNonNumericDomain
         * @classdesc EMLNonNumericDomain represents the measurement scale of a nominal
         * or ordinal measurement scale attribute, and is an extension of
         * EMLMeasurementScale.
         * @classcategory Models/Metadata/EML211
         * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_AttributeType_measurementScale_AttributeType_AttributeType_measurementScale_nominal_nonNumericDomain
         * @extends Backbone.Model
         * @constructor
         */
        var EMLNonNumericDomain = Backbone.Model.extend(
            /** @lends EMLNonNumericDomain.prototype */{

        	type: "EMLNonNumericDomain",

            /* Attributes of an EMLNonNumericDomain object */
            defaults: function(){
                return {
                  /* Attributes from EML, extends attributes from EMLMeasurementScale */
                  measurementScale: null, // the name of this measurement scale
                  nonNumericDomain: [] // One or more of enumeratedDomain, textDomain, references
                }
            },

            /**
             * 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().
             * @type {object}
             */
            nodeNameMap: {
                "nonnumericdomain": "nonNumericDomain",
                "enumerateddomain": "enumeratedDomain",
                "textdomain": "textDomain",
                "externalcodeset": "externalCodeSet",
                "codesetname": "codesetName",
                "codeseturl": "codesetURL",
                "entityCodeList": "entityCodeList",
                "entityreference": "entityReference",
                "valueattributereference": "valueAttributeReference",
                "definitionattributereference": "definitionAttributeReference",
                "orderattributereference": "orderAttributeReference",
                "sourced": "source"
            },

            /* Initialize an EMLNonNumericDomain object */
            initialize: function(attributes, options) {

                this.on("change:nonNumericDomain", this.trickleUpChange);
            },

            /**
             * Parse the incoming measurementScale's XML elements
             */
            parse: function(attributes, options) {

                var $objectDOM;
                var nonNumericDomainNodeList;
                var domainNodeList; // the list of domain elements
                var domain; // the text or enumerated domain to parse
                var domainObject; // The parsed domain object to be added to attributes.nonNumericDomain
                var rootNodeName; // Name of the fragment root elements

                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", "nominal", "ordinal"], rootNodeName);
                if ( index == -1 ) {
                    throw new Error("The measurement scale XML does not have a root " +
                        "node of 'measurementScale', 'nominal', or 'ordinal'.");
                }

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

                nonNumericDomainNodeList = $objectDOM.find("nonnumericdomain");

                if ( nonNumericDomainNodeList && nonNumericDomainNodeList.length > 0 ) {
                    domainNodeList = nonNumericDomainNodeList[0].children;

                } else {
                    // No content is available, return
                    return attributes;
                }

                // Initialize an array of nonNumericDomain objects
                attributes.nonNumericDomain = [];

                // Set each domain if we have it
                if ( domainNodeList && domainNodeList.length > 0 ) {

                    _.each(domainNodeList, function(domain) {
                        if ( domain ) {
                            // match the camelCase name since DOMParser() is XML-aware
                            switch ( domain.localName ) {
                                case "textdomain":
                                    domainObject = this.parseTextDomain(domain);
                                    break;
                                case "enumerateddomain":
                                    domainObject = this.parseEnumeratedDomain(domain);
                                    break;
                                case "references":
                                    // TODO: Support references
                                    console.log("In EMLNonNumericDomain.parse()" +
                                        "We don't support references yet ");
                                default:
                                    console.log("Unrecognized nonNumericDomain: " + domain.nodeName);
                            }
                        }
                        attributes.nonNumericDomain.push(domainObject);
                    }, this);

                }

                // Add in the textDomain content if present
                // TODO

                attributes.objectDOM = $objectDOM[0];

                return attributes;
            },

            /* Parse the nonNumericDomain/textDomain fragment
             * returning an object with a textDomain attribute, like:
             * {
             *     textDomain: {
             *         definition: "Some definition",
             *         pattern: ["*", "\w", "[0-9]"],
             *         source: "Some source reference"
             *     }
             * }
             */
            parseTextDomain: function(domain) {
                var domainObject = {};
                domainObject.textDomain = {};
                var xmlID;
                var definition;
                let patterns = [];
                var source;

                // Add the XML id attribute
                if ( $(domain).attr("id") ) {
                    xmlID = $(domain).attr("id");
                } else {
                    // Generate an id if it's not found
                    xmlID = DataONEObject.generateId();
                }
                domainObject.textDomain.xmlID = xmlID;

                // Add the definition
                definition = $(domain).children("definition").text();
                domainObject.textDomain.definition = definition;

                // Add the pattern
                _.each($(domain).children("pattern"), function(pattern) {
                     patterns.push(pattern.textContent);
                });
                domainObject.textDomain.pattern = patterns;

                // Add the source
                source = $(domain).children("sourced").text();
                domainObject.textDomain.source = source;

                 return domainObject;
            },

            /* Parse the nonNumericDomain/enumeratedDomain fragment
             * returning an object with an enumeratedDomain attribute, like:
             * var emlCitation = {};
             * var nonNumericDomain = [
             *     {
             *         enumeratedDomain: {
             *             codeDefinition: [
             *                 {
             *                     code: "Some code", // required
             *                     definition: "Some definition", // required
             *                     source: "Some source"
             *                 } // repeatable
             *             ]
             *         }
             *     }, // or
             *     {
             *         enumeratedDomain: {
             *             externalCodeSet: [
             *                 {
             *                     codesetName: "Some code", // required
             *                     citation: [emlCitation], // one of citation or codesetURL
             *                     codesetURL: ["Some URL"] // is required, both repeatable
             *                 } // repeatable
             *             ]
             *         }
             *     }, // or
             *     {
             *         enumeratedDomain: {
             *             entityCodeList: {
             *                 entityReference: "Some reference", // required
             *                 valueAttributeReference: "Some attr reference", // required
             *                 definitionAttributeReference: "Some definition attr reference", // required
             *                 orderAttributeReference: "Some order attr reference"
             *             }
             *         }
             *     }
             * ]
             */
            parseEnumeratedDomain: function(domain) {
                var domainObject = {};
                domainObject.enumeratedDomain = {};
                var codeDefinition = {};
                var externalCodeSet = {};
                var entityCodeList = {};
                var xmlID;

                // Add the XML id attribute
                if ( $(domain).attr("id") ) {
                    xmlID = $(domain).attr("id");
                } else {
                    // Generate an id if it's not found
                    xmlID = DataONEObject.generateId();
                }
                domainObject.enumeratedDomain.xmlID = xmlID;

                // Add the codeDefinitions if present
                var codeDefinitions = $(domain).children("codedefinition");

                if ( codeDefinitions.length ) {
                    domainObject.enumeratedDomain.codeDefinition = [];
                    _.each(codeDefinitions, function(codeDef) {
                        var code = $(codeDef).children("code").text();
                        var definition = $(codeDef).children("definition").text();
                        var source = $(codeDef).children("sourced").text() || undefined;
                        domainObject.enumeratedDomain.codeDefinition.push({
                            code: code,
                            definition: definition,
                            source: source
                        });
                    })
                }
                return domainObject;
            },

            /* 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 objectDOM;
                var xmlID; // The id of the textDomain or enumeratedDomain fragment
                var nonNumericDomainNode;
                var domainType; // Either textDomain or enumeratedDomain
                var $domainInDOM; // The jQuery object of the text or enumerated domain from the DOM
                var nodeToInsertAfter;
                var domainNode; // Either a textDomain or enumeratedDomain node
                var definitionNode;
                var patternNode;
                var sourceNode;
                var enumeratedDomainNode;
                var codeDefinitions;
                var codeDefinitionNode;
                var codeNode;

                var type = this.get("measurementScale");
                if ( typeof type === "undefined") {
                    console.warn("Defaulting to an nominal measurementScale.");
                    type = "nominal";
                }
                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);

                }

                if ( this.get("nonNumericDomain").length ) {

                    // Update each nonNumericDomain in the DOM
                    _.each(this.get("nonNumericDomain"), function(domain, i) {

                        // Is this a textDomain or enumeratedDomain?
                        if ( typeof domain.textDomain === "object" ) {
                            domainType = "textDomain";
                            xmlID = domain.textDomain.xmlID;

                        } else if ( typeof domain.enumeratedDomain === "object" ) {
                            domainType = "enumeratedDomain";
                            xmlID = domain.enumeratedDomain.xmlID;
                        } else {
                            console.log("Unrecognized NonNumericDomain type. Skipping.");
                            // TODO: Handle references here
                        }

                        // Update the existing DOM node by id
                        if ( xmlID && $(objectDOM).find("#" + xmlID).length ) {

                            if ( domainType === "textDomain" ) {

                                let originalTextDomain = $(objectDOM).find("#" + xmlID).find("textdomain");

                                //If there are existing textDomain nodes in the DOM, update them
                                if( originalTextDomain.length ){
                                    let updatedTextDomain = this.updateTextDomain(domain.textDomain, originalTextDomain);
                                    originalTextDomain.replaceWith(updatedTextDomain);
                                }
                                //If there are no textDomain nodes in the DOM, create new ones
                                else{
                                  //Create new textDomain nodes
                                  let newTextDomain = this.createTextDomain(domain.textDomain);

                                  //Insert the new textDomain nodes into the nonNumericDomain node
                                  $( $(objectDOM).children("nonnumericdomain")[i] ).html( newTextDomain );
                                }

                            } else if ( domainType === "enumeratedDomain") {

                              this.updateEnumeratedDomainDOM(domain.enumeratedDomain, $domainInDOM);
                            }


                        //If there is no XML ID but there are the same number of nonNumericDomains in the model and DOM
                      } else if( this.get("nonNumericDomain").length == $(objectDOM).children("nonnumericdomain").length
                          && $(objectDOM).children("nonnumericdomain").length >= i){

                            //If this is a text domain,
                            if( typeof domain.textDomain === "object" ){

                                let originalTextDomain = $($(objectDOM).children("nonnumericdomain")[i]).find("textdomain");

                                //If there are existing textDomain nodes in the DOM, update them
                                if( originalTextDomain.length ){
                                    let updatedTextDomain = this.updateTextDomain(domain.textDomain, originalTextDomain);
                                    originalTextDomain.replaceWith(updatedTextDomain);
                                }
                                //If there are no textDomain nodes in the DOM, create new ones
                                else{
                                  //Create new textDomain nodes
                                  var newTextDomain = this.createTextDomain(domain.textDomain);

                                  //Insert the new textDomain nodes into the nonNumericDomain node
                                  $( $(objectDOM).children("nonnumericdomain")[i] ).html( newTextDomain );
                                }

                            }
                            else if(typeof domain.enumeratedDomain === "object"){
                              //Get the nonNumericDomain node from the DOM
                              var nonNumericDomainNode = $(objectDOM).children("nonnumericdomain")[i],
                                  enumeratedDomain     = $(nonNumericDomainNode).children("enumerateddomain");

                              if( enumeratedDomain.length ){
                                this.updateEnumeratedDomainDOM(domain.enumeratedDomain, enumeratedDomain);
                              }
                              else{
                                //Remove the textDomain node and replace it with an enumeratedDomain node
                                var textDomainToReplace = $(objectDOM).find("textdomain");

                                if( textDomainToReplace.length > 0 ){
                                  $(textDomainToReplace[i]).replaceWith(this.createEnumeratedDomainDOM(domain.enumeratedDomain));
                                }
                                else{
                                  nonNumericDomainNode.html(this.createEnumeratedDomainDOM(domain.enumeratedDomain, document.createElement("enumerateddomain")));
                                }


                              }
                            }

                        // Otherwise append to the DOM
                        } else {

                            // Add the nonNumericDomain element
                            nonNumericDomainNode = document.createElement("nonnumericdomain");

                            if ( domainType === "textDomain" ) {

                                // Add the definiton element
                                domainNode = document.createElement("textdomain");
                                if ( domain.textDomain.definition ) {
                                    definitionNode = document.createElement("definition");
                                    $(definitionNode).text(domain.textDomain.definition);
                                    $(domainNode).append(definitionNode);
                                }

                                // Add the pattern element(s)
                                if ( domain.textDomain.pattern.length ) {
                                    _.each(domain.textDomain.pattern, function(pattern) {
                                        patternNode = document.createElement("pattern");
                                        $(patternNode).text(pattern);
                                        $(domainNode).append(patternNode);
                                    }, this);
                                }

                                // Add the source element
                                if ( domain.textDomain.source ) {
                                    sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
                                    $(sourceNode).text(domain.textDomain.source);
                                    $(domainNode).append(sourceNode);
                                }

                            } else if ( domainType === "enumeratedDomain" ) {

                              nonNumericDomainNode.append(this.createEnumeratedDomainDOM(domain.enumeratedDomain));

                            } else {
                                console.log("The domainType: " + domainType + " is not recognized.");
                            }
                            $(nonNumericDomainNode).append(domainNode);
                            $(objectDOM).append(nonNumericDomainNode);
                        }
                    }, this);

                } else {
                    // We have no content, so can't create a valid domain
                    console.log("In EMLNonNumericDomain.updateDOM(),\n" +
                        "references are not handled yet. Returning undefined.");
                    // TODO: handle references here
                    return undefined;
                }
                return objectDOM;
            },

            /*
             * Update the codeDefinitionList in the  first enumeratedDomain
             * found in the nonNumericDomain array.
             * TODO: Refactor this to support externalCodeSet and entityCodeList
             * TODO: Support the source field
             * TODO: Support repeatable enumeratedDomains
             * var nonNumericDomain = [
             *     {
             *         enumeratedDomain: {
             *             codeDefinition: [
             *                 {
             *                     code: "Some code", // required
             *                     definition: "Some definition", // required
             *                     source: "Some source"
             *                 } // repeatable
             *             ]
             *         }
             *     }
             * ]
             */
            updateEnumeratedDomain: function(code, definition, index) {
                var nonNumericDomain = this.get("nonNumericDomain");
                var enumeratedDomain = {};
                var codeDefinitions;

                if( typeof code == "string" && !code.trim().length ){
                  code = "";
                }

                if( typeof definition == "string" && !definition.trim().length ){
                  definition = "";
                }

                // Create from scratch
                if ( !nonNumericDomain.length || !nonNumericDomain[0] || !nonNumericDomain[0].enumeratedDomain) {
                    nonNumericDomain[0] = {
                        enumeratedDomain: {
                            codeDefinition: [
                                {
                                    code: code,
                                    definition: definition
                                }
                            ]
                        }
                    }
                }
                // Update existing
                else {
                    enumeratedDomain = this.get("nonNumericDomain")[0].enumeratedDomain;

                    if ( typeof enumeratedDomain !== "undefined" ) {

                      //If there is no code or definition, then remove it from the code list
                      if( !code && code !== 0 && !definition && definition !== 0 ){
                        this.removeCode(index);
                      }
                      else if ( enumeratedDomain.codeDefinition.length >= index ) {
                        //Create a new code object and insert it into the array
                        enumeratedDomain.codeDefinition[index] = {
                            code: code,
                            definition: definition
                        }
                      }
                      else {
                        //Create a new code object and append it to the end of the array
                        enumeratedDomain.codeDefinition.push({
                            code: code,
                            definition: definition
                        });
                      }
                    }
                }

                //Manually trigger the change event since we're updating an array on the model
                this.trigger("change:nonNumericDomain");
            },

            /*
             * Given a `codeDefinition` HTML node and an enumeratedDomain list,
             *   this function will update the HTML node code definitions with
             *   all the code definitions listed in the enumeratedDomain
             *
             * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
             * @param {DOM Element or jQuery Object} - A DOM Element or jQuery selection that represents the <enumeratedDomain> node
             */
            updateEnumeratedDomainDOM: function( enumeratedDomain, enumeratedDomainNode ){

              if ( enumeratedDomain.codeDefinition.length ) {

                  // Update each codeDefinition
                  _.each(enumeratedDomain.codeDefinition, function(codeDef, i) {

                      var codeDefNode = $($(enumeratedDomainNode).children("codedefinition")[i]);

                      if( !codeDefNode.length ){
                        codeDefNode = $(document.createElement("codedefinition"));
                        $(enumeratedDomainNode).append(codeDefNode);
                      }

                      // Update the required code element
                      if ( codeDef.code ) {
                        var codeNode = codeDefNode.children("code");

                        //If there is no <code> XML node, make one
                        if( !codeNode.length ){
                          codeNode = $(document.createElement("code"));
                          codeDefNode.append(codeNode);
                        }

                        //Add the code text to the <code> node
                        codeNode.text(codeDef.code);
                      }
                      else{
                        codeDefNode.children("code").remove();
                      }

                      // Update the required definition element
                      if ( codeDef.definition ) {
                        var defNode = codeDefNode.children("definition");

                        //If there is no <definition> XML node, make one
                        if( !defNode.length ){
                          defNode = $(document.createElement("definition"));
                          codeDefNode.append(defNode);
                        }

                        //Add the definition text to the <definition> node
                        defNode.text(codeDef.definition);
                      }
                      else{
                        codeDefNode.children("definition").remove();
                      }

                      // Update the optional source element
                      if ( codeDef.source ) {
                        // Accommodate parseHTML() with source"d"
                        var sourceNode = codeDefNode.children("sourced");

                        //If there is no <source> XML node, make one
                        if( !sourceNode.length ){
                          sourceNode = $(document.createElement("sourced"));
                          codeDefNode.append(sourceNode);
                        }

                        sourceNode.text(codeDef.source);
                      }
                      else{
                        codeDefNode.children("sourced").remove();
                      }

                  }, this);

                  // If there are more codeDefinition nodes than there are codeDefinitions
                  // in the model, then we need to remove the extraneous nodes
                  var numNodes = $(enumeratedDomainNode).children("codedefinition").length,
                      numCodes = enumeratedDomain.codeDefinition.length;

                  if( numNodes > numCodes ){
                    //Get the extraneous nodes by selecting the last X child elements
                    var nodesToRemove = $(enumeratedDomainNode).children("codedefinition").slice( (numNodes - numCodes) * -1 );
                    //Remove them from the DOM
                    nodesToRemove.remove();
                  }

              } else if ( domain.enumeratedDomain.externalCodeSet ) {
                  // TODO Handle externalCodeSet

              } else if ( domain.enumeratedDomain.entityCodeList ) {
                  // TODO Handle entityCodeList
              }

              return enumeratedDomainNode;

            },

            /*
             * Given an enumeratedDomain list, this function will create an
             *   <enumeratedDomain> HTML element with all the code definitions
             *   listed in the enumeratedDomain object
             *
             * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
             * @return {DOM Element} - An <enumerateddomain> DOM element tree with code definitions
             */
            createEnumeratedDomainDOM: function( enumeratedDomain ){

              var enumeratedDomainNode = document.createElement("enumerateddomain");

              if ( enumeratedDomain.codeDefinition.length ) {

                  // Add each codeDefinition
                  _.each(enumeratedDomain.codeDefinition, function(codeDef) {

                      var codeDefinitionNode = document.createElement("codedefinition");

                      // Add the required code element
                      if ( codeDef.code ) {
                          var codeNode = document.createElement("code");
                          $(codeNode).text(codeDef.code);
                          $(codeDefinitionNode).append(codeNode);
                      }

                      // Add the required definition element
                      if ( codeDef.definition ) {
                          var definitionNode = document.createElement("definition");
                          $(definitionNode).text(codeDef.definition);
                          $(codeDefinitionNode).append(definitionNode);
                      }

                      // Add the optional source element
                      if ( codeDef.source ) {
                          var sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
                          $(sourceNode).text(codeDef.source);
                          $(codeDefinitionNode).append(sourceNode);
                      }
                      $(enumeratedDomainNode).append(codeDefinitionNode);

                  }, this);

              } else if ( domain.enumeratedDomain.externalCodeSet ) {
                  // TODO Handle externalCodeSet

              } else if ( domain.enumeratedDomain.entityCodeList ) {
                  // TODO Handle entityCodeList
              }

              return enumeratedDomainNode;

            },

            /*
             * Given a textDomain object, and textDomain DOM object, this function
             *  will update all the DOM elements with the textDomain object values
             *
             * @param {object} textDomain - A literal object representing an EML text domain
             * @param {DOM Element} textDomainEl - The <textDomain> DOM Element to update
             * @return {DOM Element} - An <textdomain> DOM element tree to update
             */
            updateTextDomain: function(textDomain, textDomainEl){

              if( typeof textDomainEl === "undefined" || (typeof textDomainEl == "object" && textDomainEl.length == 0) )
                var textDomainEl = document.createElement("textdomain");

              //Create a shortcut to the jQuery object of the text domain element
              var $textDomainEl = $(textDomainEl);

              var definitionEl = $textDomainEl.find("definition");

              //Update the definition element text
              if( definitionEl.length > 0 )
                definitionEl.text(textDomain.definition);
              else {
                $textDomainEl.prepend( $(document.createElement("definition")).text(textDomain.definition) );
              }

              // Remove existing patterns
              $textDomainEl.find("pattern").remove();

              // Add any new patterns
              if ( textDomain.pattern && textDomain.pattern.length ) {

                  let patterns = Array.from(textDomain.pattern).reverse();

                  _.each(patterns, function(pattern) {

                    //Don't serialize strings with only empty characters
                    if( typeof pattern == "string" && !pattern.trim().length )
                      return;

                    var patternNode = document.createElement("pattern");

                    $(patternNode).text(pattern);

                    // Prepend before the sourced element if present
                    if ( $textDomainEl.find("sourced").length ) {
                        $textDomainEl.find("sourced").before(patternNode);
                    } else {
                        $textDomainEl.append(patternNode);
                    }
                  });
              }

              // Update any new source
              if ( textDomain.source ) {
                  if ( $textDomainEl.find("sourced").length ) {
                      $textDomainEl.find("sourced").text(textDomain.source);
                  } else {
                      //
                      var src = document.createElement("sourced");
                      src.textContent = textDomain.source;
                      $textDomainEl.find("textDomain").append(src);
                  }
              } else {
                  // Remove the source in the DOM not present in the textDomain
                  // TODO: Uncomment this when we support "source" in the UI
                  // $domainInDOM.children("source").remove();

              }

              return textDomainEl;
            },

            /*
             * Creates a textDomain DOM object with the textDomain object values
             *
             * @param {object} textDomain - A literal object representing an EML text domain
             * @return {DOM Element} - An <textdomain> DOM element tree to update
             */
            createTextDomain: function(textDomain){
              var textDomainEl = document.createElement("textdomain");

              this.updateTextDomain(textDomain, textDomainEl);

              return textDomainEl;

            },

            /*
             * Get the DOM node preceding the given nodeName
             * to find what position in the EML document
             * the named node should be appended
             */
            getEMLPosition: function(objectDOM, nodeName) {
                // TODO: set the node order
                var nodeOrder = ["enumerateddomain", "textdomain"];

                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];
                    }
                }
            },

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

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

            	if( !this.get("nonNumericDomain").length )
            		errors.nonNumericDomain = "Choose a possible value type.";
            	else{
            		var domain = this.get("nonNumericDomain")[0];

            		_.each(Object.keys(domain), function(key){

            			//For enumerated domain types
            			if(key == "enumeratedDomain" && domain[key].codeDefinition){

            				var isEmpty = (domain[key].codeDefinition.length == 0) ? true : false;

            				//Validate the list of codes
            				for(var i=0; i < domain[key].codeDefinition.length; i++){

            					var codeDef = domain[key].codeDefinition[i];

            					//If either the code or definition is missing in at least one codeDefinition set,
            					//then this model is invalid
            					if((codeDef.code && !codeDef.definition) || (!codeDef.code && codeDef.definition)){
            						errors.enumeratedDomain = "Provide both a code and definition in each row.";
            						i = domain[key].codeDefinition.length;
            					}
            					else if(domain[key].codeDefinition.length == 1 && !codeDef.code && !codeDef.definition)
            						isEmpty = true;

            				}

            				if(isEmpty)
            					errors.enumeratedDomain = "Define at least one code and definition.";

            			}
            			else if(key == "textDomain" && (typeof domain[key] != "object" || !domain[key].definition)){
            				errors.definition = "Provide a description of the kind of text allowed.";
            			}

            		}, this);

            	}

            	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;

            },

            removeCode: function(index){
            	var codeToRemove = this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition[index];

            	var newCodeList = _.without(this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition, codeToRemove);

            	this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition = newCodeList;

            	this.trigger("change:nonNumericDomain");
            }

        });

        return EMLNonNumericDomain;
    }
);