/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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 { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
const { getLogLimit } = require("devtools/client/webconsole/new-console-output/selectors/prefs");
const {
  MESSAGE_TYPE,
  MESSAGE_SOURCE
} = require("devtools/client/webconsole/new-console-output/constants");

function getAllMessages(state) {
  let messages = getAllMessagesById(state);
  let logLimit = getLogLimit(state);
  let filters = getAllFilters(state);

  let groups = getAllGroupsById(state);
  let messagesUI = getAllMessagesUiById(state);

  return prune(
    messages.filter(message => {
      return (
        isInOpenedGroup(message, groups, messagesUI)
        && (
          isUnfilterable(message)
          || (
            matchLevelFilters(message, filters)
            && matchNetworkFilters(message, filters)
            && matchSearchFilters(message, filters)
          )
        )
      );
    }),
    logLimit
  );
}

function getAllMessagesById(state) {
  return state.messages.messagesById;
}

function getAllMessagesUiById(state) {
  return state.messages.messagesUiById;
}

function getAllMessagesTableDataById(state) {
  return state.messages.messagesTableDataById;
}

function getAllGroupsById(state) {
  return state.messages.groupsById;
}

function getCurrentGroup(state) {
  return state.messages.currentGroup;
}

function isUnfilterable(message) {
  return [
    MESSAGE_TYPE.COMMAND,
    MESSAGE_TYPE.RESULT,
    MESSAGE_TYPE.START_GROUP,
    MESSAGE_TYPE.START_GROUP_COLLAPSED,
  ].includes(message.type);
}

function isInOpenedGroup(message, groups, messagesUI) {
  return !message.groupId
    || (
      !isGroupClosed(message.groupId, messagesUI)
      && !hasClosedParentGroup(groups.get(message.groupId), messagesUI)
    );
}

function hasClosedParentGroup(group, messagesUI) {
  return group.some(groupId => isGroupClosed(groupId, messagesUI));
}

function isGroupClosed(groupId, messagesUI) {
  return messagesUI.includes(groupId) === false;
}

function matchLevelFilters(message, filters) {
  return filters.get(message.level) === true;
}

function matchNetworkFilters(message, filters) {
  return (
    message.source !== MESSAGE_SOURCE.NETWORK
    || (filters.get("net") === true && message.isXHR === false)
    || (filters.get("netxhr") === true && message.isXHR === true)
  );
}

function matchSearchFilters(message, filters) {
  let text = filters.text || "";
  return (
    text === ""
    // @TODO currently we return true for any object grip. We should find a way to
    // search object grips.
    || (message.parameters !== null && !Array.isArray(message.parameters))
    // Look for a match in location.
    || isTextInFrame(text, message.frame)
    // Look for a match in stacktrace.
    || (
      Array.isArray(message.stacktrace) &&
      message.stacktrace.some(frame => isTextInFrame(text,
        // isTextInFrame expect the properties of the frame object to be in the same
        // order they are rendered in the Frame component.
        {
          functionName: frame.functionName ||
            l10n.getStr("stacktrace.anonymousFunction"),
          filename: frame.filename,
          lineNumber: frame.lineNumber,
          columnNumber: frame.columnNumber
        }))
    )
    // Look for a match in messageText.
    || (message.messageText !== null
          && message.messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase()))
    // Look for a match in parameters. Currently only checks value grips.
    || (message.parameters !== null
        && message.parameters.join("").toLocaleLowerCase()
            .includes(text.toLocaleLowerCase()))
    // Look for a match in notes.
    || (Array.isArray(message.notes) && message.notes.some(note =>
          // Look for a match in location.
          isTextInFrame(text, note.frame)
          // Look for a match in messageBody.
          || (note.messageBody !== null
                && note.messageBody.toLocaleLowerCase()
                     .includes(text.toLocaleLowerCase()))
        ))
  );
}

function isTextInFrame(text, frame) {
  if (!frame) {
    return false;
  }
  // @TODO Change this to Object.values once it's supported in Node's version of V8
  return Object.keys(frame)
    .map(key => frame[key])
    .join(":")
    .toLocaleLowerCase()
    .includes(text.toLocaleLowerCase());
}

function prune(messages, logLimit) {
  let messageCount = messages.count();
  if (messageCount > logLimit) {
    // If the second non-pruned message is in a group,
    // we want to return the group as the first non-pruned message.
    let firstIndex = messages.size - logLimit;
    let groupId = messages.get(firstIndex + 1).groupId;

    if (groupId) {
      return messages.splice(0, firstIndex + 1)
        .unshift(
          messages.findLast((message) => message.id === groupId)
        );
    }
    return messages.splice(0, firstIndex);
  }

  return messages;
}

exports.getAllMessages = getAllMessages;
exports.getAllMessagesUiById = getAllMessagesUiById;
exports.getAllMessagesTableDataById = getAllMessagesTableDataById;
exports.getAllGroupsById = getAllGroupsById;
exports.getCurrentGroup = getCurrentGroup;
