import { executeInterceptors, generateInterceptor } from '../interceptors/InterceptorGenerator';
import { ValidatorNotEmpty } from '../interceptors/validators/ValidatorNotEmpty';
import { FormContext } from './FormContext';
export const defaultProps = {
  extraButtons: [],
  layout: 'vertical',
  loading: false,
  passErrorsToSubmit: false,
};
/**
 * Generates a context, default language is 'en'.
 * TODO the language and dictionary structure should be changed to something that makes more sense
 * @param language
 */
export function generateContext(language) {
  return new FormContext(language);
}
/**
 * Generates the initial state from the props.
 * @param props
 * @param currentState
 */
export function generateState(fields, currentState) {
  const values = {};
  const errors = {};
  const visibilities = {};
  // TODO any
  const interceptors = {};
  for (const field of fields) {
    const value = (currentState && currentState.values) ? currentState.values[field.id] : undefined;
    values[field.id] = field.value || value;
    errors[field.id] = [];
    visibilities[field.id] = typeof field.visible === 'boolean' ? field.visible : true;
    const hasValidatorNotEmpty = (field.interceptors
      && field.interceptors.onSubmit
      && !!field.interceptors.onSubmit.find((i) => i.id === 'validatorNotEmpty'));
    const getInterceptors = (ints, type) => ints ? (ints[type] || []) : [];
    if (field.required && !hasValidatorNotEmpty) {
      // add validatorNotEmpty if the field is required and validatorNotEmpty is not in onSubmit interceptors
      field.interceptors = Object.assign(Object.assign({}, (field.interceptors || {})), { onSubmit: [new ValidatorNotEmpty(), ...getInterceptors(field.interceptors, 'onSubmit')] });
    }
    interceptors[field.id] = {
      onBlur: getInterceptors(field.interceptors, 'onBlur').map((conf) => generateInterceptor(conf)),
      onChange: getInterceptors(field.interceptors, 'onChange').map((conf) => generateInterceptor(conf)),
      onFormChange: getInterceptors(field.interceptors, 'onFormChange').map((conf) => generateInterceptor(conf)),
      onSubmit: getInterceptors(field.interceptors, 'onSubmit').map((conf) => generateInterceptor(conf)),
    };
  }
  return {
    context: generateContext(),
    errors: {},
    interceptors,
    values,
    visibilities,
  };
}
/**
 * Generates a new state when the value of a field changes.
 *
 * @param fields
 * @param state is the current state.
 * @param field that whose value is changed.
 * @param value is the new value of the given field.
 */
export function generateStateOnFieldChange(fields, state, field, value) {
  const { context, errors, interceptors, values, visibilities } = state;
  const newErrors = Object.assign({}, errors);
  const newValues = Object.assign({}, values);
  const newVisibilities = Object.assign({}, visibilities);
  // execute the interceptors on the changed field, they may modify the given value
  const result = executeInterceptors(context, values, field, value, visibilities[field.id], interceptors[field.id], 'onChange');
  newErrors[field.id] = result.error ? [result.error] : [];
  newValues[field.id] = result.value;
  // execute the onFormChange interceptors on all fields
  for (const otherField of fields) {
    if (otherField.id === field.id) {
      // skip the changed field
      continue;
    }
    const otherFieldResult = executeInterceptors(context, newValues, field, value, visibilities[otherField.id], interceptors[otherField.id], 'onFormChange', otherField);
    newVisibilities[otherField.id] = typeof otherFieldResult.visible === 'boolean' ? otherFieldResult.visible : true;
    if (newVisibilities[otherField.id]) {
      newErrors[otherField.id] = otherFieldResult.error ? [otherFieldResult.error] : [...(errors[otherField.id] || [])];
    }
    else {
      // ignore errors if the field is not visible
      newErrors[otherField.id] = [];
    }
    newValues[otherField.id] = otherFieldResult.value;
  }
  // override the previous errors of this field. the user should not see the old
  // errors before modification because modification may invalidate the existing errors
  return Object.assign(Object.assign({}, state), { errors: newErrors, values: newValues, visibilities: newVisibilities });
}
/**
 * Generates a new state when the field loses the focus.
 *
 * @param state is the current state.
 * @param field that loses the focus.
 */
export function generateStateOnFieldBlur(state, field) {
  const { context, errors, interceptors, values, visibilities } = state;
  // get the existing error of the field
  const fieldErrors = [...(errors[field.id] || [])];
  // get the value from the state
  const value = values[field.id];
  // execute the interceptors, they may modify the given value
  const result = executeInterceptors(context, values, field, value, visibilities[field.id], interceptors[field.id], 'onBlur');
  const newErrorState = Object.assign({}, errors);
  if (result.error) {
    newErrorState[field.id] = [...fieldErrors, result.error];
  }
  return Object.assign(Object.assign({}, state), { errors: newErrorState, values: Object.assign(Object.assign({}, values), { [field.id]: result.value }) });
}
/**
 * Executes the onSubmit interceptors on all fields and returns the field -> error map.
 * @param context
 * @param fields
 * @param interceptors
 * @param values
 * @param visibilities
 */
export function executeOnSubmitInterceptors(context, fields, interceptors, values, visibilities) {
  const errors = {};
  // execute onSubmit interceptors on every field
  for (const field of fields) {
    const value = values[field.id];
    const isFieldVisible = visibilities[field.id];
    // execute the onBlur interceptors first, the form may have been submitted before the last focused field looses the focus
    const onBlurResult = executeInterceptors(context, values, field, value, visibilities[field.id], interceptors[field.id], 'onBlur');
    const fieldErrors = [];
    if (isFieldVisible && onBlurResult.error) {
      fieldErrors.push(onBlurResult.error);
    }
    // execute the onSubmit interceptors
    const result = executeInterceptors(context, values, field, value, visibilities[field.id], interceptors[field.id], 'onSubmit');
    if (isFieldVisible && result.error) {
      fieldErrors.push(result.error);
    }
    errors[field.id] = fieldErrors;
  }
  return errors;
}
/**
 * Checks whether the errors map has any error and returns the first (field order) found error.
 * @param errors to check.
 * @return the error if exists, nothing otherwise.
 */
export function getFirstError(errors) {
  let error;
  for (const fieldId of Object.keys(errors)) {
    if (errors[fieldId]) {
      error = errors[fieldId].find((e) => !!e);
      if (error) {
        return error;
      }
    }
  }
  return undefined;
}
/**
 * Merges the given id -> error maps into one map.
 * @param errors1
 * @param errors2
 */
export function mergeErrors(errors1, errors2) {
  if (!errors1) {
    return errors2;
  }
  else if (!errors2) {
    return errors1;
  }
  const errors = {};
  const uniqueIds = Array.from(new Set([...Object.keys(errors1), ...Object.keys(errors2)]));
  for (const id of uniqueIds) {
    if (!errors1[id]) {
      errors[id] = errors2[id];
    }
    else if (!errors2[id]) {
      errors[id] = errors1[id];
    }
    else {
      errors[id] = [...errors1[id], ...errors2[id]];
    }
  }
  return errors;
}
/**
 * Executes the button callback and returns a new state with the values returned from the callback.
 * Returns nothing if the callback returns nothing.
 * @param fields
 * @param state
 * @param callback
 * @param passErrorsToCallback is used to call callback even though there are errors.
 */
export function handleButtonClick(fields, state, callback, passErrorsToCallback) {
  const { context, errors, interceptors, values, visibilities } = state;
  // retrieve errors that the interceptors return on submit
  const onSubmitErrors = executeOnSubmitInterceptors(context, fields, interceptors, values, visibilities);
  if (!passErrorsToCallback && getFirstError(mergeErrors(errors, onSubmitErrors))) {
    // skip the rest if the errors should not be passed to the callback
    return Object.assign(Object.assign({}, state), { errors: mergeErrors(errors, onSubmitErrors), values });
  }
  const response = callback(values, mergeErrors(errors, onSubmitErrors), context);
  if (!response || (!response.errors && !response.values)) {
    return Object.assign(Object.assign({}, state), { errors: mergeErrors(errors, onSubmitErrors), values });
  }
  const newState = Object.assign({}, state);
  if (response && response.errors) {
    newState.errors = response.errors;
  }
  if (response && response.values) {
    newState.values = response.values;
  }
  return newState;
}
