import extend from "just-extend";
import Emitter from "./emitter";
import defaultOptions from "./options";

export default class Dropzone extends Emitter {
  static initClass() {
    // Exposing the emitter class, mainly for tests
    this.prototype.Emitter = Emitter;

    /*
     This is a list of all available events you can register on a dropzone object.

     You can register an event handler like this:

     dropzone.on("dragEnter", function() { });

     */
    this.prototype.events = [
      "drop",
      "dragstart",
      "dragend",
      "dragenter",
      "dragover",
      "dragleave",
      "addedfile",
      "addedfiles",
      "removedfile",
      "thumbnail",
      "error",
      "errormultiple",
      "processing",
      "processingmultiple",
      "uploadprogress",
      "totaluploadprogress",
      "sending",
      "sendingmultiple",
      "success",
      "successmultiple",
      "canceled",
      "canceledmultiple",
      "complete",
      "completemultiple",
      "reset",
      "maxfilesexceeded",
      "maxfilesreached",
      "queuecomplete",
    ];

    this.prototype._thumbnailQueue = [];
    this.prototype._processingThumbnail = false;
  }

  constructor(el, options) {
    super();
    let fallback, left;
    this.element = el;

    this.clickableElements = [];
    this.listeners = [];
    this.files = []; // All files

    if (typeof this.element === "string") {
      this.element = document.querySelector(this.element);
    }

    // Not checking if instance of HTMLElement or Element since IE9 is extremely weird.
    if (!this.element || this.element.nodeType == null) {
      throw new Error("Invalid dropzone element.");
    }

    if (this.element.dropzone) {
      throw new Error("Dropzone already attached.");
    }

    // Now add this dropzone to the instances.
    Dropzone.instances.push(this);

    // Put the dropzone inside the element itself.
    this.element.dropzone = this;

    let elementOptions =
      (left = Dropzone.optionsForElement(this.element)) != null ? left : {};

    this.options = extend(
      true,
      {},
      defaultOptions,
      elementOptions,
      options != null ? options : {}
    );

    this.options.previewTemplate = this.options.previewTemplate.replace(
      /\n*/g,
      ""
    );

    // If the browser failed, just call the fallback and leave
    if (this.options.forceFallback || !Dropzone.isBrowserSupported()) {
      return this.options.fallback.call(this);
    }

    // @options.url = @element.getAttribute "action" unless @options.url?
    if (this.options.url == null) {
      this.options.url = this.element.getAttribute("action");
    }

    if (!this.options.url) {
      throw new Error("No URL provided.");
    }

    if (this.options.acceptedFiles && this.options.acceptedMimeTypes) {
      throw new Error(
        "You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated."
      );
    }

    if (this.options.uploadMultiple && this.options.chunking) {
      throw new Error("You cannot set both: uploadMultiple and chunking.");
    }

    if (this.options.binaryBody && this.options.uploadMultiple) {
      throw new Error("You cannot set both: binaryBody and uploadMultiple.");
    }

    // Backwards compatibility
    if (this.options.acceptedMimeTypes) {
      this.options.acceptedFiles = this.options.acceptedMimeTypes;
      delete this.options.acceptedMimeTypes;
    }

    // Backwards compatibility
    if (this.options.renameFilename != null) {
      this.options.renameFile = (file) =>
        this.options.renameFilename.call(this, file.name, file);
    }

    if (typeof this.options.method === "string") {
      this.options.method = this.options.method.toUpperCase();
    }

    if ((fallback = this.getExistingFallback()) && fallback.parentNode) {
      // Remove the fallback
      fallback.parentNode.removeChild(fallback);
    }

    // Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false
    if (this.options.previewsContainer !== false) {
      if (this.options.previewsContainer) {
        this.previewsContainer = Dropzone.getElement(
          this.options.previewsContainer,
          "previewsContainer"
        );
      } else {
        this.previewsContainer = this.element;
      }
    }

    if (this.options.clickable) {
      if (this.options.clickable === true) {
        this.clickableElements = [this.element];
      } else {
        this.clickableElements = Dropzone.getElements(
          this.options.clickable,
          "clickable"
        );
      }
    }

    this.init();
  }

  // Returns all files that have been accepted
  getAcceptedFiles() {
    return this.files.filter((file) => file.accepted).map((file) => file);
  }

  // Returns all files that have been rejected
  // Not sure when that's going to be useful, but added for completeness.
  getRejectedFiles() {
    return this.files.filter((file) => !file.accepted).map((file) => file);
  }

  getFilesWithStatus(status) {
    return this.files
      .filter((file) => file.status === status)
      .map((file) => file);
  }

  // Returns all files that are in the queue
  getQueuedFiles() {
    return this.getFilesWithStatus(Dropzone.QUEUED);
  }

  getUploadingFiles() {
    return this.getFilesWithStatus(Dropzone.UPLOADING);
  }

  getAddedFiles() {
    return this.getFilesWithStatus(Dropzone.ADDED);
  }

  // Files that are either queued or uploading
  getActiveFiles() {
    return this.files
      .filter(
        (file) =>
          file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED
      )
      .map((file) => file);
  }

  // The function that gets called when Dropzone is initialized. You
  // can (and should) setup event listeners inside this function.
  init() {
    // In case it isn't set already
    if (this.element.tagName === "form") {
      this.element.setAttribute("enctype", "multipart/form-data");
    }

    if (
      this.element.classList.contains("dropzone") &&
      !this.element.querySelector(".dz-message")
    ) {
      this.element.appendChild(
        Dropzone.createElement(
          `<div class="dz-default dz-message"><button class="dz-button" type="button">${this.options.dictDefaultMessage}</button></div>`
        )
      );
    }

    if (this.clickableElements.length) {
      let setupHiddenFileInput = () => {
        if (this.hiddenFileInput) {
          this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput);
        }
        this.hiddenFileInput = document.createElement("input");
        this.hiddenFileInput.setAttribute("type", "file");
        if (this.options.maxFiles === null || this.options.maxFiles > 1) {
          this.hiddenFileInput.setAttribute("multiple", "multiple");
        }
        this.hiddenFileInput.className = "dz-hidden-input";

        if (this.options.acceptedFiles !== null) {
          this.hiddenFileInput.setAttribute(
            "accept",
            this.options.acceptedFiles
          );
        }
        if (this.options.capture !== null) {
          this.hiddenFileInput.setAttribute("capture", this.options.capture);
        }

        // Making sure that no one can "tab" into this field.
        this.hiddenFileInput.setAttribute("tabindex", "-1");

        // Not setting `display="none"` because some browsers don't accept clicks
        // on elements that aren't displayed.
        this.hiddenFileInput.style.visibility = "hidden";
        this.hiddenFileInput.style.position = "absolute";
        this.hiddenFileInput.style.top = "0";
        this.hiddenFileInput.style.left = "0";
        this.hiddenFileInput.style.height = "0";
        this.hiddenFileInput.style.width = "0";
        Dropzone.getElement(
          this.options.hiddenInputContainer,
          "hiddenInputContainer"
        ).appendChild(this.hiddenFileInput);
        this.hiddenFileInput.addEventListener("change", () => {
          let { files } = this.hiddenFileInput;
          if (files.length) {
            for (let file of files) {
              this.addFile(file);
            }
          }
          this.emit("addedfiles", files);
          setupHiddenFileInput();
        });
      };
      setupHiddenFileInput();
    }

    this.URL = window.URL !== null ? window.URL : window.webkitURL;

    // Setup all event listeners on the Dropzone object itself.
    // They're not in @setupEventListeners() because they shouldn't be removed
    // again when the dropzone gets disabled.
    for (let eventName of this.events) {
      this.on(eventName, this.options[eventName]);
    }

    this.on("uploadprogress", () => this.updateTotalUploadProgress());

    this.on("removedfile", () => this.updateTotalUploadProgress());

    this.on("canceled", (file) => this.emit("complete", file));

    // Emit a `queuecomplete` event if all files finished uploading.
    this.on("complete", (file) => {
      if (
        this.getAddedFiles().length === 0 &&
        this.getUploadingFiles().length === 0 &&
        this.getQueuedFiles().length === 0
      ) {
        // This needs to be deferred so that `queuecomplete` really triggers after `complete`
        return setTimeout(() => this.emit("queuecomplete"), 0);
      }
    });

    const containsFiles = function (e) {
      if (e.dataTransfer.types) {
        // Because e.dataTransfer.types is an Object in
        // IE, we need to iterate like this instead of
        // using e.dataTransfer.types.some()
        for (var i = 0; i < e.dataTransfer.types.length; i++) {
          if (e.dataTransfer.types[i] === "Files") return true;
        }
      }
      return false;
    };

    let noPropagation = function (e) {
      // If there are no files, we don't want to stop
      // propagation so we don't interfere with other
      // drag and drop behaviour.
      if (!containsFiles(e)) return;
      e.stopPropagation();
      if (e.preventDefault) {
        return e.preventDefault();
      } else {
        return (e.returnValue = false);
      }
    };

    // Create the listeners
    this.listeners = [
      {
        element: this.element,
        events: {
          dragstart: (e) => {
            return this.emit("dragstart", e);
          },
          dragenter: (e) => {
            noPropagation(e);
            return this.emit("dragenter", e);
          },
          dragover: (e) => {
            // Makes it possible to drag files from chrome's download bar
            // http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar
            // Try is required to prevent bug in Internet Explorer 11 (SCRIPT65535 exception)
            let efct;
            try {
              efct = e.dataTransfer.effectAllowed;
            } catch (error) {}
            e.dataTransfer.dropEffect =
              "move" === efct || "linkMove" === efct ? "move" : "copy";

            noPropagation(e);
            return this.emit("dragover", e);
          },
          dragleave: (e) => {
            return this.emit("dragleave", e);
          },
          drop: (e) => {
            noPropagation(e);
            return this.drop(e);
          },
          dragend: (e) => {
            return this.emit("dragend", e);
          },
        },

        // This is disabled right now, because the browsers don't implement it properly.
        // "paste": (e) =>
        //   noPropagation e
        //   @paste e
      },
    ];

    this.clickableElements.forEach((clickableElement) => {
      return this.listeners.push({
        element: clickableElement,
        events: {
          click: (evt) => {
            // Only the actual dropzone or the message element should trigger file selection
            if (
              clickableElement !== this.element ||
              evt.target === this.element ||
              Dropzone.elementInside(
                evt.target,
                this.element.querySelector(".dz-message")
              )
            ) {
              this.hiddenFileInput.click(); // Forward the click
            }
            return true;
          },
        },
      });
    });

    this.enable();

    return this.options.init.call(this);
  }

  // Not fully tested yet
  destroy() {
    this.disable();
    this.removeAllFiles(true);
    if (
      this.hiddenFileInput != null ? this.hiddenFileInput.parentNode : undefined
    ) {
      this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput);
      this.hiddenFileInput = null;
    }
    delete this.element.dropzone;
    return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1);
  }

  updateTotalUploadProgress() {
    let totalUploadProgress;
    let totalBytesSent = 0;
    let totalBytes = 0;

    let activeFiles = this.getActiveFiles();

    if (activeFiles.length) {
      for (let file of this.getActiveFiles()) {
        totalBytesSent += file.upload.bytesSent;
        totalBytes += file.upload.total;
      }
      totalUploadProgress = (100 * totalBytesSent) / totalBytes;
    } else {
      totalUploadProgress = 100;
    }

    return this.emit(
      "totaluploadprogress",
      totalUploadProgress,
      totalBytes,
      totalBytesSent
    );
  }

  // @options.paramName can be a function taking one parameter rather than a string.
  // A parameter name for a file is obtained simply by calling this with an index number.
  _getParamName(n) {
    if (typeof this.options.paramName === "function") {
      return this.options.paramName(n);
    } else {
      return `${this.options.paramName}${
        this.options.uploadMultiple ? `[${n}]` : ""
      }`;
    }
  }

  // If @options.renameFile is a function,
  // the function will be used to rename the file.name before appending it to the formData
  _renameFile(file) {
    if (typeof this.options.renameFile !== "function") {
      return file.name;
    }
    return this.options.renameFile(file);
  }

  // Returns a form that can be used as fallback if the browser does not support DragnDrop
  //
  // If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided.
  // This code has to pass in IE7 :(
  getFallbackForm() {
    let existingFallback, form;
    if ((existingFallback = this.getExistingFallback())) {
      return existingFallback;
    }

    let fieldsString = '<div class="dz-fallback">';
    if (this.options.dictFallbackText) {
      fieldsString += `<p>${this.options.dictFallbackText}</p>`;
    }
    fieldsString += `<input type="file" name="${this._getParamName(0)}" ${
      this.options.uploadMultiple ? 'multiple="multiple"' : undefined
    } /><input type="submit" value="Upload!"></div>`;

    let fields = Dropzone.createElement(fieldsString);
    if (this.element.tagName !== "FORM") {
      form = Dropzone.createElement(
        `<form action="${this.options.url}" enctype="multipart/form-data" method="${this.options.method}"></form>`
      );
      form.appendChild(fields);
    } else {
      // Make sure that the enctype and method attributes are set properly
      this.element.setAttribute("enctype", "multipart/form-data");
      this.element.setAttribute("method", this.options.method);
    }
    return form != null ? form : fields;
  }

  // Returns the fallback elements if they exist already
  //
  // This code has to pass in IE7 :(
  getExistingFallback() {
    let getFallback = function (elements) {
      for (let el of elements) {
        if (/(^| )fallback($| )/.test(el.className)) {
          return el;
        }
      }
    };

    for (let tagName of ["div", "form"]) {
      var fallback;
      if (
        (fallback = getFallback(this.element.getElementsByTagName(tagName)))
      ) {
        return fallback;
      }
    }
  }

  // Activates all listeners stored in @listeners
  setupEventListeners() {
    return this.listeners.map((elementListeners) =>
      (() => {
        let result = [];
        for (let event in elementListeners.events) {
          let listener = elementListeners.events[event];
          result.push(
            elementListeners.element.addEventListener(event, listener, false)
          );
        }
        return result;
      })()
    );
  }

  // Deactivates all listeners stored in @listeners
  removeEventListeners() {
    return this.listeners.map((elementListeners) =>
      (() => {
        let result = [];
        for (let event in elementListeners.events) {
          let listener = elementListeners.events[event];
          result.push(
            elementListeners.element.removeEventListener(event, listener, false)
          );
        }
        return result;
      })()
    );
  }

  // Removes all event listeners and cancels all files in the queue or being processed.
  disable() {
    this.clickableElements.forEach((element) =>
      element.classList.remove("dz-clickable")
    );
    this.removeEventListeners();
    this.disabled = true;

    return this.files.map((file) => this.cancelUpload(file));
  }

  enable() {
    delete this.disabled;
    this.clickableElements.forEach((element) =>
      element.classList.add("dz-clickable")
    );
    return this.setupEventListeners();
  }

  // Returns a nicely formatted filesize
  filesize(size) {
    let selectedSize = 0;
    let selectedUnit = "b";

    if (size > 0) {
      let units = ["tb", "gb", "mb", "kb", "b"];

      for (let i = 0; i < units.length; i++) {
        let unit = units[i];
        let cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10;

        if (size >= cutoff) {
          selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i);
          selectedUnit = unit;
          break;
        }
      }

      selectedSize = Math.round(10 * selectedSize) / 10; // Cutting of digits
    }

    return `<strong>${selectedSize}</strong> ${this.options.dictFileSizeUnits[selectedUnit]}`;
  }

  // Adds or removes the `dz-max-files-reached` class from the form.
  _updateMaxFilesReachedClass() {
    if (
      this.options.maxFiles != null &&
      this.getAcceptedFiles().length >= this.options.maxFiles
    ) {
      if (this.getAcceptedFiles().length === this.options.maxFiles) {
        this.emit("maxfilesreached", this.files);
      }
      return this.element.classList.add("dz-max-files-reached");
    } else {
      return this.element.classList.remove("dz-max-files-reached");
    }
  }

  drop(e) {
    if (!e.dataTransfer) {
      return;
    }
    this.emit("drop", e);

    // Convert the FileList to an Array
    // This is necessary for IE11
    let files = [];
    for (let i = 0; i < e.dataTransfer.files.length; i++) {
      files[i] = e.dataTransfer.files[i];
    }

    // Even if it's a folder, files.length will contain the folders.
    if (files.length) {
      let { items } = e.dataTransfer;
      if (items && items.length && items[0].webkitGetAsEntry != null) {
        // The browser supports dropping of folders, so handle items instead of files
        this._addFilesFromItems(items);
      } else {
        this.handleFiles(files);
      }
    }

    this.emit("addedfiles", files);
  }

  paste(e) {
    if (
      __guard__(e != null ? e.clipboardData : undefined, (x) => x.items) == null
    ) {
      return;
    }

    this.emit("paste", e);
    let { items } = e.clipboardData;

    if (items.length) {
      return this._addFilesFromItems(items);
    }
  }

  handleFiles(files) {
    for (let file of files) {
      this.addFile(file);
    }
  }

  // When a folder is dropped (or files are pasted), items must be handled
  // instead of files.
  _addFilesFromItems(items) {
    return (() => {
      let result = [];
      for (let item of items) {
        var entry;
        if (
          item.webkitGetAsEntry != null &&
          (entry = item.webkitGetAsEntry())
        ) {
          if (entry.isFile) {
            result.push(this.addFile(item.getAsFile()));
          } else if (entry.isDirectory) {
            // Append all files from that directory to files
            result.push(this._addFilesFromDirectory(entry, entry.name));
          } else {
            result.push(undefined);
          }
        } else if (item.getAsFile != null) {
          if (item.kind == null || item.kind === "file") {
            result.push(this.addFile(item.getAsFile()));
          } else {
            result.push(undefined);
          }
        } else {
          result.push(undefined);
        }
      }
      return result;
    })();
  }

  // Goes through the directory, and adds each file it finds recursively
  _addFilesFromDirectory(directory, path) {
    let dirReader = directory.createReader();

    let errorHandler = (error) =>
      __guardMethod__(console, "log", (o) => o.log(error));

    var readEntries = () => {
      return dirReader.readEntries((entries) => {
        if (entries.length > 0) {
          for (let entry of entries) {
            if (entry.isFile) {
              entry.file((file) => {
                if (
                  this.options.ignoreHiddenFiles &&
                  file.name.substring(0, 1) === "."
                ) {
                  return;
                }
                file.fullPath = `${path}/${file.name}`;
                return this.addFile(file);
              });
            } else if (entry.isDirectory) {
              this._addFilesFromDirectory(entry, `${path}/${entry.name}`);
            }
          }

          // Recursively call readEntries() again, since browser only handle
          // the first 100 entries.
          // See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries
          readEntries();
        }
        return null;
      }, errorHandler);
    };

    return readEntries();
  }

  // If `done()` is called without argument the file is accepted
  // If you call it with an error message, the file is rejected
  // (This allows for asynchronous validation)
  //
  // This function checks the filesize, and if the file.type passes the
  // `acceptedFiles` check.
  accept(file, done) {
    if (
      this.options.maxFilesize &&
      file.size > this.options.maxFilesize * 1024 * 1024
    ) {
      done(
        this.options.dictFileTooBig
          .replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100)
          .replace("{{maxFilesize}}", this.options.maxFilesize)
      );
    } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) {
      done(this.options.dictInvalidFileType);
    } else if (
      this.options.maxFiles != null &&
      this.getAcceptedFiles().length >= this.options.maxFiles
    ) {
      done(
        this.options.dictMaxFilesExceeded.replace(
          "{{maxFiles}}",
          this.options.maxFiles
        )
      );
      this.emit("maxfilesexceeded", file);
    } else {
      this.options.accept.call(this, file, done);
    }
  }

  addFile(file) {
    file.upload = {
      uuid: Dropzone.uuidv4(),
      progress: 0,
      // Setting the total upload size to file.size for the beginning
      // It's actual different than the size to be transmitted.
      total: file.size,
      bytesSent: 0,
      filename: this._renameFile(file),
      // Not setting chunking information here, because the acutal data — and
      // thus the chunks — might change if `options.transformFile` is set
      // and does something to the data.
    };
    this.files.push(file);

    file.status = Dropzone.ADDED;

    this.emit("addedfile", file);

    this._enqueueThumbnail(file);

    this.accept(file, (error) => {
      if (error) {
        file.accepted = false;
        this._errorProcessing([file], error); // Will set the file.status
      } else {
        file.accepted = true;
        if (this.options.autoQueue) {
          this.enqueueFile(file);
        } // Will set .accepted = true
      }
      this._updateMaxFilesReachedClass();
    });
  }

  // Wrapper for enqueueFile
  enqueueFiles(files) {
    for (let file of files) {
      this.enqueueFile(file);
    }
    return null;
  }

  enqueueFile(file) {
    if (file.status === Dropzone.ADDED && file.accepted === true) {
      file.status = Dropzone.QUEUED;
      if (this.options.autoProcessQueue) {
        return setTimeout(() => this.processQueue(), 0); // Deferring the call
      }
    } else {
      throw new Error(
        "This file can't be queued because it has already been processed or was rejected."
      );
    }
  }

  _enqueueThumbnail(file) {
    if (
      this.options.createImageThumbnails &&
      file.type.match(/image.*/) &&
      file.size <= this.options.maxThumbnailFilesize * 1024 * 1024
    ) {
      this._thumbnailQueue.push(file);
      return setTimeout(() => this._processThumbnailQueue(), 0); // Deferring the call
    }
  }

  _processThumbnailQueue() {
    if (this._processingThumbnail || this._thumbnailQueue.length === 0) {
      return;
    }

    this._processingThumbnail = true;
    let file = this._thumbnailQueue.shift();
    return this.createThumbnail(
      file,
      this.options.thumbnailWidth,
      this.options.thumbnailHeight,
      this.options.thumbnailMethod,
      true,
      (dataUrl) => {
        this.emit("thumbnail", file, dataUrl);
        this._processingThumbnail = false;
        return this._processThumbnailQueue();
      }
    );
  }

  // Can be called by the user to remove a file
  removeFile(file) {
    if (file.status === Dropzone.UPLOADING) {
      this.cancelUpload(file);
    }
    this.files = without(this.files, file);

    this.emit("removedfile", file);
    if (this.files.length === 0) {
      return this.emit("reset");
    }
  }

  // Removes all files that aren't currently processed from the list
  removeAllFiles(cancelIfNecessary) {
    // Create a copy of files since removeFile() changes the @files array.
    if (cancelIfNecessary == null) {
      cancelIfNecessary = false;
    }
    for (let file of this.files.slice()) {
      if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) {
        this.removeFile(file);
      }
    }
    return null;
  }

  // Resizes an image before it gets sent to the server. This function is the default behavior of
  // `options.transformFile` if `resizeWidth` or `resizeHeight` are set. The callback is invoked with
  // the resized blob.
  resizeImage(file, width, height, resizeMethod, callback) {
    return this.createThumbnail(
      file,
      width,
      height,
      resizeMethod,
      true,
      (dataUrl, canvas) => {
        if (canvas == null) {
          // The image has not been resized
          return callback(file);
        } else {
          let { resizeMimeType } = this.options;
          if (resizeMimeType == null) {
            resizeMimeType = file.type;
          }
          let resizedDataURL = canvas.toDataURL(
            resizeMimeType,
            this.options.resizeQuality
          );
          if (
            resizeMimeType === "image/jpeg" ||
            resizeMimeType === "image/jpg"
          ) {
            // Now add the original EXIF information
            resizedDataURL = ExifRestore.restore(file.dataURL, resizedDataURL);
          }
          return callback(Dropzone.dataURItoBlob(resizedDataURL));
        }
      }
    );
  }

  createThumbnail(file, width, height, resizeMethod, fixOrientation, callback) {
    let fileReader = new FileReader();

    fileReader.onload = () => {
      file.dataURL = fileReader.result;

      // Don't bother creating a thumbnail for SVG images since they're vector
      if (file.type === "image/svg+xml") {
        if (callback != null) {
          callback(fileReader.result);
        }
        return;
      }

      this.createThumbnailFromUrl(
        file,
        width,
        height,
        resizeMethod,
        fixOrientation,
        callback
      );
    };

    fileReader.readAsDataURL(file);
  }

  // `mockFile` needs to have these attributes:
  //
  //     { name: 'name', size: 12345, imageUrl: '' }
  //
  // `callback` will be invoked when the image has been downloaded and displayed.
  // `crossOrigin` will be added to the `img` tag when accessing the file.
  displayExistingFile(
    mockFile,
    imageUrl,
    callback,
    crossOrigin,
    resizeThumbnail = true
  ) {
    this.emit("addedfile", mockFile);
    this.emit("complete", mockFile);

    if (!resizeThumbnail) {
      this.emit("thumbnail", mockFile, imageUrl);
      if (callback) callback();
    } else {
      let onDone = (thumbnail) => {
        this.emit("thumbnail", mockFile, thumbnail);
        if (callback) callback();
      };
      mockFile.dataURL = imageUrl;

      this.createThumbnailFromUrl(
        mockFile,
        this.options.thumbnailWidth,
        this.options.thumbnailHeight,
        this.options.thumbnailMethod,
        this.options.fixOrientation,
        onDone,
        crossOrigin
      );
    }
  }

  createThumbnailFromUrl(
    file,
    width,
    height,
    resizeMethod,
    fixOrientation,
    callback,
    crossOrigin
  ) {
    // Not using `new Image` here because of a bug in latest Chrome versions.
    // See https://github.com/enyo/dropzone/pull/226
    let img = document.createElement("img");

    if (crossOrigin) {
      img.crossOrigin = crossOrigin;
    }

    // fixOrientation is not needed anymore with browsers handling imageOrientation
    fixOrientation =
      getComputedStyle(document.body)["imageOrientation"] == "from-image"
        ? false
        : fixOrientation;

    img.onload = () => {
      let loadExif = (callback) => callback(1);
      if (typeof EXIF !== "undefined" && EXIF !== null && fixOrientation) {
        loadExif = (callback) =>
          EXIF.getData(img, function () {
            return callback(EXIF.getTag(this, "Orientation"));
          });
      }

      return loadExif((orientation) => {
        file.width = img.width;
        file.height = img.height;

        let resizeInfo = this.options.resize.call(
          this,
          file,
          width,
          height,
          resizeMethod
        );

        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");

        canvas.width = resizeInfo.trgWidth;
        canvas.height = resizeInfo.trgHeight;

        if (orientation > 4) {
          canvas.width = resizeInfo.trgHeight;
          canvas.height = resizeInfo.trgWidth;
        }

        switch (orientation) {
          case 2:
            // horizontal flip
            ctx.translate(canvas.width, 0);
            ctx.scale(-1, 1);
            break;
          case 3:
            // 180° rotate left
            ctx.translate(canvas.width, canvas.height);
            ctx.rotate(Math.PI);
            break;
          case 4:
            // vertical flip
            ctx.translate(0, canvas.height);
            ctx.scale(1, -1);
            break;
          case 5:
            // vertical flip + 90 rotate right
            ctx.rotate(0.5 * Math.PI);
            ctx.scale(1, -1);
            break;
          case 6:
            // 90° rotate right
            ctx.rotate(0.5 * Math.PI);
            ctx.translate(0, -canvas.width);
            break;
          case 7:
            // horizontal flip + 90 rotate right
            ctx.rotate(0.5 * Math.PI);
            ctx.translate(canvas.height, -canvas.width);
            ctx.scale(-1, 1);
            break;
          case 8:
            // 90° rotate left
            ctx.rotate(-0.5 * Math.PI);
            ctx.translate(-canvas.height, 0);
            break;
        }

        // This is a bugfix for iOS' scaling bug.
        drawImageIOSFix(
          ctx,
          img,
          resizeInfo.srcX != null ? resizeInfo.srcX : 0,
          resizeInfo.srcY != null ? resizeInfo.srcY : 0,
          resizeInfo.srcWidth,
          resizeInfo.srcHeight,
          resizeInfo.trgX != null ? resizeInfo.trgX : 0,
          resizeInfo.trgY != null ? resizeInfo.trgY : 0,
          resizeInfo.trgWidth,
          resizeInfo.trgHeight
        );

        let thumbnail = canvas.toDataURL("image/png");

        if (callback != null) {
          return callback(thumbnail, canvas);
        }
      });
    };

    if (callback != null) {
      img.onerror = callback;
    }

    return (img.src = file.dataURL);
  }

  // Goes through the queue and processes files if there aren't too many already.
  processQueue() {
    let { parallelUploads } = this.options;
    let processingLength = this.getUploadingFiles().length;
    let i = processingLength;

    // There are already at least as many files uploading than should be
    if (processingLength >= parallelUploads) {
      return;
    }

    let queuedFiles = this.getQueuedFiles();

    if (!(queuedFiles.length > 0)) {
      return;
    }

    if (this.options.uploadMultiple) {
      // The files should be uploaded in one request
      return this.processFiles(
        queuedFiles.slice(0, parallelUploads - processingLength)
      );
    } else {
      while (i < parallelUploads) {
        if (!queuedFiles.length) {
          return;
        } // Nothing left to process
        this.processFile(queuedFiles.shift());
        i++;
      }
    }
  }

  // Wrapper for `processFiles`
  processFile(file) {
    return this.processFiles([file]);
  }

  // Loads the file, then calls finishedLoading()
  processFiles(files) {
    for (let file of files) {
      file.processing = true; // Backwards compatibility
      file.status = Dropzone.UPLOADING;

      this.emit("processing", file);
    }

    if (this.options.uploadMultiple) {
      this.emit("processingmultiple", files);
    }

    return this.uploadFiles(files);
  }

  _getFilesWithXhr(xhr) {
    let files;
    return (files = this.files
      .filter((file) => file.xhr === xhr)
      .map((file) => file));
  }

  // Cancels the file upload and sets the status to CANCELED
  // **if** the file is actually being uploaded.
  // If it's still in the queue, the file is being removed from it and the status
  // set to CANCELED.
  cancelUpload(file) {
    if (file.status === Dropzone.UPLOADING) {
      let groupedFiles = this._getFilesWithXhr(file.xhr);
      for (let groupedFile of groupedFiles) {
        groupedFile.status = Dropzone.CANCELED;
      }
      if (typeof file.xhr !== "undefined") {
        file.xhr.abort();
      }
      for (let groupedFile of groupedFiles) {
        this.emit("canceled", groupedFile);
      }
      if (this.options.uploadMultiple) {
        this.emit("canceledmultiple", groupedFiles);
      }
    } else if (
      file.status === Dropzone.ADDED ||
      file.status === Dropzone.QUEUED
    ) {
      file.status = Dropzone.CANCELED;
      this.emit("canceled", file);
      if (this.options.uploadMultiple) {
        this.emit("canceledmultiple", [file]);
      }
    }

    if (this.options.autoProcessQueue) {
      return this.processQueue();
    }
  }

  resolveOption(option, ...args) {
    if (typeof option === "function") {
      return option.apply(this, args);
    }
    return option;
  }

  uploadFile(file) {
    return this.uploadFiles([file]);
  }

  uploadFiles(files) {
    this._transformFiles(files, (transformedFiles) => {
      if (this.options.chunking) {
        // Chunking is not allowed to be used with `uploadMultiple` so we know
        // that there is only __one__file.
        let transformedFile = transformedFiles[0];
        files[0].upload.chunked =
          this.options.chunking &&
          (this.options.forceChunking ||
            transformedFile.size > this.options.chunkSize);
        files[0].upload.totalChunkCount = Math.ceil(
          transformedFile.size / this.options.chunkSize
        );
      }

      if (files[0].upload.chunked) {
        // This file should be sent in chunks!

        // If the chunking option is set, we **know** that there can only be **one** file, since
        // uploadMultiple is not allowed with this option.
        let file = files[0];
        let transformedFile = transformedFiles[0];
        let startedChunkCount = 0;

        file.upload.chunks = [];

        let handleNextChunk = () => {
          let chunkIndex = 0;

          // Find the next item in file.upload.chunks that is not defined yet.
          while (file.upload.chunks[chunkIndex] !== undefined) {
            chunkIndex++;
          }

          // This means, that all chunks have already been started.
          if (chunkIndex >= file.upload.totalChunkCount) return;

          startedChunkCount++;

          let start = chunkIndex * this.options.chunkSize;
          let end = Math.min(
            start + this.options.chunkSize,
            transformedFile.size
          );

          let dataBlock = {
            name: this._getParamName(0),
            data: transformedFile.webkitSlice
              ? transformedFile.webkitSlice(start, end)
              : transformedFile.slice(start, end),
            filename: file.upload.filename,
            chunkIndex: chunkIndex,
          };

          file.upload.chunks[chunkIndex] = {
            file: file,
            index: chunkIndex,
            dataBlock: dataBlock, // In case we want to retry.
            status: Dropzone.UPLOADING,
            progress: 0,
            retries: 0, // The number of times this block has been retried.
          };

          this._uploadData(files, [dataBlock]);
        };

        file.upload.finishedChunkUpload = (chunk, response) => {
          let allFinished = true;
          chunk.status = Dropzone.SUCCESS;

          // Clear the data from the chunk
          chunk.dataBlock = null;
          chunk.response = chunk.xhr.responseText;
          chunk.responseHeaders = chunk.xhr.getAllResponseHeaders();
          // Leaving this reference to xhr will cause memory leaks.
          chunk.xhr = null;

          for (let i = 0; i < file.upload.totalChunkCount; i++) {
            if (file.upload.chunks[i] === undefined) {
              return handleNextChunk();
            }
            if (file.upload.chunks[i].status !== Dropzone.SUCCESS) {
              allFinished = false;
            }
          }

          if (allFinished) {
            this.options.chunksUploaded(file, () => {
              this._finished(files, response, null);
            });
          }
        };

        if (this.options.parallelChunkUploads) {
          for (let i = 0; i < file.upload.totalChunkCount; i++) {
            handleNextChunk();
          }
        } else {
          handleNextChunk();
        }
      } else {
        let dataBlocks = [];
        for (let i = 0; i < files.length; i++) {
          dataBlocks[i] = {
            name: this._getParamName(i),
            data: transformedFiles[i],
            filename: files[i].upload.filename,
          };
        }
        this._uploadData(files, dataBlocks);
      }
    });
  }

  /// Returns the right chunk for given file and xhr
  _getChunk(file, xhr) {
    for (let i = 0; i < file.upload.totalChunkCount; i++) {
      if (
        file.upload.chunks[i] !== undefined &&
        file.upload.chunks[i].xhr === xhr
      ) {
        return file.upload.chunks[i];
      }
    }
  }

  // This function actually uploads the file(s) to the server.
  //
  //  If dataBlocks contains the actual data to upload (meaning, that this could
  // either be transformed files, or individual chunks for chunked upload) then
  // they will be used for the actual data to upload.
  _uploadData(files, dataBlocks) {
    let xhr = new XMLHttpRequest();

    // Put the xhr object in the file objects to be able to reference it later.
    for (let file of files) {
      file.xhr = xhr;
    }
    if (files[0].upload.chunked) {
      // Put the xhr object in the right chunk object, so it can be associated
      // later, and found with _getChunk.
      files[0].upload.chunks[dataBlocks[0].chunkIndex].xhr = xhr;
    }

    let method = this.resolveOption(this.options.method, files, dataBlocks);
    let url = this.resolveOption(this.options.url, files, dataBlocks);
    xhr.open(method, url, true);

    // Setting the timeout after open because of IE11 issue: https://gitlab.com/meno/dropzone/issues/8
    let timeout = this.resolveOption(this.options.timeout, files);
    if (timeout) xhr.timeout = this.resolveOption(this.options.timeout, files);

    // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
    xhr.withCredentials = !!this.options.withCredentials;

    xhr.onload = (e) => {
      this._finishedUploading(files, xhr, e);
    };

    xhr.ontimeout = () => {
      this._handleUploadError(
        files,
        xhr,
        `Request timedout after ${this.options.timeout / 1000} seconds`
      );
    };

    xhr.onerror = () => {
      this._handleUploadError(files, xhr);
    };

    // Some browsers do not have the .upload property
    let progressObj = xhr.upload != null ? xhr.upload : xhr;
    progressObj.onprogress = (e) =>
      this._updateFilesUploadProgress(files, xhr, e);

    let headers = this.options.defaultHeaders
      ? {
          Accept: "application/json",
          "Cache-Control": "no-cache",
          "X-Requested-With": "XMLHttpRequest",
        }
      : {};

    if (this.options.binaryBody) {
      headers["Content-Type"] = files[0].type;
    }

    if (this.options.headers) {
      extend(headers, this.options.headers);
    }

    for (let headerName in headers) {
      let headerValue = headers[headerName];
      if (headerValue) {
        xhr.setRequestHeader(headerName, headerValue);
      }
    }

    if (this.options.binaryBody) {
      // Since the file is going to be sent as binary body, it doesn't make
      // any sense to generate `FormData` for it.
      for (let file of files) {
        this.emit("sending", file, xhr);
      }
      if (this.options.uploadMultiple) {
        this.emit("sendingmultiple", files, xhr);
      }
      this.submitRequest(xhr, null, files);
    } else {
      let formData = new FormData();

      // Adding all @options parameters
      if (this.options.params) {
        let additionalParams = this.options.params;
        if (typeof additionalParams === "function") {
          additionalParams = additionalParams.call(
            this,
            files,
            xhr,
            files[0].upload.chunked ? this._getChunk(files[0], xhr) : null
          );
        }

        for (let key in additionalParams) {
          let value = additionalParams[key];
          if (Array.isArray(value)) {
            // The additional parameter contains an array,
            // so lets iterate over it to attach each value
            // individually.
            for (let i = 0; i < value.length; i++) {
              formData.append(key, value[i]);
            }
          } else {
            formData.append(key, value);
          }
        }
      }

      // Let the user add additional data if necessary
      for (let file of files) {
        this.emit("sending", file, xhr, formData);
      }
      if (this.options.uploadMultiple) {
        this.emit("sendingmultiple", files, xhr, formData);
      }

      this._addFormElementData(formData);

      // Finally add the files
      // Has to be last because some servers (eg: S3) expect the file to be the last parameter
      for (let i = 0; i < dataBlocks.length; i++) {
        let dataBlock = dataBlocks[i];
        formData.append(dataBlock.name, dataBlock.data, dataBlock.filename);
      }

      this.submitRequest(xhr, formData, files);
    }
  }

  // Transforms all files with this.options.transformFile and invokes done with the transformed files when done.
  _transformFiles(files, done) {
    let transformedFiles = [];
    // Clumsy way of handling asynchronous calls, until I get to add a proper Future library.
    let doneCounter = 0;
    for (let i = 0; i < files.length; i++) {
      this.options.transformFile.call(this, files[i], (transformedFile) => {
        transformedFiles[i] = transformedFile;
        if (++doneCounter === files.length) {
          done(transformedFiles);
        }
      });
    }
  }

  // Takes care of adding other input elements of the form to the AJAX request
  _addFormElementData(formData) {
    // Take care of other input elements
    if (this.element.tagName === "FORM") {
      for (let input of this.element.querySelectorAll(
        "input, textarea, select, button"
      )) {
        let inputName = input.getAttribute("name");
        let inputType = input.getAttribute("type");
        if (inputType) inputType = inputType.toLowerCase();

        // If the input doesn't have a name, we can't use it.
        if (typeof inputName === "undefined" || inputName === null) continue;

        if (input.tagName === "SELECT" && input.hasAttribute("multiple")) {
          // Possibly multiple values
          for (let option of input.options) {
            if (option.selected) {
              formData.append(inputName, option.value);
            }
          }
        } else if (
          !inputType ||
          (inputType !== "checkbox" && inputType !== "radio") ||
          input.checked
        ) {
          formData.append(inputName, input.value);
        }
      }
    }
  }

  // Invoked when there is new progress information about given files.
  // If e is not provided, it is assumed that the upload is finished.
  _updateFilesUploadProgress(files, xhr, e) {
    if (!files[0].upload.chunked) {
      // Handle file uploads without chunking
      for (let file of files) {
        if (
          file.upload.total &&
          file.upload.bytesSent &&
          file.upload.bytesSent == file.upload.total
        ) {
          // If both, the `total` and `bytesSent` have already been set, and
          // they are equal (meaning progress is at 100%), we can skip this
          // file, since an upload progress shouldn't go down.
          continue;
        }

        if (e) {
          file.upload.progress = (100 * e.loaded) / e.total;
          file.upload.total = e.total;
          file.upload.bytesSent = e.loaded;
        } else {
          // No event, so we're at 100%
          file.upload.progress = 100;
          file.upload.bytesSent = file.upload.total;
        }

        this.emit(
          "uploadprogress",
          file,
          file.upload.progress,
          file.upload.bytesSent
        );
      }
    } else {
      // Handle chunked file uploads

      // Chunked upload is not compatible with uploading multiple files in one
      // request, so we know there's only one file.
      let file = files[0];

      // Since this is a chunked upload, we need to update the appropriate chunk
      // progress.
      let chunk = this._getChunk(file, xhr);

      if (e) {
        chunk.progress = (100 * e.loaded) / e.total;
        chunk.total = e.total;
        chunk.bytesSent = e.loaded;
      } else {
        // No event, so we're at 100%
        chunk.progress = 100;
        chunk.bytesSent = chunk.total;
      }

      // Now tally the *file* upload progress from its individual chunks
      file.upload.progress = 0;
      file.upload.total = 0;
      file.upload.bytesSent = 0;
      for (let i = 0; i < file.upload.totalChunkCount; i++) {
        if (
          file.upload.chunks[i] &&
          typeof file.upload.chunks[i].progress !== "undefined"
        ) {
          file.upload.progress += file.upload.chunks[i].progress;
          file.upload.total += file.upload.chunks[i].total;
          file.upload.bytesSent += file.upload.chunks[i].bytesSent;
        }
      }
      // Since the process is a percentage, we need to divide by the amount of
      // chunks we've used.
      file.upload.progress = file.upload.progress / file.upload.totalChunkCount;

      this.emit(
        "uploadprogress",
        file,
        file.upload.progress,
        file.upload.bytesSent
      );
    }
  }

  _finishedUploading(files, xhr, e) {
    let response;

    if (files[0].status === Dropzone.CANCELED) {
      return;
    }

    if (xhr.readyState !== 4) {
      return;
    }

    if (xhr.responseType !== "arraybuffer" && xhr.responseType !== "blob") {
      response = xhr.responseText;

      if (
        xhr.getResponseHeader("content-type") &&
        ~xhr.getResponseHeader("content-type").indexOf("application/json")
      ) {
        try {
          response = JSON.parse(response);
        } catch (error) {
          e = error;
          response = "Invalid JSON response from server.";
        }
      }
    }

    this._updateFilesUploadProgress(files, xhr);

    if (!(200 <= xhr.status && xhr.status < 300)) {
      this._handleUploadError(files, xhr, response);
    } else {
      if (files[0].upload.chunked) {
        files[0].upload.finishedChunkUpload(
          this._getChunk(files[0], xhr),
          response
        );
      } else {
        this._finished(files, response, e);
      }
    }
  }

  _handleUploadError(files, xhr, response) {
    if (files[0].status === Dropzone.CANCELED) {
      return;
    }

    if (files[0].upload.chunked && this.options.retryChunks) {
      let chunk = this._getChunk(files[0], xhr);
      if (chunk.retries++ < this.options.retryChunksLimit) {
        this._uploadData(files, [chunk.dataBlock]);
        return;
      } else {
        console.warn("Retried this chunk too often. Giving up.");
      }
    }

    this._errorProcessing(
      files,
      response ||
        this.options.dictResponseError.replace("{{statusCode}}", xhr.status),
      xhr
    );
  }

  submitRequest(xhr, formData, files) {
    if (xhr.readyState != 1) {
      console.warn(
        "Cannot send this request because the XMLHttpRequest.readyState is not OPENED."
      );
      return;
    }
    if (this.options.binaryBody) {
      if (files[0].upload.chunked) {
        const chunk = this._getChunk(files[0], xhr);
        xhr.send(chunk.dataBlock.data);
      } else {
        xhr.send(files[0]);
      }
    } else {
      xhr.send(formData);
    }
  }

  // Called internally when processing is finished.
  // Individual callbacks have to be called in the appropriate sections.
  _finished(files, responseText, e) {
    for (let file of files) {
      file.status = Dropzone.SUCCESS;
      this.emit("success", file, responseText, e);
      this.emit("complete", file);
    }
    if (this.options.uploadMultiple) {
      this.emit("successmultiple", files, responseText, e);
      this.emit("completemultiple", files);
    }

    if (this.options.autoProcessQueue) {
      return this.processQueue();
    }
  }

  // Called internally when processing is finished.
  // Individual callbacks have to be called in the appropriate sections.
  _errorProcessing(files, message, xhr) {
    for (let file of files) {
      file.status = Dropzone.ERROR;
      this.emit("error", file, message, xhr);
      this.emit("complete", file);
    }
    if (this.options.uploadMultiple) {
      this.emit("errormultiple", files, message, xhr);
      this.emit("completemultiple", files);
    }

    if (this.options.autoProcessQueue) {
      return this.processQueue();
    }
  }

  static uuidv4() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
      /[xy]/g,
      function (c) {
        let r = (Math.random() * 16) | 0,
          v = c === "x" ? r : (r & 0x3) | 0x8;
        return v.toString(16);
      }
    );
  }
}
Dropzone.initClass();

// This is a map of options for your different dropzones. Add configurations
// to this object for your different dropzone elemens.
//
// Example:
//
//     Dropzone.options.myDropzoneElementId = { maxFilesize: 1 };
//
// And in html:
//
//     <form action="/upload" id="my-dropzone-element-id" class="dropzone"></form>
Dropzone.options = {};

// Returns the options for an element or undefined if none available.
Dropzone.optionsForElement = function (element) {
  // Get the `Dropzone.options.elementId` for this element if it exists
  if (element.getAttribute("id")) {
    return Dropzone.options[camelize(element.getAttribute("id"))];
  } else {
    return undefined;
  }
};

// Holds a list of all dropzone instances
Dropzone.instances = [];

// Returns the dropzone for given element if any
Dropzone.forElement = function (element) {
  if (typeof element === "string") {
    element = document.querySelector(element);
  }
  if ((element != null ? element.dropzone : undefined) == null) {
    throw new Error(
      "No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone."
    );
  }
  return element.dropzone;
};

// Looks for all .dropzone elements and creates a dropzone for them
Dropzone.discover = function () {
  let dropzones;
  if (document.querySelectorAll) {
    dropzones = document.querySelectorAll(".dropzone");
  } else {
    dropzones = [];
    // IE :(
    let checkElements = (elements) =>
      (() => {
        let result = [];
        for (let el of elements) {
          if (/(^| )dropzone($| )/.test(el.className)) {
            result.push(dropzones.push(el));
          } else {
            result.push(undefined);
          }
        }
        return result;
      })();
    checkElements(document.getElementsByTagName("div"));
    checkElements(document.getElementsByTagName("form"));
  }

  return (() => {
    let result = [];
    for (let dropzone of dropzones) {
      // Create a dropzone unless auto discover has been disabled for specific element
      if (Dropzone.optionsForElement(dropzone) !== false) {
        result.push(new Dropzone(dropzone));
      } else {
        result.push(undefined);
      }
    }
    return result;
  })();
};

// Some browsers support drag and drog functionality, but not correctly.
//
// So I created a blocklist of userAgents. Yes, yes. Browser sniffing, I know.
// But what to do when browsers *theoretically* support an API, but crash
// when using it.
//
// This is a list of regular expressions tested against navigator.userAgent
//
// ** It should only be used on browser that *do* support the API, but
// incorrectly **
Dropzone.blockedBrowsers = [
  // The mac os and windows phone version of opera 12 seems to have a problem with the File drag'n'drop API.
  /opera.*(Macintosh|Windows Phone).*version\/12/i,
];

// Checks if the browser is supported
Dropzone.isBrowserSupported = function () {
  let capableBrowser = true;

  if (
    window.File &&
    window.FileReader &&
    window.FileList &&
    window.Blob &&
    window.FormData &&
    document.querySelector
  ) {
    if (!("classList" in document.createElement("a"))) {
      capableBrowser = false;
    } else {
      if (Dropzone.blacklistedBrowsers !== undefined) {
        // Since this has been renamed, this makes sure we don't break older
        // configuration.
        Dropzone.blockedBrowsers = Dropzone.blacklistedBrowsers;
      }
      // The browser supports the API, but may be blocked.
      for (let regex of Dropzone.blockedBrowsers) {
        if (regex.test(navigator.userAgent)) {
          capableBrowser = false;
          continue;
        }
      }
    }
  } else {
    capableBrowser = false;
  }

  return capableBrowser;
};

Dropzone.dataURItoBlob = function (dataURI) {
  // convert base64 to raw binary data held in a string
  // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
  let byteString = atob(dataURI.split(",")[1]);

  // separate out the mime component
  let mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];

  // write the bytes of the string to an ArrayBuffer
  let ab = new ArrayBuffer(byteString.length);
  let ia = new Uint8Array(ab);
  for (
    let i = 0, end = byteString.length, asc = 0 <= end;
    asc ? i <= end : i >= end;
    asc ? i++ : i--
  ) {
    ia[i] = byteString.charCodeAt(i);
  }

  // write the ArrayBuffer to a blob
  return new Blob([ab], { type: mimeString });
};

// Returns an array without the rejected item
const without = (list, rejectedItem) =>
  list.filter((item) => item !== rejectedItem).map((item) => item);

// abc-def_ghi -> abcDefGhi
const camelize = (str) =>
  str.replace(/[\-_](\w)/g, (match) => match.charAt(1).toUpperCase());

// Creates an element from string
Dropzone.createElement = function (string) {
  let div = document.createElement("div");
  div.innerHTML = string;
  return div.childNodes[0];
};

// Tests if given element is inside (or simply is) the container
Dropzone.elementInside = function (element, container) {
  if (element === container) {
    return true;
  } // Coffeescript doesn't support do/while loops
  while ((element = element.parentNode)) {
    if (element === container) {
      return true;
    }
  }
  return false;
};

Dropzone.getElement = function (el, name) {
  let element;
  if (typeof el === "string") {
    element = document.querySelector(el);
  } else if (el.nodeType != null) {
    element = el;
  }
  if (element == null) {
    throw new Error(
      `Invalid \`${name}\` option provided. Please provide a CSS selector or a plain HTML element.`
    );
  }
  return element;
};

Dropzone.getElements = function (els, name) {
  let el, elements;
  if (els instanceof Array) {
    elements = [];
    try {
      for (el of els) {
        elements.push(this.getElement(el, name));
      }
    } catch (e) {
      elements = null;
    }
  } else if (typeof els === "string") {
    elements = [];
    for (el of document.querySelectorAll(els)) {
      elements.push(el);
    }
  } else if (els.nodeType != null) {
    elements = [els];
  }

  if (elements == null || !elements.length) {
    throw new Error(
      `Invalid \`${name}\` option provided. Please provide a CSS selector, a plain HTML element or a list of those.`
    );
  }

  return elements;
};

// Asks the user the question and calls accepted or rejected accordingly
//
// The default implementation just uses `window.confirm` and then calls the
// appropriate callback.
Dropzone.confirm = function (question, accepted, rejected) {
  if (window.confirm(question)) {
    return accepted();
  } else if (rejected != null) {
    return rejected();
  }
};

// Validates the mime type like this:
//
// https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept
Dropzone.isValidFile = function (file, acceptedFiles) {
  if (!acceptedFiles) {
    return true;
  } // If there are no accepted mime types, it's OK
  acceptedFiles = acceptedFiles.split(",");

  let mimeType = file.type;
  let baseMimeType = mimeType.replace(/\/.*$/, "");

  for (let validType of acceptedFiles) {
    validType = validType.trim();
    if (validType.charAt(0) === ".") {
      if (
        file.name
          .toLowerCase()
          .indexOf(
            validType.toLowerCase(),
            file.name.length - validType.length
          ) !== -1
      ) {
        return true;
      }
    } else if (/\/\*$/.test(validType)) {
      // This is something like a image/* mime type
      if (baseMimeType === validType.replace(/\/.*$/, "")) {
        return true;
      }
    } else {
      if (mimeType === validType) {
        return true;
      }
    }
  }

  return false;
};

// Augment jQuery
if (typeof jQuery !== "undefined" && jQuery !== null) {
  jQuery.fn.dropzone = function (options) {
    return this.each(function () {
      return new Dropzone(this, options);
    });
  };
}

// Dropzone file status codes
Dropzone.ADDED = "added";

Dropzone.QUEUED = "queued";
// For backwards compatibility. Now, if a file is accepted, it's either queued
// or uploading.
Dropzone.ACCEPTED = Dropzone.QUEUED;

Dropzone.UPLOADING = "uploading";
Dropzone.PROCESSING = Dropzone.UPLOADING; // alias

Dropzone.CANCELED = "canceled";
Dropzone.ERROR = "error";
Dropzone.SUCCESS = "success";

/*

 Bugfix for iOS 6 and 7
 Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
 based on the work of https://github.com/stomita/ios-imagefile-megapixel

 */

// Detecting vertical squash in loaded image.
// Fixes a bug which squash image vertically while drawing into canvas for some images.
// This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel
let detectVerticalSquash = function (img) {
  let iw = img.naturalWidth;
  let ih = img.naturalHeight;
  let canvas = document.createElement("canvas");
  canvas.width = 1;
  canvas.height = ih;
  let ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  let { data } = ctx.getImageData(1, 0, 1, ih);

  // search image edge pixel position in case it is squashed vertically.
  let sy = 0;
  let ey = ih;
  let py = ih;
  while (py > sy) {
    let alpha = data[(py - 1) * 4 + 3];

    if (alpha === 0) {
      ey = py;
    } else {
      sy = py;
    }

    py = (ey + sy) >> 1;
  }
  let ratio = py / ih;

  if (ratio === 0) {
    return 1;
  } else {
    return ratio;
  }
};

// A replacement for context.drawImage
// (args are for source and destination).
var drawImageIOSFix = function (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) {
  let vertSquashRatio = detectVerticalSquash(img);
  return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio);
};

// Based on MinifyJpeg
// Source: http://www.perry.cz/files/ExifRestorer.js
// http://elicon.blog57.fc2.com/blog-entry-206.html
class ExifRestore {
  static initClass() {
    this.KEY_STR =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  }

  static encode64(input) {
    let output = "";
    let chr1 = undefined;
    let chr2 = undefined;
    let chr3 = "";
    let enc1 = undefined;
    let enc2 = undefined;
    let enc3 = undefined;
    let enc4 = "";
    let i = 0;
    while (true) {
      chr1 = input[i++];
      chr2 = input[i++];
      chr3 = input[i++];
      enc1 = chr1 >> 2;
      enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
      enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
      enc4 = chr3 & 63;
      if (isNaN(chr2)) {
        enc3 = enc4 = 64;
      } else if (isNaN(chr3)) {
        enc4 = 64;
      }
      output =
        output +
        this.KEY_STR.charAt(enc1) +
        this.KEY_STR.charAt(enc2) +
        this.KEY_STR.charAt(enc3) +
        this.KEY_STR.charAt(enc4);
      chr1 = chr2 = chr3 = "";
      enc1 = enc2 = enc3 = enc4 = "";
      if (!(i < input.length)) {
        break;
      }
    }
    return output;
  }

  static restore(origFileBase64, resizedFileBase64) {
    if (!origFileBase64.match("data:image/jpeg;base64,")) {
      return resizedFileBase64;
    }
    let rawImage = this.decode64(
      origFileBase64.replace("data:image/jpeg;base64,", "")
    );
    let segments = this.slice2Segments(rawImage);
    let image = this.exifManipulation(resizedFileBase64, segments);
    return `data:image/jpeg;base64,${this.encode64(image)}`;
  }

  static exifManipulation(resizedFileBase64, segments) {
    let exifArray = this.getExifArray(segments);
    let newImageArray = this.insertExif(resizedFileBase64, exifArray);
    let aBuffer = new Uint8Array(newImageArray);
    return aBuffer;
  }

  static getExifArray(segments) {
    let seg = undefined;
    let x = 0;
    while (x < segments.length) {
      seg = segments[x];
      if ((seg[0] === 255) & (seg[1] === 225)) {
        return seg;
      }
      x++;
    }
    return [];
  }

  static insertExif(resizedFileBase64, exifArray) {
    let imageData = resizedFileBase64.replace("data:image/jpeg;base64,", "");
    let buf = this.decode64(imageData);
    let separatePoint = buf.indexOf(255, 3);
    let mae = buf.slice(0, separatePoint);
    let ato = buf.slice(separatePoint);
    let array = mae;
    array = array.concat(exifArray);
    array = array.concat(ato);
    return array;
  }

  static slice2Segments(rawImageArray) {
    let head = 0;
    let segments = [];
    while (true) {
      var length;
      if ((rawImageArray[head] === 255) & (rawImageArray[head + 1] === 218)) {
        break;
      }
      if ((rawImageArray[head] === 255) & (rawImageArray[head + 1] === 216)) {
        head += 2;
      } else {
        length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3];
        let endPoint = head + length + 2;
        let seg = rawImageArray.slice(head, endPoint);
        segments.push(seg);
        head = endPoint;
      }
      if (head > rawImageArray.length) {
        break;
      }
    }
    return segments;
  }

  static decode64(input) {
    let output = "";
    let chr1 = undefined;
    let chr2 = undefined;
    let chr3 = "";
    let enc1 = undefined;
    let enc2 = undefined;
    let enc3 = undefined;
    let enc4 = "";
    let i = 0;
    let buf = [];
    // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
    let base64test = /[^A-Za-z0-9\+\/\=]/g;
    if (base64test.exec(input)) {
      console.warn(
        "There were invalid base64 characters in the input text.\nValid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\nExpect errors in decoding."
      );
    }
    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    while (true) {
      enc1 = this.KEY_STR.indexOf(input.charAt(i++));
      enc2 = this.KEY_STR.indexOf(input.charAt(i++));
      enc3 = this.KEY_STR.indexOf(input.charAt(i++));
      enc4 = this.KEY_STR.indexOf(input.charAt(i++));
      chr1 = (enc1 << 2) | (enc2 >> 4);
      chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
      chr3 = ((enc3 & 3) << 6) | enc4;
      buf.push(chr1);
      if (enc3 !== 64) {
        buf.push(chr2);
      }
      if (enc4 !== 64) {
        buf.push(chr3);
      }
      chr1 = chr2 = chr3 = "";
      enc1 = enc2 = enc3 = enc4 = "";
      if (!(i < input.length)) {
        break;
      }
    }
    return buf;
  }
}
ExifRestore.initClass();

/*
 * contentloaded.js
 *
 * Author: Diego Perini (diego.perini at gmail.com)
 * Summary: cross-browser wrapper for DOMContentLoaded
 * Updated: 20101020
 * License: MIT
 * Version: 1.2
 *
 * URL:
 * http://javascript.nwbox.com/ContentLoaded/
 * http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE
 */

// @win window reference
// @fn function reference
let contentLoaded = function (win, fn) {
  let done = false;
  let top = true;
  let doc = win.document;
  let root = doc.documentElement;
  let add = doc.addEventListener ? "addEventListener" : "attachEvent";
  let rem = doc.addEventListener ? "removeEventListener" : "detachEvent";
  let pre = doc.addEventListener ? "" : "on";
  var init = function (e) {
    if (e.type === "readystatechange" && doc.readyState !== "complete") {
      return;
    }
    (e.type === "load" ? win : doc)[rem](pre + e.type, init, false);
    if (!done && (done = true)) {
      return fn.call(win, e.type || e);
    }
  };

  var poll = function () {
    try {
      root.doScroll("left");
    } catch (e) {
      setTimeout(poll, 50);
      return;
    }
    return init("poll");
  };

  if (doc.readyState !== "complete") {
    if (doc.createEventObject && root.doScroll) {
      try {
        top = !win.frameElement;
      } catch (error) {}
      if (top) {
        poll();
      }
    }
    doc[add](pre + "DOMContentLoaded", init, false);
    doc[add](pre + "readystatechange", init, false);
    return win[add](pre + "load", init, false);
  }
};

function __guard__(value, transform) {
  return typeof value !== "undefined" && value !== null
    ? transform(value)
    : undefined;
}
function __guardMethod__(obj, methodName, transform) {
  if (
    typeof obj !== "undefined" &&
    obj !== null &&
    typeof obj[methodName] === "function"
  ) {
    return transform(obj, methodName);
  } else {
    return undefined;
  }
}

export { Dropzone };
