import { weakMapMemoize } from './weakMapMemoize'

import type {
  Combiner,
  CreateSelectorOptions,
  DropFirstParameter,
  ExtractMemoizerFields,
  GetParamsFromSelectors,
  GetStateFromSelectors,
  InterruptRecursion,
  OutputSelector,
  Selector,
  SelectorArray,
  SetRequired,
  Simplify,
  UnknownMemoizer
} from './types'

import {
  assertIsFunction,
  collectInputSelectorResults,
  ensureIsArray,
  getDependencies,
  getDevModeChecksExecutionInfo
} from './utils'

/**
 * An instance of `createSelector`, customized with a given memoize implementation.
 *
 * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
 * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). If none is explicitly provided, `weakMapMemoize` will be used.
 * @template StateType - The type of state that the selectors created with this selector creator will operate on.
 *
 * @public
 */
export interface CreateSelectorFunction<
  MemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
  ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
  StateType = any
> {
  /**
   * Creates a memoized selector function.
   *
   * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function.
   * @returns A memoized output selector.
   *
   * @template InputSelectors - The type of the input selectors as an array.
   * @template Result - The return type of the `combiner` as well as the output selector.
   * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
   * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
   *
   * @see {@link https://reselect.js.org/api/createselector `createSelector`}
   */
  <InputSelectors extends SelectorArray<StateType>, Result>(
    ...createSelectorArgs: [
      ...inputSelectors: InputSelectors,
      combiner: Combiner<InputSelectors, Result>
    ]
  ): OutputSelector<
    InputSelectors,
    Result,
    MemoizeFunction,
    ArgsMemoizeFunction
  > &
    InterruptRecursion

  /**
   * Creates a memoized selector function.
   *
   * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object.
   * @returns A memoized output selector.
   *
   * @template InputSelectors - The type of the input selectors as an array.
   * @template Result - The return type of the `combiner` as well as the output selector.
   * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
   * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
   *
   * @see {@link https://reselect.js.org/api/createselector `createSelector`}
   */
  <
    InputSelectors extends SelectorArray<StateType>,
    Result,
    OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
    OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
  >(
    ...createSelectorArgs: [
      ...inputSelectors: InputSelectors,
      combiner: Combiner<InputSelectors, Result>,
      createSelectorOptions: Simplify<
        CreateSelectorOptions<
          MemoizeFunction,
          ArgsMemoizeFunction,
          OverrideMemoizeFunction,
          OverrideArgsMemoizeFunction
        >
      >
    ]
  ): OutputSelector<
    InputSelectors,
    Result,
    OverrideMemoizeFunction,
    OverrideArgsMemoizeFunction
  > &
    InterruptRecursion

  /**
   * Creates a memoized selector function.
   *
   * @param inputSelectors - An array of input selectors.
   * @param combiner - A function that Combines the input selectors and returns an output selector. Otherwise known as the result function.
   * @param createSelectorOptions - An optional options object that allows for further customization per selector.
   * @returns A memoized output selector.
   *
   * @template InputSelectors - The type of the input selectors array.
   * @template Result - The return type of the `combiner` as well as the output selector.
   * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
   * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
   *
   * @see {@link https://reselect.js.org/api/createselector `createSelector`}
   */
  <
    InputSelectors extends SelectorArray<StateType>,
    Result,
    OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
    OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
  >(
    inputSelectors: [...InputSelectors],
    combiner: Combiner<InputSelectors, Result>,
    createSelectorOptions?: Simplify<
      CreateSelectorOptions<
        MemoizeFunction,
        ArgsMemoizeFunction,
        OverrideMemoizeFunction,
        OverrideArgsMemoizeFunction
      >
    >
  ): OutputSelector<
    InputSelectors,
    Result,
    OverrideMemoizeFunction,
    OverrideArgsMemoizeFunction
  > &
    InterruptRecursion

  /**
   * Creates a "pre-typed" version of {@linkcode createSelector createSelector}
   * where the `state` type is predefined.
   *
   * This allows you to set the `state` type once, eliminating the need to
   * specify it with every {@linkcode createSelector createSelector} call.
   *
   * @returns A pre-typed `createSelector` with the state type already defined.
   *
   * @example
   * ```ts
   * import { createSelector } from 'reselect'
   *
   * export interface RootState {
   *   todos: { id: number; completed: boolean }[]
   *   alerts: { id: number; read: boolean }[]
   * }
   *
   * export const createAppSelector = createSelector.withTypes<RootState>()
   *
   * const selectTodoIds = createAppSelector(
   *   [
   *     // Type of `state` is set to `RootState`, no need to manually set the type
   *     state => state.todos
   *   ],
   *   todos => todos.map(({ id }) => id)
   * )
   * ```
   * @template OverrideStateType - The specific type of state used by all selectors created with this selector creator.
   *
   * @see {@link https://reselect.js.org/api/createselector#defining-a-pre-typed-createselector `createSelector.withTypes`}
   *
   * @since 5.1.0
   */
  withTypes: <OverrideStateType extends StateType>() => CreateSelectorFunction<
    MemoizeFunction,
    ArgsMemoizeFunction,
    OverrideStateType
  >
}

/**
 * Creates a selector creator function with the specified memoization function
 * and options for customizing memoization behavior.
 *
 * @param options - An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional.
 * @returns A customized `createSelector` function.
 *
 * @example
 * ```ts
 * const customCreateSelector = createSelectorCreator({
 *   memoize: customMemoize, // Function to be used to memoize `resultFunc`
 *   memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards
 *   argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments
 *   argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards
 * })
 *
 * const customSelector = customCreateSelector(
 *   [inputSelector1, inputSelector2],
 *   resultFunc // `resultFunc` will be passed as the first argument to `customMemoize`
 * )
 *
 * customSelector(
 *   ...selectorArgs // Will be memoized by `customArgsMemoize`
 * )
 * ```
 *
 * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
 * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). If none is explicitly provided, `weakMapMemoize` will be used.
 *
 * @see {@link https://reselect.js.org/api/createSelectorCreator#using-options-since-500 `createSelectorCreator`}
 *
 * @since 5.0.0
 * @public
 */
export function createSelectorCreator<
  MemoizeFunction extends UnknownMemoizer,
  ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize
>(
  options: Simplify<
    SetRequired<
      CreateSelectorOptions<
        typeof weakMapMemoize,
        typeof weakMapMemoize,
        MemoizeFunction,
        ArgsMemoizeFunction
      >,
      'memoize'
    >
  >
): CreateSelectorFunction<MemoizeFunction, ArgsMemoizeFunction>

/**
 * Creates a selector creator function with the specified memoization function
 * and options for customizing memoization behavior.
 *
 * @param memoize - The `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
 * @param memoizeOptionsFromArgs - Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards.
 * @returns A customized `createSelector` function.
 *
 * @example
 * ```ts
 * const customCreateSelector = createSelectorCreator(customMemoize, // Function to be used to memoize `resultFunc`
 *   option1, // Will be passed as second argument to `customMemoize`
 *   option2, // Will be passed as third argument to `customMemoize`
 *   option3 // Will be passed as fourth argument to `customMemoize`
 * )
 *
 * const customSelector = customCreateSelector(
 *   [inputSelector1, inputSelector2],
 *   resultFunc // `resultFunc` will be passed as the first argument to `customMemoize`
 * )
 * ```
 *
 * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
 *
 * @see {@link https://reselect.js.org/api/createSelectorCreator#using-memoize-and-memoizeoptions `createSelectorCreator`}
 *
 * @public
 */
export function createSelectorCreator<MemoizeFunction extends UnknownMemoizer>(
  memoize: MemoizeFunction,
  ...memoizeOptionsFromArgs: DropFirstParameter<MemoizeFunction>
): CreateSelectorFunction<MemoizeFunction>

/**
 * Creates a selector creator function with the specified memoization
 * function and options for customizing memoization behavior.
 *
 * @param memoizeOrOptions - Either A `memoize` function or an `options` object containing the `memoize` function.
 * @param memoizeOptionsFromArgs - Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards.
 * @returns A customized `createSelector` function.
 *
 * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
 * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). If none is explicitly provided, `weakMapMemoize` will be used.
 * @template MemoizeOrOptions - The type of the first argument. It can either be a `memoize` function or an `options` object containing the `memoize` function.
 */
export function createSelectorCreator<
  MemoizeFunction extends UnknownMemoizer,
  ArgsMemoizeFunction extends UnknownMemoizer,
  MemoizeOrOptions extends
    | MemoizeFunction
    | SetRequired<
        CreateSelectorOptions<MemoizeFunction, ArgsMemoizeFunction>,
        'memoize'
      >
>(
  memoizeOrOptions: MemoizeOrOptions,
  ...memoizeOptionsFromArgs: MemoizeOrOptions extends SetRequired<
    CreateSelectorOptions<MemoizeFunction, ArgsMemoizeFunction>,
    'memoize'
  >
    ? never
    : DropFirstParameter<MemoizeFunction>
) {
  /** options initially passed into `createSelectorCreator`. */
  const createSelectorCreatorOptions: SetRequired<
    CreateSelectorOptions<MemoizeFunction, ArgsMemoizeFunction>,
    'memoize'
  > = typeof memoizeOrOptions === 'function'
    ? {
        memoize: memoizeOrOptions as MemoizeFunction,
        memoizeOptions: memoizeOptionsFromArgs
      }
    : memoizeOrOptions

  const createSelector = <
    InputSelectors extends SelectorArray,
    Result,
    OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
    OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
  >(
    ...createSelectorArgs: [
      ...inputSelectors: [...InputSelectors],
      combiner: Combiner<InputSelectors, Result>,
      createSelectorOptions?: CreateSelectorOptions<
        MemoizeFunction,
        ArgsMemoizeFunction,
        OverrideMemoizeFunction,
        OverrideArgsMemoizeFunction
      >
    ]
  ) => {
    let recomputations = 0
    let dependencyRecomputations = 0
    let lastResult: Result

    // Due to the intricacies of rest params, we can't do an optional arg after `...createSelectorArgs`.
    // So, start by declaring the default value here.
    // (And yes, the words 'memoize' and 'options' appear too many times in this next sequence.)
    let directlyPassedOptions: CreateSelectorOptions<
      MemoizeFunction,
      ArgsMemoizeFunction,
      OverrideMemoizeFunction,
      OverrideArgsMemoizeFunction
    > = {}

    // Normally, the result func or "combiner" is the last arg
    let resultFunc = createSelectorArgs.pop() as
      | Combiner<InputSelectors, Result>
      | CreateSelectorOptions<
          MemoizeFunction,
          ArgsMemoizeFunction,
          OverrideMemoizeFunction,
          OverrideArgsMemoizeFunction
        >

    // If the result func is actually an _object_, assume it's our options object
    if (typeof resultFunc === 'object') {
      directlyPassedOptions = resultFunc
      // and pop the real result func off
      resultFunc = createSelectorArgs.pop() as Combiner<InputSelectors, Result>
    }

    assertIsFunction(
      resultFunc,
      `createSelector expects an output function after the inputs, but received: [${typeof resultFunc}]`
    )

    // Determine which set of options we're using. Prefer options passed directly,
    // but fall back to options given to `createSelectorCreator`.
    const combinedOptions = {
      ...createSelectorCreatorOptions,
      ...directlyPassedOptions
    }

    const {
      memoize,
      memoizeOptions = [],
      argsMemoize = weakMapMemoize,
      argsMemoizeOptions = []
    } = combinedOptions

    // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer
    // is an array. In most libs I've looked at, it's an equality function or options object.
    // Based on that, if `memoizeOptions` _is_ an array, we assume it's a full
    // user-provided array of options. Otherwise, it must be just the _first_ arg, and so
    // we wrap it in an array so we can apply it.
    const finalMemoizeOptions = ensureIsArray(memoizeOptions)
    const finalArgsMemoizeOptions = ensureIsArray(argsMemoizeOptions)
    const dependencies = getDependencies(createSelectorArgs) as InputSelectors

    const memoizedResultFunc = memoize(function recomputationWrapper() {
      recomputations++
      // apply arguments instead of spreading for performance.
      // @ts-ignore
      return (resultFunc as Combiner<InputSelectors, Result>).apply(
        null,
        arguments as unknown as Parameters<Combiner<InputSelectors, Result>>
      )
    }, ...finalMemoizeOptions) as Combiner<InputSelectors, Result> &
      ExtractMemoizerFields<OverrideMemoizeFunction>

    let firstRun = true

    // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
    const selector = argsMemoize(function dependenciesChecker() {
      dependencyRecomputations++
      /** Return values of input selectors which the `resultFunc` takes as arguments. */
      const inputSelectorResults = collectInputSelectorResults(
        dependencies,
        arguments
      )

      // apply arguments instead of spreading for performance.
      // @ts-ignore
      lastResult = memoizedResultFunc.apply(null, inputSelectorResults)

      if (process.env.NODE_ENV !== 'production') {
        const { devModeChecks = {} } = combinedOptions
        const { identityFunctionCheck, inputStabilityCheck } =
          getDevModeChecksExecutionInfo(firstRun, devModeChecks)
        if (identityFunctionCheck.shouldRun) {
          identityFunctionCheck.run(
            resultFunc as Combiner<InputSelectors, Result>,
            inputSelectorResults,
            lastResult
          )
        }

        if (inputStabilityCheck.shouldRun) {
          // make a second copy of the params, to check if we got the same results
          const inputSelectorResultsCopy = collectInputSelectorResults(
            dependencies,
            arguments
          )

          inputStabilityCheck.run(
            { inputSelectorResults, inputSelectorResultsCopy },
            { memoize, memoizeOptions: finalMemoizeOptions },
            arguments
          )
        }

        if (firstRun) firstRun = false
      }

      return lastResult
    }, ...finalArgsMemoizeOptions) as unknown as Selector<
      GetStateFromSelectors<InputSelectors>,
      Result,
      GetParamsFromSelectors<InputSelectors>
    > &
      ExtractMemoizerFields<OverrideArgsMemoizeFunction>

    return Object.assign(selector, {
      resultFunc,
      memoizedResultFunc,
      dependencies,
      dependencyRecomputations: () => dependencyRecomputations,
      resetDependencyRecomputations: () => {
        dependencyRecomputations = 0
      },
      lastResult: () => lastResult,
      recomputations: () => recomputations,
      resetRecomputations: () => {
        recomputations = 0
      },
      memoize,
      argsMemoize
    }) as OutputSelector<
      InputSelectors,
      Result,
      OverrideMemoizeFunction,
      OverrideArgsMemoizeFunction
    >
  }

  Object.assign(createSelector, {
    withTypes: () => createSelector
  })

  return createSelector as CreateSelectorFunction<
    MemoizeFunction,
    ArgsMemoizeFunction
  >
}

/**
 * Accepts one or more "input selectors" (either as separate arguments or a single array),
 * a single "result function" / "combiner", and an optional options object, and
 * generates a memoized selector function.
 *
 * @see {@link https://reselect.js.org/api/createSelector `createSelector`}
 *
 * @public
 */
export const createSelector =
  /* #__PURE__ */ createSelectorCreator(weakMapMemoize)
