import React, {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react";

type ProviderProps<S> = {
  children: React.ReactChild;
  initialContextValue?: S;
};

type ContextType<T> = {
  context: T;
  setContext: (T) => void;
};

type ContextOptions<T, S> = {
  selector: (context: S, ...hookArgs: any[]) => T | null;
  prepareContext: (newContext: T, currentContext?: S, ...hookArgs: any[]) => S;
};

export function createZappPipesContext<T, S = T>(
  initialContext: ContextType<S>,
  options?: ContextOptions<T, S>
) {
  const Context = createContext<ContextType<S>>(initialContext);

  const defaultSelector = (c: any) => c;
  const defaultPrepareContext = (n: any) => n;
  const joinArgs = (args: any[]) => args.join("-");

  const { selector = defaultSelector, prepareContext = defaultPrepareContext } =
    options || {};

  function useZappPipesContext(...hookArgs: any[]): [T, (T) => void] {
    const { context, setContext } = useContext(Context);
    const joinedArgs = joinArgs(hookArgs);

    const contextValue = useMemo(
      () => selector(context, ...hookArgs),
      // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
      [context, joinedArgs]
    );

    const contextSetter = useCallback(
      (newContext: T) => {
        setContext(prepareContext(newContext, context, ...hookArgs));
      },
      // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
      [context, joinedArgs]
    );

    return useMemo(
      () => [contextValue, contextSetter],
      [contextValue, contextSetter]
    );
  }

  /** Provider accepts `initialContextValue` prop to set the initial context value */
  function Provider({ children, initialContextValue }: ProviderProps<S>) {
    const [context, setContext] = useState<S>(
      initialContextValue ?? initialContext.context
    );

    return (
      <Context.Provider value={{ context, setContext }}>
        {children}
      </Context.Provider>
    );
  }

  function withProvider(Component: React.ComponentType<any>) {
    return function WithProvider(props: any) {
      return (
        <Provider>
          <Component {...props} />
        </Provider>
      );
    };
  }

  return {
    Context,
    useZappPipesContext,
    Provider,
    withProvider,
  };
}
