# Source: src/js/models/metadata/eml211/EMLParty.js

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

          /**
          * @class EMLParty
          * @classcategory Models/Metadata/EML211
          * @classdesc EMLParty represents a single Party from the EML 2.1.1 and 2.2.0
          * metadata schema. This can be a person or organization.
          * @see https://eml.ecoinformatics.org/schema/eml-party_xsd.html#ResponsibleParty
          * @extends Backbone.Model
          * @constructor
          */
      var EMLParty = Backbone.Model.extend(
        /** @lends EMLParty.prototype */{

        defaults: function(){
          return {
            objectXML: null,
            objectDOM: null,
            individualName: null,
            organizationName: null,
            positionName: null,
            address: [],
            phone: [],
            fax: [],
            email: [],
            onlineUrl: [],
            roles: [],
            references: null,
            userId: [],
            xmlID: null,
            type: null,
            typeOptions: ["associatedParty", "contact", "creator", "metadataProvider", "publisher"],
            roleOptions: ["custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator",
                          "coPrincipalInvestigator", "user"],
            parentModel: null,
            removed: false //Indicates whether this model has been removed from the parent model
          }
        },

        initialize: function(options){
          if(options && options.objectDOM)
            this.set(this.parse(options.objectDOM));

          if(!this.get("xmlID"))
            this.createID();
          this.on("change:roles", this.setType);
        },

        /*
             * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
             * Used during parse() and serialize()
             */
        nodeNameMap: function(){
          return {
            "administrativearea"    : "administrativeArea",
            "associatedparty"       : "associatedParty",
            "deliverypoint"         : "deliveryPoint",
            "electronicmailaddress" : "electronicMailAddress",
            "givenname"             : "givenName",
            "individualname"        : "individualName",
            "metadataprovider"      : "metadataProvider",
            "onlineurl"             : "onlineUrl",
            "organizationname"      : "organizationName",
            "positionname"          : "positionName",
            "postalcode"            : "postalCode",
            "surname"               : "surName",
            "userid"                : "userId"
          }
        },

            /*
                Parse the object DOM to create the model
                @param objectDOM the XML DOM to parse
                @return modelJSON the resulting model attributes object
             */
        parse: function(objectDOM){
          if(!objectDOM)
            var objectDOM = this.get("objectDOM");

          var model = this,
            modelJSON = {};

          //Set the name
          var person = $(objectDOM).children("individualname, individualName");

          if(person.length)
            modelJSON.individualName = this.parsePerson(person);

          //Set the phone and fax numbers
          var phones = $(objectDOM).children("phone"),
            phoneNums = [],
            faxNums = [];

          phones.each(function(i, phone){
            if($(phone).attr("phonetype") == "voice")
              phoneNums.push($(phone).text());
            else if($(phone).attr("phonetype") == "facsimile")
              faxNums.push($(phone).text());
          });

          modelJSON.phone = phoneNums;
          modelJSON.fax   = faxNums;

          //Set the address
          var addresses = $(objectDOM).children("address") || [],
            addressesJSON = [];

          addresses.each(function(i, address){
            addressesJSON.push(model.parseAddress(address));
          });

          modelJSON.address = addressesJSON;

          //Set the text fields
          modelJSON.organizationName = $(objectDOM).children("organizationname, organizationName").text() || null;
          modelJSON.positionName     = $(objectDOM).children("positionname, positionName").text() || null;
          // roles
          modelJSON.roles = [];
          $(objectDOM).find("role").each(function(i,role){
            modelJSON.roles.push($(role).text());
          });

          //Set the id attribute
          modelJSON.xmlID = $(objectDOM).attr("id");

          //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
          if( $(objectDOM).children("electronicmailaddress, electronicMailAddress").length ){
            modelJSON.email = _.map($(objectDOM).children("electronicmailaddress, electronicMailAddress"), function(email){
                        return  $(email).text();
                      });
          }

          //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
          if( $(objectDOM).find("onlineurl, onlineUrl").length ){
            // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()];
            modelJSON.onlineUrl = $(objectDOM).find("onlineurl, onlineUrl").map(function(i,v) {
              return $(v).text();
            }).get();
          }

          //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
          if( $(objectDOM).find("userid, userId").length ){
            modelJSON.userId = [$(objectDOM).find("userid, userId").first().text()];
          }

          return modelJSON;
        },

        parseNode: function(node){
          if(!node || (Array.isArray(node) && !node.length))
            return;

          this.set($(node)[0].localName, $(node).text());
        },

        parsePerson: function(personXML){
          var person = {
              givenName: [],
              surName: "",
              salutation: []
            },
            givenName   = $(personXML).find("givenname, givenName"),
            surName     = $(personXML).find("surname, surName"),
            salutations = $(personXML).find("salutation");

          //Concatenate all the given names into one, for now
          //TODO: Support multiple given names
          givenName.each(function(i, name){
            if(i==0)
              person.givenName[0] = "";

            person.givenName[0] += $(name).text() + " ";

            if(i==givenName.length-1)
              person.givenName[0] = person.givenName[0].trim();
          });

          person.surName = surName.text();

          salutations.each(function(i, name){
            person.salutation.push($(name).text());
          });

          return person;
        },

        parseAddress: function(addressXML){
          var address    = {},
            delPoint   = $(addressXML).find("deliverypoint, deliveryPoint"),
            city       = $(addressXML).find("city"),
            adminArea  = $(addressXML).find("administrativearea, administrativeArea"),
            postalCode = $(addressXML).find("postalcode, postalCode"),
            country    = $(addressXML).find("country");

          address.city               = city.length? city.text() : "";
          address.administrativeArea = adminArea.length? adminArea.text() : "";
          address.postalCode         = postalCode.length? postalCode.text() : "";
          address.country            = country.length? country.text() : "";

          //Get an array of all the address line (or delivery point) values
          var addressLines = [];
          _.each(delPoint, function(addressLine, i){
            addressLines.push($(addressLine).text());
          }, this);

          address.deliveryPoint = addressLines;

          return  address;
        },

        serialize: function(){
          var objectDOM = this.updateDOM(),
            xmlString = objectDOM.outerHTML;

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

            return xmlString;
        },

        /*
         * Updates the attributes on this model based on the application user (the app UserModel)
         */
        createFromUser: function(){
          //Create the name from the user
          var name = this.get("individualName") || {};
          name.givenName  = [MetacatUI.appUserModel.get("firstName")];
          name.surName    = MetacatUI.appUserModel.get("lastName");
          this.set("individualName", name);

          //Get the email and username
          if(MetacatUI.appUserModel.get("email"))
            this.set("email", [MetacatUI.appUserModel.get("email")]);

          this.set("userId", [MetacatUI.appUserModel.get("username")]);
        },

        /*
         * Makes a copy of the original XML DOM and updates it with the new values from the model.
         */
        updateDOM: function(){
          var type = this.get("type") || "associatedParty",
            objectDOM = this.get("objectDOM");

          // If there is already an XML node for this model and it is the wrong type,
          //   then replace the XML node contents
          if(objectDOM && objectDOM.nodeName != type.toUpperCase()){
            objectDOM = $(document.createElement(type)).html( objectDOM.innerHTML );
          }
          // If there is already an XML node for this model and it is the correct type,
          //   then simply clone the XML node
          else if(objectDOM){
            objectDOM = objectDOM.cloneNode(true);
          }
          // Otherwise, create a new XML node
          else{
            objectDOM = document.createElement(type);
          }

          //There needs to be at least one individual name, organization name, or position name
          if( this.nameIsEmpty() && !this.get("organizationName") && !this.get("positionName"))
            return "";

          var name = this.get("individualName");
          if(name){
            //Get the individualName node
            var nameNode = $(objectDOM).find("individualname");
            if(!nameNode.length){
              nameNode = document.createElement("individualname");
              $(objectDOM).prepend(nameNode);
            }

            //Empty the individualName node
            $(nameNode).empty();

             // salutation[s]
             if(!Array.isArray(name.salutation) && name.salutation)
              name.salutation = [name.salutation];

             _.each(name.salutation, function(salutation) {
               $(nameNode).prepend("<salutation>" + salutation + "</salutation>");
             });

             //Given name
             if(!Array.isArray(name.givenName) && name.givenName) name.givenName = [name.givenName];
             _.each(name.givenName, function(givenName) {

              //If there is a given name string, create a givenName node
              if(typeof givenName == "string" && givenName){
                $(nameNode).append("<givenname>" + givenName + "</givenname>");
              }

             });

             // surname
             if(name.surName)
               $(nameNode).append("<surname>" +  name.surName + "</surname>");
          }
          //If there is no name set on the model, remove it from the DOM
          else{
            $(objectDOM).find("individualname").remove();
          }

           // organizationName
          if(this.get("organizationName")){
            //Get the organization name node
            if($(objectDOM).find("organizationname").length)
              var orgNameNode = $(objectDOM).find("organizationname").detach();
            else
              var orgNameNode = document.createElement("organizationname");

            //Insert the text
            $(orgNameNode).text(this.get("organizationName"));

            //If the DOM is empty, append it
            if( !$(objectDOM).children().length )
              $(objectDOM).append(orgNameNode);
            else{
              var insertAfter = this.getEMLPosition(objectDOM, "organizationname");

              if(insertAfter && insertAfter.length)
                insertAfter.after(orgNameNode);
              else
                $(objectDOM).prepend(orgNameNode);
            }
          }
          //Remove the organization name node if there is no organization name
          else{
            var orgNameNode = $(objectDOM).find("organizationname").remove();
          }

           // positionName
          if(this.get("positionName")){
            //Get the name node
            if($(objectDOM).find("positionname").length)
              var posNameNode = $(objectDOM).find("positionname").detach();
            else
              var posNameNode = document.createElement("positionname");

            //Insert the text
            $(posNameNode).text(this.get("positionName"));

            //If the DOM is empty, append it
            if( !$(objectDOM).children().length )
              $(objectDOM).append(posNameNode);
            else{
              let insertAfter = this.getEMLPosition(objectDOM, "positionname")
              if(insertAfter)
                insertAfter.after(posNameNode);
              else
                $(objectDOM).prepend(posNameNode);
            }
          }
          //Remove the position name node if there is no position name
          else{
            $(objectDOM).find("positionname").remove();
          }

           // address
           _.each(this.get("address"), function(address, i) {

             var addressNode =  $(objectDOM).find("address")[i];

             if(!addressNode){
               addressNode = document.createElement("address");
               this.getEMLPosition(objectDOM, "address").after(addressNode);
             }

             //Remove all the delivery points since they'll be reserialized
             $(addressNode).find("deliverypoint").remove();

             _.each(address.deliveryPoint, function(deliveryPoint, ii){
               if(!deliveryPoint) return;

               var delPointNode = $(addressNode).find("deliverypoint")[ii];

               if(!delPointNode){
                 delPointNode = document.createElement("deliverypoint");

                 //Add the deliveryPoint node to the address node
                 //Insert after the last deliveryPoint node
                 var appendAfter = $(addressNode).find("deliverypoint")[ii-1];
                 if(appendAfter)
                   $(appendAfter).after(delPointNode);
                 //Or just prepend to the beginning
                 else
                   $(addressNode).prepend(delPointNode);
               }

               $(delPointNode).text(deliveryPoint);
             });

             if(address.city){
               var cityNode = $(addressNode).find("city");

               if(!cityNode.length){
                 cityNode = document.createElement("city");

                 if(this.getEMLPosition(addressNode, "city")){
                   this.getEMLPosition(addressNode, "city").after(cityNode);
                 }
                 else{
                    $(addressNode).append(cityNode);
                 }
               }

               $(cityNode).text(address.city);
             }
             else{
               $(addressNode).find("city").remove();
             }

             if(address.administrativeArea){
               var adminAreaNode = $(addressNode).find("administrativearea");

               if(!adminAreaNode.length){
                 adminAreaNode = document.createElement("administrativearea");

                 if(this.getEMLPosition(addressNode, "administrativearea")){
                   this.getEMLPosition(addressNode, "administrativearea").after(adminAreaNode);
                 }
                 else{
                    $(addressNode).append(adminAreaNode);
                 }

               }

               $(adminAreaNode).text(address.administrativeArea);
             }
             else{
               $(addressNode).find("administrativearea").remove();
             }

             if(address.postalCode){
               var postalcodeNode = $(addressNode).find("postalcode");

               if(!postalcodeNode.length){
                 postalcodeNode = document.createElement("postalcode");

                 if(this.getEMLPosition(addressNode, "postalcode")){
                   this.getEMLPosition(addressNode, "postalcode").after(postalcodeNode);
                 }
                 else{
                    $(addressNode).append(postalcodeNode);
                 }

               }

               $(postalcodeNode).text(address.postalCode);
             }
             else{
               $(addressNode).find("postalcode").remove();
             }

             if(address.country){
               var countryNode = $(addressNode).find("country");

               if(!countryNode.length){
                 countryNode = document.createElement("country");

                 if(this.getEMLPosition(addressNode, "country")){
                   this.getEMLPosition(addressNode, "country").after(countryNode);
                 }
                 else{
                    $(addressNode).append(countryNode);
                 }

               }

               $(countryNode).text(address.country);
             }
             else{
               $(addressNode).find("country").remove();
             }

           }, this);

           if( this.get("address").length == 0 ){
             $(objectDOM).find("address").remove();
           }

           // phone[s]
           $(objectDOM).find("phone[phonetype='voice']").remove();
           _.each(this.get("phone"), function(phone) {

            var phoneNode = $(document.createElement("phone")).attr("phonetype", "voice").text(phone);
            this.getEMLPosition(objectDOM, "phone").after(phoneNode);

           }, this);

           // fax[es]
           $(objectDOM).find("phone[phonetype='facsimile']").remove();
           _.each(this.get("fax"), function(fax) {

            var faxNode = $(document.createElement("phone")).attr("phonetype", "facsimile").text(fax);
            this.getEMLPosition(objectDOM, "phone").after(faxNode);

           }, this);

           // electronicMailAddress[es]
           $(objectDOM).find("electronicmailaddress").remove();
           _.each(this.get("email"), function(email) {

             var emailNode = document.createElement("electronicmailaddress");
             this.getEMLPosition(objectDOM, "electronicmailaddress").after(emailNode);

             $(emailNode).text(email);

           }, this);

          // online URL[es]
          $(objectDOM).find("onlineurl").remove();
           _.each(this.get("onlineUrl"), function(onlineUrl, i) {

            var urlNode = document.createElement("onlineurl");
            this.getEMLPosition(objectDOM, "onlineurl").after(urlNode);

            $(urlNode).text(onlineUrl);

           }, this);

           //user ID
           var userId = Array.isArray(this.get("userId")) ? this.get("userId") : [this.get("userId")];
           _.each(userId, function(id) {
             if(!id) return;

             var idNode = $(objectDOM).find("userid");

             //Create the userid node
             if(!idNode.length){

               idNode = $(document.createElement("userid"));

               this.getEMLPosition(objectDOM, "userid").after(idNode);
             }

             //If this is an orcid identifier, format it correctly
             if(this.isOrcid(id)){
                // Add the directory attribute
               idNode.attr("directory", "https://orcid.org");

               //If this ORCID does not start with "http"
               if( id.indexOf("http") == -1 ){
                 //If this is an ORCID with just the 16-digit numbers and hyphens, then add
                 // the https://orcid.org/ prefix to it
                 if( id.length == 19){
                   id = "https://orcid.org/" + id;
                 }
                 //If it starts with "orcid.org", then add the "https://" prefix
                 else if( id.indexOf("orcid.org") == 0 ){
                   id = "https://" + id;
                 }
                 //If it starts with "www.orcid.org", then add the "https" prefix and remove the "www"
                 else if( id.indexOf("www.orcid.org") == 0 ){
                   id = "https://" + id.replace("www.orcid.org", "orcid.org");
                 }
               }

               //If there is a "www", remove it
               if( id.indexOf("www.orcid.org") > -1 ){
                 id = id.replace("www.orcid.org", "orcid.org");
               }

               //If it has the http:// prefix, add the 's' for secure protocol
               if( id.indexOf("http://") == 0){
                 id = id.replace("http", "https");
               }

             }
             else{
               idNode.attr("directory", "unknown");
             }

             $(idNode).text(id);

           }, this);

           //Remove all the user id's if there aren't any in the model
           if( userId.length == 0 ){
             $(objectDOM).find("userid").remove();
           }

          // role
          //If this party type is not an associated party, then remove the role element
          if( type != "associatedParty" && type != "personnel" ){
            $(objectDOM).find("role").remove();
          }
          //Otherwise, change the value of the role element
          else {
            // If for some reason there is no role, create a default role
            if( !this.get("roles").length ){
              var roles = ["Associated Party"];
            } else {
              var roles = this.get("roles");
            }
            _.each(roles, function(role, i){
              var roleSerialized = $(objectDOM).find("role");
              if(roleSerialized.length){
                $(roleSerialized[i]).text(role)
              } else {
                roleSerialized = $(document.createElement("role")).text(role);
                this.getEMLPosition(objectDOM, "role").after( roleSerialized );
              }
            }, this);

          }

          //XML id attribute
          this.createID();
          //if(this.get("xmlID"))
            $(objectDOM).attr("id", this.get("xmlID"));
          //else
          //  $(objectDOM).removeAttr("id");

          // Remove empty (zero-length or whitespace-only) nodes
          $(objectDOM).find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove();

           return objectDOM;
        },

          /*
          * Adds this EMLParty model to it's parent EML211 model in the appropriate role array
          *
          * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled
          */
          mergeIntoParent: function(){
            //Get the type of EML Party, in relation to the parent model
            if(this.get("type") && this.get("type") != "associatedParty")
              var type = this.get("type");
            else
              var type = "associatedParty";

            //Update the list of EMLParty models in the parent model
            var parentEML = this.getParentEML();

            if(parentEML.type != "EML")
              return false;

            //Add this model to the EML model
            var successfulAdd = parentEML.addParty(this);

            //Validate the model
            this.isValid();

            return successfulAdd;
          },

          isEmpty: function(){
            // If we add any new fields, be sure to add the attribute here
            var attributes = ["userId", "fax", "phone", "onlineUrl",
                              "email", "positionName", "organizationName"];

             //Check each value in the model that gets serialized to see if there is a value
             for(var i in attributes) {
               //Get the value from the model for this attribute
                var modelValue = this.get(attributes[i]);

                //If this is an array, then we want to check if there are any values in it
                if( Array.isArray(modelValue) ){
                 if( modelValue.length > 0 )
                    return false;
                }
                //Otherwise, check if this value differs from the default value
                else if(this.get(attributes[i]) !== this.defaults()[attributes[i]]){
                  return false;
                }
             }

             //Check for a first and last name
             if( this.get("individualName") && (this.get("individualName").givenName || this.get("individualName").surName) )
              return false;

              //Check for addresses
              var isAddress = false;

              if( this.get("address") ){

                //Checks if there are any values anywhere in the address
                _.each(this.get("address"), function(address){
                  //Delivery point is an array so we need to check the first and second
                  //values of that array
                  if( address.administrativeArea  || address.city ||
                      address.country || address.postalCode ||
                      (address.deliveryPoint && address.deliveryPoint.length &&
                      (address.deliveryPoint[0] || address.deliveryPoint[1]) ) ){
                    isAddress = true;
                  }
                });

              }

              //If we found an address value anywhere, then it is not empty
              if(isAddress)
                return false;

             //If we never found a value, then return true because this model is empty
             return true;
          },

        /*
             * Returns the node in the given EML snippet that the given node type should be inserted after
             */
            getEMLPosition: function(objectDOM, nodeName){
              var nodeOrder = [ "individualname", "organizationname", "positionname", "address", "phone",
                                "electronicmailaddress", "onlineurl", "userid", "role"];
              var addressOrder = ["deliverypoint", "city", "administrativearea", "postalcode", "country"];

              //If this is an address node, find the position within the address
              if( _.contains(addressOrder, nodeName) ){
                nodeOrder = addressOrder;
              }

              var position = _.indexOf(nodeOrder, nodeName);
              if(position == -1)
                return $(objectDOM).children().last();

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

              return false;
            },

        createID: function(){
          this.set("xmlID", Math.ceil(Math.random() * (9999999999999999 - 1000000000000000) + 1000000000000000));
        },

        setType: function(){
          if(this.get("roles")){
            if(this.get("roles").length && !this.get("type")){
              this.set("type", "associatedParty");
            }
          }
        },

        trickleUpChange: function(){
            if ( this.get("parentModel") ) {
                MetacatUI.rootDataPackage.packageModel.set("changed", true);
            }
        },

        removeFromParent: function(){
            if( !this.get("parentModel") )
                return;
            else if( typeof this.get("parentModel").removeParty != "function" )
                return;

            this.get("parentModel").removeParty(this);

            this.set("removed", true);
        },

        /*
         * Checks the values of the model to determine if it is EML-valid
         */
        validate: function(){

          var individualName = this.get("individualName") || {},
            givenName = individualName.givenName || [],
            surName   = individualName.surName || null,
        errors = {};

          //If there are no values in this model that would be serialized, then the model is valid
          if( !this.get("organizationName") && !this.get("positionName") && !givenName[0]?.length && !surName
              && !this.get("address").length && !this.get("phone").length && !this.get("fax").length
              && !this.get("email").length && !this.get("onlineUrl").length && !this.get("userId").length){

            return;

          }

          //The EMLParty must have either an organization name, position name, or surname.
          // It must ALSO have a type or role.
          if ( !this.get("organizationName") && !this.get("positionName") &&
              (!this.get("individualName") || !surName ) ){

            errors = {
              surName: "Either a last name, position name, or organization name is required.",
              positionName: "",
              organizationName: ""
            }

          }
          //If there is a first name and no last name, then this is not a valid individualName
          else if( (givenName[0]?.length && !surName) && this.get("organizationName") && this.get("positionName") ){

            errors = { surName: "Provide a last name." }

          }

          //Check that each required field has a value. Required fields are configured in the {@link AppConfig}
          let roles = this.get("type") == "associatedParty"? this.get("roles") : [this.get("type")];
          for( role of roles ){
            let requiredFields = MetacatUI.appModel.get("emlEditorRequiredFields_EMLParty")[role];
            requiredFields?.forEach(field=>{
              let currentVal = this.get(field);
              if( !currentVal || !currentVal?.length ){
                errors[field] = `Provide a${(["a","e","i","o","u"].includes(field.charAt(0))? "n " : " ")} ${field}. `;
              }
            });
          }

        return Object.keys(errors)?.length? errors : false;

        },

        isOrcid: function(username){
          if(!username) return false;

          //If the ORCID URL is anywhere in this username string, it is an ORCID
          if(username.indexOf("orcid.org") > -1){
            return true;
          }

          /* The ORCID checksum algorithm to determine is a character string is an ORCiD
           * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
           */
          var total = 0,
              baseDigits = username.replace(/-/g, "").substr(0, 15);

          for(var i=0; i<baseDigits.length; i++){
            var digit = parseInt(baseDigits.charAt(i));
            total = (total + digit) * 2;
          }

          var remainder = total % 11,
            result = (12 - remainder) % 11,
            checkDigit = (result == 10) ? "X" : result.toString(),
            isOrcid = (checkDigit == username.charAt(username.length-1));

          return isOrcid;
        },

        /*
        *  Clones all the values of this array into a new JS Object.
        *  Special care is needed for nested objects and arrays
        *  This is helpful when copying this EMLParty to another role in the EML
        */
        copyValues: function(){
          //Get a JSON object of all the model copyValues
          var modelValues = this.toJSON();

          //Go through each model value and properly clone the arrays
          _.each( Object.keys(modelValues), function(key, i){

            //Clone the array via slice()
            if( Array.isArray(modelValues[key]) )
              modelValues[key] = modelValues[key].slice(0);

          });

          //Individual Names are objects, so properly clone them
          if( modelValues.individualName ){
            modelValues.individualName = Object.assign({}, modelValues.individualName);
          }

          //Addresses are objects, so properly clone them
          if( modelValues.address.length ){
            _.each(modelValues.address, function(address, i){
              modelValues.address[i] = Object.assign({}, address);

              //The delivery point is an array of strings, so properly clone the array
              if( Array.isArray(modelValues.address[i].deliveryPoint) )
                modelValues.address[i].deliveryPoint = modelValues.address[i].deliveryPoint.slice(0);
            });
          }
          return modelValues;
      },

      /**
       * getName - For an individual, returns the first and last name as a string. Otherwise,
       * returns the organization or position name.
       *
       * @return {string} Returns the name of the party or an empty string if one cannot be found
       *
       * @since 2.15.0
       */
      getName: function(){
        return this.get("individualName") ?
          this.get("individualName").givenName + " " + this.get("individualName").surName :
          this.get("organizationName") || this.get("positionName") || "";
          },

        /**
         * Return the EML Party as a CSL JSON object. See
         * {@link https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#names}.
         * @return {object} The CSL JSON object
         * @since 2.23.0
         */
        toCSLJSON: function () {
          const name = this.get("individualName");
          const csl= {
            family: name?.surName || null,
            given: name?.givenName || null,
            literal: this.get("organizationName") || this.get("positionName") || "",
            "dropping-particle": name?.salutation || null,
          }
          // If any of the fields are arrays, join them with a space
          for (const key in csl) {
            if (Array.isArray(csl[key])) {
              csl[key] = csl[key].join(" ");
            }
          }
          return csl;
        },

        /*
        * function nameIsEmpty - Returns true if the individualName set on this
        * model contains only empty values. Otherwise, returns false. This is just a
        * shortcut for manually checking each name field individually.
        *
        * @return {boolean}
        */
        nameIsEmpty: function(){
          var name = this.get("individualName");

          if( !name || typeof name != "object" )
            return true;

          //Check if there are given names
          var givenName = name.givenName,
              givenNameEmpty = false;

          if( !givenName || (Array.isArray(givenName) && givenName.length == 0) ||
            (typeof givenName == "string" && givenName.trim().length == 0) )
              givenNameEmpty = true;

          //Check if there are no sur names
          var surName = name.surName,
              surNameEmpty = false;

          if( !surName || (Array.isArray(surName) && surName.length == 0) ||
            (typeof surName == "string" && surName.trim().length == 0) )
              surNameEmpty = true;

          //Check if there are no salutations
          var salutation = name.salutation,
              salutationEmpty = false;

          if( !salutation || (Array.isArray(salutation) && salutation.length == 0) ||
            (typeof salutation == "string" && salutation.trim().length == 0) )
              salutationEmpty = true;

          if( givenNameEmpty && surNameEmpty && salutationEmpty )
            return true;
          else
            return false;

        },

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

        },

      /**
       * @type {object[]}
       * @property {string} label - The name of the party category to display to the user
       * @property {string} dataCategory - The string that is used to represent this party. This value
       *  should exactly match one of the strings listed in EMLParty typeOptions or EMLParty roleOptions.
       * @property {string} description - An optional description to display below the label to help the user
       *  with this category.
       * @property {boolean} createFromUser - If set to true, the information from the logged-in user will be
       *  used to create an EML party for this category if none exist already when the view loads.
       * @property {number} limit - If the number of parties allowed for this category is not unlimited,
       *  then limit should be set to the maximum allowable number.
       * @since 2.21.0
       */
       partyTypes: [
        {
          label: "Dataset Creators (Authors/Owners/Originators)",
          dataCategory: "creator",
          description: "Each person or organization listed as a Creator will be listed in the data" +
          " citation. At least one person, organization, or position with a 'Creator'" +
          " role is required.",
          createFromUser: true
        },
        {
          label: "Contact",
          dataCategory: "contact",
          createFromUser: true
        },
        {
          label: "Principal Investigators",
          dataCategory: "principalInvestigator"
        },
        {
          label: "Co-Principal Investigators",
          dataCategory: "coPrincipalInvestigator"
        },
        {
          label: "Collaborating-Principal Investigators",
          dataCategory: "collaboratingPrincipalInvestigator"
        },
        {
          label: "Metadata Provider",
          dataCategory: "metadataProvider"
        },
        {
          label: "Custodians/Stewards",
          dataCategory: "custodianSteward"
        },
        {
          label: "Publisher",
          dataCategory: "publisher",
          description: "Only one publisher can be specified.",
          limit: 1
        },
        {
          label: "Users",
          dataCategory: "user"
        }
        ],

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

      return EMLParty;
    });