/*
 * Copyright 2015, Yahoo Inc.
 * Copyrights licensed under the New BSD License.
 * See the accompanying LICENSE file for terms.
 */

import {Component, createElement, isValidElement} from 'react';
import PropTypes from 'prop-types';
import IntlMessageFormat from 'intl-messageformat';
import memoizeIntlConstructor from 'intl-format-cache';
import {intlShape, messageDescriptorPropTypes} from '../types';
import {
  invariantIntlContext,
  shallowEquals,
  shouldIntlComponentUpdate,
} from '../utils';
import {formatMessage as baseFormatMessage} from '../format';

const defaultFormatMessage = (descriptor, values) => {
  if (process.env.NODE_ENV !== 'production') {
    console.error(
      '[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry. Using default message as fallback.'
    );
  }
  return baseFormatMessage(
    {},
    {getMessageFormat: memoizeIntlConstructor(IntlMessageFormat)},
    descriptor,
    values
  );
};

export default class FormattedMessage extends Component {
  static displayName = 'FormattedMessage';

  static contextTypes = {
    intl: intlShape,
  };

  static propTypes = {
    ...messageDescriptorPropTypes,
    values: PropTypes.object,
    tagName: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    children: PropTypes.func,
  };

  static defaultProps = {
    values: {},
  };

  constructor(props, context) {
    super(props, context);
    if (!props.defaultMessage) {
      invariantIntlContext(context);
    }
  }

  shouldComponentUpdate(nextProps, ...next) {
    const {values} = this.props;
    const {values: nextValues} = nextProps;

    if (!shallowEquals(nextValues, values)) {
      return true;
    }

    // Since `values` has already been checked, we know they're not
    // different, so the current `values` are carried over so the shallow
    // equals comparison on the other props isn't affected by the `values`.
    let nextPropsToCheck = {
      ...nextProps,
      values,
    };

    return shouldIntlComponentUpdate(this, nextPropsToCheck, ...next);
  }

  render() {
    const {formatMessage = defaultFormatMessage, textComponent: Text = 'span'} =
      this.context.intl || {};

    const {
      id,
      description,
      defaultMessage,
      values,
      tagName: Component = Text,
      children,
    } = this.props;

    let tokenDelimiter;
    let tokenizedValues;
    let elements;

    let hasValues = values && Object.keys(values).length > 0;
    if (hasValues) {
      // Creates a token with a random UID that should not be guessable or
      // conflict with other parts of the `message` string.
      let uid = Math.floor(Math.random() * 0x10000000000).toString(16);

      let generateToken = (() => {
        let counter = 0;
        return () => `ELEMENT-${uid}-${(counter += 1)}`;
      })();

      // Splitting with a delimiter to support IE8. When using a regex
      // with a capture group IE8 does not include the capture group in
      // the resulting array.
      tokenDelimiter = `@__${uid}__@`;
      tokenizedValues = {};
      elements = {};

      // Iterates over the `props` to keep track of any React Element
      // values so they can be represented by the `token` as a placeholder
      // when the `message` is formatted. This allows the formatted
      // message to then be broken-up into parts with references to the
      // React Elements inserted back in.
      Object.keys(values).forEach(name => {
        let value = values[name];

        if (isValidElement(value)) {
          let token = generateToken();
          tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
          elements[token] = value;
        } else {
          tokenizedValues[name] = value;
        }
      });
    }

    let descriptor = {id, description, defaultMessage};
    let formattedMessage = formatMessage(descriptor, tokenizedValues || values);

    let nodes;

    let hasElements = elements && Object.keys(elements).length > 0;
    if (hasElements) {
      // Split the message into parts so the React Element values captured
      // above can be inserted back into the rendered message. This
      // approach allows messages to render with React Elements while
      // keeping React's virtual diffing working properly.
      nodes = formattedMessage
        .split(tokenDelimiter)
        .filter(part => !!part)
        .map(part => elements[part] || part);
    } else {
      nodes = [formattedMessage];
    }

    if (typeof children === 'function') {
      return children(...nodes);
    }

    // Needs to use `createElement()` instead of JSX, otherwise React will
    // warn about a missing `key` prop with rich-text message formatting.
    return createElement(Component, null, ...nodes);
  }
}
