import React, {FormEvent, useCallback, useState} from 'react';
import _ from 'lodash';
import {TForm, TFormError, TFormFields, TFormErrors} from '../types';

const Form = ({
  className,
  action,
  method = 'GET',
  initialValues,
  onChange,
  onValidate,
  onValid,
  onSubmit,
  onError,
  children
}: TForm): JSX.Element => {
  const [isSubmitting, setSubmitting] = useState(false);
  const [isValid, setValid] = useState(false);
  const [isSubmitted, setSubmitted] = useState(false);
  const [fields, setFields] = useState<TFormFields>(
    _.mapValues(initialValues, value => ({
      value: value,
      error: false,
      touched: false
    }))
  );
  const [error, setError] = useState<TFormError>(null);

  const handleValidate = useCallback(
    (targetFields = fields, {ignoreTouched} = {}): void => {
      if (_.isFunction(onValidate)) {
        onValidate({fields: targetFields, isSubmitted, isValid})
          .then((validatedFields: TFormErrors) => {
            if (_.isObject(validatedFields)) {
              _.forIn(targetFields, ({value, touched}, name) => {
                targetFields[name] = {
                  value,
                  touched,
                  error: ignoreTouched
                    ? touched && Boolean(validatedFields[name])
                    : Boolean(validatedFields[name])
                };
              });
            }

            setFields(targetFields);
            if (
              _.isEmpty(validatedFields) ||
              !_.every(validatedFields, Boolean)
            ) {
              !isValid && setValid(true);
              _.isFunction(onValid) &&
                onValid({fields: targetFields, isSubmitted, isValid});
            } else {
              isValid && setValid(false);
            }
          })
          .catch(_.noop);
      } else {
        setValid(true);
        _.isFunction(onValid) &&
          onValid({fields: targetFields, isSubmitted, isValid});
        setFields(
          _.mapValues(targetFields, ({value, touched}) => ({
            value,
            touched,
            error: false
          }))
        );
      }
    },
    [fields, onValidate, isSubmitted, isValid, onValid]
  );

  const handleChange = useCallback(
    (name: string, value: string | undefined): void => {
      setError(null);

      const newFields = {
        ...fields,
        [name]: {
          value,
          touched: value !== initialValues[name],
          error: fields[name].error
        }
      };

      handleValidate(newFields, {ignoreTouched: true});
    },
    [fields, initialValues, handleValidate]
  );

  const updateForm = useCallback((): void => {
    handleValidate();

    if (_.isFunction(onChange)) {
      onChange({fields, isSubmitted, isValid})
        .then(newFields => {
          if (newFields && !_.isEmpty(newFields)) {
            setFields(newFields);
          }
        })
        .catch(_.noop);
    }
  }, [fields, isValid, isSubmitted, onChange, handleValidate]);

  const handleReset = useCallback((): void => {
    setFields(
      _.mapValues(initialValues, value => ({
        value: value,
        error: false,
        touched: false
      }))
    );
    setSubmitted(false);
    setError(null);
  }, [initialValues]);

  const handleSubmit = useCallback(
    (event: FormEvent): void => {
      event.preventDefault();
      setSubmitting(true);

      _.isFunction(onSubmit) &&
        onSubmit({fields, isSubmitted, isValid, resetForm: handleReset})
          .then(() => {
            setSubmitted(true);
          })
          .catch(error => {
            setError(error);
            _.isFunction(onError) &&
              onError({
                fields,
                isSubmitted,
                isValid,
                error,
                resetForm: handleReset
              });
          })
          .finally(() => setSubmitting(false));
    },
    [onSubmit, fields, isSubmitted, isValid, handleReset, onError]
  );

  const renderChildren = useCallback(
    () =>
      children({
        fields,
        isValid,
        isSubmitted,
        isSubmitting,
        updateForm,
        formError: error,
        onChange: handleChange,
        submitForm: handleSubmit,
        resetForm: handleReset
      }),
    [
      isValid,
      isSubmitted,
      isSubmitting,
      children,
      fields,
      error,
      handleReset,
      handleChange,
      updateForm,
      handleSubmit
    ]
  );

  return (
    <form
      className={className}
      action={action}
      method={method}
      onSubmit={handleSubmit}>
      {renderChildren()}
    </form>
  );
};

export default Form;
