;(function (exports, DOMParser, xmlbuilder) {
	// Checks if running in a non-browser environment

	var inNode = typeof process !== "undefined" && process.versions && !!process.versions.node
		  , utf8_to_b64
		, b64_to_utf8;



 	// this library runs in browsers and nodejs, set up functions accordingly
 	if (inNode) {
 		exports.parseFile = function (filename, callback) {
 			console.warn('parseFile is deprecated. Please use parseFileSync instead.');
    		var fs = require('fs');
			var inxml = fs.readFileSync(filename, 'utf8');
			exports.parseString(inxml, callback);
  		}

  		exports.parseFileSync = function (filename) {
    		var fs = require('fs');
			var inxml = fs.readFileSync(filename, 'utf8');
			return exports.parseStringSync(inxml);
  		}

  		// set up base64 encode/decode functions
  		utf8_to_b64= function ( str ) {
  			return new Buffer(str).toString('base64');
		}
		b64_to_utf8 = function ( str ) {
		    return new Buffer(str, 'base64').toString('utf8');
		}
  	} else {
  		// we're in a browser context, find the DOMParser
		if (typeof window.DOMParser != 'undefined') {
		    DOMParser = function(xmlStr) {
		        return ( new window.DOMParser() ).parseFromString(xmlStr, 'text/xml');
		    };
		} else if (typeof window.ActiveXObject != 'undefined' &&
		       new window.ActiveXObject('Microsoft.XMLDOM')) {
		    DOMParser = function(xmlStr) {
		        var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM');
		        xmlDoc.async = 'false';
		        xmlDoc.loadXML(xmlStr);
		        return xmlDoc;
		    };
		} else {
		    throw new Error('No XML parser found');
		}

		// set up base64 encode/decode functions
		if (typeof window.btoa == 'undefined') {
			throw new Error('No base64 support found');
			// TODO: shim btoa and atob if not present (ie < 10)
			//http://stackoverflow.com/questions/246801/how-can-you-encode-to-base64-using-javascript/247261#247261
		} else {
			utf8_to_b64= function ( str ) {
			    return window.btoa(unescape(encodeURIComponent( str )));
			}

			b64_to_utf8 = function ( str ) {
			    return decodeURIComponent(escape(window.atob( str )));
			}
		}

	}

	exports.parseString = function (xml, callback) {
		console.warn('parseString is deprecated. Please use parseStringSync instead.');
		var doc, error, plist;
		try {
			doc = buildDOMParser().parseFromString(xml);
			plist = parsePlistXML(doc.documentElement);
		} catch(e) {
			error = e;
		}
		callback(error, plist);
	}

	exports.parseStringSync = function (xml) {
		var doc = buildDOMParser().parseFromString(xml);
    var plist;
		if (doc.documentElement.nodeName !== 'plist') {
			throw new Error('malformed document. First element should be <plist>');
		}
		plist = parsePlistXML(doc.documentElement);

		// if the plist is an array with 1 element, pull it out of the array
		if(isArray(plist) && plist.length == 1) {
			plist = plist[0];
		}
		return plist;
	}

	/**
	 * convert an XML based plist document into a JSON representation
	 *
	 * @param object xml_node current XML node in the plist
	 * @return built up JSON object
	 */
	function parsePlistXML(node) {
		var i, new_obj, key, val, new_arr;
		if (!node)
			return null;

		if (node.nodeName === 'plist') {
			new_arr = [];
			for (i=0;i < node.childNodes.length;i++) {
				// ignore comment nodes (text)
				if (node.childNodes[i].nodeType !== 3) {
					new_arr.push( parsePlistXML(node.childNodes[i]));
				}
			}
			return new_arr;
		}
		else if(node.nodeName === 'dict') {
			new_obj = {};
			key = null;
			for (i=0;i < node.childNodes.length;i++) {
				// ignore comment nodes (text)
				if (node.childNodes[i].nodeType !== 3) {
					if (key === null) {
						key = parsePlistXML(node.childNodes[i]);
					} else {
						new_obj[key] = parsePlistXML(node.childNodes[i]);
						key = null;
					}
				}
			}
			return new_obj;
		}
		else if(node.nodeName === 'array') {
			new_arr = [];
			for (i=0;i < node.childNodes.length;i++) {
				// ignore comment nodes (text)
				if (node.childNodes[i].nodeType !== 3) {
					res = parsePlistXML(node.childNodes[i]);
					if (res) new_arr.push( res );
				}
			}
			return new_arr;
		}
		else if(node.nodeName === '#text') {
			// TODO: what should we do with text types? (CDATA sections)
		}
		else if(node.nodeName === 'key') {
			return node.childNodes[0].nodeValue;
		}
		else if(node.nodeName === 'string') {
			var res = '';
			for (var d=0; d < node.childNodes.length; d++)
			{
				res += node.childNodes[d].nodeValue;
			}
			return res;
		}
		else if(node.nodeName === 'integer') {
			// parse as base 10 integer
			return parseInt(node.childNodes[0].nodeValue, 10);
		}
		else if(node.nodeName === 'real') {
			var res = '';
			for (var d=0; d < node.childNodes.length; d++)
			{
				if(node.childNodes[d].nodeType === 3) {
					res += node.childNodes[d].nodeValue;
				}
			}
			return parseFloat(res);
		}
		else if(node.nodeName === 'data') {
			var res = '';
			for (var d=0; d < node.childNodes.length; d++)
			{
				if(node.childNodes[d].nodeType === 3) {
					res += node.childNodes[d].nodeValue;
				}
			}

			// validate that the string is encoded as base64
			var base64Matcher = new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$");
			if (!base64Matcher.test(res.replace(/\s/g,'')) ) {
				throw new Error('malformed document. <data> element is not base64 encoded');
			}

			// decode base64 data as utf8 string
			return b64_to_utf8(res);
		}
		else if(node.nodeName === 'date') {
			return new Date(node.childNodes[0].nodeValue);
		}
		else if(node.nodeName === 'true') {
			return true;
		}
		else if(node.nodeName === 'false') {
			return false;
		}
	}


  function ISODateString(d){
    function pad(n){return n<10 ? '0'+n : n}
    return d.getUTCFullYear()+'-'
        + pad(d.getUTCMonth()+1)+'-'
        + pad(d.getUTCDate())+'T'
        + pad(d.getUTCHours())+':'
        + pad(d.getUTCMinutes())+':'
        + pad(d.getUTCSeconds())+'Z'
  }

  // instanceof is horribly unreliable so we use these hackish but safer checks
  // http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray
  function isArray(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
  }

  function isDate(obj) {
    return Object.prototype.toString.call(obj) === '[object Date]';
  }

  function isBoolean(obj) {
    return (obj === true || obj === false || toString.call(obj) == '[object Boolean]');
  }

  function isNumber(obj) {
    return Object.prototype.toString.call(obj) === '[object Number]';
  }

  function isObject(obj) {
    return Object.prototype.toString.call(obj) === '[object Object]';
  }

  function isString(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
  }

  /**
   * generate an XML plist string from the input object
   *
   * @param object obj the object to convert
   * @return string converted plist
   */
  exports.build = function(obj) {
    var XMLHDR = { 'version': '1.0','encoding': 'UTF-8'}
        , XMLDTD = { 'ext': 'PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"'}
        , doc = xmlbuilder.create()
        , child = doc.begin('plist', XMLHDR, XMLDTD).att('version', '1.0');

    walk_obj(obj, child);
    return child.end({pretty: true });
  }

  // depth first, recursive traversal of a javascript object. when complete,
  // next_child contains a reference to the build XML object.
  function walk_obj(next, next_child) {
    var tag_type, i, prop;

    if(isArray(next)) {
      next_child = next_child.ele('array');
      for(i=0 ;i < next.length;i++) {
        walk_obj(next[i], next_child);
      }
    }
    else if (isObject(next)) {
      if (inNode && next instanceof Buffer) {
        next_child.ele('data').raw(next.toString('base64'));
      } else {
        next_child = next_child.ele('dict');
        for(prop in next) {
          if(next.hasOwnProperty(prop)) {
            next_child.ele('key').txt(prop);
            walk_obj(next[prop], next_child);
          }
        }
      }
    }
    else if(isNumber(next)) {
      // detect if this is an integer or real
      tag_type =(next % 1 === 0) ? 'integer' : 'real';
      next_child.ele(tag_type).txt(next.toString());
    }
    else if(isDate(next)) {
      next_child.ele('date').raw(ISODateString(new Date(next)));
    }
    else if(isBoolean(next)) {
      val = next ? 'true' : 'false';
      next_child.ele(val);
    }
    else if(isString(next)) {
      //if (str!=obj || str.indexOf("\n")>=0) str = "<![CDATA["+str+"]]>";
      var base64Matcher = new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$");
      if (!inNode && base64Matcher.test(next.replace(/\s/g,''))) {
      	// data is base 64 encoded so assume it's a <data> node
        next_child.ele('data').raw(utf8_to_b64(next));
      } else {
      	// it's not base 64 encoded, assume it's a <string> node
      	next_child.ele('string').raw(next);
      }
    }
  };

	function buildDOMParser() {
		// prevent the parser from logging non-fatel errors
		return new DOMParser({errorHandler: function() {}});
	}

})(typeof exports === 'undefined' ? plist = {} : exports, typeof require !== 'undefined' ? require('xmldom').DOMParser : null, typeof require !== 'undefined' ? require('xmlbuilder') : xmlbuilder)
// the above line checks for exports (defined in node) and uses it, or creates
// a global variable and exports to that. also, if in node, require DOMParser
// node-style, in browser it should already be present
