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

import {shorthandToLonghand} from './CSSShorthandProperty';

import hyphenateStyleName from '../shared/hyphenateStyleName';
import warnValidStyle from '../shared/warnValidStyle';
import isUnitlessNumber from '../shared/isUnitlessNumber';
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';

/**
 * Operations for dealing with CSS properties.
 */

/**
 * This creates a string that is expected to be equivalent to the style
 * attribute generated by server-side rendering. It by-passes warnings and
 * security checks so it's not safe to use this value for anything other than
 * comparison. It is only used in DEV for SSR validation.
 */
export function createDangerousStringForStyles(styles) {
  if (__DEV__) {
    let serialized = '';
    let delimiter = '';
    for (const styleName in styles) {
      if (!styles.hasOwnProperty(styleName)) {
        continue;
      }
      const value = styles[styleName];
      if (value != null && typeof value !== 'boolean' && value !== '') {
        const isCustomProperty = styleName.indexOf('--') === 0;
        if (isCustomProperty) {
          if (__DEV__) {
            checkCSSPropertyStringCoercion(value, styleName);
          }
          serialized += delimiter + styleName + ':' + ('' + value).trim();
        } else {
          if (
            typeof value === 'number' &&
            value !== 0 &&
            !isUnitlessNumber(styleName)
          ) {
            serialized +=
              delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
          } else {
            if (__DEV__) {
              checkCSSPropertyStringCoercion(value, styleName);
            }
            serialized +=
              delimiter +
              hyphenateStyleName(styleName) +
              ':' +
              ('' + value).trim();
          }
        }
        delimiter = ';';
      }
    }
    return serialized || null;
  }
}

function setValueForStyle(style, styleName, value) {
  const isCustomProperty = styleName.indexOf('--') === 0;
  if (__DEV__) {
    if (!isCustomProperty) {
      warnValidStyle(styleName, value);
    }
  }

  if (value == null || typeof value === 'boolean' || value === '') {
    if (isCustomProperty) {
      style.setProperty(styleName, '');
    } else if (styleName === 'float') {
      style.cssFloat = '';
    } else {
      style[styleName] = '';
    }
  } else if (isCustomProperty) {
    style.setProperty(styleName, value);
  } else if (
    typeof value === 'number' &&
    value !== 0 &&
    !isUnitlessNumber(styleName)
  ) {
    style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
  } else {
    if (styleName === 'float') {
      style.cssFloat = value;
    } else {
      if (__DEV__) {
        checkCSSPropertyStringCoercion(value, styleName);
      }
      style[styleName] = ('' + value).trim();
    }
  }
}

/**
 * Sets the value for multiple styles on a node.  If a value is specified as
 * '' (empty string), the corresponding style property will be unset.
 *
 * @param {DOMElement} node
 * @param {object} styles
 */
export function setValueForStyles(node, styles, prevStyles) {
  if (styles != null && typeof styles !== 'object') {
    throw new Error(
      'The `style` prop expects a mapping from style properties to values, ' +
        "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
        'using JSX.',
    );
  }
  if (__DEV__) {
    if (styles) {
      // Freeze the next style object so that we can assume it won't be
      // mutated. We have already warned for this in the past.
      Object.freeze(styles);
    }
  }

  const style = node.style;

  if (prevStyles != null) {
    if (__DEV__) {
      validateShorthandPropertyCollisionInDev(prevStyles, styles);
    }

    for (const styleName in prevStyles) {
      if (
        prevStyles.hasOwnProperty(styleName) &&
        (styles == null || !styles.hasOwnProperty(styleName))
      ) {
        // Clear style
        const isCustomProperty = styleName.indexOf('--') === 0;
        if (isCustomProperty) {
          style.setProperty(styleName, '');
        } else if (styleName === 'float') {
          style.cssFloat = '';
        } else {
          style[styleName] = '';
        }
      }
    }
    for (const styleName in styles) {
      const value = styles[styleName];
      if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
        setValueForStyle(style, styleName, value);
      }
    }
  } else {
    for (const styleName in styles) {
      if (styles.hasOwnProperty(styleName)) {
        const value = styles[styleName];
        setValueForStyle(style, styleName, value);
      }
    }
  }
}

function isValueEmpty(value) {
  return value == null || typeof value === 'boolean' || value === '';
}

/**
 * Given {color: 'red', overflow: 'hidden'} returns {
 *   color: 'color',
 *   overflowX: 'overflow',
 *   overflowY: 'overflow',
 * }. This can be read as "the overflowY property was set by the overflow
 * shorthand". That is, the values are the property that each was derived from.
 */
function expandShorthandMap(styles) {
  const expanded = {};
  for (const key in styles) {
    const longhands = shorthandToLonghand[key] || [key];
    for (let i = 0; i < longhands.length; i++) {
      expanded[longhands[i]] = key;
    }
  }
  return expanded;
}

/**
 * When mixing shorthand and longhand property names, we warn during updates if
 * we expect an incorrect result to occur. In particular, we warn for:
 *
 * Updating a shorthand property (longhand gets overwritten):
 *   {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
 *   becomes .style.font = 'baz'
 * Removing a shorthand property (longhand gets lost too):
 *   {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
 *   becomes .style.font = ''
 * Removing a longhand property (should revert to shorthand; doesn't):
 *   {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
 *   becomes .style.fontVariant = ''
 */
function validateShorthandPropertyCollisionInDev(prevStyles, nextStyles) {
  if (__DEV__) {
    if (!nextStyles) {
      return;
    }

    // Compute the diff as it would happen elsewhere.
    const expandedUpdates = {};
    if (prevStyles) {
      for (const key in prevStyles) {
        if (prevStyles.hasOwnProperty(key) && !nextStyles.hasOwnProperty(key)) {
          const longhands = shorthandToLonghand[key] || [key];
          for (let i = 0; i < longhands.length; i++) {
            expandedUpdates[longhands[i]] = key;
          }
        }
      }
    }
    for (const key in nextStyles) {
      if (
        nextStyles.hasOwnProperty(key) &&
        (!prevStyles || prevStyles[key] !== nextStyles[key])
      ) {
        const longhands = shorthandToLonghand[key] || [key];
        for (let i = 0; i < longhands.length; i++) {
          expandedUpdates[longhands[i]] = key;
        }
      }
    }

    const expandedStyles = expandShorthandMap(nextStyles);
    const warnedAbout = {};
    for (const key in expandedUpdates) {
      const originalKey = expandedUpdates[key];
      const correctOriginalKey = expandedStyles[key];
      if (correctOriginalKey && originalKey !== correctOriginalKey) {
        const warningKey = originalKey + ',' + correctOriginalKey;
        if (warnedAbout[warningKey]) {
          continue;
        }
        warnedAbout[warningKey] = true;
        console.error(
          '%s a style property during rerender (%s) when a ' +
            'conflicting property is set (%s) can lead to styling bugs. To ' +
            "avoid this, don't mix shorthand and non-shorthand properties " +
            'for the same value; instead, replace the shorthand with ' +
            'separate values.',
          isValueEmpty(nextStyles[originalKey]) ? 'Removing' : 'Updating',
          originalKey,
          correctOriginalKey,
        );
      }
    }
  }
}
