/**
 * @license MIT
 */
(function(window, document, undefined) {'use strict';
  if (!window || !document) {
    console.warn('Flowjs needs window and document objects to work');
    return;
  }
  // ie10+
  var ie10plus = window.navigator.msPointerEnabled;
  /**
   * Flow.js is a library providing multiple simultaneous, stable and
   * resumable uploads via the HTML5 File API.
   * @param [opts]
   * @param {number} [opts.chunkSize]
   * @param {bool} [opts.forceChunkSize]
   * @param {number} [opts.simultaneousUploads]
   * @param {bool} [opts.singleFile]
   * @param {string} [opts.fileParameterName]
   * @param {number} [opts.progressCallbacksInterval]
   * @param {number} [opts.speedSmoothingFactor]
   * @param {Object|Function} [opts.query]
   * @param {Object|Function} [opts.headers]
   * @param {bool} [opts.withCredentials]
   * @param {Function} [opts.preprocess]
   * @param {string} [opts.method]
   * @param {string|Function} [opts.testMethod]
   * @param {string|Function} [opts.uploadMethod]
   * @param {bool} [opts.prioritizeFirstAndLastChunk]
   * @param {bool} [opts.allowDuplicateUploads]
   * @param {string|Function} [opts.target]
   * @param {number} [opts.maxChunkRetries]
   * @param {number} [opts.chunkRetryInterval]
   * @param {Array.<number>} [opts.permanentErrors]
   * @param {Array.<number>} [opts.successStatuses]
   * @param {Function} [opts.initFileFn]
   * @param {Function} [opts.readFileFn]
   * @param {Function} [opts.generateUniqueIdentifier]
   * @constructor
   */
  function Flow(opts) {
    /**
     * Supported by browser?
     * @type {boolean}
     */
    this.support = (
        typeof File !== 'undefined' &&
        typeof Blob !== 'undefined' &&
        typeof FileList !== 'undefined' &&
        (
          !!Blob.prototype.slice || !!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice ||
          false
        ) // slicing files support
    );

    if (!this.support) {
      return ;
    }

    /**
     * Check if directory upload is supported
     * @type {boolean}
     */
    this.supportDirectory = (
        /Chrome/.test(window.navigator.userAgent) ||
        /Firefox/.test(window.navigator.userAgent) ||
        /Edge/.test(window.navigator.userAgent)
    );

    /**
     * List of FlowFile objects
     * @type {Array.<FlowFile>}
     */
    this.files = [];

    /**
     * Default options for flow.js
     * @type {Object}
     */
    this.defaults = {
      chunkSize: 1024 * 1024,
      forceChunkSize: false,
      simultaneousUploads: 3,
      singleFile: false,
      fileParameterName: 'file',
      progressCallbacksInterval: 500,
      speedSmoothingFactor: 0.1,
      query: {},
      headers: {},
      withCredentials: false,
      preprocess: null,
      method: 'multipart',
      testMethod: 'GET',
      uploadMethod: 'POST',
      prioritizeFirstAndLastChunk: false,
      allowDuplicateUploads: false,
      target: '/',
      testChunks: true,
      generateUniqueIdentifier: null,
      maxChunkRetries: 0,
      chunkRetryInterval: null,
      permanentErrors: [404, 413, 415, 500, 501],
      successStatuses: [200, 201, 202],
      onDropStopPropagation: false,
      initFileFn: null,
      readFileFn: webAPIFileRead
    };

    /**
     * Current options
     * @type {Object}
     */
    this.opts = {};

    /**
     * List of events:
     *  key stands for event name
     *  value array list of callbacks
     * @type {}
     */
    this.events = {};

    var $ = this;

    /**
     * On drop event
     * @function
     * @param {MouseEvent} event
     */
    this.onDrop = function (event) {
      if ($.opts.onDropStopPropagation) {
        event.stopPropagation();
      }
      event.preventDefault();
      var dataTransfer = event.dataTransfer;
      if (dataTransfer.items && dataTransfer.items[0] &&
        dataTransfer.items[0].webkitGetAsEntry) {
        $.webkitReadDataTransfer(event);
      } else {
        $.addFiles(dataTransfer.files, event);
      }
    };

    /**
     * Prevent default
     * @function
     * @param {MouseEvent} event
     */
    this.preventEvent = function (event) {
      event.preventDefault();
    };

    /**
     * Current options
     * @type {Object}
     */
    this.opts = Flow.extend({}, this.defaults, opts || {});

  }

  Flow.prototype = {
    /**
     * Set a callback for an event, possible events:
     * fileSuccess(file), fileProgress(file), fileAdded(file, event),
     * fileRemoved(file), fileRetry(file), fileError(file, message),
     * complete(), progress(), error(message, file), pause()
     * @function
     * @param {string} event
     * @param {Function} callback
     */
    on: function (event, callback) {
      event = event.toLowerCase();
      if (!this.events.hasOwnProperty(event)) {
        this.events[event] = [];
      }
      this.events[event].push(callback);
    },

    /**
     * Remove event callback
     * @function
     * @param {string} [event] removes all events if not specified
     * @param {Function} [fn] removes all callbacks of event if not specified
     */
    off: function (event, fn) {
      if (event !== undefined) {
        event = event.toLowerCase();
        if (fn !== undefined) {
          if (this.events.hasOwnProperty(event)) {
            arrayRemove(this.events[event], fn);
          }
        } else {
          delete this.events[event];
        }
      } else {
        this.events = {};
      }
    },

    /**
     * Fire an event
     * @function
     * @param {string} event event name
     * @param {...} args arguments of a callback
     * @return {bool} value is false if at least one of the event handlers which handled this event
     * returned false. Otherwise it returns true.
     */
    fire: function (event, args) {
      // `arguments` is an object, not array, in FF, so:
      args = Array.prototype.slice.call(arguments);
      event = event.toLowerCase();
      var preventDefault = false;
      if (this.events.hasOwnProperty(event)) {
        each(this.events[event], function (callback) {
          preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault;
        }, this);
      }
      if (event != 'catchall') {
        args.unshift('catchAll');
        preventDefault = this.fire.apply(this, args) === false || preventDefault;
      }
      return !preventDefault;
    },

    /**
     * Read webkit dataTransfer object
     * @param event
     */
    webkitReadDataTransfer: function (event) {
      var $ = this;
      var queue = event.dataTransfer.items.length;
      var files = [];
      each(event.dataTransfer.items, function (item) {
        var entry = item.webkitGetAsEntry();
        if (!entry) {
          decrement();
          return ;
        }
        if (entry.isFile) {
          // due to a bug in Chrome's File System API impl - #149735
          fileReadSuccess(item.getAsFile(), entry.fullPath);
        } else {
          readDirectory(entry.createReader());
        }
      });
      function readDirectory(reader) {
        reader.readEntries(function (entries) {
          if (entries.length) {
            queue += entries.length;
            each(entries, function(entry) {
              if (entry.isFile) {
                var fullPath = entry.fullPath;
                entry.file(function (file) {
                  fileReadSuccess(file, fullPath);
                }, readError);
              } else if (entry.isDirectory) {
                readDirectory(entry.createReader());
              }
            });
            readDirectory(reader);
          } else {
            decrement();
          }
        }, readError);
      }
      function fileReadSuccess(file, fullPath) {
        // relative path should not start with "/"
        file.relativePath = fullPath.substring(1);
        files.push(file);
        decrement();
      }
      function readError(fileError) {
        throw fileError;
      }
      function decrement() {
        if (--queue == 0) {
          $.addFiles(files, event);
        }
      }
    },

    /**
     * Generate unique identifier for a file
     * @function
     * @param {FlowFile} file
     * @returns {string}
     */
    generateUniqueIdentifier: function (file) {
      var custom = this.opts.generateUniqueIdentifier;
      if (typeof custom === 'function') {
        return custom(file);
      }
      // Some confusion in different versions of Firefox
      var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name;
      return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '');
    },

    /**
     * Upload next chunk from the queue
     * @function
     * @returns {boolean}
     * @private
     */
    uploadNextChunk: function (preventEvents) {
      // In some cases (such as videos) it's really handy to upload the first
      // and last chunk of a file quickly; this let's the server check the file's
      // metadata and determine if there's even a point in continuing.
      var found = false;
      if (this.opts.prioritizeFirstAndLastChunk) {
        each(this.files, function (file) {
          if (!file.paused && file.chunks.length &&
            file.chunks[0].status() === 'pending') {
            file.chunks[0].send();
            found = true;
            return false;
          }
          if (!file.paused && file.chunks.length > 1 &&
            file.chunks[file.chunks.length - 1].status() === 'pending') {
            file.chunks[file.chunks.length - 1].send();
            found = true;
            return false;
          }
        });
        if (found) {
          return found;
        }
      }

      // Now, simply look for the next, best thing to upload
      each(this.files, function (file) {
        if (!file.paused) {
          each(file.chunks, function (chunk) {
            if (chunk.status() === 'pending') {
              chunk.send();
              found = true;
              return false;
            }
          });
        }
        if (found) {
          return false;
        }
      });
      if (found) {
        return true;
      }

      // The are no more outstanding chunks to upload, check is everything is done
      var outstanding = false;
      each(this.files, function (file) {
        if (!file.isComplete()) {
          outstanding = true;
          return false;
        }
      });
      if (!outstanding && !preventEvents) {
        // All chunks have been uploaded, complete
        async(function () {
          this.fire('complete');
        }, this);
      }
      return false;
    },

    /**
     * Assign a browse action to one or more DOM nodes.
     * @function
     * @param {Element|Array.<Element>} domNodes
     * @param {boolean} isDirectory Pass in true to allow directories to
     * @param {boolean} singleFile prevent multi file upload
     * @param {Object} attributes set custom attributes:
     *  http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
     *  eg: accept: 'image/*'
     * be selected (Chrome only).
     */
    assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
      if (domNodes instanceof Element) {
        domNodes = [domNodes];
      }

      each(domNodes, function (domNode) {
        var input;
        if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
          input = domNode;
        } else {
          input = document.createElement('input');
          input.setAttribute('type', 'file');
          // display:none - not working in opera 12
          extend(input.style, {
            visibility: 'hidden',
            position: 'absolute',
            width: '1px',
            height: '1px'
          });
          // for opera 12 browser, input must be assigned to a document
          domNode.appendChild(input);
          // https://developer.mozilla.org/en/using_files_from_web_applications)
          // event listener is executed two times
          // first one - original mouse click event
          // second - input.click(), input is inside domNode
          domNode.addEventListener('click', function() {
            input.click();
          }, false);
        }
        if (!this.opts.singleFile && !singleFile) {
          input.setAttribute('multiple', 'multiple');
        }
        if (isDirectory) {
          input.setAttribute('webkitdirectory', 'webkitdirectory');
        }
        each(attributes, function (value, key) {
          input.setAttribute(key, value);
        });
        // When new files are added, simply append them to the overall list
        var $ = this;
        input.addEventListener('change', function (e) {
       	  if (e.target.value) {
            $.addFiles(e.target.files, e);
            e.target.value = '';
       	  }
        }, false);
      }, this);
    },

    /**
     * Assign one or more DOM nodes as a drop target.
     * @function
     * @param {Element|Array.<Element>} domNodes
     */
    assignDrop: function (domNodes) {
      if (typeof domNodes.length === 'undefined') {
        domNodes = [domNodes];
      }
      each(domNodes, function (domNode) {
        domNode.addEventListener('dragover', this.preventEvent, false);
        domNode.addEventListener('dragenter', this.preventEvent, false);
        domNode.addEventListener('drop', this.onDrop, false);
      }, this);
    },

    /**
     * Un-assign drop event from DOM nodes
     * @function
     * @param domNodes
     */
    unAssignDrop: function (domNodes) {
      if (typeof domNodes.length === 'undefined') {
        domNodes = [domNodes];
      }
      each(domNodes, function (domNode) {
        domNode.removeEventListener('dragover', this.preventEvent);
        domNode.removeEventListener('dragenter', this.preventEvent);
        domNode.removeEventListener('drop', this.onDrop);
      }, this);
    },

    /**
     * Returns a boolean indicating whether or not the instance is currently
     * uploading anything.
     * @function
     * @returns {boolean}
     */
    isUploading: function () {
      var uploading = false;
      each(this.files, function (file) {
        if (file.isUploading()) {
          uploading = true;
          return false;
        }
      });
      return uploading;
    },

    /**
     * should upload next chunk
     * @function
     * @returns {boolean|number}
     */
    _shouldUploadNext: function () {
      var num = 0;
      var should = true;
      var simultaneousUploads = this.opts.simultaneousUploads;
      each(this.files, function (file) {
        each(file.chunks, function(chunk) {
          if (chunk.status() === 'uploading') {
            num++;
            if (num >= simultaneousUploads) {
              should = false;
              return false;
            }
          }
        });
      });
      // if should is true then return uploading chunks's length
      return should && num;
    },

    /**
     * Start or resume uploading.
     * @function
     */
    upload: function () {
      // Make sure we don't start too many uploads at once
      var ret = this._shouldUploadNext();
      if (ret === false) {
        return;
      }
      // Kick off the queue
      this.fire('uploadStart');
      var started = false;
      for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
        started = this.uploadNextChunk(true) || started;
      }
      if (!started) {
        async(function () {
          this.fire('complete');
        }, this);
      }
    },

    /**
     * Resume uploading.
     * @function
     */
    resume: function () {
      each(this.files, function (file) {
        if (!file.isComplete()) {
          file.resume();
        }
      });
    },

    /**
     * Pause uploading.
     * @function
     */
    pause: function () {
      each(this.files, function (file) {
        file.pause();
      });
    },

    /**
     * Cancel upload of all FlowFile objects and remove them from the list.
     * @function
     */
    cancel: function () {
      for (var i = this.files.length - 1; i >= 0; i--) {
        this.files[i].cancel();
      }
    },

    /**
     * Returns a number between 0 and 1 indicating the current upload progress
     * of all files.
     * @function
     * @returns {number}
     */
    progress: function () {
      var totalDone = 0;
      var totalSize = 0;
      // Resume all chunks currently being uploaded
      each(this.files, function (file) {
        totalDone += file.progress() * file.size;
        totalSize += file.size;
      });
      return totalSize > 0 ? totalDone / totalSize : 0;
    },

    /**
     * Add a HTML5 File object to the list of files.
     * @function
     * @param {File} file
     * @param {Event} [event] event is optional
     */
    addFile: function (file, event) {
      this.addFiles([file], event);
    },

    /**
     * Add a HTML5 File object to the list of files.
     * @function
     * @param {FileList|Array} fileList
     * @param {Event} [event] event is optional
     */
    addFiles: function (fileList, event) {
      var files = [];
      each(fileList, function (file) {
        // https://github.com/flowjs/flow.js/issues/55
        if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
          var uniqueIdentifier = this.generateUniqueIdentifier(file);
          if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
            var f = new FlowFile(this, file, uniqueIdentifier);
            if (this.fire('fileAdded', f, event)) {
              files.push(f);
            }
          }
        }
      }, this);
      if (this.fire('filesAdded', files, event)) {
        each(files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0]);
          }
          this.files.push(file);
        }, this);
        this.fire('filesSubmitted', files, event);
      }
    },

    /**
     * Cancel upload of a specific FlowFile object from the list.
     * @function
     * @param {FlowFile} file
     */
    removeFile: function (file) {
      for (var i = this.files.length - 1; i >= 0; i--) {
        if (this.files[i] === file) {
          this.files.splice(i, 1);
          file.abort();
          this.fire('fileRemoved', file);
        }
      }
    },

    /**
     * Look up a FlowFile object by its unique identifier.
     * @function
     * @param {string} uniqueIdentifier
     * @returns {boolean|FlowFile} false if file was not found
     */
    getFromUniqueIdentifier: function (uniqueIdentifier) {
      var ret = false;
      each(this.files, function (file) {
        if (file.uniqueIdentifier === uniqueIdentifier) {
          ret = file;
        }
      });
      return ret;
    },

    /**
     * Returns the total size of all files in bytes.
     * @function
     * @returns {number}
     */
    getSize: function () {
      var totalSize = 0;
      each(this.files, function (file) {
        totalSize += file.size;
      });
      return totalSize;
    },

    /**
     * Returns the total size uploaded of all files in bytes.
     * @function
     * @returns {number}
     */
    sizeUploaded: function () {
      var size = 0;
      each(this.files, function (file) {
        size += file.sizeUploaded();
      });
      return size;
    },

    /**
     * Returns remaining time to upload all files in seconds. Accuracy is based on average speed.
     * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
     * @function
     * @returns {number}
     */
    timeRemaining: function () {
      var sizeDelta = 0;
      var averageSpeed = 0;
      each(this.files, function (file) {
        if (!file.paused && !file.error) {
          sizeDelta += file.size - file.sizeUploaded();
          averageSpeed += file.averageSpeed;
        }
      });
      if (sizeDelta && !averageSpeed) {
        return Number.POSITIVE_INFINITY;
      }
      if (!sizeDelta && !averageSpeed) {
        return 0;
      }
      return Math.floor(sizeDelta / averageSpeed);
    }
  };

  /**
   * FlowFile class
   * @name FlowFile
   * @param {Flow} flowObj
   * @param {File} file
   * @param {string} uniqueIdentifier
   * @constructor
   */
  function FlowFile(flowObj, file, uniqueIdentifier) {

    /**
     * Reference to parent Flow instance
     * @type {Flow}
     */
    this.flowObj = flowObj;

    /**
     * Used to store the bytes read
     * @type {Blob|string}
     */
    this.bytes = null;

    /**
     * Reference to file
     * @type {File}
     */
    this.file = file;

    /**
     * File name. Some confusion in different versions of Firefox
     * @type {string}
     */
    this.name = file.fileName || file.name;

    /**
     * File size
     * @type {number}
     */
    this.size = file.size;

    /**
     * Relative file path
     * @type {string}
     */
    this.relativePath = file.relativePath || file.webkitRelativePath || this.name;

    /**
     * File unique identifier
     * @type {string}
     */
    this.uniqueIdentifier = (uniqueIdentifier === undefined ? flowObj.generateUniqueIdentifier(file) : uniqueIdentifier);

    /**
     * List of chunks
     * @type {Array.<FlowChunk>}
     */
    this.chunks = [];

    /**
     * Indicated if file is paused
     * @type {boolean}
     */
    this.paused = false;

    /**
     * Indicated if file has encountered an error
     * @type {boolean}
     */
    this.error = false;

    /**
     * Average upload speed
     * @type {number}
     */
    this.averageSpeed = 0;

    /**
     * Current upload speed
     * @type {number}
     */
    this.currentSpeed = 0;

    /**
     * Date then progress was called last time
     * @type {number}
     * @private
     */
    this._lastProgressCallback = Date.now();

    /**
     * Previously uploaded file size
     * @type {number}
     * @private
     */
    this._prevUploadedSize = 0;

    /**
     * Holds previous progress
     * @type {number}
     * @private
     */
    this._prevProgress = 0;

    this.bootstrap();
  }

  FlowFile.prototype = {
    /**
     * Update speed parameters
     * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately
     * @function
     */
    measureSpeed: function () {
      var timeSpan = Date.now() - this._lastProgressCallback;
      if (!timeSpan) {
        return ;
      }
      var smoothingFactor = this.flowObj.opts.speedSmoothingFactor;
      var uploaded = this.sizeUploaded();
      // Prevent negative upload speed after file upload resume
      this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0);
      this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed;
      this._prevUploadedSize = uploaded;
    },

    /**
     * For internal usage only.
     * Callback when something happens within the chunk.
     * @function
     * @param {FlowChunk} chunk
     * @param {string} event can be 'progress', 'success', 'error' or 'retry'
     * @param {string} [message]
     */
    chunkEvent: function (chunk, event, message) {
      switch (event) {
        case 'progress':
          if (Date.now() - this._lastProgressCallback <
            this.flowObj.opts.progressCallbacksInterval) {
            break;
          }
          this.measureSpeed();
          this.flowObj.fire('fileProgress', this, chunk);
          this.flowObj.fire('progress');
          this._lastProgressCallback = Date.now();
          break;
        case 'error':
          this.error = true;
          this.abort(true);
          this.flowObj.fire('fileError', this, message, chunk);
          this.flowObj.fire('error', message, this, chunk);
          break;
        case 'success':
          if (this.error) {
            return;
          }
          this.measureSpeed();
          this.flowObj.fire('fileProgress', this, chunk);
          this.flowObj.fire('progress');
          this._lastProgressCallback = Date.now();
          if (this.isComplete()) {
            this.currentSpeed = 0;
            this.averageSpeed = 0;
            this.flowObj.fire('fileSuccess', this, message, chunk);
          }
          break;
        case 'retry':
          this.flowObj.fire('fileRetry', this, chunk);
          break;
      }
    },

    /**
     * Pause file upload
     * @function
     */
    pause: function() {
      this.paused = true;
      this.abort();
    },

    /**
     * Resume file upload
     * @function
     */
    resume: function() {
      this.paused = false;
      this.flowObj.upload();
    },

    /**
     * Abort current upload
     * @function
     */
    abort: function (reset) {
      this.currentSpeed = 0;
      this.averageSpeed = 0;
      var chunks = this.chunks;
      if (reset) {
        this.chunks = [];
      }
      each(chunks, function (c) {
        if (c.status() === 'uploading') {
          c.abort();
          this.flowObj.uploadNextChunk();
        }
      }, this);
    },

    /**
     * Cancel current upload and remove from a list
     * @function
     */
    cancel: function () {
      this.flowObj.removeFile(this);
    },

    /**
     * Retry aborted file upload
     * @function
     */
    retry: function () {
      this.bootstrap();
      this.flowObj.upload();
    },

    /**
     * Clear current chunks and slice file again
     * @function
     */
    bootstrap: function () {
      if (typeof this.flowObj.opts.initFileFn === "function") {
        this.flowObj.opts.initFileFn(this);
      }

      this.abort(true);
      this.error = false;
      // Rebuild stack of chunks from file
      this._prevProgress = 0;
      var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor;
      var chunks = Math.max(
        round(this.size / this.flowObj.opts.chunkSize), 1
      );
      for (var offset = 0; offset < chunks; offset++) {
        this.chunks.push(
          new FlowChunk(this.flowObj, this, offset)
        );
      }
    },

    /**
     * Get current upload progress status
     * @function
     * @returns {number} from 0 to 1
     */
    progress: function () {
      if (this.error) {
        return 1;
      }
      if (this.chunks.length === 1) {
        this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress());
        return this._prevProgress;
      }
      // Sum up progress across everything
      var bytesLoaded = 0;
      each(this.chunks, function (c) {
        // get chunk progress relative to entire file
        bytesLoaded += c.progress() * (c.endByte - c.startByte);
      });
      var percent = bytesLoaded / this.size;
      // We don't want to lose percentages when an upload is paused
      this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent);
      return this._prevProgress;
    },

    /**
     * Indicates if file is being uploaded at the moment
     * @function
     * @returns {boolean}
     */
    isUploading: function () {
      var uploading = false;
      each(this.chunks, function (chunk) {
        if (chunk.status() === 'uploading') {
          uploading = true;
          return false;
        }
      });
      return uploading;
    },

    /**
     * Indicates if file is has finished uploading and received a response
     * @function
     * @returns {boolean}
     */
    isComplete: function () {
      var outstanding = false;
      each(this.chunks, function (chunk) {
        var status = chunk.status();
        if (status === 'pending' || status === 'uploading' || status === 'reading' || chunk.preprocessState === 1 || chunk.readState === 1) {
          outstanding = true;
          return false;
        }
      });
      return !outstanding;
    },

    /**
     * Count total size uploaded
     * @function
     * @returns {number}
     */
    sizeUploaded: function () {
      var size = 0;
      each(this.chunks, function (chunk) {
        size += chunk.sizeUploaded();
      });
      return size;
    },

    /**
     * Returns remaining time to finish upload file in seconds. Accuracy is based on average speed.
     * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
     * @function
     * @returns {number}
     */
    timeRemaining: function () {
      if (this.paused || this.error) {
        return 0;
      }
      var delta = this.size - this.sizeUploaded();
      if (delta && !this.averageSpeed) {
        return Number.POSITIVE_INFINITY;
      }
      if (!delta && !this.averageSpeed) {
        return 0;
      }
      return Math.floor(delta / this.averageSpeed);
    },

    /**
     * Get file type
     * @function
     * @returns {string}
     */
    getType: function () {
      return this.file.type && this.file.type.split('/')[1];
    },

    /**
     * Get file extension
     * @function
     * @returns {string}
     */
    getExtension: function () {
      return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase();
    }
  };

  /**
   * Default read function using the webAPI
   *
   * @function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk)
   *
   */
  function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) {
    var function_name = 'slice';

    if (fileObj.file.slice)
      function_name =  'slice';
    else if (fileObj.file.mozSlice)
      function_name = 'mozSlice';
    else if (fileObj.file.webkitSlice)
      function_name = 'webkitSlice';

    chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType));
  }

  /**
   * Class for storing a single chunk
   * @name FlowChunk
   * @param {Flow} flowObj
   * @param {FlowFile} fileObj
   * @param {number} offset
   * @constructor
   */
  function FlowChunk(flowObj, fileObj, offset) {

    /**
     * Reference to parent flow object
     * @type {Flow}
     */
    this.flowObj = flowObj;

    /**
     * Reference to parent FlowFile object
     * @type {FlowFile}
     */
    this.fileObj = fileObj;

    /**
     * File offset
     * @type {number}
     */
    this.offset = offset;

    /**
     * Indicates if chunk existence was checked on the server
     * @type {boolean}
     */
    this.tested = false;

    /**
     * Number of retries performed
     * @type {number}
     */
    this.retries = 0;

    /**
     * Pending retry
     * @type {boolean}
     */
    this.pendingRetry = false;

    /**
     * Preprocess state
     * @type {number} 0 = unprocessed, 1 = processing, 2 = finished
     */
    this.preprocessState = 0;

    /**
     * Read state
     * @type {number} 0 = not read, 1 = reading, 2 = finished
     */
    this.readState = 0;

    /**
     * Bytes transferred from total request size
     * @type {number}
     */
    this.loaded = 0;

    /**
     * Total request size
     * @type {number}
     */
    this.total = 0;

    /**
     * Size of a chunk
     * @type {number}
     */
    this.chunkSize = this.flowObj.opts.chunkSize;

    /**
     * Chunk start byte in a file
     * @type {number}
     */
    this.startByte = this.offset * this.chunkSize;

    /**
      * Compute the endbyte in a file
      *
      */
    this.computeEndByte = function() {
      var endByte = Math.min(this.fileObj.size, (this.offset + 1) * this.chunkSize);
      if (this.fileObj.size - endByte < this.chunkSize && !this.flowObj.opts.forceChunkSize) {
        // The last chunk will be bigger than the chunk size,
        // but less than 2 * this.chunkSize
        endByte = this.fileObj.size;
      }
      return endByte;
    }

    /**
     * Chunk end byte in a file
     * @type {number}
     */
    this.endByte = this.computeEndByte();

    /**
     * XMLHttpRequest
     * @type {XMLHttpRequest}
     */
    this.xhr = null;

    var $ = this;

    /**
     * Send chunk event
     * @param event
     * @param {...} args arguments of a callback
     */
    this.event = function (event, args) {
      args = Array.prototype.slice.call(arguments);
      args.unshift($);
      $.fileObj.chunkEvent.apply($.fileObj, args);
    };
    /**
     * Catch progress event
     * @param {ProgressEvent} event
     */
    this.progressHandler = function(event) {
      if (event.lengthComputable) {
        $.loaded = event.loaded ;
        $.total = event.total;
      }
      $.event('progress', event);
    };

    /**
     * Catch test event
     * @param {Event} event
     */
    this.testHandler = function(event) {
      var status = $.status(true);
      if (status === 'error') {
        $.event(status, $.message());
        $.flowObj.uploadNextChunk();
      } else if (status === 'success') {
        $.tested = true;
        $.event(status, $.message());
        $.flowObj.uploadNextChunk();
      } else if (!$.fileObj.paused) {
        // Error might be caused by file pause method
        // Chunks does not exist on the server side
        $.tested = true;
        $.send();
      }
    };

    /**
     * Upload has stopped
     * @param {Event} event
     */
    this.doneHandler = function(event) {
      var status = $.status();
      if (status === 'success' || status === 'error') {
        delete this.data;
        $.event(status, $.message());
        $.flowObj.uploadNextChunk();
      } else {
        $.event('retry', $.message());
        $.pendingRetry = true;
        $.abort();
        $.retries++;
        var retryInterval = $.flowObj.opts.chunkRetryInterval;
        if (retryInterval !== null) {
          setTimeout(function () {
            $.send();
          }, retryInterval);
        } else {
          $.send();
        }
      }
    };
  }

  FlowChunk.prototype = {
    /**
     * Get params for a request
     * @function
     */
    getParams: function () {
      return {
        flowChunkNumber: this.offset + 1,
        flowChunkSize: this.flowObj.opts.chunkSize,
        flowCurrentChunkSize: this.endByte - this.startByte,
        flowTotalSize: this.fileObj.size,
        flowIdentifier: this.fileObj.uniqueIdentifier,
        flowFilename: this.fileObj.name,
        flowRelativePath: this.fileObj.relativePath,
        flowTotalChunks: this.fileObj.chunks.length
      };
    },

    /**
     * Get target option with query params
     * @function
     * @param params
     * @returns {string}
     */
    getTarget: function(target, params){
      if(target.indexOf('?') < 0) {
        target += '?';
      } else {
        target += '&';
      }
      return target + params.join('&');
    },

    /**
     * Makes a GET request without any data to see if the chunk has already
     * been uploaded in a previous session
     * @function
     */
    test: function () {
      // Set up request and listen for event
      this.xhr = new XMLHttpRequest();
      this.xhr.addEventListener("load", this.testHandler, false);
      this.xhr.addEventListener("error", this.testHandler, false);
      var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this);
      var data = this.prepareXhrRequest(testMethod, true);
      this.xhr.send(data);
    },

    /**
     * Finish preprocess state
     * @function
     */
    preprocessFinished: function () {
      // Re-compute the endByte after the preprocess function to allow an
      // implementer of preprocess to set the fileObj size
      this.endByte = this.computeEndByte();

      this.preprocessState = 2;
      this.send();
    },

    /**
     * Finish read state
     * @function
     */
    readFinished: function (bytes) {
      this.readState = 2;
      this.bytes = bytes;
      this.send();
    },

    /**
     * Uploads the actual data in a POST call
     * @function
     */
    send: function () {
      var preprocess = this.flowObj.opts.preprocess;
      var read = this.flowObj.opts.readFileFn;
      if (typeof preprocess === 'function') {
        switch (this.preprocessState) {
          case 0:
            this.preprocessState = 1;
            preprocess(this);
            return;
          case 1:
            return;
        }
      }
      switch (this.readState) {
        case 0:
          this.readState = 1;
          read(this.fileObj, this.startByte, this.endByte, this.fileObj.file.type, this);
          return;
        case 1:
          return;
      }
      if (this.flowObj.opts.testChunks && !this.tested) {
        this.test();
        return;
      }

      this.loaded = 0;
      this.total = 0;
      this.pendingRetry = false;

      // Set up request and listen for event
      this.xhr = new XMLHttpRequest();
      this.xhr.upload.addEventListener('progress', this.progressHandler, false);
      this.xhr.addEventListener("load", this.doneHandler, false);
      this.xhr.addEventListener("error", this.doneHandler, false);

      var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this);
      var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, this.bytes);
      this.xhr.send(data);
    },

    /**
     * Abort current xhr request
     * @function
     */
    abort: function () {
      // Abort and reset
      var xhr = this.xhr;
      this.xhr = null;
      if (xhr) {
        xhr.abort();
      }
    },

    /**
     * Retrieve current chunk upload status
     * @function
     * @returns {string} 'pending', 'uploading', 'success', 'error'
     */
    status: function (isTest) {
      if (this.readState === 1) {
        return 'reading';
      } else if (this.pendingRetry || this.preprocessState === 1) {
        // if pending retry then that's effectively the same as actively uploading,
        // there might just be a slight delay before the retry starts
        return 'uploading';
      } else if (!this.xhr) {
        return 'pending';
      } else if (this.xhr.readyState < 4) {
        // Status is really 'OPENED', 'HEADERS_RECEIVED'
        // or 'LOADING' - meaning that stuff is happening
        return 'uploading';
      } else {
        if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) {
          // HTTP 200, perfect
		      // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed.
          return 'success';
        } else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 ||
            !isTest && this.retries >= this.flowObj.opts.maxChunkRetries) {
          // HTTP 413/415/500/501, permanent error
          return 'error';
        } else {
          // this should never happen, but we'll reset and queue a retry
          // a likely case for this would be 503 service unavailable
          this.abort();
          return 'pending';
        }
      }
    },

    /**
     * Get response from xhr request
     * @function
     * @returns {String}
     */
    message: function () {
      return this.xhr ? this.xhr.responseText : '';
    },

    /**
     * Get upload progress
     * @function
     * @returns {number}
     */
    progress: function () {
      if (this.pendingRetry) {
        return 0;
      }
      var s = this.status();
      if (s === 'success' || s === 'error') {
        return 1;
      } else if (s === 'pending') {
        return 0;
      } else {
        return this.total > 0 ? this.loaded / this.total : 0;
      }
    },

    /**
     * Count total size uploaded
     * @function
     * @returns {number}
     */
    sizeUploaded: function () {
      var size = this.endByte - this.startByte;
      // can't return only chunk.loaded value, because it is bigger than chunk size
      if (this.status() !== 'success') {
        size = this.progress() * size;
      }
      return size;
    },

    /**
     * Prepare Xhr request. Set query, headers and data
     * @param {string} method GET or POST
     * @param {bool} isTest is this a test request
     * @param {string} [paramsMethod] octet or form
     * @param {Blob} [blob] to send
     * @returns {FormData|Blob|Null} data to send
     */
    prepareXhrRequest: function(method, isTest, paramsMethod, blob) {
      // Add data from the query options
      var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest);
      query = extend(query || {}, this.getParams());

      var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest);
      var data = null;
      if (method === 'GET' || paramsMethod === 'octet') {
        // Add data from the query options
        var params = [];
        each(query, function (v, k) {
          params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
        });
        target = this.getTarget(target, params);
        data = blob || null;
      } else {
        // Add data from the query options
        data = new FormData();
        each(query, function (v, k) {
          data.append(k, v);
        });
        if (typeof blob !== "undefined") data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name);
      }

      this.xhr.open(method, target, true);
      this.xhr.withCredentials = this.flowObj.opts.withCredentials;

      // Add data from header options
      each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) {
        this.xhr.setRequestHeader(k, v);
      }, this);

      return data;
    }
  };

  /**
   * Remove value from array
   * @param array
   * @param value
   */
  function arrayRemove(array, value) {
    var index = array.indexOf(value);
    if (index > -1) {
      array.splice(index, 1);
    }
  }

  /**
   * If option is a function, evaluate it with given params
   * @param {*} data
   * @param {...} args arguments of a callback
   * @returns {*}
   */
  function evalOpts(data, args) {
    if (typeof data === "function") {
      // `arguments` is an object, not array, in FF, so:
      args = Array.prototype.slice.call(arguments);
      data = data.apply(null, args.slice(1));
    }
    return data;
  }
  Flow.evalOpts = evalOpts;

  /**
   * Execute function asynchronously
   * @param fn
   * @param context
   */
  function async(fn, context) {
    setTimeout(fn.bind(context), 0);
  }

  /**
   * Extends the destination object `dst` by copying all of the properties from
   * the `src` object(s) to `dst`. You can specify multiple `src` objects.
   * @function
   * @param {Object} dst Destination object.
   * @param {...Object} src Source object(s).
   * @returns {Object} Reference to `dst`.
   */
  function extend(dst, src) {
    each(arguments, function(obj) {
      if (obj !== dst) {
        each(obj, function(value, key){
          dst[key] = value;
        });
      }
    });
    return dst;
  }
  Flow.extend = extend;

  /**
   * Iterate each element of an object
   * @function
   * @param {Array|Object} obj object or an array to iterate
   * @param {Function} callback first argument is a value and second is a key.
   * @param {Object=} context Object to become context (`this`) for the iterator function.
   */
  function each(obj, callback, context) {
    if (!obj) {
      return ;
    }
    var key;
    // Is Array?
    // Array.isArray won't work, not only arrays can be iterated by index https://github.com/flowjs/ng-flow/issues/236#
    if (typeof(obj.length) !== 'undefined') {
      for (key = 0; key < obj.length; key++) {
        if (callback.call(context, obj[key], key) === false) {
          return ;
        }
      }
    } else {
      for (key in obj) {
        if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) {
          return ;
        }
      }
    }
  }
  Flow.each = each;

  /**
   * FlowFile constructor
   * @type {FlowFile}
   */
  Flow.FlowFile = FlowFile;

  /**
   * FlowFile constructor
   * @type {FlowChunk}
   */
  Flow.FlowChunk = FlowChunk;

  /**
   * Library version
   * @type {string}
   */
  Flow.version = '2.13.1';

  if ( typeof module === "object" && module && typeof module.exports === "object" ) {
    // Expose Flow as module.exports in loaders that implement the Node
    // module pattern (including browserify). Do not create the global, since
    // the user will be storing it themselves locally, and globals are frowned
    // upon in the Node module world.
    module.exports = Flow;
  } else {
    // Otherwise expose Flow to the global object as usual
    window.Flow = Flow;

    // Register as a named AMD module, since Flow can be concatenated with other
    // files that may use define, but not via a proper concatenation script that
    // understands anonymous AMD modules. A named AMD is safest and most robust
    // way to register. Lowercase flow is used because AMD module names are
    // derived from file names, and Flow is normally delivered in a lowercase
    // file name. Do this after creating the global so that if an AMD module wants
    // to call noConflict to hide this version of Flow, it will work.
    if ( typeof define === "function" && define.amd ) {
      define( "flow", [], function () { return Flow; } );
    }
  }
})(typeof window !== 'undefined' && window, typeof document !== 'undefined' && document);


// const electron = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');

var pack = null
try {
    pack = require('../../package.json')
} catch (e) {

}

const platform = os.platform();

var appName = ''
if (pack !== null) {
    appName = pack.name
}

let userData = '';

if (platform === 'win32') {
    userData = path.join(process.env.APPDATA, appName);
} else if (platform === 'darwin') {
    userData = path.join(process.env.HOME, 'Library', 'Application Support', appName);
} else {
    userData = path.join('var', 'local', appName);
}

/**
 * Create a table | a json file
 * The second argument is optional, if ommitted, the file
 * will be created at the default location.
 * @param  {[string]} arguments[0] [Table name]
 * @param {[string]} arguments[1] [Location of the database file] (Optional)
 * @param {[function]} arguments[2] [Callbak ]
 */
// function createTable(tableName, callback) {
function createTable() {
    tableName = arguments[0];
    var fname = '';
    var callback;
    if (arguments.length === 2) {

        callback = arguments[1];
        fname = path.join(userData, tableName + '.json');
    } else if (arguments.length === 3) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        callback = arguments[2];
    }

    // Check if the file with the tablename.json exists
    let exists = fs.existsSync(fname);

    if (exists) {
        // The file exists, do not recreate the table/json file
        callback(false, tableName + '.json already exists!');
        return;
    } else {
        // Create an empty object and pass an empty array as value
        let obj = new Object();
        obj[tableName] = [];

        // Write the object to json file
        try {
            fs.writeFileSync(fname, JSON.stringify(obj, null, 2), (err) => {

            })
            callback(true, "Success!")
            return;
        } catch (e) {
            callback(false, e.toString());
            return;
        }

    }
}

/**
 * Checks if a json file contains valid JSON string
 */
function valid() {
    var fName = ''
    if (arguments.length == 2) {
        // Given the database name and location
        const dbName = arguments[0]
        const dbLocation = arguments[1]
        var fName = path.join(dbLocation, dbName + '.json')
    } else if (arguments.length == 1) {
        const dbName = arguments[0]
        fname = path.join(userData, dbName + '.json')
    }

    const content = fs.readFileSync(fName, 'utf-8')
    try {
        JSON.parse(content)
    } catch (e) {
        return false
    }
    return true
}

/**
 * Insert object to table. The object will be appended with the property, id
 * which uses timestamp as value.
 * There are 3 required arguments.
 * @param  {string} arguments[0]  [Table name]
 * @param  {string} arguments[1] [Location of the database file] (Optional)
 * @param  {string} arguments[2] [Row object]
 * @param  {Function} arguments[3] [Callback function]
 */
// function insertTableContent(tableName, tableRow, callback) {
function insertTableContent() {
    let tableName = arguments[0];
    var fname = '';
    var callback;
    var tableRow;
    if (arguments.length === 3) {
        callback = arguments[2];
        fname = path.join(userData, arguments[0] + '.json');
        tableRow = arguments[1];
    } else if (arguments.length === 4) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        callback = arguments[3];
        tableRow = arguments[2];
    }

    let exists = fs.existsSync(fname);

    if (exists) {
        // Table | json parsed
        let table = JSON.parse(fs.readFileSync(fname));

        let date = new Date();
        let id = date.getTime();
        tableRow['id'] = id;

        table[tableName].push(tableRow);

        try {
            fs.writeFileSync(fname, JSON.stringify(table, null, 2), (err) => {

            })

            callback(true, "Object written successfully!");
            return;
        } catch (e) {
            callback(false, "Error writing object.");
            return;
        }
    }
    callback(false, "Table/json file doesn't exist!");
    return;
}

/**
 * Get all contents of the table/json file object
 * @param  {string} arguments[0] [Table name]
 * @param  {string} arguments[1] [Location of the database file] (Optional)
 * @param  {Function} arguments[2]  [callback function]
 */
// function getAll(tableName, callback) {
function getAll() {
    var fname = '';
    var callback;
    var tableName = arguments[0];

    if (arguments.length === 2) {
        fname = path.join(userData, tableName + '.json');
        callback = arguments[1];
    } else if (arguments.length === 3) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        callback = arguments[2];
    }

    let exists = fs.existsSync(fname);

    if (exists) {
        try {
            let table = JSON.parse(fs.readFileSync(fname));
            callback(true, table[tableName]);
            return;
        } catch (e) {
            callback(false, []);
            return;
        }
    } else {
        callback(false, 'Table file does not exist!');
        return;
    }
}

/**
 * Find rows of a given field/key.
 * @param  {string} arguments[0] Table name
 * @param  {string} arguments[1] Location of the database file (Optional)
 * @param  {string} arguments[2] They fey/field to retrieve.
 */
function getField() {
    let fname = ''
    let tableName = arguments[0]
    let callback
    let key

    if (arguments.length === 3) {
        fname = path.join(userData, tableName + '.json');
        callback = arguments[2];
        key = arguments[1]
    } else if (arguments.length === 4) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        callback = arguments[3];
        key = arguments[2]
    }

    let exists = fs.existsSync(fname)

    if (exists) {
        let table = JSON.parse(fs.readFileSync(fname));
        const rows = table[tableName]
        let data = []

        for (let i = 0; i < rows.length; i++) {
            if (rows[i].hasOwnProperty(key)) {
                data.push(rows[i][key])
            }
        }

        callback(true, data)

    } else {
        callback(false, 'The table you are trying to access does not exist.')
        return
    }
}

/**
 * Clears an existing table leaving an empty list in the json file.
 * @param  {string} arguments[0] [Table name]
 * @param  {string} arguments[1] [Location of the database file] (Optional)
 * @param  {Function} arguments[2]  [callback function]
 */
function clearTable() {
    let fname = ''
    let tableName = arguments[0]
    let callback

    if (arguments.length === 2) {
        fname = path.join(userData, tableName + '.json');
        callback = arguments[1];
    } else if (arguments.length === 3) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        callback = arguments[2];
    }

    let exists = fs.existsSync(fname)

    if (exists) {
        let obj = new Object();
        obj[tableName] = [];

        // Write the object to json file
        try {
            fs.writeFileSync(fname, JSON.stringify(obj, null, 2), (err) => {

            })
            callback(true, "Table cleared successfully!")
            return;
        } catch (e) {
            callback(false, e.toString());
            return;
        }
    } else {
        callback(false, 'The table you are trying to clear does not exist.')
        return
    }
}

/**
 * Count the number of rows for a given table.
 * @param {string} FirstArgument Table name
 * @param {string} SecondArgument Location of the database file (Optional)
 * @param {callback} ThirdArgument Function callback
 */
function count() {
    let tableName = arguments[0]
    let callback
    if (arguments.length === 2) {
        callback = arguments[1]
        getAll(tableName, (succ, data) => {
            if (succ) {
                callback(true, data.length)
                return
            } else {
                callback(false, data)
                return
            }
        })
    } else if (arguments.length === 3) {
        callback = arguments[2]
        getAll(tableName, arguments[1], (succ, data) => {
            if (succ) {
                callback(true, data.length)
                return
            } else {
                callback(false, data)
                return
            }
        })
    } else {
        callback(false,'Wrong number of arguments. Must be either 2 or 3 arguments including callback function.')
        return
    }
}

/**
 * Get row or rows that matched the given condition(s) in WHERE argument
 * @param {string} FirstArgument Table name
 * @param {string} SecondArgument Location of the database file (Optional)
 * @param {object} ThirdArgument Collection of conditions to be met
 ```
 {
      key1: value1,
      key2: value2,
      ...
 }
 ```
 * @param {callback} FourthArgument Function callback
 */
function getRows() {

    let tableName = arguments[0];
    var fname = '';
    var callback;
    var where;

    if (arguments.length === 3) {
        fname = path.join(userData, tableName + '.json');
        where = arguments[1];
        callback = arguments[2];
    } else if (arguments.length === 4) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        where = arguments[2];
        callback = arguments[3];
    }

    let exists = fs.existsSync(fname);
    let whereKeys;

    // Check if where is an object
    if (Object.prototype.toString.call(where) === "[object Object]") {
        // Check for number of keys
        whereKeys = Object.keys(where);
        if (whereKeys === 0) {
            callback(false, "There are no conditions passed to the WHERE clause.");
            return;
        }
    } else {
        callback(false, "WHERE clause should be an object.");
        return;
    }

    // Check if the json file exists, if it is, parse it.
    if (exists) {
        try {
            let table = JSON.parse(fs.readFileSync(fname));
            let rows = table[tableName];

            let objs = [];

            for (let i = 0; i < rows.length; i++) {
                let matched = 0; // Number of matched complete where clause
                for (var j = 0; j < whereKeys.length; j++) {
                    // Test if there is a matched key with where clause
                    if (rows[i].hasOwnProperty(whereKeys[j])) {
                        if (rows[i][whereKeys[j]] === where[whereKeys[j]]) {
                            matched++;
                        }
                    }
                }

                // Check if all conditions in the WHERE clause are matched
                if (matched === whereKeys.length) {
                    objs.push(rows[i])
                }
            }

            callback(true, objs);
            return;
        } catch (e) {
            callback(false, e.toString());
            return;
        }
    } else {
        callback(false, 'Table file does not exist!');
        return;
    }
}

/**
 * Update a row or record which satisfies the where clause
 * @param  {[string]} arguments[0] [Table name]
 * @param {string} arguments[1] [Location of the database file] (Optional)
 * @param  {[object]} arguments[2]     [Objet for WHERE clause]
 * @param  {[object]} arguments[3]       [Object for SET clause]
 * @param  {Function} arguments[4]  [Callback function]
 */
// function updateRow(tableName, where, set, callback) {
function updateRow() {
    let tableName = arguments[0];
    var fname = '';
    var where;
    var set;
    var callback;

    if (arguments.length === 4) {
        fname = path.join(userData, tableName + '.json');
        where = arguments[1];
        set = arguments[2];
        callback = arguments[3];
    } else if (arguments.length === 5) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        where = arguments[2];
        set = arguments[3];
        callback = arguments[4];
    }

    let exists = fs.existsSync(fname);

    let whereKeys = Object.keys(where);
    let setKeys = Object.keys(set);

    if (exists) {
        let table = JSON.parse(fs.readFileSync(fname));
        let rows = table[tableName];

        let matched = 0; // Number of matched complete where clause
        let matchedIndex = 0;

        for (var i = 0; i < rows.length; i++) {

            for (var j = 0; j < whereKeys.length; j++) {
                // Test if there is a matched key with where clause and single row of table
                if (rows[i].hasOwnProperty(whereKeys[j])) {
                    if (rows[i][whereKeys[j]] === where[whereKeys[j]]) {
                        matched++;
                        matchedIndex = i;
                    }
                }
            }
        }

        if (matched === whereKeys.length) {
            // All field from where clause are present in this particular
            // row of the database table
            try {
                for (var k = 0; k < setKeys.length; k++) {
                    // rows[i][setKeys[k]] = set[setKeys[k]];
                    rows[matchedIndex][setKeys[k]] = set[setKeys[k]];
                }

                // Create a new object and pass the rows
                let obj = new Object();
                obj[tableName] = rows;

                // Write the object to json file
                try {
                    fs.writeFileSync(fname, JSON.stringify(obj, null, 2), (err) => {

                    })

                    callback(true, "Success!")
                    return;
                } catch (e) {
                    callback(false, e.toString());
                    return;
                }

                callback(true, rows);
            } catch (e) {
                callback(false, e.toString());
                return;
            }
        } else {
            callback(false, "Cannot find the specified record.");
            return;
        }

    } else {
        callback(false, 'Table file does not exist!');
        return;
    }
}

/**
 * Searching function
 * @param {string} arguments[0] Name of the table to search for
 * @param {string} arguments[1] [Location of the database file] (Optional)
 * @param {string} arguments[2] Name of the column/key to match
 * @param {object} arguments[3] The part of the value of the key that is being lookup
 * @param {function} arguments[4] Callback function
 */
// function search(tableName, field, keyword, callback) {
function search() {

    let tableName = arguments[0];
    var fname = '';
    var field;
    var keyword;
    var callback;

    if (arguments.length === 4) {
        fname = path.join(userData, tableName + '.json');
        field = arguments[1];
        keyword = arguments[2];
        callback = arguments[3];
    } else if (arguments.length === 5) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        field = arguments[2];
        keyword = arguments[3];
        callback = arguments[4];
    }

    let exists = fs.existsSync(fname);

    if (exists) {
        let table = JSON.parse(fs.readFileSync(fname));
        let rows = table[tableName];

        if (rows.length > 0) {

            // Declare an empty list
            let foundRows = [];

            for (var i = 0; i < rows.length; i++) {
                // Check if key exists
                if (rows[i].hasOwnProperty(field)) {
                    // Make sure that an object is converted to string before
                    // applying toLowerCase()
                    let value = rows[i][field].toString().toLowerCase();
                    let n = value.search(keyword.toString().toLowerCase());

                    if (n !== -1) {
                        // The substring is found, add object to the list.
                        foundRows.push(rows[i]);
                    }

                } else {
                    callback(false, 2);
                    return;
                }
            }

            callback(true, foundRows);
            return;

        } else {
            callback(false, []);
            return;
        }
    } else {
        callback(false, 'Table file does not exist!');
        return;
    }
}

/**
 * Delete a row specified.
 * @param {*} tableName
 * @param {string} arguments[1] [Location of the database file] (Optional)
 * @param {*} where
 * @param {*} callback
 */
// function deleteRow(tableName, where, callback) {
function deleteRow() {
    let tableName = arguments[0];

    var fname = '';
    var where;
    var callback;

    if (arguments.length === 3) {
        fname = path.join(userData, tableName + '.json');
        where = arguments[1];
        callback = arguments[2];
    } else if (arguments.length === 4) {
        fname = path.join(arguments[1], arguments[0] + '.json');
        where = arguments[2];
        callback = arguments[3];
    }

    let exists = fs.existsSync(fname);

    let whereKeys = Object.keys(where);

    if (exists) {
        let table = JSON.parse(fs.readFileSync(fname));
        let rows = table[tableName];

        if (rows.length > 0) {
            let matched = 0;
            let matchedIndices = [];

            for (let i = 0; i < rows.length; i++) {
                // Iterate throught the rows
                for (var j = 0; j < whereKeys.length; j++) {
                    // Test if there is a matched key with where clause and single row of table
                    if (rows[i].hasOwnProperty(whereKeys[j])) {
                        if (rows[i][whereKeys[j]] === where[whereKeys[j]]) {
                            matched++;
                            matchedIndices.push(i);
                        }
                    }
                }
            }

            if (matchedIndices.length === 0) {
                callback(false, 'Row does not exist!');
                return;
            }

            for (let k = 0; k < matchedIndices.length; k++) {
                rows.splice(matchedIndices[k], 1);
            }

            // Create a new object and pass the rows
            let obj = new Object();
            obj[tableName] = rows;

            // Write the object to json file
            try {
                fs.writeFileSync(fname, JSON.stringify(obj, null, 2), (err) => {

                })
                callback(true, "Row(s) deleted successfully!")
                return;
            } catch (e) {
                callback(false, e.toString());
                return;
            }

        } else {
            callback(false, 'Table is empty!');
            return;
        }
    } else {
        callback(false, 'Table file does not exist!');
        return;
    }

}

// Export the public available functions
module.exports = {
    createTable,
    insertTableContent,
    getAll,
    getRows,
    updateRow,
    search,
    deleteRow,
    valid,
    clearTable,
    getField,
    count
};


/*!
 * method-override
 * Copyright(c) 2010 Sencha Inc.
 * Copyright(c) 2011 TJ Holowaychuk
 * Copyright(c) 2014 Jonathan Ong
 * Copyright(c) 2014 Douglas Christopher Wilson
 * MIT Licensed
 */

'use strict'

/**
 * Module dependencies.
 */

var debug = require('debug')('method-override')
var methods = require('methods')
var parseurl = require('parseurl')
var querystring = require('querystring')
var vary = require('vary')

/**
 * Method Override:
 *
 * Provides faux HTTP method support.
 *
 * Pass an optional `getter` to use when checking for
 * a method override.
 *
 * A string is converted to a getter that will look for
 * the method in `req.body[getter]` and a function will be
 * called with `req` and expects the method to be returned.
 * If the string starts with `X-` then it will look in
 * `req.headers[getter]` instead.
 *
 * The original method is available via `req.originalMethod`.
 *
 * @param {string|function} [getter=X-HTTP-Method-Override]
 * @param {object} [options]
 * @return {function}
 * @api public
 */

module.exports = function methodOverride (getter, options) {
  var opts = options || {}

  // get the getter fn
  var get = typeof getter === 'function'
    ? getter
    : createGetter(getter || 'X-HTTP-Method-Override')

  // get allowed request methods to examine
  var methods = opts.methods === undefined
    ? ['POST']
    : opts.methods

  return function methodOverride (req, res, next) {
    var method
    var val

    req.originalMethod = req.originalMethod || req.method

    // validate request is an allowed method
    if (methods && methods.indexOf(req.originalMethod) === -1) {
      return next()
    }

    val = get(req, res)
    method = Array.isArray(val)
      ? val[0]
      : val

    // replace
    if (method !== undefined && supports(method)) {
      req.method = method.toUpperCase()
      debug('override %s as %s', req.originalMethod, req.method)
    }

    next()
  }
}

/**
 * Create a getter for the given string.
 */

function createGetter (str) {
  if (str.substr(0, 2).toUpperCase() === 'X-') {
    // header getter
    return createHeaderGetter(str)
  }

  return createQueryGetter(str)
}

/**
 * Create a getter for the given query key name.
 */

function createQueryGetter (key) {
  return function (req, res) {
    var url = parseurl(req)
    var query = querystring.parse(url.query || '')
    return query[key]
  }
}

/**
 * Create a getter for the given header name.
 */

function createHeaderGetter (str) {
  var name = str.toLowerCase()

  return function (req, res) {
    // set appropriate Vary header
    vary(res, str)

    // get header
    var header = req.headers[name]

    if (!header) {
      return undefined
    }

    // multiple headers get joined with comma by node.js core
    var index = header.indexOf(',')

    // return first value
    return index !== -1
      ? header.substr(0, index).trim()
      : header.trim()
  }
}

/**
 * Check if node supports `method`.
 */

function supports (method) {
  return method &&
    typeof method === 'string' &&
    methods.indexOf(method.toLowerCase()) !== -1
}


import Expression from './expression';
import Condition from './condition';
import Configuration from './configuration';

import {
  assign,
  camelCase,
  cloneDeep,
  compact,
  contains,
  difference,
  identity,
  isArray,
  isEmpty,
  isFunction,
  isPlainObject,
  isString,
  snakeCase
} from './util';

import {
  Param,
  ArrayParam,
  LayerParam,
  RangeParam,
  RawParam,
  TransformationParam
} from "./parameters";

/**
 * Assign key, value to target, when value is not null.

 *   This function mutates the target!
 * @param {object} target the object to assign the values to
 * @param {object} sources one or more objects to get values from
 * @returns {object} the target after the assignment
 */
function assignNotNull(target, ...sources) {
  sources.forEach(source => {
    Object.keys(source).forEach(key => {
      if (source[key] != null) {
        target[key] = source[key];
      }
    })
  });
  return target;
}

/**
 * TransformationBase
 * Depends on 'configuration', 'parameters','util'
 * @internal
 */

var TransformationBase = class TransformationBase {
  /**
   * The base class for transformations.
   * Members of this class are documented as belonging to the {@link Transformation} class for convenience.
   * @class TransformationBase
   */
  constructor(options) {
    /** @private */
    /** @private */
    var parent, trans;
    parent = void 0;
    trans = {};
    /**
     * Return an options object that can be used to create an identical Transformation
     * @function Transformation#toOptions
     * @return {Object} Returns a plain object representing this transformation
     */
    this.toOptions = function (withChain) {
      let opt = {};
      if(withChain == null) {
        withChain = true;
      }
      Object.keys(trans).forEach(key => opt[key] = trans[key].origValue);
      assignNotNull(opt, this.otherOptions);
      if (withChain && !isEmpty(this.chained)) {
        let list = this.chained.map(tr => tr.toOptions());
        list.push(opt);
        opt = {};
        assignNotNull(opt, this.otherOptions);
        opt.transformation = list;
      }
      return opt;
    };
    /**
     * Set a parent for this object for chaining purposes.
     *
     * @function Transformation#setParent
     * @param {Object} object - the parent to be assigned to
     * @returns {Transformation} Returns this instance for chaining purposes.
     */
    this.setParent = function (object) {
      parent = object;
      if (object != null) {
        this.fromOptions(typeof object.toOptions === "function" ? object.toOptions() : void 0);
      }
      return this;
    };
    /**
     * Returns the parent of this object in the chain
     * @function Transformation#getParent
     * @protected
     * @return {Object} Returns the parent of this object if there is any
     */
    this.getParent = function () {
      return parent;
    };

    // Helper methods to create parameter methods
    // These methods are defined here because they access `trans` which is
    // a private member of `TransformationBase`

    /** @protected */
    this.param = function (value, name, abbr, defaultValue, process) {
      if (process == null) {
        if (isFunction(defaultValue)) {
          process = defaultValue;
        } else {
          process = identity;
        }
      }
      trans[name] = new Param(name, abbr, process).set(value);
      return this;
    };
    /** @protected */
    this.rawParam = function (value, name, abbr, defaultValue, process = identity) {
      process = lastArgCallback(arguments);
      trans[name] = new RawParam(name, abbr, process).set(value);
      return this;
    };
    /** @protected */
    this.rangeParam = function (value, name, abbr, defaultValue, process = identity) {
      process = lastArgCallback(arguments);
      trans[name] = new RangeParam(name, abbr, process).set(value);
      return this;
    };
    /** @protected */
    this.arrayParam = function (value, name, abbr, sep = ":", defaultValue = [], process = identity) {
      process = lastArgCallback(arguments);
      trans[name] = new ArrayParam(name, abbr, sep, process).set(value);
      return this;
    };
    /** @protected */
    this.transformationParam = function (value, name, abbr, sep = ".", defaultValue, process = identity) {
      process = lastArgCallback(arguments);
      trans[name] = new TransformationParam(name, abbr, sep, process).set(value);
      return this;
    };
    this.layerParam = function (value, name, abbr) {
      trans[name] = new LayerParam(name, abbr).set(value);
      return this;
    };

    // End Helper methods

    /**
     * Get the value associated with the given name.
     * @function Transformation#getValue
     * @param {string} name - the name of the parameter
     * @return {*} the processed value associated with the given name
     * @description Use {@link get}.origValue for the value originally provided for the parameter
     */
    this.getValue = function (name) {
      let value = trans[name] && trans[name].value();
      return value != null ? value : this.otherOptions[name];
    };
    /**
     * Get the parameter object for the given parameter name
     * @function Transformation#get
     * @param {string} name the name of the transformation parameter
     * @returns {Param} the param object for the given name, or undefined
     */
    this.get = function (name) {
      return trans[name];
    };
    /**
     * Remove a transformation option from the transformation.
     * @function Transformation#remove
     * @param {string} name - the name of the option to remove
     * @return {*} Returns the option that was removed or null if no option by that name was found. The type of the
     *              returned value depends on the value.
     */
    this.remove = function (name) {
      var temp;
      switch (false) {
        case trans[name] == null:
          temp = trans[name];
          delete trans[name];
          return temp.origValue;
        case this.otherOptions[name] == null:
          temp = this.otherOptions[name];
          delete this.otherOptions[name];
          return temp;
        default:
          return null;
      }
    };
    /**
     * Return an array of all the keys (option names) in the transformation.
     * @return {Array<string>} the keys in snakeCase format
     */
    this.keys = function () {
      var key;
      return ((function () {
        var results;
        results = [];
        for (key in trans) {
          if (key != null) {
            results.push(key.match(VAR_NAME_RE) ? key : snakeCase(key));
          }
        }
        return results;
      })()).sort();
    };
    /**
     * Returns a plain object representation of the transformation. Values are processed.
     * @function Transformation#toPlainObject
     * @return {Object} the transformation options as plain object
     */
    this.toPlainObject = function () {
      var hash, key, list;
      hash = {};
      for (key in trans) {
        hash[key] = trans[key].value();
        if (isPlainObject(hash[key])) {
          hash[key] = cloneDeep(hash[key]);
        }
      }
      if (!isEmpty(this.chained)) {
        list = this.chained.map(tr => tr.toPlainObject());
        list.push(hash);
        hash = {
          transformation: list
        };
      }
      return hash;
    };
    /**
     * Complete the current transformation and chain to a new one.
     * In the URL, transformations are chained together by slashes.
     * @function Transformation#chain
     * @return {Transformation} Returns this transformation for chaining
     * @example
     * var tr = cloudinary.Transformation.new();
     * tr.width(10).crop('fit').chain().angle(15).serialize()
     * // produces "c_fit,w_10/a_15"
     */
    this.chain = function () {
      var names, tr;
      names = Object.getOwnPropertyNames(trans);
      if (names.length !== 0) {
        tr = new this.constructor(this.toOptions(false));
        this.resetTransformations();
        this.chained.push(tr);
      }
      return this;
    };
    this.resetTransformations = function () {
      trans = {};
      return this;
    };
    this.otherOptions = {};
    this.chained = [];
    this.fromOptions(options);
  }

  /**
   * Merge the provided options with own's options
   * @param {Object} [options={}] key-value list of options
   * @returns {Transformation} Returns this instance for chaining
   */
  fromOptions(options) {
    if (!isEmpty(options)) {
      if (options instanceof TransformationBase) {
        this.fromTransformation(options);
      } else {
        options || (options = {});
        if (isString(options) || isArray(options)) {
          options = {
            transformation: options
          };
        }
        options = cloneDeep(options, function (value) {
          if (value instanceof TransformationBase) {
            return new value.constructor(value.toOptions());
          }
        });
        // Handling of "if" statements precedes other options as it creates a chained transformation
        if (options["if"]) {
          this.set("if", options["if"]);
          delete options["if"];
        }
        for (let key in options) {
          let opt = options[key];
          if (key.match(VAR_NAME_RE)) {
            if (key !== '$attr') {
              this.set('variable', key, opt);
            }
          } else {
            this.set(key, opt);
          }
        }
      }
    }
    return this;
  }

  fromTransformation(other) {
    if (other instanceof TransformationBase) {
      other.keys().forEach(key =>
        this.set(key, other.get(key).origValue)
      );
    }
    return this;
  }

  /**
   * Set a parameter.
   * The parameter name `key` is converted to
   * @param {string} key - the name of the parameter
   * @param {*} values - the value of the parameter
   * @returns {Transformation} Returns this instance for chaining
   */
  set(key, ...values) {
    var camelKey;
    camelKey = camelCase(key);
    if (contains(Transformation.methods, camelKey)) {
      this[camelKey].apply(this, values);
    } else {
      this.otherOptions[key] = values[0];
    }
    return this;
  }

  hasLayer() {
    return this.getValue("overlay") || this.getValue("underlay");
  }

  /**
   * Generate a string representation of the transformation.
   * @function Transformation#serialize
   * @return {string} Returns the transformation as a string
   */
  serialize() {
    var ifParam, j, len, paramList, ref, ref1, ref2, ref3, ref4, resultArray, t, transformationList,
      transformationString, transformations, value, variables, vars;
    resultArray = this.chained.map(tr => tr.serialize());
    paramList = this.keys();
    transformations = (ref = this.get("transformation")) != null ? ref.serialize() : void 0;
    ifParam = (ref1 = this.get("if")) != null ? ref1.serialize() : void 0;
    variables = processVar((ref2 = this.get("variables")) != null ? ref2.value() : void 0);
    paramList = difference(paramList, ["transformation", "if", "variables"]);
    vars = [];
    transformationList = [];
    for (j = 0, len = paramList.length; j < len; j++) {
      t = paramList[j];
      if (t.match(VAR_NAME_RE)) {
        vars.push(t + "_" + Expression.normalize((ref3 = this.get(t)) != null ? ref3.value() : void 0));
      } else {
        transformationList.push((ref4 = this.get(t)) != null ? ref4.serialize() : void 0);
      }
    }
    switch (false) {
      case !isString(transformations):
        transformationList.push(transformations);
        break;
      case !isArray(transformations):
        resultArray = resultArray.concat(transformations);
    }
    transformationList = (function () {
      var k, len1, results;
      results = [];
      for (k = 0, len1 = transformationList.length; k < len1; k++) {
        value = transformationList[k];
        if (isArray(value) && !isEmpty(value) || !isArray(value) && value) {
          results.push(value);
        }
      }
      return results;
    })();
    transformationList = vars.sort().concat(variables).concat(transformationList.sort());
    if (ifParam === "if_end") {
      transformationList.push(ifParam);
    } else if (!isEmpty(ifParam)) {
      transformationList.unshift(ifParam);
    }
    transformationString = compact(transformationList).join(this.param_separator);
    if (!isEmpty(transformationString)) {
      resultArray.push(transformationString);
    }
    return compact(resultArray).join(this.trans_separator);
  }

  /**
   * Provide a list of all the valid transformation option names
   * @function Transformation#listNames
   * @private
   * @return {Array<string>} a array of all the valid option names
   */
  listNames() {
    return Transformation.methods;
  }

  /**
   * Returns attributes for an HTML tag.
   * @function Cloudinary.toHtmlAttributes
   * @return PlainObject
   */
  toHtmlAttributes() {
    var attrName, height, key, options, ref2, ref3, value, width;
    options = {};
    for (key in this.otherOptions) {
      value = this.otherOptions[key];
      if (contains(Transformation.PARAM_NAMES, snakeCase(key))) {
        continue;
      }
      attrName = /^html_/.test(key) ? key.slice(5) : key;
      options[attrName] = value;
    }
    // convert all "html_key" to "key" with the same value
    this.keys().forEach(key => {
      if (/^html_/.test(key)) {
        options[camelCase(key.slice(5))] = this.getValue(key);
      }
    });
    if (!(this.hasLayer() || this.getValue("angle") || contains(["fit", "limit", "lfill"], this.getValue("crop")))) {
      width = (ref2 = this.get("width")) != null ? ref2.origValue : void 0;
      height = (ref3 = this.get("height")) != null ? ref3.origValue : void 0;
      if (parseFloat(width) >= 1.0) {
        if (options['width'] == null) {
          options['width'] = width;
        }
      }
      if (parseFloat(height) >= 1.0) {
        if (options['height'] == null) {
          options['height'] = height;
        }
      }
    }
    return options;
  }

  isValidParamName(name) {
    return Transformation.methods.indexOf(camelCase(name)) >= 0;
  }

  /**
   * Delegate to the parent (up the call chain) to produce HTML
   * @function Transformation#toHtml
   * @return {string} HTML representation of the parent if possible.
   * @example
   * tag = cloudinary.ImageTag.new("sample", {cloud_name: "demo"})
   * // ImageTag {name: "img", publicId: "sample"}
   * tag.toHtml()
   * //
   * tag.transformation().crop("fit").width(300).toHtml()
   * //
   */
  toHtml() {
    var ref;
    return (ref = this.getParent()) != null ? typeof ref.toHtml === "function" ? ref.toHtml() : void 0 : void 0;
  }

  toString() {
    return this.serialize();
  }

};

const VAR_NAME_RE = /^\$[a-zA-Z0-9]+$/;

TransformationBase.prototype.trans_separator = '/';

TransformationBase.prototype.param_separator = ',';

function lastArgCallback(args) {
  var callback;
  callback = args != null ? args[args.length - 1] : void 0;
  if (isFunction(callback)) {
    return callback;
  } else {
    return void 0;
  }
}

function processVar(varArray) {
  var j, len, name, results, v;
  if (isArray(varArray)) {
    results = [];
    for (j = 0, len = varArray.length; j < len; j++) {
      [name, v] = varArray[j];
      results.push(`${name}_${Expression.normalize(v)}`);
    }
    return results;
  } else {
    return varArray;
  }
}

/**
 * Transformation Class methods.
 * This is a list of the parameters defined in Transformation.
 * Values are camelCased.
 * @const Transformation.methods
 * @private
 * @ignore
 * @type {Array<string>}
 */
/**
 * Parameters that are filtered out before passing the options to an HTML tag.
 *
 * The list of parameters is a combination of `Transformation::methods` and `Configuration::CONFIG_PARAMS`
 * @const {Array<string>} Transformation.PARAM_NAMES
 * @private
 * @ignore
 * @see toHtmlAttributes
 */
var Transformation = class Transformation extends TransformationBase {
  /**
   *  Represents a single transformation.
   *  @class Transformation
   *  @example
   *  t = new cloudinary.Transformation();
   * t.angle(20).crop("scale").width("auto");
   *
   * // or
   *
   * t = new cloudinary.Transformation( {angle: 20, crop: "scale", width: "auto"});
   */
  constructor(options) {
    super(options);
  }

  /**
   * Convenience constructor
   * @param {Object} options
   * @return {Transformation}
   * @example cl = cloudinary.Transformation.new( {angle: 20, crop: "scale", width: "auto"})
   */
  static new(options) {
    return new Transformation(options);
  }

  /*
    Transformation Parameters
  */
  angle(value) {
    return this.arrayParam(value, "angle", "a", ".", Expression.normalize);
  }

  audioCodec(value) {
    return this.param(value, "audio_codec", "ac");
  }

  audioFrequency(value) {
    return this.param(value, "audio_frequency", "af");
  }

  aspectRatio(value) {
    return this.param(value, "aspect_ratio", "ar", Expression.normalize);
  }

  background(value) {
    return this.param(value, "background", "b", Param.norm_color);
  }

  bitRate(value) {
    return this.param(value, "bit_rate", "br");
  }

  border(value) {
    return this.param(value, "border", "bo", function (border) {
      if (isPlainObject(border)) {
        border = assign({}, {
          color: "black",
          width: 2
        }, border);
        return `${border.width}px_solid_${Param.norm_color(border.color)}`;
      } else {
        return border;
      }
    });
  }

  color(value) {
    return this.param(value, "color", "co", Param.norm_color);
  }

  colorSpace(value) {
    return this.param(value, "color_space", "cs");
  }

  crop(value) {
    return this.param(value, "crop", "c");
  }

  defaultImage(value) {
    return this.param(value, "default_image", "d");
  }

  delay(value) {
    return this.param(value, "delay", "dl");
  }

  density(value) {
    return this.param(value, "density", "dn");
  }

  duration(value) {
    return this.rangeParam(value, "duration", "du");
  }

  dpr(value) {
    return this.param(value, "dpr", "dpr", (dpr) => {
      dpr = dpr.toString();
      if (dpr != null ? dpr.match(/^\d+$/) : void 0) {
        return dpr + ".0";
      } else {
        return Expression.normalize(dpr);
      }
    });
  }

  effect(value) {
    return this.arrayParam(value, "effect", "e", ":", Expression.normalize);
  }

  else() {
    return this.if('else');
  }

  endIf() {
    return this.if('end');
  }

  endOffset(value) {
    return this.rangeParam(value, "end_offset", "eo");
  }

  fallbackContent(value) {
    return this.param(value, "fallback_content");
  }

  fetchFormat(value) {
    return this.param(value, "fetch_format", "f");
  }

  format(value) {
    return this.param(value, "format");
  }

  flags(value) {
    return this.arrayParam(value, "flags", "fl", ".");
  }

  gravity(value) {
    return this.param(value, "gravity", "g");
  }

  fps(value) {
    return this.param(value, "fps", "fps", (fps) => {
      if (isString(fps)) {
        return fps;
      } else if (isArray(fps)) {
        return fps.join("-");
      } else {
        return fps;
      }
    });
  }

  height(value) {
    return this.param(value, "height", "h", () => {
      if (this.getValue("crop") || this.getValue("overlay") || this.getValue("underlay")) {
        return Expression.normalize(value);
      } else {
        return null;
      }
    });
  }

  htmlHeight(value) {
    return this.param(value, "html_height");
  }

  htmlWidth(value) {
    return this.param(value, "html_width");
  }

  if(value = "") {
    var i, ifVal, j, ref, trIf, trRest;
    switch (value) {
      case "else":
        this.chain();
        return this.param(value, "if", "if");
      case "end":
        this.chain();
        for (i = j = ref = this.chained.length - 1; j >= 0; i = j += -1) {
          ifVal = this.chained[i].getValue("if");
          if (ifVal === "end") {
            break;
          } else if (ifVal != null) {
            trIf = Transformation.new().if(ifVal);
            this.chained[i].remove("if");
            trRest = this.chained[i];
            this.chained[i] = Transformation.new().transformation([trIf, trRest]);
            if (ifVal !== "else") {
              break;
            }
          }
        }
        return this.param(value, "if", "if");
      case "":
        return Condition.new().setParent(this);
      default:
        return this.param(value, "if", "if", function (value) {
          return Condition.new(value).toString();
        });
    }
  }

  keyframeInterval(value) {
    return this.param(value, "keyframe_interval", "ki");
  }

  offset(value) {
    var end_o, start_o;
    [start_o, end_o] = (isFunction(value != null ? value.split : void 0)) ? value.split('..') : isArray(value) ? value : [null, null];
    if (start_o != null) {
      this.startOffset(start_o);
    }
    if (end_o != null) {
      return this.endOffset(end_o);
    }
  }

  opacity(value) {
    return this.param(value, "opacity", "o", Expression.normalize);
  }

  overlay(value) {
    return this.layerParam(value, "overlay", "l");
  }

  page(value) {
    return this.param(value, "page", "pg");
  }

  poster(value) {
    return this.param(value, "poster");
  }

  prefix(value) {
    return this.param(value, "prefix", "p");
  }

  quality(value) {
    return this.param(value, "quality", "q", Expression.normalize);
  }

  radius(value) {
    return this.arrayParam(value, "radius", "r", ":", Expression.normalize);
  }

  rawTransformation(value) {
    return this.rawParam(value, "raw_transformation");
  }

  size(value) {
    var height, width;
    if (isFunction(value != null ? value.split : void 0)) {
      [width, height] = value.split('x');
      this.width(width);
      return this.height(height);
    }
  }

  sourceTypes(value) {
    return this.param(value, "source_types");
  }

  sourceTransformation(value) {
    return this.param(value, "source_transformation");
  }

  startOffset(value) {
    return this.rangeParam(value, "start_offset", "so");
  }

  streamingProfile(value) {
    return this.param(value, "streaming_profile", "sp");
  }

  transformation(value) {
    return this.transformationParam(value, "transformation", "t");
  }

  underlay(value) {
    return this.layerParam(value, "underlay", "u");
  }

  variable(name, value) {
    return this.param(value, name, name);
  }

  variables(values) {
    return this.arrayParam(values, "variables");
  }

  videoCodec(value) {
    return this.param(value, "video_codec", "vc", Param.process_video_params);
  }

  videoSampling(value) {
    return this.param(value, "video_sampling", "vs");
  }

  width(value) {
    return this.param(value, "width", "w", () => {
      if (this.getValue("crop") || this.getValue("overlay") || this.getValue("underlay")) {
        return Expression.normalize(value);
      } else {
        return null;
      }
    });
  }

  customFunction(value) {
    return this.param(value, "custom_function", "fn", () => {
      if(value.function_type === "remote"){
        return [value.function_type, btoa(value.source)].join(":")
      }
      else if (value.function_type === "wasm")
        return [value.function_type, value.source].join(":")
    })
  }

  x(value) {
    return this.param(value, "x", "x", Expression.normalize);
  }

  y(value) {
    return this.param(value, "y", "y", Expression.normalize);
  }

  zoom(value) {
    return this.param(value, "zoom", "z", Expression.normalize);
  }

};

/**
 * Transformation Class methods.
 * This is a list of the parameters defined in Transformation.
 * Values are camelCased.
 */
Transformation.methods = [
  "angle",
  "audioCodec",
  "audioFrequency",
  "aspectRatio",
  "background",
  "bitRate",
  "border",
  "color",
  "colorSpace",
  "crop",
  "customFunction",
  "defaultImage",
  "delay",
  "density",
  "duration",
  "dpr",
  "effect",
  "else",
  "endIf",
  "endOffset",
  "fallbackContent",
  "fetchFormat",
  "format",
  "flags",
  "gravity",
  "fps",
  "height",
  "htmlHeight",
  "htmlWidth",
  "if",
  "keyframeInterval",
  "offset",
  "opacity",
  "overlay",
  "page",
  "poster",
  "prefix",
  "quality",
  "radius",
  "rawTransformation",
  "size",
  "sourceTypes",
  "sourceTransformation",
  "startOffset",
  "streamingProfile",
  "transformation",
  "underlay",
  "variable",
  "variables",
  "videoCodec",
  "videoSampling",
  "width",
  "x",
  "y",
  "zoom"
];

/**
 * Parameters that are filtered out before passing the options to an HTML tag.
 *
 * The list of parameters is a combination of `Transformation::methods` and `Configuration::CONFIG_PARAMS`
 */
Transformation.PARAM_NAMES = Transformation.methods.map(snakeCase).concat(Configuration.CONFIG_PARAMS);

export default Transformation;