/**
 * @requires javelin-install
 *           javelin-util
 *           javelin-request
 *           javelin-dom
 *           javelin-uri
 *           phabricator-file-upload
 * @provides phabricator-drag-and-drop-file-upload
 * @javelin
 */

JX.install('PhabricatorDragAndDropFileUpload', {

  construct : function(target) {
    if (JX.DOM.isNode(target)) {
      this._node = target;
    } else {
      this._sigil = target;
    }
  },

  events : [
    'didBeginDrag',
    'didEndDrag',
    'willUpload',
    'progress',
    'didUpload',
    'didError'],

  statics : {
    isSupported : function() {
      // TODO: Is there a better capability test for this? This seems okay in
      // Safari, Firefox and Chrome.

      return !!window.FileList;
    },
    isPasteSupported : function() {
      // TODO: Needs to check if event.clipboardData is available.
      // Works in Chrome, doesn't work in Firefox 10.
      return !!window.FileList;
    }
  },

  members : {
    _node : null,
    _sigil: null,
    _depth : 0,
    _isEnabled: false,

    setIsEnabled: function(bool) {
      this._isEnabled = bool;
      return this;
    },

    getIsEnabled: function() {
      return this._isEnabled;
    },

    _updateDepth : function(delta) {
      if (this._depth === 0 && delta > 0) {
        this.invoke('didBeginDrag', this._getTarget());
      }

      this._depth += delta;

      if (this._depth === 0 && delta < 0) {
        this.invoke('didEndDrag', this._getTarget());
      }
    },

    _getTarget: function() {
      return this._target || this._node;
    },

    start : function() {

      // TODO: move this to JX.DOM.contains()?
      function contains(container, child) {
        do {
          if (child === container) {
            return true;
          }
          child = child.parentNode;
        } while (child);

        return false;
      }

      // Firefox has some issues sometimes; implement this click handler so
      // the user can recover. See T5188.
      var on_click = JX.bind(this, function (e) {
        if (!this.getIsEnabled()) {
          return;
        }

        if (this._depth) {
          e.kill();
          // Force depth to 0.
          this._updateDepth(-this._depth);
        }
      });

      // We track depth so that the _node may have children inside of it and
      // not become unselected when they are dragged over.
      var on_dragenter = JX.bind(this, function(e) {
        if (!this.getIsEnabled()) {
          return;
        }

        if (!this._node) {
          var target = e.getNode(this._sigil);
          if (target !== this._target) {
            this._updateDepth(-this._depth);
            this._target = target;
          }
        }

        if (contains(this._getTarget(), e.getTarget())) {
          this._updateDepth(1);
        }

      });

      var on_dragleave = JX.bind(this, function(e) {
        if (!this.getIsEnabled()) {
          return;
        }

        if (!this._getTarget()) {
          return;
        }

        if (contains(this._getTarget(), e.getTarget())) {
          this._updateDepth(-1);
        }
      });

      var on_dragover = JX.bind(this, function(e) {
        if (!this.getIsEnabled()) {
          return;
        }

        // NOTE: We must set this, or Chrome refuses to drop files from the
        // download shelf.
        e.getRawEvent().dataTransfer.dropEffect = 'copy';
        e.kill();
      });

      var on_drop = JX.bind(this, function(e) {
        if (!this.getIsEnabled()) {
          return;
        }

        e.kill();

        var files = e.getRawEvent().dataTransfer.files;
        for (var ii = 0; ii < files.length; ii++) {
          this.sendRequest(files[ii]);
        }

        // Force depth to 0.
        this._updateDepth(-this._depth);
      });

      if (this._node) {
        JX.DOM.listen(this._node, 'click', null, on_click);
        JX.DOM.listen(this._node, 'dragenter', null, on_dragenter);
        JX.DOM.listen(this._node, 'dragleave', null, on_dragleave);
        JX.DOM.listen(this._node, 'dragover', null, on_dragover);
        JX.DOM.listen(this._node, 'drop', null, on_drop);
      } else {
        JX.Stratcom.listen('click', this._sigil, on_click);
        JX.Stratcom.listen('dragenter', this._sigil, on_dragenter);
        JX.Stratcom.listen('dragleave', this._sigil, on_dragleave);
        JX.Stratcom.listen('dragover', this._sigil, on_dragover);
        JX.Stratcom.listen('drop', this._sigil, on_drop);
      }

      if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported() &&
          this._node) {
        JX.DOM.listen(
          this._node,
          'paste',
          null,
          JX.bind(this, function(e) {
            if (!this.getIsEnabled()) {
              return;
            }

            var clipboard = e.getRawEvent().clipboardData;
            if (!clipboard) {
              return;
            }

            // If there's any text on the clipboard, just let the event fire
            // normally, choosing the text over any images. See T5437 / D9647.
            var text = clipboard.getData('text/plain').toString();
            if (text.length) {
              return;
            }

            // Safari and Firefox have clipboardData, but no items. They
            // don't seem to provide a way to get image data directly yet.
            if (!clipboard.items) {
              return;
            }

            for (var ii = 0; ii < clipboard.items.length; ii++) {
              var item = clipboard.items[ii];
              if (!/^image\//.test(item.type)) {
                continue;
              }
              var spec = item.getAsFile();
              // pasted files don't have a name; see
              // https://code.google.com/p/chromium/issues/detail?id=361145
              if (!spec.name) {
                spec.name = 'pasted_file';
              }
              this.sendRequest(spec);
            }
          }));
      }

      this.setIsEnabled(true);
    },

    sendRequest : function(spec) {
      var file = new JX.PhabricatorFileUpload()
        .setRawFileObject(spec)
        .setName(spec.name)
        .setTotalBytes(spec.size);

      var threshold = this.getChunkThreshold();
      if (threshold && (file.getTotalBytes() > threshold)) {
        // This is a large file, so we'll go through allocation so we can
        // pick up support for resume and chunking.
        this._allocateFile(file);
      } else {
        // If this file is smaller than the chunk threshold, skip the round
        // trip for allocation and just upload it directly.
        this._sendDataRequest(file);
      }
    },

    _allocateFile: function(file) {
      file
        .setStatus('allocate')
        .update();

      this.invoke('willUpload', file);

      var alloc_uri = this._getUploadURI(file)
        .setQueryParam('allocate', 1);

      new JX.Workflow(alloc_uri)
        .setHandler(JX.bind(this, this._didAllocateFile, file))
        .start();
    },

    _getUploadURI: function(file) {
      var uri = JX.$U(this.getURI())
        .setQueryParam('name', file.getName())
        .setQueryParam('length', file.getTotalBytes());

      if (this.getViewPolicy()) {
        uri.setQueryParam('viewPolicy', this.getViewPolicy());
      }

      if (file.getAllocatedPHID()) {
        uri.setQueryParam('phid', file.getAllocatedPHID());
      }

      return uri;
    },

    _didAllocateFile: function(file, r) {
      var phid = r.phid;
      var upload = r.upload;

      if (!upload) {
        if (phid) {
          this._completeUpload(file, r);
        } else {
          this._failUpload(file, r);
        }
        return;
      } else {
        if (phid) {
          // Start or resume a chunked upload.
          file.setAllocatedPHID(phid);
          this._loadChunks(file);
        } else {
          // Proceed with non-chunked upload.
          this._sendDataRequest(file);
        }
      }
    },

    _loadChunks: function(file) {
      file
        .setStatus('chunks')
        .update();

      var chunks_uri = this._getUploadURI(file)
        .setQueryParam('querychunks', 1);

      new JX.Workflow(chunks_uri)
        .setHandler(JX.bind(this, this._didLoadChunks, file))
        .start();
    },

    _didLoadChunks: function(file, r) {
      file.setChunks(r);
      this._uploadNextChunk(file);
    },

    _uploadNextChunk: function(file) {
      var chunks = file.getChunks();
      var chunk;
      for (var ii = 0; ii < chunks.length; ii++) {
        chunk = chunks[ii];
        if (!chunk.complete) {
          this._uploadChunk(file, chunk);
          break;
        }
      }
    },

    _uploadChunk: function(file, chunk, callback) {
      file
        .setStatus('upload')
        .update();

      var chunkup_uri = this._getUploadURI(file)
        .setQueryParam('uploadchunk', 1)
        .setQueryParam('__upload__', 1)
        .setQueryParam('byteStart', chunk.byteStart)
        .toString();

      var callback = JX.bind(this, this._didUploadChunk, file, chunk);

      var req = new JX.Request(chunkup_uri, callback);

      var seen_bytes = 0;
      var onprogress = JX.bind(this, function(progress) {
        file
          .addUploadedBytes(progress.loaded - seen_bytes)
          .update();

        seen_bytes = progress.loaded;
        this.invoke('progress', file);
      });

      req.listen('error', JX.bind(this, this._onUploadError, req, file));
      req.listen('uploadprogress', onprogress);

      var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd);

      req
        .setRawData(blob)
        .send();
    },

    _didUploadChunk: function(file, chunk, r) {
      file.didCompleteChunk(chunk);

      if (r.complete) {
        this._completeUpload(file, r);
      } else {
        this._uploadNextChunk(file);
      }
    },

    _sendDataRequest: function(file) {
      file
        .setStatus('uploading')
        .update();

      this.invoke('willUpload', file);

      var up_uri = this._getUploadURI(file)
        .setQueryParam('__upload__', 1)
        .toString();

      var onupload = JX.bind(this, function(r) {
        if (r.error) {
          this._failUpload(file, r);
        } else {
          this._completeUpload(file, r);
        }
      });

      var req = new JX.Request(up_uri, onupload);

      var onprogress = JX.bind(this, function(progress) {
        file
          .setTotalBytes(progress.total)
          .setUploadedBytes(progress.loaded)
          .update();

        this.invoke('progress', file);
      });

      req.listen('error', JX.bind(this, this._onUploadError, req, file));
      req.listen('uploadprogress', onprogress);

      req
        .setRawData(file.getRawFileObject())
        .send();
    },

    _completeUpload: function(file, r) {
      file
        .setID(r.id)
        .setPHID(r.phid)
        .setURI(r.uri)
        .setMarkup(r.html)
        .setStatus('done')
        .setTargetNode(this._getTarget())
        .update();

      this.invoke('didUpload', file);
    },

    _failUpload: function(file, r) {
      file
        .setStatus('error')
        .setError(r.error)
        .update();

      this.invoke('didError', file);
    },

    _onUploadError: function(req, file, error) {
      file.setStatus('error');

      if (error) {
        file.setError(error.code + ': ' + error.info);
      } else {
        var xhr = req.getTransport();
        if (xhr.responseText) {
          file.setError('Server responded: ' + xhr.responseText);
        }
      }

      file.update();
      this.invoke('didError', file);
    }

  },
  properties: {
    URI: null,
    activatedClass: null,
    viewPolicy: null,
    chunkThreshold: null
  }
});
