"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.searchKey = exports.isPending = exports.isDirty = exports.getState = exports.getSelectedItems = exports.getResult = exports.getMostRecentDescriptor = exports.getIndexesByCsid = exports.getError = exports.default = void 0;
var _immutable = _interopRequireDefault(require("immutable"));
var _objectHelpers = require("../helpers/objectHelpers");
var _actionCodes = require("../constants/actionCodes");
var _searchNames = require("../constants/searchNames");
var _recordDataHelpers = require("../helpers/recordDataHelpers");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
 * Generates a search key for a given search descriptor. All search descriptors that represent the
 * same search should have the same search key.
 */
const searchKey = searchDescriptor => {
  // First convert the search descriptor to an array of name/value pairs, sorted by name. This
  // ensures that the key is not sensitive to the order in which properties are iterated out of the
  // search descriptor object.

  const pairs = (0, _objectHelpers.asPairs)(_immutable.default.Map.isMap(searchDescriptor) ? searchDescriptor.toJS() : searchDescriptor);

  // Serialize the name/value pairs to JSON.

  return JSON.stringify(pairs);
};
exports.searchKey = searchKey;
const handleSetMostRecentSearch = (state, action) => {
  const {
    searchName,
    searchDescriptor
  } = action.meta;
  const key = searchKey(searchDescriptor);
  const namedSearch = state.get(searchName);
  if (namedSearch && namedSearch.getIn(['byKey', key])) {
    return state.set(searchName, namedSearch.set('mostRecentKey', key));
  }
  return state;
};
const handleSearchStarted = (state, action) => {
  const {
    listTypeConfig,
    searchName,
    searchDescriptor
  } = action.meta;
  const {
    listNodeName,
    itemNodeName
  } = listTypeConfig;
  const namedSearch = state.get(searchName) || _immutable.default.Map();
  const key = searchKey(searchDescriptor);
  const mostRecentKey = namedSearch.get('mostRecentKey');
  let updatedNamedSearch = namedSearch;
  let result = null;
  let itemsInPage = null;
  let totalItems = null;
  let items = null;
  if (mostRecentKey) {
    const mostRecentSearchState = namedSearch.getIn(['byKey', mostRecentKey]);
    const mostRecentSearchDescriptor = mostRecentSearchState.get('descriptor');
    const changes = (0, _objectHelpers.diff)(searchDescriptor.toJS(), mostRecentSearchDescriptor.toJS());
    const changeCount = Object.keys(changes).length;
    const pageChanged = ('searchQuery.p' in changes);
    const sortChanged = ('searchQuery.sort' in changes);
    const seqIDChanged = ('seqID' in changes);
    if (pageChanged && changeCount === 1) {
      // Only the page number changed from the last search. Seed these results with the total items
      // count from the previous results, since it probably will remain the same. This allows the
      // UI to render a smooth transition from this page to the next, without the count information
      // and pager blinking out.

      totalItems = mostRecentSearchState.getIn(['result', listNodeName, 'totalItems']);
    } else {
      // Something other than the page number changed from the last search. Clear out stored
      // search states. (If only the page number changed, keep the previous states around as a
      // cache so that flipping between pages will be fast.)

      updatedNamedSearch = updatedNamedSearch.set('byKey', _immutable.default.Map());
      if (sortChanged && changeCount === 1) {
        // Only the sort changed from the last search. As above, seed these results with the total
        // items count. Also seed the itemsInPage count, since this is not expected to change.

        totalItems = mostRecentSearchState.getIn(['result', listNodeName, 'totalItems']);
        itemsInPage = mostRecentSearchState.getIn(['result', listNodeName, 'itemsInPage']);
      } else if (seqIDChanged && changeCount === 1) {
        // Only the seq id changed from the last search. This means that the search parameters
        // themselves haven't changed, but the search is being triggered because the data may have
        // changed. Seed the result with the previous total items, items in page, and items. This
        // makes for the smoothest possible transition to the new result set.

        totalItems = mostRecentSearchState.getIn(['result', listNodeName, 'totalItems']);
        itemsInPage = mostRecentSearchState.getIn(['result', listNodeName, 'itemsInPage']);
        items = mostRecentSearchState.getIn(['result', listNodeName, itemNodeName]);
      }
      delete changes['searchQuery.p'];
      delete changes['searchQuery.size'];
      delete changes['searchQuery.sort'];
      delete changes.seqID;
      if (Object.keys(changes).length > 0) {
        // Something other than page num, page size, sort, or seq id changed. Clear selected items.
        updatedNamedSearch = updatedNamedSearch.delete('selected');
      }
    }
  }

  // Seed the search state with a blank result that has the page num and size of this search, and
  // the total items and items in page counts from the last search, if they're not expected to
  // change. This allows a blank result table to be rendered while the search is pending,
  // preventing a flash of empty content.

  const searchQuery = searchDescriptor.get('searchQuery');
  const p = searchQuery.get('p');
  const size = searchQuery.get('size');
  const pageNum = typeof p === 'number' ? p.toString() : p;
  const pageSize = typeof size === 'number' ? size.toString() : size;
  result = _immutable.default.fromJS({
    [listNodeName]: {
      itemsInPage,
      totalItems,
      pageNum,
      pageSize,
      [itemNodeName]: items
    }
  });
  updatedNamedSearch = updatedNamedSearch.set('mostRecentKey', key).setIn(['byKey', key], _immutable.default.fromJS({
    result,
    descriptor: searchDescriptor,
    isPending: true
  }));
  return state.set(searchName, updatedNamedSearch);
};
const computeIndexesByCsid = (listTypeConfig, result) => {
  const {
    listNodeName,
    itemNodeName
  } = listTypeConfig;
  let indexesByCsid;
  let items = result.getIn([listNodeName, itemNodeName]);
  if (items) {
    if (!_immutable.default.List.isList(items)) {
      items = _immutable.default.List.of(items);
    }
    indexesByCsid = _immutable.default.Map(items.map((item, index) => [item.get('csid') || item.get('docId'), index]));
  }
  return indexesByCsid;
};
const setSearchResult = (state, listTypeConfig, searchName, searchDescriptor, result) => {
  const namedSearch = state.get(searchName);
  const key = searchKey(searchDescriptor);
  if (namedSearch && namedSearch.hasIn(['byKey', key])) {
    const {
      itemNodeName,
      listNodeName,
      normalizeListData
    } = listTypeConfig;
    const normalizedResult = normalizeListData ? normalizeListData(result, listTypeConfig) : result;
    const searchState = namedSearch.getIn(['byKey', key]);
    const updatedSearchState = searchState.set('isPending', false).set('result', normalizedResult).set('indexesByCsid', computeIndexesByCsid(listTypeConfig, normalizedResult)).set('listNodeName', listNodeName).set('itemNodeName', itemNodeName).delete('error').delete('isDirty');
    const updatedNamedSearch = namedSearch.setIn(['byKey', key], updatedSearchState);
    return state.set(searchName, updatedNamedSearch);
  }
  return state;
};
const handleCreateEmptySearchResult = (state, action) => {
  const {
    listTypeConfig,
    searchName,
    searchDescriptor
  } = action.meta;
  const searchQuery = searchDescriptor.get('searchQuery');
  const p = searchQuery.get('p');
  const size = searchQuery.get('size');
  const pageNum = typeof p === 'number' ? p.toString() : p;
  const pageSize = typeof size === 'number' ? size.toString() : size;
  const result = _immutable.default.fromJS({
    [listTypeConfig.listNodeName]: {
      itemsInPage: '0',
      totalItems: '0',
      pageNum,
      pageSize
    }
  });
  return setSearchResult(state, listTypeConfig, searchName, searchDescriptor, result);
};
const handleSearchFulfilled = (state, action) => {
  const {
    listTypeConfig,
    searchName,
    searchDescriptor
  } = action.meta;
  return setSearchResult(state, listTypeConfig, searchName, searchDescriptor, _immutable.default.fromJS(action.payload.data));
};
const handleSearchRejected = (state, action) => {
  const {
    searchName,
    searchDescriptor
  } = action.meta;
  const namedSearch = state.get(searchName);
  const key = searchKey(searchDescriptor);
  if (namedSearch && namedSearch.hasIn(['byKey', key])) {
    const updatedNamedSearch = namedSearch.setIn(['byKey', key], namedSearch.getIn(['byKey', key]).set('isPending', false).set('error', _immutable.default.fromJS(action.payload)).delete('result').delete('isDirty'));
    return state.set(searchName, updatedNamedSearch);
  }
  return state;
};
const handleSetAllResultItemsSelected = (state, action) => {
  const isSelected = action.payload;
  const {
    filter,
    listTypeConfig,
    searchName,
    searchDescriptor
  } = action.meta;
  const {
    listNodeName,
    itemNodeName
  } = listTypeConfig;
  const namedSearch = state.get(searchName);
  if (namedSearch) {
    const key = searchKey(searchDescriptor);
    const path = ['byKey', key, 'result', listNodeName, itemNodeName];
    let items = namedSearch.getIn(path);
    if (!_immutable.default.List.isList(items)) {
      items = _immutable.default.List.of(items);
    }
    if (filter) {
      items = items.filter(filter);
    }
    const selectedItems = namedSearch.get('selected') || _immutable.default.Map();
    let updatedSelectedItems;
    if (isSelected) {
      updatedSelectedItems = selectedItems.withMutations(map => items.reduce((updatedMap, item) => updatedMap.set(item.get('csid'), item), map));
    } else {
      updatedSelectedItems = selectedItems.withMutations(map => items.reduce((updatedMap, item) => updatedMap.delete(item.get('csid')), map));
    }
    return state.set(searchName, namedSearch.set('selected', updatedSelectedItems));
  }
  return state;
};
const handleSetResultItemSelected = (state, action) => {
  const isSelected = action.payload;
  const {
    listTypeConfig,
    searchName,
    searchDescriptor,
    index
  } = action.meta;
  const {
    listNodeName,
    itemNodeName
  } = listTypeConfig;
  const namedSearch = state.get(searchName);
  if (namedSearch) {
    const key = searchKey(searchDescriptor);
    const path = ['byKey', key, 'result', listNodeName, itemNodeName, index];
    const item = (0, _recordDataHelpers.deepGet)(namedSearch, path);
    const csid = item.get('csid');
    const updatedNamedSearch = isSelected ? namedSearch.setIn(['selected', csid], item) : namedSearch.deleteIn(['selected', csid]);
    return state.set(searchName, updatedNamedSearch);
  }
  return state;
};
const clearFilteredResults = (state, filter) => {
  let nextState = state;
  state.filter(filter).forEach((searchState, searchName) => {
    nextState = nextState.delete(searchName);
  });
  return nextState;
};
const clearNamedResults = (state, action) => state.delete(action.meta.searchName);
const clearAllResults = state => state.clear();
const handleRecordSaveFulfilled = state => {
  // We don't really know which search results will be affected by a record being created, so clear
  // them all, with the following exceptions:
  //  - The search result page. We want to keep those results so that if the current record has a
  //    search context, it won't be lost. Instead, the search result page results are only marked
  //    as dirty, so that the next time the page is viewed, the results can be cleared and
  //    reloaded at that point.
  //  - Report and batch panels. These probably won't be affected.

  let nextState = state;
  nextState = clearFilteredResults(nextState, (searchState, searchName) => searchName !== _searchNames.SEARCH_RESULT_PAGE_SEARCH_NAME && searchName !== _searchNames.RECORD_BATCH_PANEL_SEARCH_NAME && searchName !== _searchNames.RECORD_REPORT_PANEL_SEARCH_NAME);
  const searchResultPageState = nextState.get(_searchNames.SEARCH_RESULT_PAGE_SEARCH_NAME);
  if (searchResultPageState) {
    nextState = nextState.set(_searchNames.SEARCH_RESULT_PAGE_SEARCH_NAME, searchResultPageState.set('isDirty', true));
  }
  return nextState;
};
const handleRecordDeleteFulfilled = state => clearAllResults(state);
const handleRecordTransitionFulfilled = (state, action) => {
  const {
    transitionName
  } = action.meta;
  if (transitionName === 'delete') {
    return clearAllResults(state);
  }
  return state;
};
const handleLoginFulfilled = (state, action) => {
  const {
    prevUsername,
    username
  } = action.meta;
  if (prevUsername !== username) {
    // The logged in user has changed. Remove all search state, because the new user may not be
    // permitted to list some records that the previous user could.

    return clearAllResults(state);
  }
  return state;
};
var _default = function _default() {
  let state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _immutable.default.Map();
  let action = arguments.length > 1 ? arguments[1] : undefined;
  switch (action.type) {
    case _actionCodes.BATCH_INVOKE_FULFILLED:
      if (action.meta.numAffected) {
        // We don't really know which search results were affected by the batch job invocation, so
        // clear them all.

        return clearAllResults(state);
      }
      return state;
    case _actionCodes.CLEAR_SELECTED:
      return state.deleteIn([action.meta.searchName, 'selected']);
    case _actionCodes.CLEAR_SEARCH_RESULTS:
      return clearNamedResults(state, action);
    case _actionCodes.SET_MOST_RECENT_SEARCH:
      return handleSetMostRecentSearch(state, action);
    case _actionCodes.CREATE_EMPTY_SEARCH_RESULT:
      return handleCreateEmptySearchResult(state, action);
    case _actionCodes.SEARCH_STARTED:
      return handleSearchStarted(state, action);
    case _actionCodes.SEARCH_FULFILLED:
      return handleSearchFulfilled(state, action);
    case _actionCodes.SEARCH_REJECTED:
      return handleSearchRejected(state, action);
    case _actionCodes.SET_ALL_RESULT_ITEMS_SELECTED:
      return handleSetAllResultItemsSelected(state, action);
    case _actionCodes.SET_RESULT_ITEM_SELECTED:
      return handleSetResultItemSelected(state, action);
    case _actionCodes.DESELECT_RESULT_ITEM:
      return state.deleteIn([action.meta.searchName, 'selected', action.meta.csid]);
    case _actionCodes.SUBRECORD_CREATED:
      return clearNamedResults(state, action);
    case _actionCodes.RECORD_SAVE_FULFILLED:
      return handleRecordSaveFulfilled(state, action);
    case _actionCodes.RECORD_DELETE_FULFILLED:
      return handleRecordDeleteFulfilled(state, action);
    case _actionCodes.RECORD_TRANSITION_FULFILLED:
      return handleRecordTransitionFulfilled(state, action);
    case _actionCodes.LOGIN_FULFILLED:
      return handleLoginFulfilled(state, action);
    case _actionCodes.LOGOUT_FULFILLED:
      return clearAllResults(state);
    default:
      return state;
  }
};
exports.default = _default;
const isDirty = (state, searchName) => state.getIn([searchName, 'isDirty']);
exports.isDirty = isDirty;
const isPending = (state, searchName, searchDescriptor) => state.getIn([searchName, 'byKey', searchKey(searchDescriptor), 'isPending']);
exports.isPending = isPending;
const getState = (state, searchName, searchDescriptor) => state.getIn([searchName, 'byKey', searchKey(searchDescriptor)]);
exports.getState = getState;
const getIndexesByCsid = (state, searchName, searchDescriptor) => state.getIn([searchName, 'byKey', searchKey(searchDescriptor), 'indexesByCsid']);
exports.getIndexesByCsid = getIndexesByCsid;
const getMostRecentDescriptor = (state, searchName) => state.getIn([searchName, 'byKey', state.getIn([searchName, 'mostRecentKey']), 'descriptor']);
exports.getMostRecentDescriptor = getMostRecentDescriptor;
const getResult = (state, searchName, searchDescriptor) => state.getIn([searchName, 'byKey', searchKey(searchDescriptor), 'result']);
exports.getResult = getResult;
const getError = (state, searchName, searchDescriptor) => state.getIn([searchName, 'byKey', searchKey(searchDescriptor), 'error']);
exports.getError = getError;
const getSelectedItems = (state, searchName) => state.getIn([searchName, 'selected']);
exports.getSelectedItems = getSelectedItems;