'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getCreatedUser = exports.getCreatedTimestamp = exports.getUpdatedUser = exports.getUpdatedTimestamp = exports.getCoreFieldValue = exports.prepareForSending = exports.attributePropertiesToTop = exports.getDocument = exports.cloneRecordData = exports.clearUncloneable = exports.createRecordData = exports.applyDefaults = exports.spreadDefaultValue = exports.createBlankRecord = exports.deepDelete = exports.deepSet = exports.deepGet = exports.getPartNSPropertyName = exports.getPart = exports.getPartPropertyName = exports.DOCUMENT_PROPERTY_NAME = exports.NS_PREFIX = undefined;

var _immutable = require('immutable');

var _immutable2 = _interopRequireDefault(_immutable);

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

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

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }

var numericPattern = /^[0-9]$/;

var NS_PREFIX = exports.NS_PREFIX = 'ns2';
var DOCUMENT_PROPERTY_NAME = exports.DOCUMENT_PROPERTY_NAME = 'document';

var getPartPropertyName = exports.getPartPropertyName = function getPartPropertyName(partName) {
  return NS_PREFIX + ':' + partName;
};

var getPart = exports.getPart = function getPart(cspaceDocument, partName) {
  return cspaceDocument.get(getPartPropertyName(partName));
};

var getPartNSPropertyName = exports.getPartNSPropertyName = function getPartNSPropertyName() {
  return '@xmlns:' + NS_PREFIX;
};

/**
 * Deeply get a value in an Immutable.Map. This is similar to Immutable.Map.getIn, but differs in
 * one way:
 *
 * When a key of '0' is encountered, and that key is used to index a data item that is not a List,
 * the data item itself is returned. This accommodates data in which a list containing a single
 * item may be represented by that one item.
 */
var deepGet = exports.deepGet = function deepGet(data, path) {
  if (!Array.isArray(path) || path.length === 0) {
    throw new Error('path must be a non-empty array');
  }

  if (!data) {
    return undefined;
  }

  var _path = _toArray(path),
      key = _path[0],
      rest = _path.slice(1);

  var value = void 0;

  if ((key === '0' || key === 0) && !_immutable2.default.List.isList(data)) {
    // Allow a key of 0 to refer to a single non-list value.
    value = data;
  } else {
    value = data.get(key);
  }

  if (!value || rest.length === 0) {
    return value;
  }

  return deepGet(value, rest);
};

/**
 * Deeply set a value in an Immutable.Map. This is similar to Immutable.Map.setIn, but differs in
 * two ways:
 *
 * When a non-existent key is encountered in the middle of a path, this function may create a List
 * or a Map at that location, depending on the key. If the key is a numeric string, a List is
 * created. Otherwise, a Map is created. Immutable.Map.setIn always creates a Map.
 *
 * This function also promotes an existing singular (non-List) item to a List, if any numeric key
 * is applied to it.
 */
var deepSet = exports.deepSet = function deepSet(data, path, value) {
  if (!Array.isArray(path) || path.length === 0) {
    throw new Error('path must be a non-empty array');
  }

  var _path2 = _toArray(path),
      key = _path2[0],
      rest = _path2.slice(1);

  var isKeyNumeric = numericPattern.test(key);

  var normalizedData = void 0;

  if (data) {
    if (isKeyNumeric && !_immutable2.default.List.isList(data)) {
      // Promote a single (non-list) value into a list when a numeric key is supplied.
      normalizedData = _immutable2.default.List.of(data);
    } else {
      normalizedData = data;
    }
  } else if (isKeyNumeric) {
    normalizedData = _immutable2.default.List();
  } else {
    normalizedData = _immutable2.default.Map();
  }

  var resolvedValue = rest.length === 0 ? value : deepSet(normalizedData.get(key), rest, value);

  return normalizedData.set(key, resolvedValue);
};

/**
 * Deeply delete a value in an Immutable.Map. This is similar to Immutable.Map.deleteIn, but differs
 * in two ways:
 *
 * When a non-existent key is encountered in the middle of a path, this function may create a List
 * or a Map at that location, depending on the key. If the key is a numeric string, a List is
 * created. Otherwise, a Map is created. Immutable.Map.deleteIn makes no change when a key in the
 * path does not exist.
 *
 * This function also promotes an existing singular (non-List) item to a List, if any numeric key
 * is applied to it.
 */
var deepDelete = exports.deepDelete = function deepDelete(data, path) {
  return (
    // First call deepSet with undefined value to ensure the path exists. Then call deleteIn.
    deepSet(data, path).deleteIn(path)
  );
};

/**
 * Create a blank data record for a given CollectionSpace record type.
 */
var createBlankRecord = exports.createBlankRecord = function createBlankRecord(recordTypeConfig) {
  var fields = recordTypeConfig.fields,
      serviceConfig = recordTypeConfig.serviceConfig;


  var documentKey = Object.keys(fields)[0];
  var documentDescriptor = fields[documentKey];
  var partKeys = Object.keys(documentDescriptor);

  var document = {
    '@name': serviceConfig.documentName
  };

  partKeys.forEach(function (partKey) {
    var partDescriptor = documentDescriptor[partKey];

    document[partKey] = _defineProperty({}, getPartNSPropertyName(), partDescriptor[_configHelpers.configKey].service.ns);
  });

  return _immutable2.default.fromJS(_defineProperty({}, documentKey, document));
};

/**
 * Set a default value into record data. When declared on a repeating field, a default value will
 * be set on all instances of that field existing in the record data. If a repeating field has no
 * instances, a single instance will be created.
 */
var spreadDefaultValue = exports.spreadDefaultValue = function spreadDefaultValue(value, fieldDescriptorPath, data) {
  if (!fieldDescriptorPath || fieldDescriptorPath.length === 0) {
    return typeof data === 'undefined' ? value : data;
  }

  var map = void 0;

  if (typeof data === 'undefined') {
    map = _immutable2.default.Map();
  } else if (_immutable2.default.Map.isMap(data)) {
    map = data;
  } else {
    return data;
  }

  var _fieldDescriptorPath = _toArray(fieldDescriptorPath),
      key = _fieldDescriptorPath[0],
      rest = _fieldDescriptorPath.slice(1);

  var child = map.get(key);

  if (_immutable2.default.List.isList(child)) {
    return child.reduce(function (updatedData, instance, index) {
      return updatedData.setIn([key, index], spreadDefaultValue(value, rest, instance));
    }, map);
  }

  return map.set(key, spreadDefaultValue(value, rest, child));
};

/**
 * Set default values in record data.
 */
var applyDefaults = exports.applyDefaults = function applyDefaults(fieldDescriptor, data) {
  return (0, _configHelpers.getDefaults)(fieldDescriptor).reduce(function (updatedData, defaultDescriptor) {
    return spreadDefaultValue(defaultDescriptor.value, defaultDescriptor.path, updatedData);
  }, data);
};

/**
 * Create a skeletal data record for a given CollectionSpace service.
 */
var createRecordData = exports.createRecordData = function createRecordData(recordTypeConfig) {
  return applyDefaults(recordTypeConfig.fields, createBlankRecord(recordTypeConfig));
};

/**
 * Clear uncloneable fields from record data. Existing (not undefined) values in fields that are
 * not cloneable are set to the default value if one exists, or undefined otherwise.
 */
var clearUncloneable = exports.clearUncloneable = function clearUncloneable(fieldDescriptor, data) {
  if (!fieldDescriptor) {
    return data;
  }

  if (typeof data !== 'undefined' && !(0, _configHelpers.isCloneable)(fieldDescriptor)) {
    // If the field has been configured as not cloneable and there is an existing value, replace
    // the existing value with the default value if there is one, or undefined otherwise. The old
    // UI did not set uncloneable fields to the default value, but I think this was an oversight.

    return (0, _configHelpers.getDefaultValue)(fieldDescriptor);
  }

  if (_immutable2.default.Map.isMap(data)) {
    return data.reduce(function (updatedData, child, name) {
      return updatedData.set(name, clearUncloneable(fieldDescriptor[name], child));
    }, data);
  }

  if (_immutable2.default.List.isList(data)) {
    return data.reduce(function (updatedData, child, index) {
      return updatedData.set(index, clearUncloneable(fieldDescriptor, child));
    }, data);
  }

  return data;
};

/**
 * Create a new record as a clone of a given record.
 */
var cloneRecordData = exports.cloneRecordData = function cloneRecordData(recordTypeConfig, data) {
  if (!data) {
    return data;
  }

  var clone = data;

  // Delete parts that should not exist in new records.

  clone = clone.deleteIn(['document', NS_PREFIX + ':collectionspace_core']);
  clone = clone.deleteIn(['document', NS_PREFIX + ':account_permission']);

  // Reset fields that are configured as not cloneable.

  clone = clearUncloneable(recordTypeConfig.fields, clone);

  return clone;
};

/**
 * Get the document from the data record.
 */
var getDocument = exports.getDocument = function getDocument(data) {
  return data.get(DOCUMENT_PROPERTY_NAME);
};

/**
 * Comparator function to sort properties that represent XML attributes and namespace declarations
 * (those that start with '@') to the top.
 */
var attributePropertiesToTop = exports.attributePropertiesToTop = function attributePropertiesToTop(propertyNameA, propertyNameB) {
  var firstCharA = propertyNameA.charAt(0);
  var firstCharB = propertyNameB.charAt(0);

  if (firstCharA === firstCharB) {
    return 0;
  }

  if (firstCharA === '@') {
    return -1;
  }

  if (firstCharB === '@') {
    return 1;
  }

  return 0;
};

/**
 * Prepare record data for POST or PUT to the CollectionSpace REST API. Document parts that may
 * be present in data retrieved from the REST API, but that do not need to be present in data sent
 * to the API, are removed. In the remaining parts, properties beginning with '@', which represent
 * XML attributes and namespace declarations, are moved to the top. This is required by the REST
 * API in order to properly translate the payload to XML.
 */
var prepareForSending = exports.prepareForSending = function prepareForSending(data) {
  // Filter out the core schema and account information parts.

  var cspaceDocument = data.get(DOCUMENT_PROPERTY_NAME).filter(function (value, key) {
    return key !== NS_PREFIX + ':collectionspace_core' && key !== NS_PREFIX + ':account_permission';
  });

  // For each remaining part, move XML attribute and namespace declaration properties (those
  // that start with @) to the top, since the REST API requires this.

  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = cspaceDocument.keys()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var key = _step.value;

      if (key.charAt(0) !== '@') {
        var part = cspaceDocument.get(key);
        var sortedPart = part.sortBy(function (value, name) {
          return name;
        }, attributePropertiesToTop);

        cspaceDocument = cspaceDocument.set(key, sortedPart);
      }
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  return data.set(DOCUMENT_PROPERTY_NAME, cspaceDocument);
};

var getCoreFieldValue = exports.getCoreFieldValue = function getCoreFieldValue(data, fieldName) {
  if (data) {
    var document = getDocument(data);

    if (document) {
      var corePart = getPart(document, 'collectionspace_core');

      if (corePart) {
        return corePart.get(fieldName);
      }
    }
  }

  return undefined;
};

var getUpdatedTimestamp = exports.getUpdatedTimestamp = function getUpdatedTimestamp(data) {
  return getCoreFieldValue(data, 'updatedAt');
};

var getUpdatedUser = exports.getUpdatedUser = function getUpdatedUser(data) {
  return getCoreFieldValue(data, 'updatedBy');
};

var getCreatedTimestamp = exports.getCreatedTimestamp = function getCreatedTimestamp(data) {
  return getCoreFieldValue(data, 'createdAt');
};

var getCreatedUser = exports.getCreatedUser = function getCreatedUser(data) {
  return getCoreFieldValue(data, 'createdBy');
};