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

define(["jquery", "underscore", "backbone", "uuid",
        "models/metadata/eml211/EMLMeasurementScale", "models/metadata/eml211/EMLAnnotation",
        "collections/metadata/eml/EMLMissingValueCodes",
        "models/DataONEObject"],
    function ($, _, Backbone, uuid, EMLMeasurementScale, EMLAnnotation,
        EMLMissingValueCodes,
        DataONEObject) {

        /**
         * @class EMLAttribute
         * @classdesc EMLAttribute represents a data attribute within an entity, such as
         * a column variable in a data table, or a feature attribute in a shapefile.
         * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html
         * @classcategory Models/Metadata/EML211
         */
        var EMLAttribute = Backbone.Model.extend(
          /** @lends EMLAttribute.prototype */{

            /* Attributes of an EML attribute object */
            defaults: function(){
            	return {
	                /* Attributes from EML */
	                xmlID: null, // The XML id of the attribute
	                attributeName: null,
	                attributeLabel: [], // Zero or more human readable labels
	                attributeDefinition: null,
	                storageType: [], // Zero or more storage types
	                typeSystem: [], // Zero or more system types for storage type
	                measurementScale: null, // An EML{Non}NumericDomain or EMLDateTimeDomain object
	                missingValueCodes: new EMLMissingValueCodes(), // An EMLMissingValueCodes collection
	                accuracy: null, // An EMLAccuracy object
	                coverage: null, // an EMLCoverage object
	                methods: [], // Zero or more EMLMethods objects
                    references: null, // A reference to another EMLAttribute by id (needs work)
                    annotation: [], // Zero or more EMLAnnotation objects

	                /* Attributes not from EML */
	                type: "attribute", // The element type in the DOM
	                parentModel: null, // The parent model this attribute belongs to
	                objectXML: null, // The serialized XML of this EML attribute
	                objectDOM: null,  // The DOM of this EML attribute
	                nodeOrder: [ // The order of the top level XML element nodes
	                    "attributeName",
	                    "attributeLabel",
	                    "attributeDefinition",
	                    "storageType",
	                    "measurementScale",
	                    "missingValueCode",
	                    "accuracy",
	                    "coverage",
	                    "methods",
	                    "annotation"
	                ]
            	}
            },

            /*
             * 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: {
                "attributename": "attributeName",
                "attributelabel": "attributeLabel",
                "attributedefinition": "attributeDefinition",
                "sourced" : "source",
                "storagetype": "storageType",
                "typesystem": "typeSystem",
                "measurementscale": "measurementScale",
                "missingvaluecode": "missingValueCode",
                "propertyuri": "propertyURI",
                "valueuri" : "valueURI"
            },

            /* Initialize an EMLAttribute object */
            initialize: function(attributes, options) {
                
                if (!attributes) {
                    var attributes = {};
                }

                // If initialized with missingValueCode as an array, convert it to a collection
                if (
                    attributes.missingValueCodes &&
                    attributes.missingValueCodes instanceof Array
                ) {
                    this.missingValueCodes =
                        new EMLMissingValueCodes(attributes.missingValueCode);
                }   

                this.stopListening(this.get("missingValueCodes"));
                this.listenTo(
                    this.get("missingValueCodes"),
                    "update",
                    this.trickleUpChange
                )
                this.on(
                    "change:attributeName " +
                    "change:attributeLabel " +
                    "change:attributeDefinition " +
                    "change:storageType " +
                    "change:measurementScale " +
                    "change:missingValueCodes " +
                    "change:accuracy " +
                    "change:coverage " +
                    "change:methods " +
                    "change:references " +
                    "change:annotation",
                    this.trickleUpChange);
            },

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

                if ( attributes.objectDOM ) {
                    $objectDOM = $(attributes.objectDOM);
                } else if ( attributes.objectXML ) {
                    $objectDOM = $(attributes.objectXML);
                } else {
                    return {};
                }

                // Add the XML id
                if ( typeof $objectDOM.attr("id") !== "undefined" ) {
                    attributes.xmlID = $objectDOM.attr("id");
                }

                // Add the attributeName
                attributes.attributeName = $objectDOM.children("attributename").text();

                // Add the attributeLabel
                attributes.attributeLabel = [];
                var attributeLabels = $objectDOM.children("attributelabel");
                _.each(attributeLabels, function(attributeLabel) {
                    attributes.attributeLabel.push(attributeLabel.textContent);
                });

                // Add the attributeDefinition
                attributes.attributeDefinition = $objectDOM.children("attributedefinition").text();

                // Add the storageType
                attributes.storageType = [];
                attributes.typeSystem = [];
                var storageTypes = $objectDOM.children("storagetype");
                _.each(storageTypes, function(storageType) {
                    attributes.storageType.push(storageType.textContent);
                    var type = $(storageType).attr("typesystem");
                    attributes.typeSystem.push(type || null);
                });

                var measurementScale = $objectDOM.find("measurementscale")[0];
                if ( measurementScale ) {
                    attributes.measurementScale =
                        EMLMeasurementScale.getInstance(measurementScale.outerHTML);
                    attributes.measurementScale.set("parentModel", this);
                }

                // Add annotations
                var annotations = $objectDOM.children("annotation");
                attributes.annotation = [];

                _.each(annotations, function(anno) {
                    annotation = new EMLAnnotation({
                            objectDOM: anno,
                            objectXML: anno.outerHTML
                    }, { parse: true });

                    attributes.annotation.push(annotation);
                }, this);

                // Add the missingValueCodes as a collection
                attributes.missingValueCodes = new EMLMissingValueCodes();
                attributes.missingValueCodes.parse(
                    $objectDOM.children("missingvaluecode")
                );

                attributes.objectDOM = $objectDOM[0];

                return attributes;
            },

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

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

		    	return xmlString;
            },

            /* Copy the original XML and update fields in a DOM object */
            updateDOM: function(objectDOM){

                var nodeToInsertAfter;
                var type = this.get("type") || "attribute";
                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 id attribute
                var xmlID = this.get("xmlID");
                if ( xmlID ) {
                    $(objectDOM).attr("id", xmlID);
                }

                // Update the attributeName
                if ( typeof this.get("attributeName") == "string" && this.get("attributeName").trim().length ) {
                    if ( $(objectDOM).find("attributename").length ) {
                        $(objectDOM).find("attributename").text(this.get("attributeName"));
                    } else {
                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeName");

                        if( ! nodeToInsertAfter ) {
                            $(objectDOM).append($(document.createElement("attributename"))
                                .text(this.get("attributeName"))[0]);
                        } else {
                            $(nodeToInsertAfter).after(
                                $(document.createElement("attributename")).text(this.get("attributeName"))[0]
                            );
                        }
                    }
                }
                //If there is no attribute name, return an empty string because it
                // is invalid
                else{
                  return "";
                }

                // Update the attributeLabels
                nodeToInsertAfter = undefined;
                var attributeLabels = this.get("attributeLabel");
                if ( attributeLabels ) {
                    if ( attributeLabels.length ) {
                        // Copy and reverse the array for inserting
                        attributeLabels = Array.from(attributeLabels).reverse();
                        // Remove all current attributeLabels
                        $(objectDOM).find("attributelabel").remove();
                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeLabel");

                        if( ! nodeToInsertAfter ) {
                            // Add the new list back in
                            _.each(attributeLabels, function(attributeLabel) {

                              //If there is an empty string or falsey value in the label, don't add it to the XML
                              // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
                              // it's ok to serialize labels that are the number 0.
                              if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
                                  attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
                                    return;
                              }

                              $(objectDOM).append(
                                  $(document.createElement("attributelabel"))
                                      .text(attributeLabel)[0]);
                            });
                        } else {
                            // Add the new list back in after its previous sibling
                            _.each(attributeLabels, function(attributeLabel) {

                                //If there is an empty string or falsey value in the label, don't add it to the XML
                                // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
                                // it's ok to serialize labels that are the number 0.
                                if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
                                    attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
                                      return;
                                }

                                $(nodeToInsertAfter).after(
                                    $(document.createElement("attributelabel"))
                                        .text(attributeLabel)[0]);
                            });
                        }
                    }
                    //If the label array is empty, remove all the labels from the DOM
                    else{
                      $(objectDOM).find("attributelabel").remove();
                    }
                }
                //If there is no attribute label, remove them from the DOM
                else{
                  $(objectDOM).find("attributelabel").remove();
                }

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

                        if( ! nodeToInsertAfter ) {
                            $(objectDOM).append($(document.createElement("attributedefinition"))
                                .text(this.get("attributeDefinition"))[0]);
                        } else {
                            $(nodeToInsertAfter).after($(document.createElement("attributedefinition"))
                                .text(this.get("attributeDefinition"))[0]);
                        }
                    }
                }
                // If there is no attribute definition, then return an empty String
                // because it is invalid
                else{
                  return "";
                }

                // Update the storageTypes
                nodeToInsertAfter = undefined;
                var storageTypes = this.get("storageTypes");
                if ( storageTypes ) {
                    if ( storageTypes.length ) {
                        // Copy and reverse the array for inserting
                        storageTypes = Array.from(storageTypes).reverse();
                        // Remove all current attributeLabels
                        $(objectDOM).find("storagetype").remove();
                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "storageType");

                        if( ! nodeToInsertAfter ) {
                            // Add the new list back in
                            _.each(storageTypes, function(storageType) {

                              if(!storageType)
                                return;

                              $(objectDOM).append(
                                  $(document.createElement("storagetype"))
                                      .text(storageType)[0]);
                            });
                        } else {
                            // Add the new list back in after its previous sibling
                            _.each(storageTypes, function(storageType) {

                              if(!storageType)
                                return;

                              $(nodeToInsertAfter).after(
                                  $(document.createElement("storagetype"))
                                      .text(storageType)[0]);
                            });
                        }
                    }
                }
                /*If there are no storage types, remove them all from the DOM.
                TODO: Uncomment this out when storage type is supported in editor
                else{
                  $(objectDOM).find("storagetype").remove();
                }
                */

                // Update the measurementScale
                nodeToInsertAfter = undefined;
                var measurementScale = this.get("measurementScale");
                var measurementScaleNodes;
                var measurementScaleNode;
                var domainNode;
                if ( typeof measurementScale !== "undefined" && measurementScale) {

                    // Find the measurementScale child or create a new one
                    measurementScaleNodes = $(objectDOM).children("measurementscale");
                    if ( measurementScaleNodes.length ) {
                        measurementScaleNode = measurementScaleNodes[0];

                    } else {
                        measurementScaleNode = document.createElement("measurementscale");
                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "measurementScale");

                        if ( typeof nodeToInsertAfter === "undefined" ) {
                            $(objectDOM).append(measurementScaleNode);
                        } else {
                            $(nodeToInsertAfter).after(measurementScaleNode);
                        }
                    }

                    // Append the measurementScale domain content
                    domainNode = measurementScale.updateDOM();
                    if (typeof domainNode !== "undefined" ) {
                        $(measurementScaleNode).children().remove();
                        $(measurementScaleNode).append(domainNode);
                    }

                } else {
                    console.log("No measurementScale object has been defined.");
                }

                // Update annotations
                var annotation = this.get("annotation");

                // Always remove all annotations to start with
                $(objectDOM).children("annotation").remove();

                _.each(annotation, function(anno) {
                    if (anno.isEmpty()) {
                        return;
                    }

                    var after = this.getEMLPosition(objectDOM, "annotation");
                    $(after).after(anno.updateDOM());
                }, this);

                // Update the missingValueCodes
                nodeToInsertAfter = undefined;
                var missingValueCodes = this.get("missingValueCodes");
                $(objectDOM).children("missingvaluecode").remove();
                if (missingValueCodes) {
                    var missingValueCodeNodes = missingValueCodes.updateDOM();
                    if (missingValueCodeNodes) {
                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "missingValueCode");
                        if (typeof nodeToInsertAfter === "undefined") {
                            $(objectDOM).append(missingValueCodeNodes);
                        } else {
                            $(nodeToInsertAfter).after(missingValueCodeNodes);
                        }
                    }
                }

                return objectDOM;
            },

            /*
             * 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) {
                var nodeOrder = this.get("nodeOrder");

                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].toLowerCase()).length ) {
                        return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
                    }
                }
            },

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

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

            	//If there is no attribute name, add that error message
            	if(!this.get("attributeName"))
            		errors.attributeName = "Provide a name for this attribute.";

            	//If there is no attribute definition, add that error message
            	if(!this.get("attributeDefinition"))
            		errors.attributeDefinition = "Provide a definition for this attribute.";

            	//Get the EML measurement scale model
            	var measurementScaleModel = this.get("measurementScale");

            	// If there is no measurement scale model, then add that error message
            	if( !measurementScaleModel ){
            		errors.measurementScale = "Choose a measurement scale category for this attribute.";
            	}
            	else{
                    if( !measurementScaleModel.isValid() ){
                		errors.measurementScale = "More information is needed.";
                	}
                }
                
                // Validate the missing value codes
                var missingValueCodesErrors = this.get("missingValueCodes")?.validate();
                if (missingValueCodesErrors) {
                    // Just display the first error message
                    errors.missingValueCodes = Object.values(missingValueCodesErrors)[0]
                }

                // If there is a measurement scale model and it is valid and there are no other
                // errors, then trigger this model as valid and exit.
                if (!Object.keys(errors).length) {
                    this.trigger("valid", this);
                    return;
                } else {
                    //If there is at least one error, then return the errors object
                    return errors;
                }
            },

            /*
            * Validates each of the EMLAnnotation models on this model
            *
            * @return {Array} - Returns an array of error messages for all the EMLAnnotation models
            */
           validateAnnotations: function(){
             var errors = [];

             //Validate each of the EMLAttributes
             _.each(this.get("annotation"), function (anno) {
               if (anno.isValid()) {
                 return;
               }

               errors.push(anno.validationError);
             });

             return errors;
           },

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

            createID: function() {
                this.set("xmlID", uuid.v4());
            }
        });

        return EMLAttribute;
    }
);