text: /*jslint newcap: true */
  /*global XMLHttpRequest: false, FormData: false */
  /*
   * Inline Text Attachment
   *
   * Author: Roy van Kaathoven
   * Contact: ik@royvankaathoven.nl
   */
  (function(document, window) {
    'use strict';
  
    var inlineAttachment = function(options, instance) {
      this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults);
      this.editor = instance;
      this.filenameTag = '{filename}';
      this.lastValue = null;
    };
  
    /**
     * Will holds the available editors
     *
     * @type {Object}
     */
    inlineAttachment.editors = {};
  
    /**
     * Utility functions
     */
    inlineAttachment.util = {
  
      /**
       * Simple function to merge the given objects
       *
       * @param {Object[]} object Multiple object parameters
       * @returns {Object}
       */
      merge: function() {
        var result = {};
        for (var i = arguments.length - 1; i >= 0; i--) {
          var obj = arguments[i];
          for (var k in obj) {
            if (obj.hasOwnProperty(k)) {
              result[k] = obj[k];
            }
          }
        }
        return result;
      },
  
      /**
       * Append a line of text at the bottom, ensuring there aren't unnecessary newlines
       *
       * @param {String} appended Current content
       * @param {String} previous Value which should be appended after the current content
       */
      appendInItsOwnLine: function(previous, appended) {
        return (previous + "\n\n[[D]]" + appended)
          .replace(/(\n{2,})\[\[D\]\]/, "\n\n")
          .replace(/^(\n*)/, "");
      },
  
      /**
       * Inserts the given value at the current cursor position of the textarea element
       *
       * @param  {HtmlElement} el
       * @param  {String} value Text which will be inserted at the cursor position
       */
      insertTextAtCursor: function(el, text) {
        var scrollPos = el.scrollTop,
          strPos = 0,
          browser = false,
          range;
  
        if ((el.selectionStart || el.selectionStart === '0')) {
          browser = "ff";
        } else if (document.selection) {
          browser = "ie";
        }
  
        if (browser === "ie") {
          el.focus();
          range = document.selection.createRange();
          range.moveStart('character', -el.value.length);
          strPos = range.text.length;
        } else if (browser === "ff") {
          strPos = el.selectionStart;
        }
  
        var front = (el.value).substring(0, strPos);
        var back = (el.value).substring(strPos, el.value.length);
        el.value = front + text + back;
        strPos = strPos + text.length;
        if (browser === "ie") {
          el.focus();
          range = document.selection.createRange();
          range.moveStart('character', -el.value.length);
          range.moveStart('character', strPos);
          range.moveEnd('character', 0);
          range.select();
        } else if (browser === "ff") {
          el.selectionStart = strPos;
          el.selectionEnd = strPos;
          el.focus();
        }
        el.scrollTop = scrollPos;
      }
    };
  
    /**
     * Default configuration options
     *
     * @type {Object}
     */
    inlineAttachment.defaults = {
      /**
       * URL where the file will be send
       */
      uploadUrl: 'uploadimage',
  
      /**
       * Which method will be used to send the file to the upload URL
       */
      uploadMethod: 'POST',
  
      /**
       * Name in which the file will be placed
       */
      uploadFieldName: 'image',
  
      /**
       * Extension which will be used when a file extension could not
       * be detected
       */
      defaultExtension: 'png',
  
      /**
       * JSON field which refers to the uploaded file URL
       */
      jsonFieldName: 'link',
  
      /**
       * Allowed MIME types
       */
      allowedTypes: window.allowedUploadMimeTypes,
  
      /**
       * Text which will be inserted when dropping or pasting a file.
       * Acts as a placeholder which will be replaced when the file is done with uploading
       */
      progressText: '![Uploading file...{filename}]()',
  
      /**
       * When a file has successfully been uploaded the progressText
       * will be replaced by the urlText, the {filename} tag will be replaced
       * by the filename that has been returned by the server
       */
      urlText: "![]({filename})",
  
      /**
       * Text which will be used when uploading has failed
       */
      errorText: "Error uploading file",
  
      /**
       * Extra parameters which will be send when uploading a file
       */
      extraParams: {},
  
      /**
       * Extra headers which will be send when uploading a file
       */
      extraHeaders: {},
  
      /**
       * Before the file is send
       */
      beforeFileUpload: function() {
        return true;
      },
  
      /**
       * Triggers when a file is dropped or pasted
       */
      onFileReceived: function() {},
  
      /**
       * Custom upload handler
       *
       * @return {Boolean} when false is returned it will prevent default upload behavior
       */
      onFileUploadResponse: function() {
        return true;
      },
  
      /**
       * Custom error handler. Runs after removing the placeholder text and before the alert().
       * Return false from this function to prevent the alert dialog.
       *
       * @return {Boolean} when false is returned it will prevent default error behavior
       */
      onFileUploadError: function() {
        return true;
      },
  
      /**
       * When a file has succesfully been uploaded
       */
      onFileUploaded: function() {}
    };
  
    /**
     * Uploads the blob
     *
     * @param  {Blob} file blob data received from event.dataTransfer object
     * @return {XMLHttpRequest} request object which sends the file
     */
    inlineAttachment.prototype.uploadFile = function(file, id) {
      var me = this,
        formData = new FormData(),
        xhr = new XMLHttpRequest(),
        id = id,
        settings = this.settings,
        extension = settings.defaultExtension;
  
      if (typeof settings.setupFormData === 'function') {
        settings.setupFormData(formData, file);
      }
  
      // Attach the file. If coming from clipboard, add a default filename (only works in Chrome for now)
      // http://stackoverflow.com/questions/6664967/how-to-give-a-blob-uploaded-as-formdata-a-file-name
      if (file.name) {
        var fileNameMatches = file.name.match(/\.(.+)$/);
        if (fileNameMatches) {
          extension = fileNameMatches[1];
        }
      }
  
      var remoteFilename = "image-" + Date.now() + "." + extension;
      if (typeof settings.remoteFilename === 'function') {
        remoteFilename = settings.remoteFilename(file);
      }
  
      formData.append(settings.uploadFieldName, file, remoteFilename);
  
      // Append the extra parameters to the formdata
      if (typeof settings.extraParams === "object") {
        for (var key in settings.extraParams) {
          if (settings.extraParams.hasOwnProperty(key)) {
            formData.append(key, settings.extraParams[key]);
          }
        }
      }
  
      xhr.open('POST', settings.uploadUrl);
  
      // Add any available extra headers
      if (typeof settings.extraHeaders === "object") {
          for (var header in settings.extraHeaders) {
              if (settings.extraHeaders.hasOwnProperty(header)) {
                  xhr.setRequestHeader(header, settings.extraHeaders[header]);
              }
          }
      }
  
      xhr.onload = function() {
        // If HTTP status is OK or Created
        if (xhr.status === 200 || xhr.status === 201) {
          me.onFileUploadResponse(xhr, id);
        } else {
          me.onFileUploadError(xhr, id);
        }
      };
      if (settings.beforeFileUpload(xhr) !== false) {
        xhr.send(formData);
      }
      return xhr;
    };
  
    /**
     * Returns if the given file is allowed to handle
     *
     * @param {File} clipboard data file
     */
    inlineAttachment.prototype.isFileAllowed = function(file) {
      if (this.settings.allowedTypes.indexOf('*') === 0){
        return true;
      } else {
        return this.settings.allowedTypes.indexOf(file.type) >= 0;
      }
    };
  
    /**
     * Handles upload response
     *
     * @param  {XMLHttpRequest} xhr
     * @return {Void}
     */
    inlineAttachment.prototype.onFileUploadResponse = function(xhr, id) {
      if (this.settings.onFileUploadResponse.call(this, xhr) !== false) {
        var result = JSON.parse(xhr.responseText),
          filename = result[this.settings.jsonFieldName];
  
        if (result && filename) {
            var replacements = [];
            var string = this.settings.progressText.replace(this.filenameTag, id);
            var lines = this.editor.getValue().split('\n');
          var newValue = this.settings.urlText.replace(this.filenameTag, filename);
            for(var i = 0; i < lines.length; i++) {
              var ch = lines[i].indexOf(string);
              if(ch != -1)
                  replacements.push({replacement:newValue, from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
          }
            for(var i = 0; i < replacements.length; i++)
              this.editor.replaceRange(replacements[i]);
        }
      }
    };
  
  
    /**
     * Called when a file has failed to upload
     *
     * @param  {XMLHttpRequest} xhr
     * @return {Void}
     */
    inlineAttachment.prototype.onFileUploadError = function(xhr, id) {
      if (this.settings.onFileUploadError.call(this, xhr) !== false) {
        var replacements = [];
        var string = this.settings.progressText.replace(this.filenameTag, id);
        var lines = this.editor.getValue().split('\n');
          for(var i = 0; i < lines.length; i++) {
              var ch = lines[i].indexOf(this.lastValue);
              if(ch != -1)
                  replacements.push({replacement:"", from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
          }
          for(var i = 0; i < replacements.length; i++)
              this.editor.replaceRange(replacements[i]);
      }
    };
  
    /**
     * Called when a file has been inserted, either by drop or paste
     *
     * @param  {File} file
     * @return {Void}
     */
    inlineAttachment.prototype.onFileInserted = function(file, id) {
      if (this.settings.onFileReceived.call(this, file) !== false) {
        this.lastValue = this.settings.progressText.replace(this.filenameTag, id);
        this.editor.insertValue(this.lastValue + "\n");
      }
    };
  
  
    /**
     * Called when a paste event occured
     * @param  {Event} e
     * @return {Boolean} if the event was handled
     */
    inlineAttachment.prototype.onPaste = function(e) {
      var result = false,
        clipboardData = e.clipboardData,
        items;
  
      if (typeof clipboardData === "object") {
        items = clipboardData.items || clipboardData.files || [];
  
        for (var i = 0; i < items.length; i++) {
          var item = items[i];
          if (this.isFileAllowed(item)) {
            result = true;
            var id = ID();
            var file = item.getAsFile();
            if (file !== null) {
              this.onFileInserted(file, id);
              this.uploadFile(file, id);
            }
          }
        }
      }
  
      if (result) { e.preventDefault(); }
  
      return result;
    };
  
    /**
     * Called when a drop event occures
     * @param  {Event} e
     * @return {Boolean} if the event was handled
     */
    inlineAttachment.prototype.onDrop = function(e) {
      var result = false;
      for (var i = 0; i < e.dataTransfer.files.length; i++) {
        var file = e.dataTransfer.files[i];
        if (this.isFileAllowed(file)) {
          result = true;
          var id = ID();
          this.onFileInserted(file, id);
          this.uploadFile(file, id);
        }
      }
  
      return result;
    };
  
    window.inlineAttachment = inlineAttachment;
  
  })(document, window);
  
  // Generate unique IDs for use as pseudo-private/protected names.
  // Similar in concept to
  // <http://wiki.ecmascript.org/doku.php?id=strawman:names>.
  //
  // The goals of this function are twofold:
  //
  // * Provide a way to generate a string guaranteed to be unique when compared
  //   to other strings generated by this function.
  // * Make the string complex enough that it is highly unlikely to be
  //   accidentally duplicated by hand (this is key if you're using `ID`
  //   as a private/protected name on an object).
  //
  // Use:
  //
  //     var privateName = ID();
  //     var o = { 'public': 'foo' };
  //     o[privateName] = 'bar';
  var ID = function () {
    // Math.random should be unique because of its seeding algorithm.
    // Convert it to base 36 (numbers + letters), and grab the first 9 characters
    // after the decimal.
    return '_' + Math.random().toString(36).substr(2, 9);
  };
  