import React, { ComponentProps, ElementType, LegacyRef } from 'react';
import { FieldNameContext } from './FieldNameContext';
import { InjectedFieldProps } from './InjectedFieldProps';
import useStandardFormInput from './useStandardField';
import { NormalizationFunction } from '../Normalization/NormalizationFunction';
import { ValidationFunction } from '../Validation/ValidationFunction';
import { required as requiredValidator } from '../Validation/validators';

// we attempted to support generic components but failed
// so, we assume the actual TRenderComponent has no generic arguments
// instead, any concrete TRenderComponent can utilize a TRenderComponent<TValue> as needed

export type RenderComponent<
  TValue,
  TRenderComponent extends ElementType
> = Partial<ComponentProps<TRenderComponent>> extends Partial<
  InjectedFieldProps<TValue | undefined | null>
>
  ? TRenderComponent
  : never;

export type RenderComponentProps<
  TValue,
  TRenderComponent extends ElementType
> = Partial<ComponentProps<TRenderComponent>> extends Partial<
  InjectedFieldProps<TValue | undefined | null>
>
  ? ComponentProps<TRenderComponent>
  : never;

/** A specific Field instance to be rendered by the given TRenderComponent or by whatever default is reasonable */
export type FieldProps<
  TForm extends object,
  TProp extends keyof TForm,
  TRenderComponent extends ElementType
> = {
  /** Name of the field. Used on submission. */
  name: TProp; // somewhat duplicated from useStandardFormInputProps but better for autocomplete
  /** Component to be rendered. Usually this is a type of input group e.g. `<StringInputGroup/>` */
  Component: RenderComponent<TForm[TProp], TRenderComponent>;
  /** Id of the field. */
  id?: string;
  /** Whether the field should be disabled. */
  disabled?: boolean;
  /** Client side validation functions */
  validate?:
    | ValidationFunction<TForm[TProp]>
    | ValidationFunction<TForm[TProp]>[];
  /** Function to modify the field value without making the form dirty. (e.g. phone number) */
  normalize?: NormalizationFunction<TForm[TProp]>;
} & Omit<
  RenderComponentProps<TForm[TProp], TRenderComponent>,
  keyof InjectedFieldProps<TForm[TProp]>
>;

/**
 * Renders whatever Component is passed - injecting the formik values needed to finish wiring up that individual field.
 * Should no Component be used then the default will be provided by the default lookup based on typeof(TForm[TProp])
 */
function Field<
  TForm extends object,
  TProp extends keyof TForm,
  TRenderComponent extends ElementType
>(
  {
    name,
    Component,
    id,
    disabled,
    validate,
    normalize,
    ...rest
  }: FieldProps<TForm, TProp, TRenderComponent>,
  ref: LegacyRef<any>
) {
  const [input, meta] = useStandardFormInput<TForm[TProp]>({
    name: String(name),
    id: id,
    disabled: disabled,
    validate: validate,
    normalize: normalize,
  });

  const isRequired =
    rest?.required !== undefined
      ? rest.required
      : validate === requiredValidator ||
        (Array.isArray(validate) && validate.includes(requiredValidator));

  // a bit of a hack so JSX is happy with us
  const Wrapped = Component as React.ComponentType<
    InjectedFieldProps<TForm[TProp]>
  >;

  return (
    <FieldNameContext.Provider value={input.name}>
      <Wrapped
        {...rest}
        ref={ref}
        id={input.id}
        input={input}
        meta={meta}
        required={isRequired}
        disabled={disabled}
      />
    </FieldNameContext.Provider>
  );
}

// hack to get forwarded refs to work
const FieldWithRef = React.forwardRef(Field as any);

export default FieldWithRef as typeof Field;
