import ButtonContext from '../silke-button/context';
import { FormFieldProps, FormValidator } from '../silke-form';
import { FieldRegisterModel, FormObjectProvider } from '../silke-form/form-context';
import { useFormField } from '../../hooks/form';
import React, { useCallback, useRef, useState } from 'react';

export type ErrorMap<T extends { [key: string]: any }> = { [key in keyof T]?: string };
export interface SilkeObjectFieldProps<T extends { [key: string]: any }> extends FormFieldProps<T> {
  children?: React.ReactNode;
  showAllErrors?: boolean;
  errors?: ErrorMap<T>;
  onErrors?: (errors: ErrorMap<T>) => void;
}

const validator: FormValidator<SilkeObjectFieldProps<any>> = (props, value) => undefined;

export function hasErrors(errors: ErrorMap<any>): boolean {
  for (const key in errors) {
    if (errors[key]) return true;
  }

  return false;
}

export function SilkeObjectField<T extends Record<string, any>>(props: SilkeObjectFieldProps<T>) {
  const {
    children,
    value = {} as T,
    errors: externalErrors,
    onErrors,
    onChange,
  } = useFormField(props, validator);
  const fieldRefs = useRef<any>({});
  const [hasHiddenErrors, setHasHiddenErrors] = useState<boolean>(false);
  const hiddenErrorRefs = useRef<ErrorMap<T>>({});
  const [errors, setErrors] = useState<ErrorMap<T>>({});

  // Need to copy errors and value into ref so the un-registerField can clear errors and value related to a field
  const errorsRef = useRef<ErrorMap<T>>();
  const valueRef = useRef<T>(value);
  errorsRef.current = errors;
  valueRef.current = value || {};

  const fieldHasErrors = hasErrors(errors) || hasHiddenErrors;

  const validateField = (name: keyof T) => {
    const error = fieldRefs.current[name]?.getError();
    delete hiddenErrorRefs.current[name];
    const newHasHiddenErrors = hasErrors(hiddenErrorRefs.current);
    if (hasHiddenErrors !== newHasHiddenErrors) setHasHiddenErrors(newHasHiddenErrors);
    errorsRef.current = { ...errors, [name]: error };
    setErrors(errorsRef.current);
    if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current });
  };

  const registerField = useCallback(
    (name: keyof T, v: FieldRegisterModel) => {
      fieldRefs.current[name] = v;
      const error = v.getError();
      if (error) hiddenErrorRefs.current[name] = error;
      else delete hiddenErrorRefs.current[name];

      setHasHiddenErrors(hasErrors(hiddenErrorRefs.current));
      if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current });

      return () => {
        // Remove hidden errors if field disappears
        const hiddenErrors = hiddenErrorRefs.current as ErrorMap<T>;
        if (hiddenErrors) delete hiddenErrors[name];

        // Remove error if field disappears
        const newErrors = { ...errorsRef.current } as ErrorMap<T>;
        delete newErrors[name];
        setErrors(newErrors);

        setHasHiddenErrors(hasErrors(hiddenErrorRefs.current));
        if (onErrors) onErrors({ ...hiddenErrorRefs.current, ...errorsRef.current });

        // This code deletes values if the form un mounts
        // it is supposed to delete value if field unmounts,
        // but react unmounts bottom up so seems impossible to detect if form is unmounted
        // if (onChange) {
        //   const newValue = { ...valueRef.current };
        //   delete newValue[name];
        //   onChange(newValue);
        // }
      };
    },
    [hiddenErrorRefs],
  );

  return (
    <FormObjectProvider
      value={value || {}}
      valueRef={valueRef}
      errors={{ ...props.errors, ...errors, ...externalErrors }}
      onChange={(name, update) => {
        const newValue = {
          ...value,
          [name]: update,
        } as T;
        valueRef.current = newValue;
        validateField(name);
        if (onChange) onChange(newValue);
      }}
      validate={validateField}
      register={registerField}
    >
      <ButtonContext disableSubmit={fieldHasErrors} gap="m">
        {children}
      </ButtonContext>
    </FormObjectProvider>
  );
}
