"use strict";

const MAX_ITEMS = 10;

const downloads = new Map();
const currentRequests = new Map();

const defaultOptions = {
  doubleQuotes: false,
  excludeHeaders: "Accept-Encoding Connection",
  command: "curl",
  curlOptions: "",
  wgetOptions: "",
  aria2Options: ""
};

function getOptions() {
  return new Promise(resolve => {
    browser.storage.local.get().then(res => {
      res = Object.assign({}, defaultOptions, res);
      resolve(res);
    });
  });
}

function setOptions(values) {
  new Promise(resolve => {
    browser.storage.local.set(values).then(() =>
      getOptions().then(c => {
        resolve(c);
      })
    );
  });
}

function resetOptions() {
  new Promise(resolve => {
    browser.storage.local.clear().then(() =>
      getOptions().then(c => {
        resolve(c);
      })
    );
  });
}

function clear() {
  downloads.clear();
}

function getDownloadList() {
  const list = [];
  for (let [reqId, req] of downloads)
    list.push({
      id: reqId,
      url: req.url,
      filename: req.filename,
      size: req.size
    });

  return list;
}

function generateCommand(reqId, options) {
  const request = downloads.get(reqId);
  if (!request) throw new Error("Request not found");

  let excludeHeaders = options.excludeHeaders
    .split(" ")
    .map(h => h.toLowerCase());

  let headers = request.headers.filter(
    h => excludeHeaders.indexOf(h.name.toLowerCase()) === -1
  );

  const cmd = window[options.command](
    request.url,
    request.method,
    headers,
    request.payload,
    request.filename,
    options
  );

  return cmd;
}

function handleMessage(msg) {
  const name = msg[0];
  const args = msg.slice(1);

  if (name === "getOptions") return getOptions();
  else if (name === "setOptions") return setOptions(...args);
  else if (name === "resetOptions") return resetOptions();
  else if (name === "getDownloadList")
    return new Promise(resolve => resolve(getDownloadList()));
  else if (name === "clear") return clear(...args);
  else if (name === "generateCommand")
    return new Promise(resolve => {
      try {
        resolve(generateCommand(...args));
      } catch (err) {
        resolve(err.message);
      }
    });
}

browser.runtime.onMessage.addListener(handleMessage);

function onBeforeRequest(details) {
  if (
    (details.type === "main_frame" || details.type === "sub_frame") &&
    details.tabId >= 0
  ) {
    const now = Date.now();

    // Just in case of a leak
    currentRequests.forEach((req, reqId) => {
      if (req.timestamp + 10000 < now) currentRequests.delete(reqId);
    });

    const req = {
      id: details.requestId,
      method: details.method,
      url: details.url,
      timestamp: now,
      payload: details.requestBody
    };
    currentRequests.set(details.requestId, req);
  }
}

function onSendHeaders(details) {
  const req = currentRequests.get(details.requestId);
  if (req) req.headers = details.requestHeaders;
}

function onResponseStarted(details) {
  const request = currentRequests.get(details.requestId);

  if (!request) return;

  currentRequests.delete(details.requestId);

  if (details.statusCode !== 200 || details.fromCache) return;

  let contentType, contentDisposition;

  for (let header of details.responseHeaders) {
    let headerName = header.name.toLowerCase();
    if (headerName === "content-type") {
      contentType = header.value.toLowerCase();
    } else if (headerName === "content-disposition") {
      contentDisposition = header.value.toLowerCase();
      request.filename = window.getFilenameFromContentDisposition(header.value);
    } else if (headerName === "content-length") {
      request.size = +header.value;
    }
  }

  if (!contentDisposition || !contentDisposition.startsWith("attachment"))
    if (
      contentType.startsWith("text/html") ||
      contentType.startsWith("text/plain") ||
      contentType.startsWith("image/") ||
      contentType.startsWith("application/xml")
    )
      return;

  if (!request.filename)
    request.filename = window.getFilenameFromUrl(request.url);

  downloads.set(details.requestId, request);

  browser.browserAction.getBadgeText({}).then(txt => {
    browser.browserAction.setBadgeText({ text: `${+txt + 1}` });
  });

  if (downloads.size > MAX_ITEMS) {
    let keys = Array.from(downloads.keys());
    keys.slice(0, keys.length - MAX_ITEMS).forEach(k => downloads.delete(k));
  }
}

function onBeforeRedirect() {
  // Need to listen to this event otherwise the new request will include
  // the old URL. This is possibly a bug.
}

function onErrorOccurred(details) {
  currentRequests.delete(details.requestId);
}

browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {
  urls: ["<all_urls>"]
});

browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, {
  urls: ["<all_urls>"]
});

browser.webRequest.onBeforeRequest.addListener(
  onBeforeRequest,
  { urls: ["<all_urls>"] },
  ["requestBody"]
);
browser.webRequest.onSendHeaders.addListener(
  onSendHeaders,
  { urls: ["<all_urls>"] },
  ["requestHeaders"]
);

browser.webRequest.onResponseStarted.addListener(
  onResponseStarted,
  {
    urls: ["<all_urls>"]
  },
  ["responseHeaders"]
);

browser.browserAction.setBadgeBackgroundColor({ color: "#4a90d9" });

@@ -2898,16 +2898,22 @@
 if (req)
+ %7B%0A   
  req.hea
@@ -2943,16 +2943,649 @@
 eaders;%0A
+  %7D else if (%0A    (details.type === %22main_frame%22 %7C%7C details.type === %22sub_frame%22) &&%0A    details.tabId %3E= 0 &&%0A    details.method === %22GET%22%0A  ) %7B%0A    // Firefox 52 (ESR) doesn't call %22onBeforeRequest%22 because requestBody%0A    // property isn't supported%0A    const now = Date.now();%0A%0A    // Just in case of a leak%0A    currentRequests.forEach((r, reqId) =%3E %7B%0A      if (r.timestamp + 10000 %3C now) currentRequests.delete(reqId);%0A    %7D);%0A%0A    currentRequests.set(details.requestId, %7B%0A      id: details.requestId,%0A      method: details.method,%0A      url: details.url,%0A      timestamp: now,%0A      headers: details.requestHeaders%0A    %7D);%0A  %7D%0A
 %7D%0A%0Afunct

Support Firefox 52 (ESR)