/* 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 {
  APPEND_TO_HISTORY,
  CLEAR_HISTORY,
  EVALUATE_EXPRESSION,
  HISTORY_LOADED,
  UPDATE_HISTORY_POSITION,
  HISTORY_BACK,
  HISTORY_FORWARD,
  REVERSE_SEARCH_INPUT_TOGGLE,
  REVERSE_SEARCH_INPUT_CHANGE,
  REVERSE_SEARCH_BACK,
  REVERSE_SEARCH_NEXT,
  SET_TERMINAL_INPUT,
  SET_TERMINAL_EAGER_RESULT,
} = require("resource://devtools/client/webconsole/constants.js");

/**
 * Create default initial state for this reducer.
 */
function getInitialState() {
  return {
    // Array with history entries
    entries: [],

    // Holds position (index) in history entries that the user is
    // currently viewing. This is reset to this.entries.length when
    // APPEND_TO_HISTORY action is fired.
    position: undefined,

    // Backups the original user value (if any) that can be set in
    // the input field. It might be used again if the user doesn't
    // pick up anything from the history and wants to return all
    // the way back to see the original input text.
    originalUserValue: null,

    reverseSearchEnabled: false,
    currentReverseSearchResults: null,
    currentReverseSearchResultsPosition: null,

    terminalInput: null,
    terminalEagerResult: null,
  };
}

function history(state = getInitialState(), action, prefsState) {
  switch (action.type) {
    case APPEND_TO_HISTORY:
    case EVALUATE_EXPRESSION:
      return appendToHistory(state, prefsState, action.expression);
    case CLEAR_HISTORY:
      return clearHistory(state);
    case HISTORY_LOADED:
      return historyLoaded(state, action.entries);
    case UPDATE_HISTORY_POSITION:
      return updateHistoryPosition(state, action.direction, action.expression);
    case REVERSE_SEARCH_INPUT_TOGGLE:
      return reverseSearchInputToggle(state, action);
    case REVERSE_SEARCH_INPUT_CHANGE:
      return reverseSearchInputChange(state, action.value);
    case REVERSE_SEARCH_BACK:
      return reverseSearchBack(state);
    case REVERSE_SEARCH_NEXT:
      return reverseSearchNext(state);
    case SET_TERMINAL_INPUT:
      return setTerminalInput(state, action.expression);
    case SET_TERMINAL_EAGER_RESULT:
      return setTerminalEagerResult(state, action.result);
  }
  return state;
}

function appendToHistory(state, prefsState, expression) {
  // Clone state
  state = { ...state };
  state.entries = [...state.entries];

  // Append new expression only if it isn't the same as
  // the one recently added.
  if (expression.trim() != state.entries[state.entries.length - 1]) {
    state.entries.push(expression);
  }

  // Remove entries if the limit is reached
  if (state.entries.length > prefsState.historyCount) {
    state.entries.splice(0, state.entries.length - prefsState.historyCount);
  }

  state.position = state.entries.length;
  state.originalUserValue = null;

  return state;
}

function clearHistory() {
  return getInitialState();
}

/**
 * Handling HISTORY_LOADED action that is fired when history
 * entries created in previous Firefox session are loaded
 * from async-storage.
 *
 * Loaded entries are appended before the ones that were
 * added to the state in this session.
 */
function historyLoaded(state, entries) {
  const newEntries = [...entries, ...state.entries];
  return {
    ...state,
    entries: newEntries,
    // Default position is at the end of the list
    // (at the latest inserted item).
    position: newEntries.length,
    originalUserValue: null,
  };
}

function updateHistoryPosition(state, direction, expression) {
  // Handle UP arrow key => HISTORY_BACK
  // Handle DOWN arrow key => HISTORY_FORWARD
  if (direction == HISTORY_BACK) {
    if (state.position <= 0) {
      return state;
    }

    // Clone state
    state = { ...state };

    // Store the current input value when the user starts
    // browsing through the history.
    if (state.position == state.entries.length) {
      state.originalUserValue = expression || "";
    }

    state.position--;
  } else if (direction == HISTORY_FORWARD) {
    if (state.position >= state.entries.length) {
      return state;
    }

    state = {
      ...state,
      position: state.position + 1,
    };
  }

  return state;
}

function reverseSearchInputToggle(state, action) {
  const { initialValue = "" } = action;

  // We're going to close the reverse search, let's clean the state
  if (state.reverseSearchEnabled) {
    return {
      ...state,
      reverseSearchEnabled: false,
      position: undefined,
      currentReverseSearchResults: null,
      currentReverseSearchResultsPosition: null,
    };
  }

  // If we're enabling the reverse search, we treat it as a reverse search input change,
  // since we can have an initial value.
  return reverseSearchInputChange(state, initialValue);
}

function reverseSearchInputChange(state, searchString) {
  if (searchString === "") {
    return {
      ...state,
      position: undefined,
      currentReverseSearchResults: null,
      currentReverseSearchResultsPosition: null,
    };
  }

  searchString = searchString.toLocaleLowerCase();
  const matchingEntries = state.entries.filter(entry =>
    entry.toLocaleLowerCase().includes(searchString)
  );
  // We only return unique entries, but we want to keep the latest entry in the array if
  // it's duplicated (e.g. if we have [1,2,1], we want to get [2,1], not [1,2]).
  // To do that, we need to reverse the matching entries array, provide it to a Set,
  // transform it back to an array and reverse it again.
  const uniqueEntries = new Set(matchingEntries.reverse());
  const currentReverseSearchResults = Array.from(
    new Set(uniqueEntries)
  ).reverse();

  return {
    ...state,
    position: undefined,
    currentReverseSearchResults,
    currentReverseSearchResultsPosition: currentReverseSearchResults.length - 1,
  };
}

function reverseSearchBack(state) {
  let nextPosition = state.currentReverseSearchResultsPosition - 1;
  if (nextPosition < 0) {
    nextPosition = state.currentReverseSearchResults.length - 1;
  }

  return {
    ...state,
    currentReverseSearchResultsPosition: nextPosition,
  };
}

function reverseSearchNext(state) {
  let previousPosition = state.currentReverseSearchResultsPosition + 1;
  if (previousPosition >= state.currentReverseSearchResults.length) {
    previousPosition = 0;
  }

  return {
    ...state,
    currentReverseSearchResultsPosition: previousPosition,
  };
}

function setTerminalInput(state, expression) {
  return {
    ...state,
    terminalInput: expression,
    terminalEagerResult: !expression ? null : state.terminalEagerResult,
  };
}

function setTerminalEagerResult(state, result) {
  return {
    ...state,
    terminalEagerResult: result,
  };
}

exports.history = history;
