(function () {
  var sax;

  if (
    typeof module !== "undefined" &&
    module.exports &&
    !global.xmldocAssumeBrowser
  ) {
    // We're being used in a Node-like environment
    sax = require("sax");
  } else {
    // assume it's attached to the Window object in a browser
    sax = this.sax;

    if (!sax) {
      // no sax for you!
      throw new Error(
        "Expected sax to be defined. Make sure you're including sax.js before this file.",
      );
    }
  }

  /**
   * XmlElement is our basic building block. Everything is an XmlElement; even XmlDocument
   * behaves like an XmlElement by inheriting its attributes and functions.
   */
  function XmlElement(tag, parser) {
    // If you didn't hand us a parser (common case) see if we can grab one
    // from the current execution stack.
    if (!parser) {
      var delegate = delegates[delegates.length - 1];

      if (delegate.parser) {
        parser = delegate.parser;
      }
    }

    this.name = tag.name;
    this.attr = tag.attributes;
    this.val = "";
    this.children = [];
    this.firstChild = null;
    this.lastChild = null;

    // Assign parse information
    this.line = parser ? parser.line : null;
    this.column = parser ? parser.column : null;
    this.position = parser ? parser.position : null;
    this.startTagPosition = parser ? parser.startTagPosition : null;
  }

  // Private methods

  XmlElement.prototype._addChild = function (child) {
    // add to our children array
    this.children.push(child);

    // update first/last pointers
    if (!this.firstChild) this.firstChild = child;
    this.lastChild = child;
  };

  // SaxParser handlers

  XmlElement.prototype._opentag = function (tag) {
    var child = new XmlElement(tag);

    this._addChild(child);

    delegates.unshift(child);
  };

  XmlElement.prototype._closetag = function () {
    delegates.shift();
  };

  XmlElement.prototype._text = function (text) {
    if (typeof this.children === "undefined") return;

    this.val += text;

    this._addChild(new XmlTextNode(text));
  };

  XmlElement.prototype._cdata = function (cdata) {
    this.val += cdata;

    this._addChild(new XmlCDataNode(cdata));
  };

  XmlElement.prototype._comment = function (comment) {
    if (typeof this.children === "undefined") return;

    this._addChild(new XmlCommentNode(comment));
  };

  XmlElement.prototype._error = function (err) {
    throw err;
  };

  // Useful functions

  XmlElement.prototype.eachChild = function (iterator, context) {
    for (var i = 0, l = this.children.length; i < l; i++)
      if (this.children[i].type === "element")
        if (
          iterator.call(context, this.children[i], i, this.children) === false
        )
          return;
  };

  XmlElement.prototype.childNamed = function (name) {
    for (var i = 0, l = this.children.length; i < l; i++) {
      var child = this.children[i];
      if (child.name === name) return child;
    }
    return undefined;
  };

  XmlElement.prototype.childrenNamed = function (name) {
    var matches = [];

    for (var i = 0, l = this.children.length; i < l; i++)
      if (this.children[i].name === name) matches.push(this.children[i]);

    return matches;
  };

  XmlElement.prototype.childWithAttribute = function (name, value) {
    for (var i = 0, l = this.children.length; i < l; i++) {
      var child = this.children[i];
      if (
        child.type === "element" &&
        ((value && child.attr[name] === value) || (!value && child.attr[name]))
      )
        return child;
    }
    return undefined;
  };

  XmlElement.prototype.descendantsNamed = function (name) {
    var matches = [];

    for (var i = 0, l = this.children.length; i < l; i++) {
      var child = this.children[i];
      if (child.type === "element") {
        if (child.name === name) matches.push(child);
        matches = matches.concat(child.descendantsNamed(name));
      }
    }

    return matches;
  };

  XmlElement.prototype.descendantWithPath = function (path) {
    var descendant = this;
    var components = path.split(".");

    for (var i = 0, l = components.length; i < l; i++)
      if (descendant && descendant.type === "element")
        descendant = descendant.childNamed(components[i]);
      else return undefined;

    return descendant;
  };

  XmlElement.prototype.valueWithPath = function (path) {
    var components = path.split("@");
    var descendant = this.descendantWithPath(components[0]);
    if (descendant)
      return components.length > 1
        ? descendant.attr[components[1]]
        : descendant.val;
    else return undefined;
  };

  // String formatting (for debugging)

  XmlElement.prototype.toString = function (options) {
    return this.toStringWithIndent("", options);
  };

  XmlElement.prototype.toStringWithIndent = function (indent, options) {
    var s = indent + "<" + this.name;
    var linebreak = options && options.compressed ? "" : "\n";
    var preserveWhitespace = options && options.preserveWhitespace;

    for (var name in this.attr)
      if (Object.prototype.hasOwnProperty.call(this.attr, name))
        s += " " + name + '="' + escapeXML(this.attr[name]) + '"';

    if (this.children.length === 1 && this.children[0].type !== "element") {
      s += ">" + this.children[0].toString(options) + "</" + this.name + ">";
    } else if (this.children.length) {
      s += ">" + linebreak;

      var childIndent = indent + (options && options.compressed ? "" : "  ");

      for (var i = 0, l = this.children.length; i < l; i++) {
        s +=
          this.children[i].toStringWithIndent(childIndent, options) + linebreak;
      }

      s += indent + "</" + this.name + ">";
    } else if (options && options.html) {
      var whiteList = [
        "area",
        "base",
        "br",
        "col",
        "embed",
        "frame",
        "hr",
        "img",
        "input",
        "keygen",
        "link",
        "menuitem",
        "meta",
        "param",
        "source",
        "track",
        "wbr",
      ];
      if (whiteList.indexOf(this.name) !== -1) s += "/>";
      else s += "></" + this.name + ">";
    } else {
      s += "/>";
    }

    return s;
  };

  // Alternative XML nodes

  function XmlTextNode(text) {
    this.text = text;
  }

  XmlTextNode.prototype.toString = function (options) {
    return formatText(escapeXML(this.text), options);
  };

  XmlTextNode.prototype.toStringWithIndent = function (indent, options) {
    return indent + this.toString(options);
  };

  function XmlCDataNode(cdata) {
    this.cdata = cdata;
  }

  XmlCDataNode.prototype.toString = function (options) {
    return "<![CDATA[" + formatText(this.cdata, options) + "]]>";
  };

  XmlCDataNode.prototype.toStringWithIndent = function (indent, options) {
    return indent + this.toString(options);
  };

  function XmlCommentNode(comment) {
    this.comment = comment;
  }

  XmlCommentNode.prototype.toString = function (options) {
    return "<!--" + formatText(escapeXML(this.comment), options) + "-->";
  };

  XmlCommentNode.prototype.toStringWithIndent = function (indent, options) {
    return indent + this.toString(options);
  };

  // Node type tag

  XmlElement.prototype.type = "element";
  XmlTextNode.prototype.type = "text";
  XmlCDataNode.prototype.type = "cdata";
  XmlCommentNode.prototype.type = "comment";

  /**
   * XmlDocument is the class we expose to the user; it uses the sax parser to create a hierarchy
   * of XmlElements.
   */
  function XmlDocument(xml) {
    xml && (xml = xml.toString().trim());

    if (!xml) throw new Error("No XML to parse!");

    // Stores doctype (if defined)
    this.doctype = "";

    // Expose the parser to the other delegates while the parser is running
    this.parser = sax.parser(true); // strict
    addParserEvents(this.parser);

    // We'll use the file-scoped "delegates" var to remember what elements we're currently
    // parsing; they will push and pop off the stack as we get deeper into the XML hierarchy.
    // It's safe to use a global because JS is single-threaded.
    delegates = [this];

    try {
      this.parser.write(xml);
    } finally {
      // Remove the parser as it is no longer needed and should not be exposed to clients
      delete this.parser;
    }
  }

  // make XmlDocument inherit XmlElement's methods
  extend(XmlDocument.prototype, XmlElement.prototype);

  XmlDocument.prototype._opentag = function (tag) {
    if (typeof this.children === "undefined")
      // the first tag we encounter should be the root - we'll "become" the root XmlElement
      XmlElement.call(this, tag);
    // all other tags will be the root element's children
    else XmlElement.prototype._opentag.apply(this, arguments);
  };

  XmlDocument.prototype._doctype = function (doctype) {
    this.doctype += doctype;
  };

  // file-scoped global stack of delegates
  var delegates = null;

  /*
   * Helper functions
   */

  function addParserEvents(parser) {
    parser.onopentag = parser_opentag;
    parser.onclosetag = parser_closetag;
    parser.ontext = parser_text;
    parser.oncdata = parser_cdata;
    parser.oncomment = parser_comment;
    parser.ondoctype = parser_doctype;
    parser.onerror = parser_error;
  }

  // create these closures and cache them by keeping them file-scoped
  function parser_opentag() {
    delegates[0] && delegates[0]._opentag.apply(delegates[0], arguments);
  }
  function parser_closetag() {
    delegates[0] && delegates[0]._closetag.apply(delegates[0], arguments);
  }
  function parser_text() {
    delegates[0] && delegates[0]._text.apply(delegates[0], arguments);
  }
  function parser_cdata() {
    delegates[0] && delegates[0]._cdata.apply(delegates[0], arguments);
  }
  function parser_comment() {
    delegates[0] && delegates[0]._comment.apply(delegates[0], arguments);
  }
  function parser_doctype() {
    delegates[0] && delegates[0]._doctype.apply(delegates[0], arguments);
  }
  function parser_error() {
    delegates[0] && delegates[0]._error.apply(delegates[0], arguments);
  }

  // a relatively standard extend method
  function extend(destination, source) {
    for (var prop in source)
      if (source.hasOwnProperty(prop)) destination[prop] = source[prop];
  }

  // escapes XML entities like "<", "&", etc.
  function escapeXML(value) {
    return value
      .toString()
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/'/g, "&apos;")
      .replace(/"/g, "&quot;");
  }

  // formats some text for debugging given a few options
  function formatText(text, options) {
    var finalText = text;

    if (options && options.trimmed && text.length > 25) {
      finalText = finalText.substring(0, 25).trim() + "…";
    }

    if (!(options && options.preserveWhitespace)) {
      finalText = finalText.trim();
    }

    return finalText;
  }

  // Are we being used in a Node-like environment?
  if (
    typeof module !== "undefined" &&
    module.exports &&
    !global.xmldocAssumeBrowser
  ) {
    module.exports.XmlDocument = XmlDocument;
    module.exports.XmlElement = XmlElement;
    module.exports.XmlTextNode = XmlTextNode;
    module.exports.XmlCDataNode = XmlCDataNode;
    module.exports.XmlCommentNode = XmlCommentNode;
  } else {
    this.XmlDocument = XmlDocument;
    this.XmlElement = XmlElement;
    this.XmlTextNode = XmlTextNode;
    this.XmlCDataNode = XmlCDataNode;
    this.XmlCommentNode = XmlCommentNode;
  }
})();
