// Serves a file with a given mime type and size at an optionally given rate.

function getQuery(request) {
  var query = {};
  request.queryString.split('&').forEach(function (val) {
    var [name, value] = val.split('=');
    query[name] = unescape(value);
  });
  return query;
}

function handleResponse() {
  // Is this a rate limited response?
  if (this.state.rate > 0) {
    // Calculate how many bytes we have left to send.
    var bytesToWrite = this.state.totalBytes - this.state.sentBytes;

    // Do we have any bytes left to send? If not we'll just fall thru and
    // cancel our repeating timer and finalize the response.
    if (bytesToWrite > 0) {
      // Figure out how many bytes to send, based on the rate limit.
      bytesToWrite =
        (bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite;

      for (let i = 0; i < bytesToWrite; i++) {
        try {
          this.response.bodyOutputStream.write("0", 1);
        } catch (e) {
          // Connection was closed by client.
          if (e == Components.results.NS_ERROR_NOT_AVAILABLE) {
            // There's no harm in calling this multiple times.
            this.response.finish();

            // It's possible that our timer wasn't cancelled in time
            // and we'll be called again.
            if (this.timer) {
              this.timer.cancel();
              this.timer = null;
            }

            return;
          }
        }
      }

      // Update the number of bytes we've sent to the client.
      this.state.sentBytes += bytesToWrite;

      // Wait until the next call to do anything else.
      return;
    }
  }
  else {
    // Not rate limited, write it all out.
    for (let i = 0; i < this.state.totalBytes; i++) {
      this.response.write("0");
    }
  }

  // Finalize the response.
  this.response.finish();

  // All done sending, go ahead and cancel our repeating timer.
  this.timer.cancel();

  // Clear the timer.
  this.timer = null;
}

function handleRequest(request, response) {
  var query = getQuery(request);

  // sending at a specific rate requires our response to be asynchronous so
  // we handle all requests asynchronously. See handleResponse().
  response.processAsync();

  // Default status when responding.
  var version = "1.1";
  var statusCode = 200;
  var description = "OK";

  // Default values for content type, size and rate.
  var contentType = "text/plain";
  var contentRange = null;
  var size = 1024;
  var rate = 0;

  // optional content type to be used by our response.
  if ("contentType" in query) {
    contentType = query["contentType"];
  }

  // optional size (in bytes) for generated file.
  if ("size" in query) {
    size = parseInt(query["size"]);
  }

  // optional range request check.
  if (request.hasHeader("range")) {
    version = "1.1";
    statusCode = 206;
    description = "Partial Content";

    // We'll only support simple range byte style requests.
    var [offset, total] = request.getHeader("range").slice("bytes=".length).split("-");
    // Enforce valid Number values.
    offset = parseInt(offset);
    offset = isNaN(offset) ? 0 : offset;
    // Same.
    total = parseInt(total);
    total = isNaN(total) ? 0 : total;

    // We'll need to original total size as part of the Content-Range header
    // value in our response.
    var originalSize = size;

    // If we have a total size requested, we must make sure to send that number
    // of bytes only (minus the start offset).
    if (total && total < size) {
      size = total - offset;
    } else if (offset) {
      // Looks like we just have a byte offset to deal with.
      size = size - offset;
    }

    // We specifically need to add a Content-Range header to all responses for
    // requests that include a range request header.
    contentRange = "bytes " + offset + "-" + (size - 1) + "/" + originalSize;
  }

  // optional rate (in bytes/s) at which to send the file.
  if ("rate" in query) {
    rate = parseInt(query["rate"]);
  }

  // The context for the responseHandler.
  var context = {
    response: response,
    state: {
      contentType: contentType,
      totalBytes: size,
      sentBytes: 0,
      rate: rate
    },
    timer: null
  };

  // The notify implementation for the timer.
  context.notify = handleResponse.bind(context);

  context.timer =
    Components.classes["@mozilla.org/timer;1"]
              .createInstance(Components.interfaces.nsITimer);

  // generate the content.
  response.setStatusLine(version, statusCode, description);
  response.setHeader("Content-Type", contentType, false);
  if (contentRange) {
    response.setHeader("Content-Range", contentRange, false);
  }
  response.setHeader("Content-Length", size.toString(), false);

  // initialize the timer and start writing out the response.
  context.timer.initWithCallback(
    context,
    1000,
    Components.interfaces.nsITimer.TYPE_REPEATING_SLACK
  );

}