/**
 * 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
 */

import hasOwnProperty from 'shared/hasOwnProperty';

type Props = {[string]: mixed};

export function warnOnMissingHrefAndRel(
  pendingProps: Props,
  currentProps: ?Props,
) {
  if (__DEV__) {
    if (currentProps != null) {
      const originalResourceName =
        typeof currentProps.href === 'string'
          ? `Resource with href "${currentProps.href}"`
          : 'Resource';
      const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
        currentProps.rel,
      );
      const pendingRel = getValueDescriptorExpectingEnumForWarning(
        pendingProps.rel,
      );
      const pendingHref = getValueDescriptorExpectingEnumForWarning(
        pendingProps.href,
      );
      if (typeof pendingProps.rel !== 'string') {
        console.error(
          'A <link> previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
            ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
            ' do not render the <link> anymore.',
          originalResourceName,
          originalRelStatement,
          pendingRel,
        );
      } else if (typeof pendingProps.href !== 'string') {
        console.error(
          'A <link> previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
            ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
            ' do not render the <link> anymore.',
          originalResourceName,
          pendingHref,
        );
      }
    } else {
      const pendingRel = getValueDescriptorExpectingEnumForWarning(
        pendingProps.rel,
      );
      const pendingHref = getValueDescriptorExpectingEnumForWarning(
        pendingProps.href,
      );
      if (typeof pendingProps.rel !== 'string') {
        console.error(
          'A <link> is rendering with an invalid rel: %s. When a link' +
            ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
            ' do not render the <link> anymore.',
          pendingRel,
        );
      } else if (typeof pendingProps.href !== 'string') {
        console.error(
          'A <link> is rendering with an invalid href: %s. When a link' +
            ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
            ' do not render the <link> anymore.',
          pendingHref,
        );
      }
    }
  }
}

export function validatePreloadResourceDifference(
  originalProps: any,
  originalImplicit: boolean,
  latestProps: any,
  latestImplicit: boolean,
) {
  if (__DEV__) {
    const {href} = originalProps;
    const originalWarningName = getResourceNameForWarning(
      'preload',
      originalProps,
      originalImplicit,
    );
    const latestWarningName = getResourceNameForWarning(
      'preload',
      latestProps,
      latestImplicit,
    );

    if (latestProps.as !== originalProps.as) {
      console.error(
        'A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
          ' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
          ' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
          ' update the hrefs to be distinct for different Resource types.',
        latestWarningName,
        href,
        originalWarningName,
      );
    } else {
      let missingProps = null;
      let extraProps = null;
      let differentProps = null;
      if (originalProps.media != null && latestProps.media == null) {
        missingProps = missingProps || ({}: {[string]: $FlowFixMe});
        missingProps.media = originalProps.media;
      }

      for (const propName in latestProps) {
        const propValue = latestProps[propName];
        const originalValue = originalProps[propName];

        if (propValue != null && propValue !== originalValue) {
          if (originalValue == null) {
            extraProps = extraProps || ({}: {[string]: $FlowFixMe});
            extraProps[propName] = propValue;
          } else {
            differentProps =
              differentProps ||
              ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
            differentProps[propName] = {
              original: originalValue,
              latest: propValue,
            };
          }
        }
      }

      if (missingProps || extraProps || differentProps) {
        warnDifferentProps(
          href,
          'href',
          originalWarningName,
          latestWarningName,
          extraProps,
          missingProps,
          differentProps,
        );
      }
    }
  }
}

export function validateStyleResourceDifference(
  originalProps: any,
  latestProps: any,
) {
  if (__DEV__) {
    const {href} = originalProps;
    // eslint-disable-next-line no-labels
    const originalWarningName = getResourceNameForWarning(
      'style',
      originalProps,
      false,
    );
    const latestWarningName = getResourceNameForWarning(
      'style',
      latestProps,
      false,
    );
    let missingProps = null;
    let extraProps = null;
    let differentProps = null;
    if (originalProps.media != null && latestProps.media == null) {
      missingProps = missingProps || ({}: {[string]: $FlowFixMe});
      missingProps.media = originalProps.media;
    }

    for (let propName in latestProps) {
      const propValue = latestProps[propName];
      const originalValue = originalProps[propName];

      if (propValue != null && propValue !== originalValue) {
        propName = propName === 'data-precedence' ? 'precedence' : propName;
        if (originalValue == null) {
          extraProps = extraProps || ({}: {[string]: $FlowFixMe});
          extraProps[propName] = propValue;
        } else {
          differentProps =
            differentProps ||
            ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
          differentProps[propName] = {
            original: originalValue,
            latest: propValue,
          };
        }
      }
    }

    if (missingProps || extraProps || differentProps) {
      warnDifferentProps(
        href,
        'href',
        originalWarningName,
        latestWarningName,
        extraProps,
        missingProps,
        differentProps,
      );
    }
  }
}

export function validateScriptResourceDifference(
  originalProps: any,
  latestProps: any,
) {
  if (__DEV__) {
    const {src} = originalProps;
    // eslint-disable-next-line no-labels
    const originalWarningName = getResourceNameForWarning(
      'script',
      originalProps,
      false,
    );
    const latestWarningName = getResourceNameForWarning(
      'script',
      latestProps,
      false,
    );
    let extraProps = null;
    let differentProps = null;

    for (const propName in latestProps) {
      const propValue = latestProps[propName];
      const originalValue = originalProps[propName];

      if (propValue != null && propValue !== originalValue) {
        if (originalValue == null) {
          extraProps = extraProps || ({}: {[string]: $FlowFixMe});
          extraProps[propName] = propValue;
        } else {
          differentProps =
            differentProps ||
            ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
          differentProps[propName] = {
            original: originalValue,
            latest: propValue,
          };
        }
      }
    }

    if (extraProps || differentProps) {
      warnDifferentProps(
        src,
        'src',
        originalWarningName,
        latestWarningName,
        extraProps,
        null,
        differentProps,
      );
    }
  }
}

export function validateStyleAndHintProps(
  preloadProps: any,
  styleProps: any,
  implicitPreload: boolean,
) {
  if (__DEV__) {
    const {href} = preloadProps;

    const originalWarningName = getResourceNameForWarning(
      'preload',
      preloadProps,
      implicitPreload,
    );
    const latestWarningName = getResourceNameForWarning(
      'style',
      styleProps,
      false,
    );

    if (preloadProps.as !== 'style') {
      console.error(
        'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
          ' "as" prop must be of type "style". This most likely ocurred by rendering a preload link with an incorrect' +
          ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
        latestWarningName,
        href,
        originalWarningName,
      );
    }

    let missingProps = null;
    let extraProps = null;
    let differentProps = null;

    for (const propName in styleProps) {
      const styleValue = styleProps[propName];
      const preloadValue = preloadProps[propName];
      switch (propName) {
        // Check for difference on specific props that cross over or influence
        // the relationship between the preload and stylesheet
        case 'crossOrigin':
        case 'referrerPolicy':
        case 'media':
        case 'title': {
          if (
            preloadValue !== styleValue &&
            !(preloadValue == null && styleValue == null)
          ) {
            if (styleValue == null) {
              missingProps = missingProps || ({}: {[string]: $FlowFixMe});
              missingProps[propName] = preloadValue;
            } else if (preloadValue == null) {
              extraProps = extraProps || ({}: {[string]: $FlowFixMe});
              extraProps[propName] = styleValue;
            } else {
              differentProps =
                differentProps ||
                ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
              differentProps[propName] = {
                original: preloadValue,
                latest: styleValue,
              };
            }
          }
        }
      }
    }

    if (missingProps || extraProps || differentProps) {
      warnDifferentProps(
        href,
        'href',
        originalWarningName,
        latestWarningName,
        extraProps,
        missingProps,
        differentProps,
      );
    }
  }
}

export function validateScriptAndHintProps(
  preloadProps: any,
  scriptProps: any,
  implicitPreload: boolean,
) {
  if (__DEV__) {
    const {href} = preloadProps;

    const originalWarningName = getResourceNameForWarning(
      'preload',
      preloadProps,
      implicitPreload,
    );
    const latestWarningName = getResourceNameForWarning(
      'script',
      scriptProps,
      false,
    );

    if (preloadProps.as !== 'script') {
      console.error(
        'While creating a %s for href "%s" a %s for this same url was found. When preloading a script the' +
          ' "as" prop must be of type "script". This most likely ocurred by rendering a preload link with an incorrect' +
          ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
        latestWarningName,
        href,
        originalWarningName,
      );
    }

    let missingProps = null;
    let extraProps = null;
    let differentProps = null;

    for (const propName in scriptProps) {
      const scriptValue = scriptProps[propName];
      const preloadValue = preloadProps[propName];
      switch (propName) {
        // Check for difference on specific props that cross over or influence
        // the relationship between the preload and stylesheet
        case 'crossOrigin':
        case 'referrerPolicy':
        case 'integrity': {
          if (
            preloadValue !== scriptValue &&
            !(preloadValue == null && scriptValue == null)
          ) {
            if (scriptValue == null) {
              missingProps = missingProps || ({}: {[string]: $FlowFixMe});
              missingProps[propName] = preloadValue;
            } else if (preloadValue == null) {
              extraProps = extraProps || ({}: {[string]: $FlowFixMe});
              extraProps[propName] = scriptValue;
            } else {
              differentProps =
                differentProps ||
                ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
              differentProps[propName] = {
                original: preloadValue,
                latest: scriptValue,
              };
            }
          }
        }
      }
    }

    if (missingProps || extraProps || differentProps) {
      warnDifferentProps(
        href,
        'href',
        originalWarningName,
        latestWarningName,
        extraProps,
        missingProps,
        differentProps,
      );
    }
  }
}

function warnDifferentProps(
  url: string,
  urlPropKey: string,
  originalName: string,
  latestName: string,
  extraProps: ?{[string]: $FlowFixMe},
  missingProps: ?{[string]: $FlowFixMe},
  differentProps: ?{[string]: {original: any, latest: any}},
): void {
  if (__DEV__) {
    const juxtaposedNameStatement =
      latestName === originalName
        ? 'an earlier instance of this Resource'
        : `a ${originalName} with the same ${urlPropKey}`;

    let comparisonStatement = '';
    if (missingProps !== null && typeof missingProps === 'object') {
      for (const propName in missingProps) {
        comparisonStatement += `\n  ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
      }
    }
    if (extraProps !== null && typeof extraProps === 'object') {
      for (const propName in extraProps) {
        comparisonStatement += `\n  ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
      }
    }
    if (differentProps !== null && typeof differentProps === 'object') {
      for (const propName in differentProps) {
        comparisonStatement += `\n  ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
      }
    }

    console.error(
      'A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
        ' that were provided the first time they are encountered so any differences will be ignored. Please' +
        ' update Resources that share an %s to have props that agree. The differences are described below.%s',
      latestName,
      urlPropKey,
      url,
      juxtaposedNameStatement,
      urlPropKey,
      comparisonStatement,
    );
  }
}

function getResourceNameForWarning(
  type: string,
  props: Object,
  implicit: boolean,
) {
  if (__DEV__) {
    switch (type) {
      case 'style': {
        return 'style Resource';
      }
      case 'script': {
        return 'script Resource';
      }
      case 'preload': {
        if (implicit) {
          return `preload for a ${props.as} Resource`;
        }
        return `preload Resource (as "${props.as}")`;
      }
    }
  }
  return 'Resource';
}

export function validateURLKeyedUpdatedProps(
  pendingProps: Props,
  currentProps: Props,
  resourceType: 'style' | 'script' | 'href',
  urlPropKey: 'href' | 'src',
): boolean {
  if (__DEV__) {
    // This function should never be called if we don't have /srcs so we don't bother considering
    // Whether they are null or undefined
    if (pendingProps[urlPropKey] === currentProps[urlPropKey]) {
      // If we have the same href/src we need all other props to be the same
      let missingProps;
      let extraProps;
      let differentProps;
      const allProps = Array.from(
        new Set(Object.keys(currentProps).concat(Object.keys(pendingProps))),
      );
      for (let i = 0; i < allProps.length; i++) {
        const propName = allProps[i];
        const pendingValue = pendingProps[propName];
        const currentValue = currentProps[propName];
        if (
          pendingValue !== currentValue &&
          !(pendingValue == null && currentValue == null)
        ) {
          if (pendingValue == null) {
            missingProps = missingProps || ({}: {[string]: mixed});
            missingProps[propName] = currentValue;
          } else if (currentValue == null) {
            extraProps = extraProps || ({}: {[string]: mixed});
            extraProps[propName] = pendingValue;
          } else {
            differentProps =
              differentProps ||
              ({}: {[string]: {latest: mixed, original: mixed}});
            differentProps[propName] = {
              original: currentValue,
              latest: pendingValue,
            };
          }
        }
      }
      if (missingProps || extraProps || differentProps) {
        const latestWarningName = getResourceNameForWarning(
          resourceType,
          currentProps,
          false,
        );

        let comparisonStatement = '';
        if (missingProps !== null && typeof missingProps === 'object') {
          for (const propName in missingProps) {
            // $FlowFixMe[incompatible-type]
            comparisonStatement += `\n  ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
          }
        }
        if (extraProps !== null && typeof extraProps === 'object') {
          for (const propName in extraProps) {
            // $FlowFixMe[incompatible-type]
            comparisonStatement += `\n  ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
          }
        }
        if (differentProps !== null && typeof differentProps === 'object') {
          for (const propName in differentProps) {
            // $FlowFixMe[incompatible-type]
            comparisonStatement += `\n  ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
          }
        }
        console.error(
          'A %s with %s "%s" received new props with different values from the props used' +
            ' when this Resource was first rendered. React will only use the props provided when' +
            ' this resource was first rendered until a new %s is provided. Unlike conventional' +
            ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
            ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
            ' (%s) must have props that agree with each other. The differences are described below.%s',
          latestWarningName,
          urlPropKey,
          currentProps[urlPropKey],
          urlPropKey,
          urlPropKey,
          comparisonStatement,
        );
        return true;
      }
    }
  }
  return false;
}

export function validateLinkPropsForStyleResource(props: Props): boolean {
  if (__DEV__) {
    // This should only be called when we know we are opting into Resource semantics (i.e. precedence is not null)
    const {href, onLoad, onError, disabled} = props;
    const allProps = ['onLoad', 'onError', 'disabled'];
    const includedProps = [];
    if (onLoad) includedProps.push('onLoad');
    if (onError) includedProps.push('onError');
    if (disabled != null) includedProps.push('disabled');

    const allPropsUnionPhrase = propNamesListJoin(allProps, 'or');
    let includedPropsPhrase = propNamesListJoin(includedProps, 'and');
    includedPropsPhrase += includedProps.length === 1 ? ' prop' : ' props';

    if (includedProps.length) {
      console.error(
        'A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
          ' When using %s React will opt out of Resource behavior. If you meant for this' +
          ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.',
        href,
        includedPropsPhrase,
        allPropsUnionPhrase,
        includedPropsPhrase,
      );
      return true;
    }
  }
  return false;
}

function propNamesListJoin(
  list: Array<string>,
  combinator: 'and' | 'or',
): string {
  switch (list.length) {
    case 0:
      return '';
    case 1:
      return list[0];
    case 2:
      return list[0] + ' ' + combinator + ' ' + list[1];
    default:
      return (
        list.slice(0, -1).join(', ') +
        ', ' +
        combinator +
        ' ' +
        list[list.length - 1]
      );
  }
}

export function validateLinkPropsForPreloadResource(linkProps: any) {
  if (__DEV__) {
    const {href, as} = linkProps;
    if (as === 'font') {
      const name = getResourceNameForWarning('preload', linkProps, false);
      if (!hasOwnProperty.call(linkProps, 'crossOrigin')) {
        console.error(
          'A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
            ' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
            ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
          name,
          href,
        );
      } else if (linkProps.crossOrigin === 'use-credentials') {
        console.error(
          'A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
            ' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
            ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
          name,
          href,
        );
      }
    }
  }
}

export function validatePreloadArguments(href: mixed, options: mixed) {
  if (__DEV__) {
    if (!href || typeof href !== 'string') {
      const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
      console.error(
        'ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.',
        typeOfArg,
      );
    } else if (typeof options !== 'object' || options === null) {
      const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
      console.error(
        'ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
          ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
        typeOfArg,
        href,
      );
    } else {
      const as = options.as;
      switch (as) {
        // Font specific validation of options
        case 'font': {
          if (options.crossOrigin === 'use-credentials') {
            console.error(
              'ReactDOM.preload() was called with an "as" type of "font" and with a "crossOrigin" option of "use-credentials".' +
                ' Fonts preloading must use crossOrigin "anonymous" to be functional. Please update your font preload to omit' +
                ' the crossOrigin option or change it to any other value than "use-credentials" (Browsers default all other values' +
                ' to anonymous mode). The href for the preload call where this warning originated is "%s"',
              href,
            );
          }
          break;
        }
        case 'script':
        case 'style': {
          break;
        }

        // We have an invalid as type and need to warn
        default: {
          const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
          console.error(
            'ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
              ' Please use one of the following valid values instead: %s. The href for the preload call where this' +
              ' warning originated is "%s".',
            typeOfAs,
            '"style", "font", or "script"',
            href,
          );
        }
      }
    }
  }
}

export function validatePreinitArguments(href: mixed, options: mixed) {
  if (__DEV__) {
    if (!href || typeof href !== 'string') {
      const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
      console.error(
        'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.',
        typeOfArg,
      );
    } else if (typeof options !== 'object' || options === null) {
      const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
      console.error(
        'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
          ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
        typeOfArg,
        href,
      );
    } else {
      const as = options.as;
      switch (as) {
        case 'style':
        case 'script': {
          break;
        }

        // We have an invalid as type and need to warn
        default: {
          const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
          console.error(
            'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
              ' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
              ' and "script". The href for the preinit call where this warning originated is "%s".',
            typeOfAs,
            href,
          );
        }
      }
    }
  }
}

function getValueDescriptorExpectingObjectForWarning(thing: any): string {
  return thing === null
    ? 'null'
    : thing === undefined
    ? 'undefined'
    : thing === ''
    ? 'an empty string'
    : `something with type "${typeof thing}"`;
}

function getValueDescriptorExpectingEnumForWarning(thing: any): string {
  return thing === null
    ? 'null'
    : thing === undefined
    ? 'undefined'
    : thing === ''
    ? 'an empty string'
    : typeof thing === 'string'
    ? JSON.stringify(thing)
    : `something with type "${typeof thing}"`;
}