'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getFirstColumnName = exports.findVocabularyUses = exports.findFieldConfigInPart = exports.findField = exports.getDefaultSearchVocabulary = exports.getDefaultSearchRecordType = exports.validateLocation = exports.isUtility = exports.isAuthority = exports.getRequiredMessage = exports.getFieldComputer = exports.getFieldCustomValidator = exports.getFieldDataType = exports.isFieldRequired = exports.isFieldRepeating = exports.isFieldCloneable = exports.getDefaults = exports.getDefaultValue = exports.getVocabularyConfigByServicePath = exports.getVocabularyConfigByShortID = exports.getRecordTypeNameByUri = exports.getRecordTypeConfigByUri = exports.getRecordTypeConfigByServicePath = exports.getRecordTypeNameByServiceObjectName = exports.getRecordTypeConfigByServiceObjectName = exports.getRecordTypeConfigByServiceDocumentName = exports.initConfig = exports.mergeConfig = exports.mergeStrategy = exports.applyPlugins = exports.applyPlugin = exports.evaluatePlugin = exports.initializeRecordTypes = exports.initializeExtensionFieldParents = exports.initializeExtensions = exports.dataPathToFieldDescriptorPath = exports.mergeKey = exports.configKey = undefined;

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _immutable = require('immutable');

var _immutable2 = _interopRequireDefault(_immutable);

var _mergeWith = require('lodash/mergeWith');

var _mergeWith2 = _interopRequireDefault(_mergeWith);

var _flatMap = require('lodash/flatMap');

var _flatMap2 = _interopRequireDefault(_flatMap);

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

var _get2 = _interopRequireDefault(_get);

var _set = require('lodash/set');

var _set2 = _interopRequireDefault(_set);

var _warning = require('warning');

var _warning2 = _interopRequireDefault(_warning);

var _errorCodes = require('../constants/errorCodes');

var _dataTypes = require('../constants/dataTypes');

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

var _csidHelpers = require('./csidHelpers');

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

const onlyDigitsPattern = /^\d+$/;
const isNotNumeric = string => !onlyDigitsPattern.test(string);

const configKey = exports.configKey = '[config]';
const mergeKey = exports.mergeKey = '[merge]';

const dataPathToFieldDescriptorPath = exports.dataPathToFieldDescriptorPath = dataPath => dataPath.filter(isNotNumeric);

/*
 * Initialize the extension configurations in a configuration object. This function mutates the
 * argument configuration.
 *
 * - Set the extensionName property of each top level field in the extension to the extension name
 */
const initializeExtensions = exports.initializeExtensions = config => {
  const extensions = config.extensions;


  if (extensions) {
    Object.keys(extensions).forEach(extensionName => {
      const extension = extensions[extensionName];
      const fields = extension.fields;


      if (fields) {
        Object.values(fields).forEach(fieldDescriptor => {
          (0, _set2.default)(fieldDescriptor, [configKey, 'extensionName'], extensionName);
        });
      }
    });
  }

  return config;
};

const initializeExtensionFieldParents = exports.initializeExtensionFieldParents = fieldDescriptor => {
  if (fieldDescriptor) {
    Object.keys(fieldDescriptor).filter(key => key !== configKey).forEach(key => {
      const childFieldDescriptor = fieldDescriptor[key];
      const isExtensionField = (0, _get2.default)(childFieldDescriptor, [configKey, 'extensionName']);

      if (isExtensionField) {
        // Make a copy of this field descriptor and its configuration, so that different
        // configuration can be applied to each use of the extension (really wishing I'd made the
        // config an Immutable, so explicit copy wouldn't be necessary).

        // Set the extension parent config in the extension field's config.

        const childFieldDescriptorCopy = Object.assign({}, childFieldDescriptor, {
          [configKey]: Object.assign({}, childFieldDescriptor[configKey], {
            extensionParentConfig: fieldDescriptor[configKey]
          })
        });

        // eslint-disable-next-line no-param-reassign
        fieldDescriptor[key] = childFieldDescriptorCopy;
      } else {
        initializeExtensionFieldParents(childFieldDescriptor);
      }
    });
  }
};

/*
 * Initialize the record type configurations in a configuration object. This function mutates the
 * argument configuration.
 *
 * - Delete any record type or vocabulary that is disabled
 * - Set the name property of each recordTypes entry to its key
 * - Set the name property of each vocabularies entry to its key
 * - Set the parent property of any extension fields
 */
const initializeRecordTypes = exports.initializeRecordTypes = config => {
  const recordTypes = config.recordTypes;


  if (recordTypes) {
    Object.keys(recordTypes).forEach(recordTypeName => {
      const recordType = recordTypes[recordTypeName];

      if (recordType.disabled) {
        delete recordTypes[recordTypeName];
      } else {
        recordType.name = recordTypeName;

        const fields = recordType.fields,
              vocabularies = recordType.vocabularies;


        if (fields) {
          Object.values(fields).forEach(fieldDescriptor => {
            initializeExtensionFieldParents(fieldDescriptor);
          });
        }

        if (vocabularies) {
          Object.keys(vocabularies).forEach(vocabularyName => {
            const vocabulary = vocabularies[vocabularyName];

            if (vocabulary.disabled) {
              delete vocabularies[vocabularyName];
            } else {
              vocabularies[vocabularyName].name = vocabularyName;
            }
          });
        }
      }
    });
  }

  return config;
};

const evaluatePlugin = exports.evaluatePlugin = (plugin, pluginContext) => {
  const pluginType = typeof plugin;

  const isValidType = plugin && (pluginType === 'function' || pluginType === 'object' && !Array.isArray(plugin));

  process.env.NODE_ENV !== 'production' ? (0, _warning2.default)(isValidType, 'A plugin must be an object or a function.') : void 0;

  if (!isValidType) {
    return {};
  }

  const config = pluginType === 'object' ? plugin : plugin(pluginContext);

  initializeExtensions(config);
  initializeRecordTypes(config);

  return config;
};

const applyPlugin = exports.applyPlugin = (targetConfig, plugin, pluginContext = {}) => {
  const pluginConfigContribution = evaluatePlugin(plugin, pluginContext);

  /* Gotta do this mutual recursion */
  /* eslint-disable no-use-before-define */
  return mergeConfig(targetConfig, pluginConfigContribution, pluginContext);
  /* eslint-enable no-use-before-define */
};

const applyPlugins = exports.applyPlugins = (targetConfig, plugins, pluginContext = {}) => {
  const isArray = Array.isArray(plugins);

  process.env.NODE_ENV !== 'production' ? (0, _warning2.default)(isArray, 'Plugins must be an array.') : void 0;

  if (!isArray) {
    return targetConfig;
  }

  return plugins.reduce((updatedConfig, plugin) => applyPlugin(updatedConfig, plugin, pluginContext), targetConfig);
};

const mergeStrategy = exports.mergeStrategy = {
  override: srcValue => Object.assign({}, srcValue, {
    [mergeKey]: 'override'
  })
};

const configMerger = (objValue, srcValue, key) => {
  if (Array.isArray(objValue)) {
    // Don't merge arrays. Just override with the source value.
    return srcValue;
  }

  if (key === 'advancedSearch') {
    // Don't merge advanced search config. Just override with the source value.
    return srcValue;
  }

  if (_react2.default.isValidElement(objValue)) {
    // Don't merge React elements, e.g. in form templates. Just override with the source value.
    return srcValue;
  }

  if (srcValue && typeof srcValue === 'object' && srcValue[mergeKey] === 'override') {
    const srcValueCopy = Object.assign({}, srcValue);

    delete srcValueCopy[mergeKey];

    return srcValueCopy;
  }

  return undefined;
};

const mergeConfig = exports.mergeConfig = (targetConfig, sourceConfig, pluginContext = {}) => {
  // eslint-disable-next-line no-param-reassign
  pluginContext.config = targetConfig;

  const pluginsAppliedConfig = sourceConfig && 'plugins' in sourceConfig ? applyPlugins(targetConfig, sourceConfig.plugins, pluginContext) : targetConfig;

  const mergedConfig = (0, _mergeWith2.default)({}, pluginsAppliedConfig, sourceConfig, configMerger);

  delete mergedConfig.plugins;

  return mergedConfig;
};

const initConfig = exports.initConfig = (config, pluginContext) => mergeConfig({}, config, pluginContext);

const getRecordTypeConfigByServiceDocumentName = exports.getRecordTypeConfigByServiceDocumentName = (config, documentName) => {
  if (!documentName) {
    return undefined;
  }

  if (!config.recordTypesByServiceDocumentName) {
    const recordTypesByServiceDocumentName = {};
    const recordTypes = config.recordTypes;


    Object.keys(recordTypes).forEach(recordType => {
      const recordTypeConfig = recordTypes[recordType];
      const serviceConfig = recordTypeConfig.serviceConfig;


      recordTypesByServiceDocumentName[serviceConfig.documentName] = recordTypeConfig;
    });

    /* eslint-disable no-param-reassign */
    config.recordTypesByServiceDocumentName = recordTypesByServiceDocumentName;
    /* eslint-enable no-param-reassign */
  }

  return config.recordTypesByServiceDocumentName[documentName];
};

const getRecordTypeConfigByServiceObjectName = exports.getRecordTypeConfigByServiceObjectName = (config, objectName) => {
  if (!objectName) {
    return undefined;
  }

  if (!config.recordTypesByServiceObjectName) {
    const recordTypesByServiceObjectName = {};
    const recordTypes = config.recordTypes;


    Object.keys(recordTypes).forEach(recordType => {
      const recordTypeConfig = recordTypes[recordType];

      recordTypesByServiceObjectName[recordTypeConfig.serviceConfig.objectName] = recordTypeConfig;
    });

    /* eslint-disable no-param-reassign */
    config.recordTypesByServiceObjectName = recordTypesByServiceObjectName;
    /* eslint-enable no-param-reassign */
  }

  return config.recordTypesByServiceObjectName[objectName];
};

const getRecordTypeNameByServiceObjectName = exports.getRecordTypeNameByServiceObjectName = (config, objectName) => {
  const recordTypeConfig = getRecordTypeConfigByServiceObjectName(config, objectName);

  return recordTypeConfig ? recordTypeConfig.name : undefined;
};

const getRecordTypeConfigByServicePath = exports.getRecordTypeConfigByServicePath = (config, servicePath) => {
  if (!servicePath) {
    return undefined;
  }

  if (!config.recordTypesByServicePath) {
    const recordTypesByServicePath = {};
    const recordTypes = config.recordTypes;


    Object.keys(recordTypes).forEach(recordType => {
      const recordTypeConfig = recordTypes[recordType];

      recordTypesByServicePath[recordTypeConfig.serviceConfig.servicePath] = recordTypeConfig;
    });

    /* eslint-disable no-param-reassign */
    config.recordTypesByServicePath = recordTypesByServicePath;
    /* eslint-enable no-param-reassign */
  }

  return config.recordTypesByServicePath[servicePath];
};

const getRecordTypeConfigByUri = exports.getRecordTypeConfigByUri = (config, uri) => {
  if (!uri) {
    return undefined;
  }

  const servicePath = uri.split('/', 2)[1];

  return getRecordTypeConfigByServicePath(config, servicePath);
};

const getRecordTypeNameByUri = exports.getRecordTypeNameByUri = (config, uri) => {
  const recordTypeConfig = getRecordTypeConfigByUri(config, uri);

  return recordTypeConfig ? recordTypeConfig.name : undefined;
};

const getVocabularyConfigByShortID = exports.getVocabularyConfigByShortID = (recordTypeConfig, shortID) => {
  if (!shortID) {
    return undefined;
  }

  if (!recordTypeConfig.vocabulariesByShortID) {
    const vocabulariesByShortID = {};
    const vocabularies = recordTypeConfig.vocabularies;


    if (vocabularies) {
      Object.keys(vocabularies).forEach(vocabulary => {
        const vocabularyConfig = vocabularies[vocabulary];
        const servicePath = vocabularyConfig.serviceConfig.servicePath;


        if (servicePath && servicePath.indexOf('urn:cspace:name(') === 0 && servicePath.lastIndexOf(')') === servicePath.length - 1) {
          const vocabularyShortID = servicePath.substring(16, servicePath.length - 1);

          vocabulariesByShortID[vocabularyShortID] = vocabularyConfig;
        }
      });

      /* eslint-disable no-param-reassign */
      recordTypeConfig.vocabulariesByShortID = vocabulariesByShortID;
      /* eslint-enable no-param-reassign */
    }
  }

  return recordTypeConfig.vocabulariesByShortID[shortID];
};

const vocabularyServicePathPattern = /^urn:cspace:name\((.*?)\)$/;

const getVocabularyConfigByServicePath = exports.getVocabularyConfigByServicePath = (recordTypeConfig, servicePath) => {
  const match = vocabularyServicePathPattern.exec(servicePath);

  if (!match) {
    return undefined;
  }

  const shortID = match[1];

  return getVocabularyConfigByShortID(recordTypeConfig, shortID);
};

const getDefaultValue = exports.getDefaultValue = fieldDescriptor => {
  const config = fieldDescriptor[configKey];

  if (config) {
    const defaultValue = config.defaultValue;


    if (typeof defaultValue === 'object' && !_immutable2.default.Map.isMap(defaultValue)) {
      // If an object is supplied as a default value, convert it to an immutable map.

      return _immutable2.default.fromJS(defaultValue);
    }

    return defaultValue;
  }

  return undefined;
};

const getDefaults = exports.getDefaults = (fieldDescriptor, currentPath = []) => {
  let results = [];

  const defaultValue = getDefaultValue(fieldDescriptor);

  if (typeof defaultValue !== 'undefined') {
    results = results.concat({
      path: currentPath,
      value: defaultValue
    });
  }

  const childKeys = Object.keys(fieldDescriptor).filter(key => key !== configKey);

  childKeys.forEach(childKey => {
    const childPath = currentPath.concat(childKey);
    const childfieldDescriptor = fieldDescriptor[childKey];
    const childResults = getDefaults(childfieldDescriptor, childPath);

    results = results.concat(childResults);
  });

  return results;
};

const isFieldCloneable = exports.isFieldCloneable = fieldDescriptor => {
  const config = fieldDescriptor[configKey];

  if (config && 'cloneable' in config) {
    return config.cloneable;
  }

  return true;
};

const isFieldRepeating = exports.isFieldRepeating = fieldDescriptor => {
  const config = fieldDescriptor[configKey];

  if (config && 'repeating' in config) {
    return config.repeating;
  }

  return false;
};

const isFieldRequired = exports.isFieldRequired = (fieldDescriptor, recordData) => {
  const required = (0, _get2.default)(fieldDescriptor, [configKey, 'required']);

  return !!(typeof required === 'function' ? required(recordData) : required);
};

const getFieldDataType = exports.getFieldDataType = fieldDescriptor => {
  let type;

  const fieldConfig = fieldDescriptor[configKey];

  if (fieldConfig) {
    type = fieldConfig.dataType;
  }

  if (!type) {
    // Check if there are child field descriptors. If so, default to map.

    const keys = Object.keys(fieldDescriptor);

    for (let i = 0; i < keys.length; i += 1) {
      if (keys[i] !== configKey) {
        type = _dataTypes.DATA_TYPE_MAP;
        break;
      }
    }
  }

  if (!type) {
    // Default to string.

    type = _dataTypes.DATA_TYPE_STRING;
  }

  return type;
};

const getFieldCustomValidator = exports.getFieldCustomValidator = fieldDescriptor => {
  let validator;

  const fieldConfig = fieldDescriptor[configKey];

  if (fieldConfig) {
    validator = fieldConfig.validate;
  }

  return validator;
};

const getFieldComputer = exports.getFieldComputer = fieldDescriptor => {
  let computer;

  const fieldConfig = fieldDescriptor[configKey];

  if (fieldConfig) {
    computer = fieldConfig.compute;
  }

  return computer;
};

const getRequiredMessage = exports.getRequiredMessage = fieldDescriptor => (0, _get2.default)(fieldDescriptor, [configKey, 'messages', 'required']);

const isAuthority = exports.isAuthority = recordTypeConfig => (0, _get2.default)(recordTypeConfig, ['serviceConfig', 'serviceType']) === 'authority';

const isUtility = exports.isUtility = recordTypeConfig => (0, _get2.default)(recordTypeConfig, ['serviceConfig', 'serviceType']) === 'utility';

const validateLocation = exports.validateLocation = (config, location) => {
  const recordType = location.recordType,
        vocabulary = location.vocabulary,
        csid = location.csid,
        subresource = location.subresource,
        relatedRecordType = location.relatedRecordType,
        relatedCsid = location.relatedCsid;


  const recordTypeConfig = (0, _get2.default)(config, ['recordTypes', recordType]);

  if (!recordTypeConfig || recordTypeConfig.disabled) {
    return {
      error: {
        recordType,
        code: _errorCodes.ERR_UNKNOWN_RECORD_TYPE
      }
    };
  }

  if (isAuthority(recordTypeConfig)) {
    if (!vocabulary) {
      return {
        error: {
          recordType,
          code: _errorCodes.ERR_MISSING_VOCABULARY
        }
      };
    }

    const vocabularyConfig = (0, _get2.default)(recordTypeConfig, ['vocabularies', vocabulary]);

    if (!vocabularyConfig || vocabularyConfig.disabled) {
      return {
        error: {
          recordType,
          vocabulary,
          code: _errorCodes.ERR_UNKNOWN_VOCABULARY
        }
      };
    }
  } else if (vocabulary) {
    return {
      error: {
        recordType,
        vocabulary,
        code: _errorCodes.ERR_UNKNOWN_VOCABULARY
      }
    };
  }

  if (csid && !(0, _csidHelpers.isCsid)(csid) && !(0, _csidHelpers.isUrnCsid)(csid)) {
    return {
      error: {
        csid,
        code: _errorCodes.ERR_INVALID_CSID
      }
    };
  }

  if (subresource) {
    const subresourceConfig = (0, _get2.default)(config, ['subresources', subresource]);

    if (!subresourceConfig) {
      return {
        error: {
          subresource,
          code: _errorCodes.ERR_UNKNOWN_SUBRESOURCE
        }
      };
    }
  }

  if (relatedRecordType) {
    const serviceType = (0, _get2.default)(recordTypeConfig, ['serviceConfig', 'serviceType']);
    const relatedServiceType = (0, _get2.default)(config, ['recordTypes', relatedRecordType, 'serviceConfig', 'serviceType']);

    if (serviceType !== 'procedure' && serviceType !== 'object' || relatedServiceType !== 'procedure' && relatedServiceType !== 'object') {
      return {
        error: {
          recordType,
          relatedRecordType,
          code: _errorCodes.ERR_INVALID_RELATED_TYPE
        }
      };
    }
  }

  if (relatedCsid && !(0, _csidHelpers.isCsid)(relatedCsid)) {
    return {
      error: {
        csid: relatedCsid,
        code: _errorCodes.ERR_INVALID_CSID
      }
    };
  }

  return {};
};

const getDefaultSearchRecordType = exports.getDefaultSearchRecordType = config => {
  const recordTypes = config.recordTypes;

  const names = Object.keys(recordTypes);

  let defaultSearchRecordType;

  for (let i = 0; i < names.length; i += 1) {
    const name = names[i];

    if (recordTypes[name].defaultForSearch) {
      defaultSearchRecordType = name;
      break;
    }
  }

  return defaultSearchRecordType || names[0];
};

const getDefaultSearchVocabulary = exports.getDefaultSearchVocabulary = recordTypeConfig => {
  const vocabularies = recordTypeConfig.vocabularies;

  const names = Object.keys(vocabularies);

  let defaultSearchVocabulary;

  for (let i = 0; i < names.length; i += 1) {
    const name = names[i];

    if (vocabularies[name].defaultForSearch) {
      defaultSearchVocabulary = name;
      break;
    }
  }

  return defaultSearchVocabulary || names[0];
};

const findField = exports.findField = (parentFieldDescriptor, fieldName) => {
  const keys = Object.keys(parentFieldDescriptor);

  for (let i = 0; i < keys.length; i += 1) {
    const key = keys[i];
    const value = parentFieldDescriptor[key];

    if (key === fieldName) {
      return value;
    }

    if (key !== configKey) {
      const fieldDescriptor = findField(value, fieldName);

      if (fieldDescriptor) {
        return fieldDescriptor;
      }
    }
  }

  return null;
};

const findFieldConfigInPart = exports.findFieldConfigInPart = (recordTypeConfig, partName, fieldName) => {
  const partDescriptor = (0, _get2.default)(recordTypeConfig, ['fields', 'document', `${_recordDataHelpers.NS_PREFIX}:${partName}`]);

  if (!partDescriptor) {
    return null;
  }

  // 1. Look for the field as a direct child of the part.

  let fieldDescriptor = partDescriptor[fieldName];

  // 2. Check for a memoized result.

  if (!fieldDescriptor) {
    fieldDescriptor = (0, _get2.default)(recordTypeConfig, ['fieldsByPart', partName, fieldName]);
  }

  // 3. Search for a nested field.

  if (typeof fieldDescriptor === 'undefined') {
    fieldDescriptor = findField(partDescriptor, fieldName);

    // Memoize the result.

    (0, _set2.default)(recordTypeConfig, ['fieldsByPart', partName, fieldName], fieldDescriptor);
  }

  return fieldDescriptor ? fieldDescriptor[configKey] : null;
};

const findFieldsWithSource = (fieldDescriptor, shortId) => {
  const fieldsWithSource = (0, _flatMap2.default)(Object.keys(fieldDescriptor).filter(key => key !== configKey), childFieldName => findFieldsWithSource(fieldDescriptor[childFieldName], shortId));

  const fieldConfig = fieldDescriptor[configKey];
  const source = (0, _get2.default)(fieldConfig, ['view', 'props', 'source']);

  if (source === shortId) {
    fieldsWithSource.push(fieldConfig);
  }

  return fieldsWithSource;
};

const findVocabularyUses = exports.findVocabularyUses = (config, shortId) => {
  if (!shortId) {
    return null;
  }

  const uses = {};

  Object.values(config.recordTypes).forEach(recordTypeConfig => {
    const fieldDescriptor = recordTypeConfig.fields;

    if (fieldDescriptor) {
      const fields = findFieldsWithSource(recordTypeConfig.fields, shortId);

      if (fields.length > 0) {
        uses[recordTypeConfig.name] = fields;
      }
    }
  });

  return uses;
};

const getFirstColumnName = exports.getFirstColumnName = (config, recordType, columnSetName = 'default') => {
  const columnConfig = (0, _get2.default)(config, ['recordTypes', recordType, 'columns', columnSetName]);

  if (!columnConfig) {
    return undefined;
  }

  const orderedColumnNames = Object.keys(columnConfig).filter(name => !columnConfig[name].disabled).sort((nameA, nameB) => {
    const orderA = columnConfig[nameA].order;
    const orderB = columnConfig[nameB].order;

    return orderA - orderB;
  });

  return orderedColumnNames[0];
};