/* 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 {
  SELECT_REQUEST,
  MSG_ADD,
  MSG_SELECT,
  MSG_OPEN_DETAILS,
  MSG_CLEAR,
  MSG_TOGGLE_FILTER_TYPE,
  MSG_TOGGLE_CONTROL,
  MSG_SET_FILTER_TEXT,
  MSG_TOGGLE_COLUMN,
  MSG_RESET_COLUMNS,
  MSG_CLOSE_CONNECTION,
  CHANNEL_TYPE,
  SET_EVENT_STREAM_FLAG,
} = require("resource://devtools/client/netmonitor/src/constants.js");

/**
 * The default column states for the MessageListItem component.
 */
const defaultColumnsState = {
  data: true,
  size: false,
  time: true,
};

const defaultWSColumnsState = {
  ...defaultColumnsState,
  opCode: false,
  maskBit: false,
  finBit: false,
};

const defaultSSEColumnsState = {
  ...defaultColumnsState,
  eventName: false,
  lastEventId: false,
  retry: false,
};

/**
 * Returns a new object of default cols.
 */
function getMessageDefaultColumnsState(channelType) {
  let columnsState = defaultColumnsState;
  const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;

  if (channelType === WEB_SOCKET) {
    columnsState = defaultWSColumnsState;
  } else if (channelType === EVENT_STREAM) {
    columnsState = defaultSSEColumnsState;
  }

  return Object.assign({}, columnsState);
}

/**
 * This structure stores list of all WebSocket and EventSource messages received
 * from the backend.
 */
function Messages(initialState = {}) {
  const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;

  return {
    // Map with all requests (key = resourceId, value = array of message objects)
    messages: new Map(),
    messageFilterText: "",
    // Default filter type is "all",
    messageFilterType: "all",
    showControlFrames: false,
    selectedMessage: null,
    messageDetailsOpen: false,
    currentChannelId: null,
    currentChannelType: null,
    currentRequestId: null,
    closedConnections: new Map(),
    columns: null,
    sseColumns: getMessageDefaultColumnsState(EVENT_STREAM),
    wsColumns: getMessageDefaultColumnsState(WEB_SOCKET),
    ...initialState,
  };
}

/**
 * When a network request is selected,
 * set the current resourceId affiliated with the connection.
 */
function setCurrentChannel(state, action) {
  if (!action.request) {
    return state;
  }

  const { id, cause, resourceId, isEventStream } = action.request;
  const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
  let currentChannelType = null;
  let columnsKey = "columns";
  if (cause.type === "websocket") {
    currentChannelType = WEB_SOCKET;
    columnsKey = "wsColumns";
  } else if (isEventStream) {
    currentChannelType = EVENT_STREAM;
    columnsKey = "sseColumns";
  }

  return {
    ...state,
    columns:
      currentChannelType === state.currentChannelType
        ? { ...state.columns }
        : { ...state[columnsKey] },
    currentChannelId: resourceId,
    currentChannelType,
    currentRequestId: id,
    // Default filter text is empty string for a new connection
    messageFilterText: "",
  };
}

/**
 * If the request is already selected and isEventStream flag
 * is added later, we need to update currentChannelType & columns.
 */
function updateCurrentChannel(state, action) {
  if (state.currentRequestId === action.id) {
    const currentChannelType = CHANNEL_TYPE.EVENT_STREAM;
    return {
      ...state,
      columns: { ...state.sseColumns },
      currentChannelType,
    };
  }
  return state;
}

/**
 * Appending new message into the map.
 */
function addMessage(state, action) {
  const { httpChannelId } = action;
  const nextState = { ...state };

  const newMessage = {
    httpChannelId,
    ...action.data,
  };

  nextState.messages = mapSet(
    nextState.messages,
    newMessage.httpChannelId,
    newMessage
  );

  return nextState;
}

/**
 * Select specific message.
 */
function selectMessage(state, action) {
  if (
    state.selectedMessage == action.message &&
    state.messageDetailsOpen == action.open
  ) {
    return state;
  }
  return {
    ...state,
    selectedMessage: action.message,
    messageDetailsOpen: action.open,
  };
}

/**
 * Shows/Hides the MessagePayload component.
 */
function openMessageDetails(state, action) {
  if (state.messageDetailsOpen == action.open) {
    return state;
  }
  return {
    ...state,
    messageDetailsOpen: action.open,
  };
}

/**
 * Clear messages of the request from the state.
 */
function clearMessages(state) {
  const nextState = { ...state };
  const defaultState = Messages();
  nextState.messages = new Map(state.messages);
  nextState.messages.delete(nextState.currentChannelId);

  // Reset fields to default state.
  nextState.selectedMessage = defaultState.selectedMessage;
  nextState.messageDetailsOpen = defaultState.messageDetailsOpen;

  return nextState;
}

/**
 * Toggle the message filter type of the connection.
 */
function toggleMessageFilterType(state, action) {
  if (state.messageFilterType == action.filter) {
    return state;
  }
  return {
    ...state,
    messageFilterType: action.filter,
  };
}

/**
 * Toggle control frames for the WebSocket connection.
 */
function toggleControlFrames(state) {
  return {
    ...state,
    showControlFrames: !state.showControlFrames,
  };
}

/**
 * Set the filter text of the current channelId.
 */
function setMessageFilterText(state, action) {
  if (state.messageFilterText == action.text) {
    return state;
  }
  return {
    ...state,
    messageFilterText: action.text,
  };
}

/**
 * Toggle the user specified column view state.
 */
function toggleColumn(state, action) {
  const { column } = action;
  let columnsKey = null;
  if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) {
    columnsKey = "wsColumns";
  } else {
    columnsKey = "sseColumns";
  }
  const newColumnsState = {
    ...state[columnsKey],
    [column]: !state[columnsKey][column],
  };
  return {
    ...state,
    columns: newColumnsState,
    [columnsKey]: newColumnsState,
  };
}

/**
 * Reset back to default columns view state.
 */
function resetColumns(state) {
  let columnsKey = null;
  if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) {
    columnsKey = "wsColumns";
  } else {
    columnsKey = "sseColumns";
  }
  const newColumnsState = getMessageDefaultColumnsState(
    state.currentChannelType
  );
  return {
    ...state,
    [columnsKey]: newColumnsState,
    columns: newColumnsState,
  };
}

function closeConnection(state, action) {
  const { httpChannelId, code, reason } = action;
  const nextState = { ...state };

  nextState.closedConnections.set(httpChannelId, {
    code,
    reason,
  });

  return nextState;
}

/**
 * Append new item into existing map and return new map.
 */
function mapSet(map, key, value) {
  const newMap = new Map(map);
  if (newMap.has(key)) {
    const messagesArray = [...newMap.get(key)];
    messagesArray.push(value);
    newMap.set(key, messagesArray);
    return newMap;
  }
  return newMap.set(key, [value]);
}

/**
 * This reducer is responsible for maintaining list of
 * messages within the Network panel.
 */
function messages(state = Messages(), action) {
  switch (action.type) {
    case SELECT_REQUEST:
      return setCurrentChannel(state, action);
    case SET_EVENT_STREAM_FLAG:
      return updateCurrentChannel(state, action);
    case MSG_ADD:
      return addMessage(state, action);
    case MSG_SELECT:
      return selectMessage(state, action);
    case MSG_OPEN_DETAILS:
      return openMessageDetails(state, action);
    case MSG_CLEAR:
      return clearMessages(state);
    case MSG_TOGGLE_FILTER_TYPE:
      return toggleMessageFilterType(state, action);
    case MSG_TOGGLE_CONTROL:
      return toggleControlFrames(state, action);
    case MSG_SET_FILTER_TEXT:
      return setMessageFilterText(state, action);
    case MSG_TOGGLE_COLUMN:
      return toggleColumn(state, action);
    case MSG_RESET_COLUMNS:
      return resetColumns(state);
    case MSG_CLOSE_CONNECTION:
      return closeConnection(state, action);
    default:
      return state;
  }
}

module.exports = {
  Messages,
  messages,
  getMessageDefaultColumnsState,
};
