(function() {

  // Convert Uint8Array to base64 string
  //  https://gist.github.com/jonleighton/958841
  function base64ArrayBuffer(bytes) {
    var base64    = ''
    var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

    var byteLength    = bytes.byteLength
    var byteRemainder = byteLength % 3
    var mainLength    = byteLength - byteRemainder

    var a, b, c, d
    var chunk

    // Main loop deals with bytes in chunks of 3
    for (var i = 0; i < mainLength; i = i + 3) {
      // Combine the three bytes into a single integer
      chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]

      // Use bitmasks to extract 6-bit segments from the triplet
      a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
      b = (chunk & 258048)   >> 12 // 258048   = (2^6 - 1) << 12
      c = (chunk & 4032)     >>  6 // 4032     = (2^6 - 1) << 6
      d = chunk & 63               // 63       = 2^6 - 1

      // Convert the raw binary segments to the appropriate ASCII encoding
      base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
    }

    // Deal with the remaining bytes and padding
    if (byteRemainder == 1) {
      chunk = bytes[mainLength]

      a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2

      // Set the 4 least significant bits to zero
      b = (chunk & 3)   << 4 // 3   = 2^2 - 1

      base64 += encodings[a] + encodings[b] + '=='
    } else if (byteRemainder == 2) {
      chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]

      a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
      b = (chunk & 1008)  >>  4 // 1008  = (2^6 - 1) << 4

      // Set the 2 least significant bits to zero
      c = (chunk & 15)    <<  2 // 15    = 2^4 - 1

      base64 += encodings[a] + encodings[b] + encodings[c] + '='
    }

    return base64
  }


  /**
  * ENMLOfPlainText
  * @param  { string } text (Plain)
  * @return string - ENML
  */
  function ENMLOfPlainText(text){

    var writer = new XMLWriter();

    writer.startDocument = writer.startDocument || writer.writeStartDocument;
    writer.endDocument = writer.endDocument || writer.writeEndDocument;
    writer.startDocument = writer.startElement || writer.writeStartElement;
    writer.startDocument = writer.endElement || writer.writeEndElement;

    writer.startDocument('1.0', 'UTF-8', false);
    writer.write('<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">');
    writer.write("\n");
    writer.startElement('en-note');
    writer.writeAttribute('style', 'word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;');

    var lines = text.match(/^.*((\r\n|\n|\r)|$)/gm);

    lines.forEach(function(line) {
      writer.text("\n");
      writer.startElement('div');
      writer.text(line.replace(/(\r\n|\n|\r)/,''));
      writer.endElement();
    });

    writer.text("\n");
    writer.endElement();
    writer.endDocument();

    return writer.toString();
  }

  /**
  * PlainTextOfENML
  * @param  { string } text (ENML)
  * @return string - text
  */
  function PlainTextOfENML(enml){

    var text = enml || '';
    text = text.replace(/(\r\n|\n|\r)/gm," ");
    text = text.replace(/(<\/(div|ui|li|p|table|tr|dl)>)/ig,"\n");
    text = text.replace(/^\s/gm,"");
    text = text.replace(/(<(li)>)/ig," - ");
    text = text.replace(/(<([^>]+)>)/ig,"");
   	text = text.trim()

    return text;
  }

  function bufferToBase64(buf) {
    var binstr = Array.prototype.map.call(buf, function (ch) {
      return String.fromCharCode(ch);
    }).join('');
    return btoa(binstr);
  }

  /**
  * HTMLOfENML
  * Convert ENML into HTML for showing in web browsers.
  *
  * @param { string } text (ENML)
  * @param  { Map <string (hash), url (string) || { url: (string), title: (string) } >, Optional } resources
  * @return string - HTML
  */
  function HTMLOfENML(text, resources){

    resources = resources || [];

    var resource_map = {}
    resources.forEach(function(resource){

      var hex = [].map.call( resource.data.bodyHash.data,
        function(v) { str = v.toString(16);
        return str.length < 2 ? "0" + str : str;  }).join("");

      resource_map[hex] = resource;
    })

    var writer = new XMLWriter();
    var parser = new SaxParser(function(cb) {

      var mediaTagStarted = false;
      var linkTagStarted = false;
      var linkTitle;

      cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {

        if(elem == 'en-note'){
          writer.startElement('html');
          writer.startElement('head');

          writer.startElement('meta');
          writer.writeAttribute('http-equiv', 'Content-Type');
          writer.writeAttribute('content', 'text/html; charset=UTF-8');
          writer.endElement();

          writer.endElement();

          writer.startElement('body');
          if(!(attrs && attrs[0] && attrs[0][0] && attrs[0][0] === 'style'))
            writer.writeAttribute('style', 'word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;');
        } else if(elem == 'en-todo'){

          writer.startElement('input');
          writer.writeAttribute('type', 'checkbox');

        } else if(elem == 'en-media'){

          var type = null;
          var hash = null;
          var width = 0;
          var height = 0;

          if(attrs) attrs.forEach(function(attr) {
            if(attr[0] == 'type') type = attr[1];
            if(attr[0] == 'hash') hash = attr[1];
            if(attr[0] == 'width') width = attr[1];
            if(attr[0] == 'height') height = attr[1];
          });

          var resource = resource_map[hash];

          if(!resource) return;
          var resourceTitle = resource.title || '';

          if(type.match('image')) {

            writer.startElement('img');
            writer.writeAttribute('title', resourceTitle);

          } else if(type.match('audio')) {


            writer.writeElement('p', resourceTitle);
            writer.startElement('audio');
            writer.writeAttribute('controls', '');
            writer.text('Your browser does not support the audio tag.');
            writer.startElement('source');
            mediaTagStarted = true;

          } else if(type.match('video')) {
            writer.writeElement('p', resourceTitle);
            writer.startElement('video');
            writer.writeAttribute('controls', '');
            writer.text('Your browser does not support the video tag.');
            writer.startElement('source');
            mediaTagStarted = true;
          } else {
            writer.startElement('a');
            linkTagStarted = true;
            linkTitle = resourceTitle;
          }

          if(resource.data.body) {
            var b64encoded = bufferToBase64(resource.data.body.data)
            var src = 'data:'+type+';base64,'+b64encoded;
            writer.writeAttribute('src', src)
          }

          if(width) writer.writeAttribute ('width', width);
          if(height) writer.writeAttribute('height', height);

        } else {
          writer.startElement(elem);
        }

        if(attrs) attrs.forEach(function(attr) {
          writer.writeAttribute(attr[0], attr[1]);
        });

      });
      cb.onEndElementNS(function(elem, prefix, uri) {

        if(elem == 'en-note'){
          writer.endElement(); //body
          writer.endElement(); //html
        }
        else if(elem == 'en-todo'){

        }
        else if(elem == 'en-media'){
          if(mediaTagStarted) {
            writer.endElement(); // source
            writer.endElement(); // audio or video
            writer.writeElement('br', '');
            mediaTagStarted = false;

          } else if(linkTagStarted) {
            writer.text(linkTitle);
            writer.endElement(); // a
            linkTagStarted = false;

          } else {
            writer.endElement();
          }

        } else {

          writer.endElement();
        }
      });
      cb.onCharacters(function(chars) {
        writer.text(chars);
      });

    });

    parser.parseString(text);
    return writer.toString();

  }


  /**
  * TodosOfENML
  * Extract data of all TODO(s) in ENML text.
  *
  * @param { string } text (ENML)
  * @return { Array [ { text: (string), done: (bool) } ] } -
  */
  function TodosOfENML(text){

    var todos = [];


    var parser = new SaxParser(function(cb) {

      var onTodo = false;
      var text = null;
      var checked = false;

      cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {
        var m = elem.match(/b|u|i|font|strong/);
        if(m && elem == m[0]){

        }
        else if(elem == 'en-todo'){

          checked = false;
          text = "";
          onTodo = true;

          if(attrs) attrs.forEach(function(attr) {
            if(attr[0] == 'checked' && attr[1] == 'true') checked = true;
          });

        } else {
          if(onTodo){
            todos.push({text: text, checked: checked});
          }
          onTodo = false;
        }

      });
      cb.onEndElementNS(function(elem, prefix, uri) {

      });
      cb.onCharacters(function(chars) {
        if(onTodo){
          text += chars;
        }
      });

      cb.onEndDocument(function(){
        if (onTodo) {
          todos.push({text: text, checked: checked});
        }
      });
    });

    parser.parseString(text);
    return todos;
  }

  /**
  * CheckTodoInENML
  * Rewrite ENML content by changing check/uncheck value of the TODO in given position.
  *
  * @param { string } text (ENML)
  * @param { int }  index
  * @param { bool } check
  * @return string - ENML (the new content)
  */
  function CheckTodoInENML(text, index, check){

    var todo_cout = 0;
    var writer = new XMLWriter();
    var parser = new SaxParser(function(cb) {

      cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {

        writer.startElement(elem);


        if(elem == 'en-todo' && index == todo_cout++){

          if(attrs) attrs.forEach(function(attr) {
            if(attr[0] == 'checked') return;
            writer.writeAttribute(attr[0], attr[1]);
          });

          if(check)  writer.writeAttribute('checked', 'true');
        }else{

          if(attrs) attrs.forEach(function(attr) {
            writer.writeAttribute(attr[0], attr[1]);
          });
        }
      });
      cb.onEndElementNS(function(elem, prefix, uri) {

        writer.endElement();
      });
      cb.onCharacters(function(chars) {
        writer.text(chars);
      });

    });

    parser.parseString(text);
    return writer.toString();
  }

  var XMLWriter;
  var SaxParser;
  if(typeof exports == 'undefined'){

    XMLWriter = window.XMLWriter;
    SaxParser = window.SaxParser;

    //Browser Code
    window.enml = {};

    window.enml.ENMLOfPlainText = ENMLOfPlainText;
    window.enml.HTMLOfENML      = HTMLOfENML;
    window.enml.PlainTextOfENML = PlainTextOfENML;
    window.enml.TodosOfENML     = TodosOfENML;
    window.enml.CheckTodoInENML = CheckTodoInENML;
  }
  else{

    //Node JS
    XMLWriter = require('./lib/xml-writer');
    SaxParser = require('./lib/xml-parser').SaxParser;

    exports.ENMLOfPlainText = ENMLOfPlainText;
    exports.HTMLOfENML      = HTMLOfENML;
    exports.PlainTextOfENML = PlainTextOfENML;
    exports.TodosOfENML     = TodosOfENML;
    exports.CheckTodoInENML = CheckTodoInENML;
  }

})();