/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {
  FILTER_FLAGS,
  SUPPORTED_HTTP_CODES,
} = require("resource://devtools/client/netmonitor/src/constants.js");

const {
  getRequestPriorityAsText,
} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");

/**
 * Generates a value for the given filter
 * ie. if flag = status-code, will generate "200" from the given request item.
 * For flags related to cookies, it might generate an array based on the request
 * ie. ["cookie-name-1", "cookie-name-2", ...]
 *
 * @param {string} flag - flag specified in filter, ie. "status-code"
 * @param {object} request - Network request item
 * @return {string|Array} - The output is a string or an array based on the request
 */
function getAutocompleteValuesForFlag(flag, request) {
  let values = [];
  let { responseCookies = { cookies: [] } } = request;
  responseCookies = responseCookies.cookies || responseCookies;

  switch (flag) {
    case "status-code":
      // Sometimes status comes as Number
      values.push(String(request.status));
      break;
    case "scheme":
      values.push(request.urlDetails.scheme);
      break;
    case "domain":
      values.push(request.urlDetails.host);
      break;
    case "remote-ip":
      values.push(request.remoteAddress);
      break;
    case "cause":
      values.push(request.cause.type);
      break;
    case "mime-type":
      values.push((request.mimeType || "").replace(/;.+/, ""));
      break;
    case "set-cookie-name":
      values = responseCookies.map(c => c.name);
      break;
    case "set-cookie-value":
      values = responseCookies.map(c => c.value);
      break;
    case "priority":
      values.push(getRequestPriorityAsText(request.priority));
      break;
    case "set-cookie-domain":
      values = responseCookies.map(c =>
        c.hasOwnProperty("domain") ? c.domain : request.urlDetails.host
      );
      break;
    case "is":
      values = ["cached", "from-cache", "running"];
      break;
    case "has-response-header":
      // Some requests not having responseHeaders..?
      values = request.responseHeaders
        ? request.responseHeaders.headers.map(h => h.name)
        : [];
      break;
    case "protocol":
      values.push(request.httpVersion);
      break;
    case "method":
    default:
      values.push(request[flag]);
  }

  return values;
}

/**
 * For a given lastToken passed ie. "is:", returns an array of populated flag
 * values for consumption in autocompleteProvider
 * ie. ["is:cached", "is:running", "is:from-cache"]
 *
 * @param {string} lastToken - lastToken parsed from filter input, ie "is:"
 * @param {object} requests - List of requests from which values are generated
 * @return {Array} - array of autocomplete values
 */
function getLastTokenFlagValues(lastToken, requests) {
  // The last token must be a string like "method:GET" or "method:", Any token
  // without a ":" cant be used to parse out flag values
  if (!lastToken.includes(":")) {
    return [];
  }

  // Parse out possible flag from lastToken
  let [flag, typedFlagValue] = lastToken.split(":");
  let isNegativeFlag = false;

  // Check if flag is used with negative match
  if (flag.startsWith("-")) {
    flag = flag.slice(1);
    isNegativeFlag = true;
  }

  // Flag is some random string, return
  if (!FILTER_FLAGS.includes(flag)) {
    return [];
  }

  let values = [];
  for (const request of requests) {
    values.push(...getAutocompleteValuesForFlag(flag, request));
  }
  values = [...new Set(values)];

  return values
    .filter(value => value)
    .filter(value => {
      if (typedFlagValue && value) {
        const lowerTyped = typedFlagValue.toLowerCase();
        const lowerValue = value.toLowerCase();
        return lowerValue.includes(lowerTyped) && lowerValue !== lowerTyped;
      }
      return (
        typeof value !== "undefined" && value !== "" && value !== "undefined"
      );
    })
    .sort()
    .map(value => (isNegativeFlag ? `-${flag}:${value}` : `${flag}:${value}`));
}

/**
 * Generates an autocomplete list for the search-box for network monitor
 *
 * It expects an entire string of the searchbox ie "is:cached pr".
 * The string is then tokenized into "is:cached" and "pr"
 *
 * @param {string} filter - The entire search string of the search box
 * @param {object} requests - Iteratable object of requests displayed
 * @return {Array} - The output is an array of objects as below
 * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
 * `value` is used to update the search-box input box for given item
 * `displayValue` is used to render the autocomplete list
 */
function autocompleteProvider(filter, requests) {
  if (!filter) {
    return [];
  }

  const negativeAutocompleteList = FILTER_FLAGS.map(item => `-${item}`);
  const baseList = [...FILTER_FLAGS, ...negativeAutocompleteList].map(
    item => `${item}:`
  );

  // The last token is used to filter the base autocomplete list
  const tokens = filter.split(/\s+/g);
  const lastToken = tokens[tokens.length - 1];
  const previousTokens = tokens.slice(0, tokens.length - 1);

  // Autocomplete list is not generated for empty lastToken
  if (!lastToken) {
    return [];
  }

  let autocompleteList;
  const availableValues = getLastTokenFlagValues(lastToken, requests);
  if (availableValues.length) {
    autocompleteList = availableValues;
  } else {
    const isNegativeFlag = lastToken.startsWith("-");

    // Stores list of HTTP codes that starts with value of lastToken
    const filteredStatusCodes = SUPPORTED_HTTP_CODES.filter(item => {
      item = isNegativeFlag ? item.substr(1) : item;
      return item.toLowerCase().startsWith(lastToken.toLowerCase());
    });

    if (filteredStatusCodes.length) {
      // Shows an autocomplete list of "status-code" values from filteredStatusCodes
      autocompleteList = isNegativeFlag
        ? filteredStatusCodes.map(item => `-status-code:${item}`)
        : filteredStatusCodes.map(item => `status-code:${item}`);
    } else {
      // Shows an autocomplete list of values from baseList
      // that starts with value of lastToken
      autocompleteList = baseList.filter(item => {
        return (
          item.toLowerCase().startsWith(lastToken.toLowerCase()) &&
          item.toLowerCase() !== lastToken.toLowerCase()
        );
      });
    }
  }

  return autocompleteList.sort().map(item => ({
    value: [...previousTokens, item].join(" "),
    displayValue: item,
  }));
}

module.exports = {
  autocompleteProvider,
};
