import React, {
  createContext,
  useContext,
  useCallback,
  useMemo,
  RefObject,
  ReactNode,
} from 'react';

import { ButtonProps } from '../../Button/Button';
import { FilterValues } from './types';
import { isDefaultValue } from './utils';
import { noop } from '../../../fp/function';

export type FiltersContextType = {
  applyButtonProps?: ButtonProps;
  clearButtonProps?: Omit<ButtonProps, 'onClick'>;
  isGrouped: boolean;
  onSave?: (values: FilterValues) => void;
  onValuesChange: (values: FilterValues) => void;
  selectedTagsRef: RefObject<HTMLElement>;
  values: FilterValues;
};

const FiltersContext = createContext<FiltersContextType>({
  values: {},
  onValuesChange: noop,
  isGrouped: false,
  selectedTagsRef: { current: null },
});

export const FiltersProvider = <T extends FilterValues>({
  value,
  children,
}: {
  children: ReactNode;
  value: Omit<FiltersContextType, 'onValuesChange' | 'onSave'> & {
    onSave?: (v: T) => void;
    onValuesChange: (v: T) => void;
  };
}) => {
  const { onValuesChange, onSave } = value;
  const retypedOnValuesChange = useCallback(
    (v: FilterValues) => {
      onValuesChange(v as T);
    },
    [onValuesChange]
  );
  const retypedOnSave = useCallback((v: FilterValues) => onSave?.(v as T), [
    onSave,
  ]);
  const providerValue = useMemo(
    () => ({
      ...value,
      onValuesChange: retypedOnValuesChange,
      onSave: retypedOnSave,
    }),
    [value, retypedOnValuesChange, retypedOnSave]
  );

  return (
    <FiltersContext.Provider value={providerValue}>
      {children}
    </FiltersContext.Provider>
  );
};

export const useFilterState = <T extends FilterValues, K extends keyof T>(
  key: K
) => {
  const { values, onValuesChange, onSave } = useContext(FiltersContext);

  const value = (values as T)[key];
  const onChange = useCallback(
    (newValue: T[K]) =>
      onValuesChange({
        ...values,
        [key]: newValue,
      }),
    [key, values, onValuesChange]
  );
  const changeAndSave = useCallback(
    (newValue: T[K]) => {
      onChange(newValue);
      onSave?.({
        [key]: newValue,
      });
    },
    [key, onChange, onSave]
  );

  return [value, onChange, changeAndSave] as const;
};

export const useChangeGroupFilter = <T extends FilterValues, K extends keyof T>(
  keys: K[]
) => {
  const { values, onValuesChange, onSave } = useContext(FiltersContext);
  const getNewFilterValues = useCallback(
    (newValues: Array<T[K]>) =>
      keys.reduce<Partial<T>>(
        (filterValuesObj, key, i) => ({
          ...filterValuesObj,
          [key]: newValues[i],
        }),
        {}
      ),
    [keys]
  );

  const onChange = useCallback(
    (newValues: Array<T[K]>) =>
      onValuesChange({
        ...values,
        ...getNewFilterValues(newValues),
      }),
    [getNewFilterValues, onValuesChange, values]
  );
  const changeAndSaveGroup = useCallback(
    (newValues: Array<T[K]>) => {
      onChange(newValues);
      onSave?.(getNewFilterValues(newValues));
    },
    [getNewFilterValues, onChange, onSave]
  );

  return [changeAndSaveGroup] as const;
};

export const useDirtyCheck = <T extends FilterValues, K extends keyof T>(
  key: K
) => {
  const { values } = useContext(FiltersContext);
  const value = (values as T)[key];

  return !isDefaultValue(value);
};

export const useGroupDirtyCheck = <T extends FilterValues, K extends keyof T>(
  keys: K[]
) => {
  const { values } = useContext(FiltersContext);

  const isGroupDirty = useMemo(
    () =>
      keys.reduce(
        (current, key) => current || !isDefaultValue((values as T)[key]),
        false
      ),
    [values, keys]
  );

  return isGroupDirty;
};

export default FiltersContext;
