/**
 * @module IO
 * @submodule Input
 * @requires core
 */

class XML {
  constructor(DOM){
    if (!DOM) {
      const xmlDoc = document.implementation.createDocument(null, 'doc');
      this.DOM = xmlDoc.createElement('root');
    } else {
      this.DOM = DOM;
    }
  }

  /**
   * Returns the element's parent element as a new <a href="#/p5.XML">p5.XML</a>
   * object.
   *
   * @return {p5.XML} parent element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get an array with all mammal elements.
   *   let mammals = myXML.getChildren('mammal');
   *
   *   // Get the first mammal element.
   *   let firstMammal = mammals[0];
   *
   *   // Get the parent element.
   *   let parent = firstMammal.getParent();
   *
   *   // Get the parent element's name.
   *   let name = parent.getName();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the parent element's name.
   *   text(name, 50, 50);
   *
   *   describe('The word "animals" written in black on a gray background.');
   * }
   */
  getParent() {
    return new XML(this.DOM.parentElement);
  }

  /**
   * Returns the element's name as a `String`.
   *
   * An XML element's name is given by its tag. For example, the element
   * `&lt;language&gt;JavaScript&lt;/language&gt;` has the name `language`.
   *
   * @return {String} name of the element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get an array with all mammal elements.
   *   let mammals = myXML.getChildren('mammal');
   *
   *   // Get the first mammal element.
   *   let firstMammal = mammals[0];
   *
   *   // Get the mammal element's name.
   *   let name = firstMammal.getName();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's name.
   *   text(name, 50, 50);
   *
   *   describe('The word "mammal" written in black on a gray background.');
   * }
   */
  getName() {
    return this.DOM.tagName;
  }

  /**
   * Sets the element's tag name.
   *
   * An XML element's name is given by its tag. For example, the element
   * `&lt;language&gt;JavaScript&lt;/language&gt;` has the name `language`.
   *
   * The parameter, `name`, is the element's new name as a string. For example,
   * calling `myXML.setName('planet')` will make the element's new tag name
   * `&lt;planet&gt;&lt;/planet&gt;`.
   *
   * @param {String} name new tag name of the element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the element's original name.
   *   let oldName = myXML.getName();
   *
   *   // Set the element's name.
   *   myXML.setName('monsters');
   *
   *   // Get the element's new name.
   *   let newName = myXML.getName();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's names.
   *   text(oldName, 50, 33);
   *   text(newName, 50, 67);
   *
   *   describe(
   *     'The words "animals" and "monsters" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   */
  setName(name) {
    const content = this.DOM.innerHTML;
    const attributes = this.DOM.attributes;
    const xmlDoc = document.implementation.createDocument(null, 'default');
    const newDOM = xmlDoc.createElement(name);
    newDOM.innerHTML = content;
    for (let i = 0; i < attributes.length; i++) {
      newDOM.setAttribute(attributes[i].nodeName, attributes[i].nodeValue);
    }
    this.DOM = newDOM;
  }

  /**
   * Returns `true` if the element has child elements and `false` if not.
   *
   * @return {boolean} whether the element has children.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Check whether the element has child elements.
   *   let isParent = myXML.hasChildren();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Style the text.
   *   if (isParent === true) {
   *     text('Parent', 50, 50);
   *   } else {
   *     text('Not Parent', 50, 50);
   *   }
   *
   *   describe('The word "Parent" written in black on a gray background.');
   * }
   */
  hasChildren() {
    return this.DOM.children.length > 0;
  }

  /**
   * Returns an array with the names of the element's child elements as
   * `String`s.
   *
   * @return {String[]} names of the child elements.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the names of the element's children as an array.
   *   let children = myXML.listChildren();
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the array.
   *   for (let i = 0; i < children.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 25;
   *
   *     // Display the child element's name.
   *     text(children[i], 10, y);
   *   }
   *
   *   describe(
   *     'The words "mammal", "mammal", "mammal", and "reptile" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   */
  listChildren() {
    const arr = [];
    for (let i = 0; i < this.DOM.childNodes.length; i++) {
      arr.push(this.DOM.childNodes[i].nodeName);
    }
    return arr;
  }

  /**
   * Returns an array with the element's child elements as new
   * <a href="#/p5.XML">p5.XML</a> objects.
   *
   * The parameter, `name`, is optional. If a string is passed, as in
   * `myXML.getChildren('cat')`, then the method will only return child elements
   * with the tag `&lt;cat&gt;`.
   *
   * @param {String} [name] name of the elements to return.
   * @return {p5.XML[]} child elements.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get an array of the child elements.
   *   let children = myXML.getChildren();
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the array.
   *   for (let i = 0; i < children.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 20;
   *
   *     // Get the child element's content.
   *     let content = children[i].getContent();
   *
   *     // Display the child element's content.
   *     text(content, 10, y);
   *   }
   *
   *   describe(
   *     'The words "Goat", "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get an array of the child elements
   *   // that are mammals.
   *   let children = myXML.getChildren('mammal');
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the array.
   *   for (let i = 0; i < children.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 20;
   *
   *     // Get the child element's content.
   *     let content = children[i].getContent();
   *
   *     // Display the child element's content.
   *     text(content, 10, y);
   *   }
   *
   *   describe(
   *     'The words "Goat", "Leopard", and "Zebra" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   */
  getChildren(param) {
    if (param) {
      return elementsToP5XML(this.DOM.getElementsByTagName(param));
    } else {
      return elementsToP5XML(this.DOM.children);
    }
  }

  /**
   * Returns the first matching child element as a new
   * <a href="#/p5.XML">p5.XML</a> object.
   *
   * The parameter, `name`, is optional. If a string is passed, as in
   * `myXML.getChild('cat')`, then the first child element with the tag
   * `&lt;cat&gt;` will be returned. If a number is passed, as in
   * `myXML.getChild(1)`, then the child element at that index will be returned.
   *
   * @param {String|Integer} name element name or index.
   * @return {p5.XML} child element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first child element that is a mammal.
   *   let goat = myXML.getChild('mammal');
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Get the child element's content.
   *   let content = goat.getContent();
   *
   *   // Display the child element's content.
   *   text(content, 50, 50);
   *
   *   describe('The word "Goat" written in black on a gray background.');
   * }
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the child element at index 1.
   *   let leopard = myXML.getChild(1);
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Get the child element's content.
   *   let content = leopard.getContent();
   *
   *   // Display the child element's content.
   *   text(content, 50, 50);
   *
   *   describe('The word "Leopard" written in black on a gray background.');
   * }
   */
  getChild(param) {
    if (typeof param === 'string') {
      for (const child of this.DOM.children) {
        if (child.tagName === param) return new XML(child);
      }
    } else {
      return new XML(this.DOM.children[param]);
    }
  }

  /**
   * Adds a new child element and returns a reference to it.
   *
   * The parameter, `child`, is the <a href="#/p5.XML">p5.XML</a> object to add
   * as a child element. For example, calling `myXML.addChild(otherXML)` inserts
   * `otherXML` as a child element of `myXML`.
   *
   * @param {p5.XML} child child element to add.
   * @return {p5.XML} added child element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Create a new p5.XML object.
   *   let newAnimal = new p5.XML();
   *
   *   // Set its properties.
   *   newAnimal.setName('hydrozoa');
   *   newAnimal.setAttribute('id', 4);
   *   newAnimal.setAttribute('species', 'Physalia physalis');
   *   newAnimal.setContent('Bluebottle');
   *
   *   // Add the child element.
   *   myXML.addChild(newAnimal);
   *
   *   // Get the first child element that is a hydrozoa.
   *   let blueBottle = myXML.getChild('hydrozoa');
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Get the child element's content.
   *   let content = blueBottle.getContent();
   *
   *   // Display the child element's content.
   *   text(content, 50, 50);
   *
   *   describe('The word "Bluebottle" written in black on a gray background.');
   * }
   */
  addChild(node) {
    if (node instanceof XML) {
      this.DOM.appendChild(node.DOM);
    }
  }

  /**
   * Removes the first matching child element.
   *
   * The parameter, `name`, is the child element to remove. If a string is
   * passed, as in `myXML.removeChild('cat')`, then the first child element
   * with the tag `&lt;cat&gt;` will be removed. If a number is passed, as in
   * `myXML.removeChild(1)`, then the child element at that index will be
   * removed.
   *
   * @param {String|Integer} name name or index of the child element to remove.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Remove the first mammal element.
   *   myXML.removeChild('mammal');
   *
   *   // Get an array of child elements.
   *   let children = myXML.getChildren();
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the array.
   *   for (let i = 0; i < children.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 25;
   *
   *     // Get the child element's content.
   *     let content = children[i].getContent();
   *
   *     // Display the child element's content.
   *     text(content, 10, y);
   *   }
   *
   *   describe(
   *     'The words "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Remove the element at index 2.
   *   myXML.removeChild(2);
   *
   *   // Get an array of child elements.
   *   let children = myXML.getChildren();
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the array.
   *   for (let i = 0; i < children.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 25;
   *
   *     // Get the child element's content.
   *     let content = children[i].getContent();
   *
   *     // Display the child element's content.
   *     text(content, 10, y);
   *   }
   *
   *   describe(
   *     'The words "Goat", "Leopard", and "Turtle" written on separate lines. The text is black on a gray background.'
   *   );
   * }
   */
  removeChild(param) {
    let ind = -1;
    if (typeof param === 'string') {
      for (let i = 0; i < this.DOM.children.length; i++) {
        if (this.DOM.children[i].tagName === param) {
          ind = i;
          break;
        }
      }
    } else {
      ind = param;
    }
    if (ind !== -1) {
      this.DOM.removeChild(this.DOM.children[ind]);
    }
  }

  /**
   * Returns the number of attributes the element has.
   *
   * @return {Integer} number of attributes.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first child element.
   *   let first = myXML.getChild(0);
   *
   *   // Get the number of attributes.
   *   let numAttributes = first.getAttributeCount();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the number of attributes.
   *   text(numAttributes, 50, 50);
   *
   *   describe('The number "2" written in black on a gray background.');
   * }
   */
  getAttributeCount() {
    return this.DOM.attributes.length;
  }

  /**
   * Returns an `Array` with the names of the element's attributes.
   *
   * Note: Use
   * <a href="#/p5.XML/getString">myXML.getString()</a> or
   * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
   *
   * @return {String[]} attribute names.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first child element.
   *   let first = myXML.getChild(0);
   *
   *   // Get the number of attributes.
   *   let attributes = first.listAttributes();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's attributes.
   *   text(attributes, 50, 50);
   *
   *   describe('The text "id,species" written in black on a gray background.');
   * }
   */
  listAttributes() {
    const arr = [];

    for (const attribute of this.DOM.attributes) {
      arr.push(attribute.nodeName);
    }

    return arr;
  }

  /**
   * Returns `true` if the element has a given attribute and `false` if not.
   *
   * The parameter, `name`, is a string with the name of the attribute being
   * checked.
   *
   * Note: Use
   * <a href="#/p5.XML/getString">myXML.getString()</a> or
   * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
   *
   * @param {String} name name of the attribute to be checked.
   * @return {boolean} whether the element has the attribute.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first mammal child element.
   *   let mammal = myXML.getChild('mammal');
   *
   *   // Check whether the element has an
   *   // species attribute.
   *   let hasSpecies = mammal.hasAttribute('species');
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display whether the element has a species attribute.
   *   if (hasSpecies === true) {
   *     text('Species', 50, 50);
   *   } else {
   *     text('No species', 50, 50);
   *   }
   *
   *   describe('The text "Species" written in black on a gray background.');
   * }
   */
  hasAttribute(name) {
    const obj = {};

    for (const attribute of this.DOM.attributes) {
      obj[attribute.nodeName] = attribute.nodeValue;
    }

    return obj[name] ? true : false;
  }

  /**
   * Return an attribute's value as a `Number`.
   *
   * The first parameter, `name`, is a string with the name of the attribute
   * being checked. For example, calling `myXML.getNum('id')` returns the
   * element's `id` attribute as a number.
   *
   * The second parameter, `defaultValue`, is optional. If a number is passed,
   * as in `myXML.getNum('id', -1)`, it will be returned if the attribute
   * doesn't exist or can't be converted to a number.
   *
   * Note: Use
   * <a href="#/p5.XML/getString">myXML.getString()</a> or
   * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
   *
   * @param {String} name name of the attribute to be checked.
   * @param {Number} [defaultValue] value to return if the attribute doesn't exist.
   * @return {Number} attribute value as a number.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Get the reptile's ID.
   *   let id = reptile.getNum('id');
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the ID attribute.
   *   text(`${content} is ${id + 1}th`, 5, 50, 90);
   *
   *   describe(`The text "${content} is ${id + 1}th" written in black on a gray background.`);
   * }
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Get the reptile's size.
   *   let weight = reptile.getNum('weight', 135);
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the ID attribute.
   *   text(`${content} is ${weight}kg`, 5, 50, 90);
   *
   *   describe(
   *     `The text "${content} is ${weight}kg" written in black on a gray background.`
   *   );
   * }
   */
  getNum(name, defaultValue) {
    const obj = {};

    for (const attribute of this.DOM.attributes) {
      obj[attribute.nodeName] = attribute.nodeValue;
    }

    return Number(obj[name]) || defaultValue || 0;
  }

  /**
   * Return an attribute's value as a string.
   *
   * The first parameter, `name`, is a string with the name of the attribute
   * being checked. For example, calling `myXML.getString('color')` returns the
   * element's `id` attribute as a string.
   *
   * The second parameter, `defaultValue`, is optional. If a string is passed,
   * as in `myXML.getString('color', 'deeppink')`, it will be returned if the
   * attribute doesn't exist.
   *
   * Note: Use
   * <a href="#/p5.XML/getString">myXML.getString()</a> or
   * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
   *
   * @param {String} name name of the attribute to be checked.
   * @param {Number} [defaultValue] value to return if the attribute doesn't exist.
   * @return {String} attribute value as a string.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Get the reptile's species.
   *   let species = reptile.getString('species');
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the species attribute.
   *   text(`${content}: ${species}`, 5, 50, 90);
   *
   *   describe(`The text "${content}: ${species}" written in black on a gray background.`);
   * }
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Get the reptile's color.
   *   let attribute = reptile.getString('color', 'green');
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *   fill(attribute);
   *
   *   // Display the element's content.
   *   text(content, 50, 50);
   *
   *   describe(`The text "${content}" written in green on a gray background.`);
   * }
   */
  getString(name, defaultValue) {
    const obj = {};

    for (const attribute of this.DOM.attributes) {
      obj[attribute.nodeName] = attribute.nodeValue;
    }

    return obj[name] ? String(obj[name]) : defaultValue || null;
  }

  /**
   * Sets an attribute to a given value.
   *
   * The first parameter, `name`, is a string with the name of the attribute
   * being set.
   *
   * The second parameter, `value`, is the attribute's new value. For example,
   * calling `myXML.setAttribute('id', 123)` sets the `id` attribute to the
   * value 123.
   *
   * @param {String} name name of the attribute to be set.
   * @param {Number|String|Boolean} value attribute's new value.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Set the reptile's color.
   *   reptile.setAttribute('color', 'green');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Get the reptile's color.
   *   let attribute = reptile.getString('color');
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's content.
   *   text(`${content} is ${attribute}`, 5, 50, 90);
   *
   *   describe(
   *     `The text "${content} is ${attribute}" written in green on a gray background.`
   *   );
   * }
   */
  setAttribute(name, value) {
    this.DOM.setAttribute(name, value);
  }

  /**
   * Returns the element's content as a `String`.
   *
   * The parameter, `defaultValue`, is optional. If a string is passed, as in
   * `myXML.getContent('???')`, it will be returned if the element has no
   * content.
   *
   * @param {String} [defaultValue] value to return if the element has no
   *                                content.
   * @return {String} element's content as a string.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's content.
   *   let content = reptile.getContent();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's content.
   *   text(content, 5, 50, 90);
   *
   *   describe(`The text "${content}" written in green on a gray background.`);
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Create a p5.XML object.
   *   let blankSpace = new p5.XML();
   *
   *   // Get the element's content and use a default value.
   *   let content = blankSpace.getContent('Your name');
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's content.
   *   text(content, 5, 50, 90);
   *
   *   describe(`The text "${content}" written in green on a gray background.`);
   * }
   */
  getContent(defaultValue) {
    let str;
    str = this.DOM.textContent;
    str = str.replace(/\s\s+/g, ',');
    return str || defaultValue || null;
  }

  /**
   * Sets the element's content.
   *
   * An element's content is the text between its tags. For example, the element
   * `&lt;language&gt;JavaScript&lt;/language&gt;` has the content `JavaScript`.
   *
   * The parameter, `content`, is a string with the element's new content.
   *
   * @method setContent
   * @param {String} content new content for the element.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get the first reptile child element.
   *   let reptile = myXML.getChild('reptile');
   *
   *   // Get the reptile's original content.
   *   let oldContent = reptile.getContent();
   *
   *   // Set the reptile's content.
   *   reptile.setContent('Loggerhead');
   *
   *   // Get the reptile's new content.
   *   let newContent = reptile.getContent();
   *
   *   // Style the text.
   *   textAlign(CENTER, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Display the element's old and new content.
   *   text(`${oldContent}: ${newContent}`, 5, 50, 90);
   *
   *   describe(
   *     `The text "${oldContent}: ${newContent}" written in green on a gray background.`
   *   );
   * }
   */
  setContent(content) {
    if (!this.DOM.children.length) {
      this.DOM.textContent = content;
    }
  }

  /**
   * Returns the element as a `String`.
   *
   * `myXML.serialize()` is useful for sending the element over the network or
   * saving it to a file.
   *
   * @return {String} element as a string.
   *
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(12);
   *
   *   // Display instructions.
   *   text('Double-click to save', 5, 50, 90);
   *
   *   describe('The text "Double-click to save" written in black on a gray background.');
   * }
   *
   * // Save the file when the user double-clicks.
   * function doubleClicked() {
   *   // Create a p5.PrintWriter object.
   *   // Use the file format .xml.
   *   let myWriter = createWriter('animals', 'xml');
   *
   *   // Serialize the XML data to a string.
   *   let data = myXML.serialize();
   *
   *   // Write the data to the print stream.
   *   myWriter.write(data);
   *
   *   // Save the file and close the print stream.
   *   myWriter.close();
   * }
   */
  serialize() {
    const xmlSerializer = new XMLSerializer();
    return xmlSerializer.serializeToString(this.DOM);
  }
}

function elementsToP5XML(elements) {
  const arr = [];
  for (let i = 0; i < elements.length; i++) {
    arr.push(new XML(elements[i]));
  }
  return arr;
}

function xml(p5, fn){
  /**
   * A class to describe an XML object.
   *
   * Each `p5.XML` object provides an easy way to interact with XML data.
   * Extensible Markup Language
   * (<a href="https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction" target="_blank">XML</a>)
   * is a standard format for sending data between applications. Like HTML, the
   * XML format is based on tags and attributes, as in
   * `&lt;time units="s"&gt;1234&lt;/time&gt;`.
   *
   * Note: Use <a href="#/p5/loadXML">loadXML()</a> to load external XML files.
   *
   * @class p5.XML
   * @example
   * let myXML;
   *
   * async function setup() {
   *   // Load the XML and create a p5.XML object.
   *   myXML = await loadXML('assets/animals.xml');
   *
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Get an array with all mammal tags.
   *   let mammals = myXML.getChildren('mammal');
   *
   *   // Style the text.
   *   textAlign(LEFT, CENTER);
   *   textFont('Courier New');
   *   textSize(14);
   *
   *   // Iterate over the mammals array.
   *   for (let i = 0; i < mammals.length; i += 1) {
   *
   *     // Calculate the y-coordinate.
   *     let y = (i + 1) * 25;
   *
   *     // Get the mammal's common name.
   *     let name = mammals[i].getContent();
   *
   *     // Display the mammal's name.
   *     text(name, 20, y);
   *   }
   *
   *   describe(
   *     'The words "Goat", "Leopard", and "Zebra" written on three separate lines. The text is black on a gray background.'
   *   );
   * }
   */
  p5.XML = XML;
}

if(typeof p5 !== 'undefined'){
  xml(p5, p5.prototype);
}

export { XML, xml as default };
