import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import debounce from 'lodash/debounce';

// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any

/**
 *
 * Directly map object values
 *
 * @private
 * @param object The object to transform
 * @param mapFunction The function to map an old value to new one
 * @returns Object with the same key but transformed values
 *
 */
function mapValues(object, mapFunction) {
  return Object.entries(object).reduce((acc, [key, value]) => ({
    ...acc,
    [key]: mapFunction(value)
  }), {});
}

// When we use the `useSharedPluginStateWithSelector` example: `useSharedPluginStateWithSelector(api, ['width'], selector)`
// it will re-render every time the component re-renders as the array "['width']" is seen as an update.
// This hook is used to prevent re-renders due to this.
function useStaticPlugins(plugins) {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useMemo(() => plugins, []);
}

/**
 *
 * ⚠️⚠️⚠️ This is a debounced hook ⚠️⚠️⚠️
 * If the plugins you are listening to generate multiple shared states while the user is typing,
 * your React Component will get only the last one.
 *
 * Used to return the current plugin state of input dependencies.
 *
 * @example
 * Example in plugin:
 *
 * ```typescript
 * function selector(states: NamedPluginStatesFromInjectionAPI<API, ['dog']>) {
 *  return {
 *   title: states.dogState?.title,
 *  }
 * }
 *
 * function ExampleContent({ api }: Props) {
 *   const { title } = useSharedPluginStateWithSelector(
 *     api,
 *     ['dog'],
 *     selector
 *   )
 *   return <p>{ title }</p>
 * }
 *
 * ```
 *
 * @param injectionApi Plugin injection API from `NextEditorPlugin`
 * @param plugins Plugin names to get the shared plugin state for
 * @param selector A function that takes the shared states of the plugins and returns a subset of a plugin state.
 * @returns A corresponding object, the keys are names of the plugin with `State` appended,
 * the values are the shared state exposed by that plugin.
 */
export function useSharedPluginStateWithSelector(injectionApi, plugins, selector) {
  const pluginNames = useStaticPlugins(plugins);
  const selectorRef = useRef(selector);

  // Create a memoized object containing the named plugins
  const namedExternalPlugins = useMemo(() => pluginNames.reduce((acc, pluginName) => ({
    ...acc,
    [`${String(pluginName)}State`]: injectionApi === null || injectionApi === void 0 ? void 0 : injectionApi[pluginName]
  }), {}), [injectionApi, pluginNames]);
  return useSharedPluginStateInternal(namedExternalPlugins, selectorRef);
}
function useSharedPluginStateInternal(externalPlugins, selector) {
  const refStates = useRef(mapValues(externalPlugins, value => value === null || value === void 0 ? void 0 : value.sharedState.currentState()));
  const [pluginStates, setPluginState] = useState(() => selector.current(refStates.current));
  const mounted = useRef(false);
  useLayoutEffect(() => {
    const debouncedPluginStateUpdate = debounce(() => {
      setPluginState(currentPluginStates => {
        const nextStates = selector.current({
          ...refStates.current
        });
        if (shallowEqual(nextStates, currentPluginStates)) {
          return currentPluginStates;
        }
        return nextStates;
      });
    });

    // If we re-render this hook due to a change in the external
    // plugins we need to push a state update to ensure we have
    // the most current state.
    if (mounted.current) {
      refStates.current = mapValues(externalPlugins, value => value === null || value === void 0 ? void 0 : value.sharedState.currentState());
      debouncedPluginStateUpdate();
    }
    const unsubs = Object.entries(externalPlugins).map(([pluginKey, externalPlugin]) => {
      return externalPlugin === null || externalPlugin === void 0 ? void 0 : externalPlugin.sharedState.onChange(({
        nextSharedState,
        prevSharedState
      }) => {
        if (prevSharedState === nextSharedState) {
          return;
        }
        refStates.current[pluginKey] = nextSharedState;
        debouncedPluginStateUpdate();
      });
    });
    mounted.current = true;
    return () => {
      refStates.current = {};
      unsubs.forEach(cb => cb === null || cb === void 0 ? void 0 : cb());
    };
  }, [externalPlugins, selector]);
  return pluginStates;
}
function shallowEqual(objA, objB) {
  if (objA === objB) {
    return true;
  }
  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) {
    return false;
  }
  for (const key of keysA) {
    if (objA[key] !== objB[key]) {
      return false;
    }
  }
  return true;
}