'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.isModified = exports.isSavePending = exports.isReadPending = exports.getValidationErrors = exports.getNewSubrecordCsid = exports.getNewData = exports.getRelationUpdatedTimestamp = exports.getSubrecordCsid = exports.getError = exports.getData = undefined;

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _immutable = require('immutable');

var _immutable2 = _interopRequireDefault(_immutable);

var _get = require('lodash/get');

var _get2 = _interopRequireDefault(_get);

var _recordDataHelpers = require('../helpers/recordDataHelpers');

var _configHelpers = require('../helpers/configHelpers');

var _login = require('../actions/login');

var _logout = require('../actions/logout');

var _record = require('../actions/record');

var _relation = require('../actions/relation');

var _idGenerator = require('../actions/idGenerator');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

var BASE_NEW_RECORD_KEY = '';

var unsavedRecordKey = function unsavedRecordKey(subrecordName) {
  return subrecordName ? BASE_NEW_RECORD_KEY + '/' + subrecordName : BASE_NEW_RECORD_KEY;
};

var getCurrentData = function getCurrentData(state, csid) {
  return state.getIn([csid, 'data', 'current']);
};
var setCurrentData = function setCurrentData(state, csid, data) {
  return state.setIn([csid, 'data', 'current'], data);
};

var getBaselineData = function getBaselineData(state, csid) {
  return state.getIn([csid, 'data', 'baseline']);
};
var setBaselineData = function setBaselineData(state, csid, data) {
  return state.setIn([csid, 'data', 'baseline'], data);
};

var clear = function clear(state, csid) {
  var recordState = state.get(csid);

  if (!recordState) {
    return state;
  }

  var nextState = state;

  var subrecord = recordState.get('subrecord');

  if (subrecord) {
    nextState = subrecord.reduce(function (reducedState, subrecordCsid) {
      return clear(reducedState, subrecordCsid);
    }, nextState);
  }

  return nextState.delete(csid);
};

var clearAll = function clearAll(state) {
  return state.clear();
};

var clearFiltered = function clearFiltered(state, filter) {
  var nextState = state;

  state.filter(filter).forEach(function (recordState, csid) {
    nextState = clear(nextState, csid);
  });

  return nextState;
};

var addFieldInstance = function addFieldInstance(state, action) {
  var _action$meta = action.meta,
      csid = _action$meta.csid,
      path = _action$meta.path,
      position = _action$meta.position,
      recordTypeConfig = _action$meta.recordTypeConfig;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var value = (0, _recordDataHelpers.deepGet)(data, path);
  var list = _immutable2.default.List.isList(value) ? value : _immutable2.default.List.of(value);

  var fieldDescriptor = (0, _get2.default)(recordTypeConfig, ['fields'].concat(_toConsumableArray((0, _configHelpers.dataPathToFieldDescriptorPath)(path))));
  var defaultData = (0, _recordDataHelpers.applyDefaults)(fieldDescriptor);

  var updatedList = typeof position === 'undefined' || position < 0 || position >= list.size ? list.push(defaultData) : list.insert(position, defaultData);

  var updatedData = (0, _recordDataHelpers.deepSet)(data, path, updatedList);

  return setCurrentData(state, csid, updatedData);
};

var sortFieldInstances = function sortFieldInstances(state, action) {
  var _action$meta2 = action.meta,
      csid = _action$meta2.csid,
      path = _action$meta2.path,
      byField = _action$meta2.byField;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var value = (0, _recordDataHelpers.deepGet)(data, path);
  var list = _immutable2.default.List.isList(value) ? value : _immutable2.default.List.of(value);

  // TODO: Check for a custom sort comparator function in field config.
  // For now just use the default.

  var comparator = undefined;

  var sortedList = byField ? list.sortBy(function (item) {
    return item.get(byField);
  }, comparator) : list.sort(comparator);

  var updatedData = (0, _recordDataHelpers.deepSet)(data, path, sortedList);

  return setCurrentData(state, csid, updatedData);
};

var doCreateNew = function doCreateNew(state, config, recordTypeConfig, cloneCsid, subrecordName) {
  var data = void 0;

  if (cloneCsid) {
    data = (0, _recordDataHelpers.cloneRecordData)(recordTypeConfig, getCurrentData(state, cloneCsid));
  }

  if (!data) {
    data = (0, _recordDataHelpers.createRecordData)(recordTypeConfig);
  }

  var csid = unsavedRecordKey(subrecordName);

  var nextState = state.delete(csid);

  var subrecords = recordTypeConfig.subrecords;


  if (subrecords) {
    Object.keys(subrecords).forEach(function (name) {
      var subrecordConfig = subrecords[name];
      var csidField = subrecordConfig.csidField;


      var cloneSubrecordCsid = void 0;

      if (csidField) {
        cloneSubrecordCsid = (0, _recordDataHelpers.deepGet)(data, csidField);
      }

      if (!cloneSubrecordCsid) {
        var subrecordType = subrecordConfig.recordType;
        var subrecordTypeConfig = (0, _get2.default)(config, ['recordTypes', subrecordType]);
        var subrecordCsid = state.getIn([cloneCsid, 'subrecord', name]);

        nextState = doCreateNew(nextState, config, subrecordTypeConfig, subrecordCsid, name);

        cloneSubrecordCsid = csid + '/' + name;

        if (csidField) {
          data = (0, _recordDataHelpers.deepSet)(data, csidField, cloneSubrecordCsid);
        }
      }

      nextState = nextState.setIn([csid, 'subrecord', name], cloneSubrecordCsid);
    });
  }

  nextState = setBaselineData(nextState, csid, data);
  nextState = setCurrentData(nextState, csid, data);

  return nextState;
};

var createNewRecord = function createNewRecord(state, action) {
  var _action$meta3 = action.meta,
      config = _action$meta3.config,
      recordTypeConfig = _action$meta3.recordTypeConfig,
      cloneCsid = _action$meta3.cloneCsid;


  return doCreateNew(state, config, recordTypeConfig, cloneCsid);
};

var deleteFieldValue = function deleteFieldValue(state, action) {
  var _action$meta4 = action.meta,
      csid = _action$meta4.csid,
      path = _action$meta4.path;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var updatedData = (0, _recordDataHelpers.deepDelete)(data, path);

  return setCurrentData(state, csid, updatedData);
};

var moveFieldValue = function moveFieldValue(state, action) {
  var _action$meta5 = action.meta,
      csid = _action$meta5.csid,
      path = _action$meta5.path,
      newPosition = _action$meta5.newPosition;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var listPath = path.slice(0, -1);
  var oldPosition = path[path.length - 1];

  var list = (0, _recordDataHelpers.deepGet)(data, listPath);

  if (!_immutable2.default.List.isList(list)) {
    return state;
  }

  var value = list.get(oldPosition);

  list = list.delete(oldPosition);
  list = list.insert(newPosition, value);

  var updatedData = (0, _recordDataHelpers.deepSet)(data, listPath, list);

  return setCurrentData(state, csid, updatedData);
};

var setFieldValue = function setFieldValue(state, action) {
  var _action$meta6 = action.meta,
      csid = _action$meta6.csid,
      path = _action$meta6.path;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var newValue = action.payload;
  var updatedData = (0, _recordDataHelpers.deepSet)(data, path, newValue);
  var nextState = setCurrentData(state, csid, updatedData);

  return nextState;
};

var handleFieldComputeFulfilled = function handleFieldComputeFulfilled(state, action) {
  var _action$meta7 = action.meta,
      csid = _action$meta7.csid,
      path = _action$meta7.path;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  if (path.length === 0) {
    // The entire record was computed.

    var computedData = action.payload;
    var updatedData = data.mergeDeep(computedData);
    var nextState = setCurrentData(state, csid, updatedData);

    return nextState;
  }

  // TODO: Handle an individual field being computed.

  return state;
};

var handleRecordReadFulfilled = function handleRecordReadFulfilled(state, action) {
  var _action$meta8 = action.meta,
      csid = _action$meta8.csid,
      recordTypeConfig = _action$meta8.recordTypeConfig;


  var data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable2.default.fromJS(action.payload.data));

  var nextState = state.deleteIn([csid, 'isReadPending']).deleteIn([csid, 'error']);

  nextState = setBaselineData(nextState, csid, data);
  nextState = setCurrentData(nextState, csid, data);

  return nextState;
};

var handleRecordSaveFulfilled = function handleRecordSaveFulfilled(state, action) {
  var _action$meta9 = action.meta,
      csid = _action$meta9.csid,
      recordTypeConfig = _action$meta9.recordTypeConfig,
      relatedSubjectCsid = _action$meta9.relatedSubjectCsid;


  var data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable2.default.fromJS(action.payload.data));

  var nextState = state;

  nextState = nextState.deleteIn([csid, 'isSavePending']);

  nextState = setBaselineData(nextState, csid, data);
  nextState = setCurrentData(nextState, csid, data);

  if (relatedSubjectCsid) {
    nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], (0, _recordDataHelpers.getUpdatedTimestamp)(data));
  }

  // Remove all record state besides the record that was just saved, any of its subrecords, and any
  // related record. This isn't strictly necessary, but it's a good time to expire record data,
  // since other records may have fields computed from this record via service layer handlers.

  var persistCsids = [csid, relatedSubjectCsid, BASE_NEW_RECORD_KEY];

  var subrecord = nextState.getIn([csid, 'subrecord']);

  if (subrecord) {
    subrecord.valueSeq().forEach(function (subrecordCsid) {
      persistCsids.push(subrecordCsid);
    });
  }

  persistCsids = new Set(persistCsids.filter(function (value) {
    return value !== null && typeof value !== 'undefined';
  }));

  nextState = clearFiltered(nextState, function (recordState, candidateCsid) {
    return !persistCsids.has(candidateCsid) && !candidateCsid.startsWith(BASE_NEW_RECORD_KEY + '/') && // Don't clear unsaved subrecord data
    !recordState.get('isSavePending');
  } // Don't clear records that are being saved
  );

  return nextState;
};

var revertRecord = function revertRecord(state, action) {
  var _action$meta10 = action.meta,
      recordTypeConfig = _action$meta10.recordTypeConfig,
      csid = _action$meta10.csid;


  var baselineData = getBaselineData(state, csid);

  var nextState = setCurrentData(state, csid, baselineData);

  // Revert subrecords.

  var subrecords = nextState.getIn([csid, 'subrecord']);

  if (subrecords) {
    subrecords.forEach(function (subrecordCsid) {
      nextState = revertRecord(nextState, {
        meta: {
          csid: subrecordCsid
        }
      });
    });
  }

  // Revert any cached subrecord csids that originated from fields in the record.

  var configuredSubrecords = (0, _get2.default)(recordTypeConfig, 'subrecords');

  if (configuredSubrecords) {
    Object.entries(configuredSubrecords).forEach(function (entry) {
      var _entry = _slicedToArray(entry, 2),
          subrecordName = _entry[0],
          subrecordConfig = _entry[1];

      var csidField = subrecordConfig.csidField;


      if (csidField) {
        var revertedSubrecordCsid = (0, _recordDataHelpers.deepGet)(baselineData, csidField);

        nextState = nextState.setIn([csid, 'subrecord', subrecordName], revertedSubrecordCsid);

        // Revert the reattached subrecord.

        nextState = revertRecord(nextState, {
          meta: {
            csid: revertedSubrecordCsid
          }
        });
      }
    });
  }

  return nextState;
};

var handleSubrecordCreated = function handleSubrecordCreated(state, action) {
  var _action$meta11 = action.meta,
      csid = _action$meta11.csid,
      csidField = _action$meta11.csidField,
      subrecordName = _action$meta11.subrecordName,
      subrecordCsid = _action$meta11.subrecordCsid,
      isDefault = _action$meta11.isDefault;


  var nextState = state.setIn([csid, 'subrecord', subrecordName], subrecordCsid);

  if (csidField) {
    var currentData = getCurrentData(state, csid);
    var baselineData = getBaselineData(state, csid);

    if (isDefault && currentData === baselineData) {
      // This subrecord was created as the default for the container. Set the csid field in both
      // the baseline and current data, so it won't be considered a modification.

      var updatedData = (0, _recordDataHelpers.deepSet)(baselineData, csidField, subrecordCsid);

      nextState = setBaselineData(nextState, csid, updatedData);
      nextState = setCurrentData(nextState, csid, updatedData);
    } else {
      var _updatedData = (0, _recordDataHelpers.deepSet)(currentData, csidField, subrecordCsid);

      nextState = setCurrentData(nextState, csid, _updatedData);
    }
  }

  return nextState;
};

var createNewSubrecord = function createNewSubrecord(state, action) {
  var _action$meta12 = action.meta,
      config = _action$meta12.config,
      csid = _action$meta12.csid,
      csidField = _action$meta12.csidField,
      subrecordName = _action$meta12.subrecordName,
      subrecordTypeConfig = _action$meta12.subrecordTypeConfig,
      cloneCsid = _action$meta12.cloneCsid,
      isDefault = _action$meta12.isDefault;


  var nextState = doCreateNew(state, config, subrecordTypeConfig, cloneCsid, subrecordName);

  var subrecordCsid = unsavedRecordKey(subrecordName);

  nextState = handleSubrecordCreated(nextState, {
    meta: {
      csid: csid,
      csidField: csidField,
      subrecordName: subrecordName,
      subrecordCsid: subrecordCsid,
      isDefault: isDefault
    }
  });

  return nextState;
};

var handleSubjectRelationsUpdated = function handleSubjectRelationsUpdated(state, action) {
  // Currently relations are only ever created (not updated), and we don't bother to retrieve
  // the relation record after creation. Technically we should retrieve the new relation
  // record and use its updatedAt value, but that's an extra request. For now the action creator
  // sets the updated time as a meta field.

  var _action$meta13 = action.meta,
      subject = _action$meta13.subject,
      updatedTime = _action$meta13.updatedTime;


  var subjectCsid = subject.csid;

  if (state.has(subjectCsid)) {
    return state.setIn([subjectCsid, 'relationUpdatedTime'], updatedTime);
  }

  return state;
};

var handleCreateIDFulfilled = function handleCreateIDFulfilled(state, action) {
  var _action$meta14 = action.meta,
      csid = _action$meta14.csid,
      path = _action$meta14.path;


  var data = getCurrentData(state, csid);

  if (!data) {
    return state;
  }

  var newValue = action.payload.data;
  var updatedData = (0, _recordDataHelpers.deepSet)(data, path, newValue);
  var nextState = setCurrentData(state, csid, updatedData);

  return nextState;
};

var handleValidationFailed = function handleValidationFailed(state, action) {
  var errors = action.payload;

  var _action$meta15 = action.meta,
      csid = _action$meta15.csid,
      path = _action$meta15.path;


  return state.setIn([csid, 'validation'].concat(_toConsumableArray(path)), errors);
};

var handleValidationPassed = function handleValidationPassed(state, action) {
  var _action$meta16 = action.meta,
      csid = _action$meta16.csid,
      path = _action$meta16.path;


  return state.deleteIn([csid, 'validation'].concat(_toConsumableArray(path)));
};

var handleTransitionFulfilled = function handleTransitionFulfilled(state, action) {
  var _action$meta17 = action.meta,
      csid = _action$meta17.csid,
      transitionName = _action$meta17.transitionName,
      recordTypeConfig = _action$meta17.recordTypeConfig,
      relatedSubjectCsid = _action$meta17.relatedSubjectCsid,
      updatedTimestamp = _action$meta17.updatedTimestamp;


  var nextState = state.deleteIn([csid, 'isSavePending']);

  if (transitionName === 'delete') {
    nextState = nextState.delete(csid);
  } else {
    var newData = (0, _get2.default)(action, ['payload', 'data']);

    if (newData) {
      var data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable2.default.fromJS(newData));

      nextState = setBaselineData(nextState, csid, data);
      nextState = setCurrentData(nextState, csid, data);
    }
  }

  if (relatedSubjectCsid) {
    nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], updatedTimestamp);
  }

  return nextState;
};

var handleDeleteFulfilled = function handleDeleteFulfilled(state, action) {
  var _action$meta18 = action.meta,
      csid = _action$meta18.csid,
      relatedSubjectCsid = _action$meta18.relatedSubjectCsid,
      updatedTimestamp = _action$meta18.updatedTimestamp;


  var nextState = state.delete(csid);

  if (relatedSubjectCsid) {
    nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], updatedTimestamp);
  }

  return nextState;
};

var detachSubrecord = function detachSubrecord(state, action) {
  return createNewSubrecord(state, action);
};

var handleLoginFulfilled = function handleLoginFulfilled(state, action) {
  var _action$meta19 = action.meta,
      prevUsername = _action$meta19.prevUsername,
      username = _action$meta19.username;


  if (prevUsername !== username) {
    // The logged in user has changed. Remove all record state, because the new user may not be
    // permitted to read some records that the previous user could.

    return clearAll(state);
  }

  return state;
};

exports.default = function () {
  var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _immutable2.default.Map();
  var action = arguments[1];

  switch (action.type) {
    case _record.VALIDATION_FAILED:
      return handleValidationFailed(state, action);
    case _record.VALIDATION_PASSED:
      return handleValidationPassed(state, action);
    case _record.ADD_FIELD_INSTANCE:
      return addFieldInstance(state, action);
    case _record.CREATE_NEW_RECORD:
      return createNewRecord(state, action);
    case _record.CREATE_NEW_SUBRECORD:
      return createNewSubrecord(state, action);
    case _record.DELETE_FIELD_VALUE:
      return deleteFieldValue(state, action);
    case _record.MOVE_FIELD_VALUE:
      return moveFieldValue(state, action);
    case _record.SET_FIELD_VALUE:
      return setFieldValue(state, action);
    case _record.FIELD_COMPUTE_FULFILLED:
      return handleFieldComputeFulfilled(state, action);
    case _record.RECORD_CREATED:
      return state.set(action.meta.newRecordCsid, state.get(action.meta.currentCsid)).delete(action.meta.currentCsid);
    case _record.RECORD_READ_STARTED:
      return state.setIn([action.meta.csid, 'isReadPending'], true);
    case _record.RECORD_READ_FULFILLED:
      return handleRecordReadFulfilled(state, action);
    case _record.RECORD_READ_REJECTED:
      return state.setIn([action.meta.csid, 'error'], _immutable2.default.fromJS(action.payload)).deleteIn([action.meta.csid, 'isReadPending']);
    case _record.RECORD_SAVE_STARTED:
      return state.setIn([action.meta.csid, 'isSavePending'], true);
    case _record.RECORD_SAVE_FULFILLED:
      return handleRecordSaveFulfilled(state, action);
    case _record.RECORD_SAVE_REJECTED:
      return state
      // I don't think there's any reason to store the save error.
      // .setIn([action.meta.csid, 'error'], Immutable.fromJS(action.payload))
      .deleteIn([action.meta.csid, 'isSavePending']);
    case _record.RECORD_TRANSITION_STARTED:
      return state.setIn([action.meta.csid, 'isSavePending'], true);
    case _record.RECORD_TRANSITION_FULFILLED:
      return handleTransitionFulfilled(state, action);
    case _record.RECORD_TRANSITION_REJECTED:
      return state.deleteIn([action.meta.csid, 'isSavePending']);
    case _record.RECORD_DELETE_STARTED:
      return state.setIn([action.meta.csid, 'isSavePending'], true);
    case _record.RECORD_DELETE_FULFILLED:
      return handleDeleteFulfilled(state, action);
    case _record.RECORD_DELETE_REJECTED:
      return state.deleteIn([action.meta.csid, 'isSavePending']);
    case _record.SUBRECORD_CREATED:
      return handleSubrecordCreated(state, action);
    case _record.SUBRECORD_READ_FULFILLED:
      return state.setIn([action.meta.csid, 'subrecord', action.meta.subrecordName], action.meta.subrecordCsid);
    case _record.REVERT_RECORD:
      return revertRecord(state, action);
    case _relation.SUBJECT_RELATIONS_UPDATED:
      return handleSubjectRelationsUpdated(state, action);
    case _idGenerator.CREATE_ID_FULFILLED:
      return handleCreateIDFulfilled(state, action);
    case _record.DETACH_SUBRECORD:
      return detachSubrecord(state, action);
    case _record.CLEAR_RECORD:
      return clear(state, action.meta.csid);
    case _login.LOGIN_FULFILLED:
      return handleLoginFulfilled(state, action);
    case _logout.LOGOUT_FULFILLED:
      return clearAll(state);
    case _record.SORT_FIELD_INSTANCES:
      return sortFieldInstances(state, action);
    default:
      return state;
  }
};

var getData = exports.getData = function getData(state, csid) {
  return getCurrentData(state, csid);
};

var getError = exports.getError = function getError(state, csid) {
  return state.getIn([csid, 'error']);
};

var getSubrecordCsid = exports.getSubrecordCsid = function getSubrecordCsid(state, csid, subrecordName) {
  return state.getIn([csid, 'subrecord', subrecordName]);
};

var getRelationUpdatedTimestamp = exports.getRelationUpdatedTimestamp = function getRelationUpdatedTimestamp(state, csid) {
  return state.getIn([csid, 'relationUpdatedTime']);
};

var getNewData = exports.getNewData = function getNewData(state) {
  return getData(state, unsavedRecordKey());
};

var getNewSubrecordCsid = exports.getNewSubrecordCsid = function getNewSubrecordCsid(state, subrecordName) {
  return getSubrecordCsid(state, unsavedRecordKey(), subrecordName);
};

var getValidationErrors = exports.getValidationErrors = function getValidationErrors(state, csid) {
  return state.getIn([csid, 'validation']);
};

var isReadPending = exports.isReadPending = function isReadPending(state, csid) {
  return state.getIn([csid, 'isReadPending']);
};

var isSavePending = exports.isSavePending = function isSavePending(state, csid) {
  return state.getIn([csid, 'isSavePending']);
};

var isModified = exports.isModified = function isModified(state, csid) {
  // Do a reference equality test between the current and baseline data. This will not detect if
  // a change is made, then another change is made that undoes the first. But it's more efficient
  // than a deep value equality test.

  if (getCurrentData(state, csid) !== getBaselineData(state, csid)) {
    return true;
  }

  // Check subrecords.

  var subrecords = state.getIn([csid, 'subrecord']);

  if (subrecords && subrecords.find(function (subrecordCsid) {
    return isModified(state, subrecordCsid);
  })) {
    return true;
  }

  return false;
};