/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps';

import composeStyles from '../../src/private/styles/composeStyles';
import View from '../Components/View/View';
import useMergeRefs from '../Utilities/useMergeRefs';
import useAnimatedProps from './useAnimatedProps';
import * as React from 'react';
import {useMemo} from 'react';

export type AnimatedProps<Props: {...}> = {
  // eslint-disable-next-line no-unused-vars
  +[_K in keyof (Props &
      $ReadOnly<{
        passthroughAnimatedPropExplicitValues?: React.ElementConfig<
          typeof View,
        >,
      }>)]: any,
};

// We could use a mapped type here to introduce acceptable Animated variants
// of properties, instead of doing so in the core StyleSheetTypes
// Inexact Props are not supported, they'll be made exact here.
export type StrictAnimatedProps<Props: {...}> = $ReadOnly<{
  ...$Exact<Props>,
  passthroughAnimatedPropExplicitValues?: ?Props,
}>;

export type AnimatedComponentType<Props: {...}, +Instance = mixed> = component(
  ref: React.RefSetter<Instance>,
  ...AnimatedProps<Props>
);

export type StrictAnimatedComponentType<
  Props: {...},
  +Instance = mixed,
> = component(ref: React.RefSetter<Instance>, ...StrictAnimatedProps<Props>);

export default function createAnimatedComponent<TProps: {...}, TInstance>(
  Component: component(ref: React.RefSetter<TInstance>, ...TProps),
): AnimatedComponentType<TProps, TInstance> {
  return unstable_createAnimatedComponentWithAllowlist(Component, null);
}

export function unstable_createAnimatedComponentWithAllowlist<
  TProps: {...},
  TInstance,
>(
  Component: component(ref: React.RefSetter<TInstance>, ...TProps),
  allowlist: ?AnimatedPropsAllowlist,
): StrictAnimatedComponentType<TProps, TInstance> {
  const AnimatedComponent = React.forwardRef<
    StrictAnimatedProps<TProps>,
    TInstance,
  >((props, forwardedRef) => {
    const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
      // $FlowFixMe[incompatible-call]
      props,
      allowlist,
    );
    const ref = useMergeRefs<TInstance>(callbackRef, forwardedRef);

    // Some components require explicit passthrough values for animation
    // to work properly. For example, if an animated component is
    // transformed and Pressable, onPress will not work after transform
    // without these passthrough values.
    // $FlowFixMe[prop-missing]
    const {passthroughAnimatedPropExplicitValues, style} = reducedProps;
    const passthroughStyle = passthroughAnimatedPropExplicitValues?.style;
    const mergedStyle = useMemo(
      () => composeStyles(style, passthroughStyle),
      [passthroughStyle, style],
    );

    // NOTE: It is important that `passthroughAnimatedPropExplicitValues` is
    // spread after `reducedProps` but before `style`.
    return (
      <Component
        {...reducedProps}
        {...passthroughAnimatedPropExplicitValues}
        style={mergedStyle}
        ref={ref}
      />
    );
  });

  AnimatedComponent.displayName = `Animated(${
    Component.displayName || 'Anonymous'
  })`;

  return AnimatedComponent;
}