/**
 * @fileoverview Common defaultProps detection functionality.
 */
'use strict';

const fromEntries = require('object.fromentries');
const astUtil = require('./ast');
const propsUtil = require('./props');
const variableUtil = require('./variable');
const propWrapperUtil = require('../util/propWrapper');

const QUOTES_REGEX = /^["']|["']$/g;

module.exports = function defaultPropsInstructions(context, components, utils) {
  const sourceCode = context.getSourceCode();

  /**
   * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
   * an Identifier, then the node is simply returned.
   * @param   {ASTNode} node The node to resolve.
   * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
   */
  function resolveNodeValue(node) {
    if (node.type === 'Identifier') {
      return variableUtil.findVariableByName(context, node.name);
    }
    if (
      node.type === 'CallExpression' &&
      propWrapperUtil.isPropWrapperFunction(context, node.callee.name) &&
      node.arguments && node.arguments[0]
    ) {
      return resolveNodeValue(node.arguments[0]);
    }
    return node;
  }

  /**
   * Extracts a DefaultProp from an ObjectExpression node.
   * @param   {ASTNode} objectExpression ObjectExpression node.
   * @returns {Object|string}            Object representation of a defaultProp, to be consumed by
   *                                     `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
   *                                     from this ObjectExpression can't be resolved.
   */
  function getDefaultPropsFromObjectExpression(objectExpression) {
    const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');

    if (hasSpread) {
      return 'unresolved';
    }

    return objectExpression.properties.map(defaultProp => ({
      name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''),
      node: defaultProp
    }));
  }

  /**
   * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
   * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
   * without risking false negatives.
   * @param   {Object} component The component to mark.
   * @returns {void}
   */
  function markDefaultPropsAsUnresolved(component) {
    components.set(component.node, {
      defaultProps: 'unresolved'
    });
  }

  /**
   * Adds defaultProps to the component passed in.
   * @param   {ASTNode}         component    The component to add the defaultProps to.
   * @param   {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved"
   *                                         if this component has defaultProps that can't be resolved.
   * @returns {void}
   */
  function addDefaultPropsToComponent(component, defaultProps) {
    // Early return if this component's defaultProps is already marked as "unresolved".
    if (component.defaultProps === 'unresolved') {
      return;
    }

    if (defaultProps === 'unresolved') {
      markDefaultPropsAsUnresolved(component);
      return;
    }

    const defaults = component.defaultProps || {};
    const newDefaultProps = Object.assign(
      {},
      defaults,
      fromEntries(defaultProps.map(prop => [prop.name, prop]))
    );

    components.set(component.node, {
      defaultProps: newDefaultProps
    });
  }

  return {
    MemberExpression: function(node) {
      const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);

      if (!isDefaultProp) {
        return;
      }

      // find component this defaultProps belongs to
      const component = utils.getRelatedComponent(node);
      if (!component) {
        return;
      }

      // e.g.:
      // MyComponent.propTypes = {
      //   foo: React.PropTypes.string.isRequired,
      //   bar: React.PropTypes.string
      // };
      //
      // or:
      //
      // MyComponent.propTypes = myPropTypes;
      if (node.parent.type === 'AssignmentExpression') {
        const expression = resolveNodeValue(node.parent.right);
        if (!expression || expression.type !== 'ObjectExpression') {
          // If a value can't be found, we mark the defaultProps declaration as "unresolved", because
          // we should ignore this component and not report any errors for it, to avoid false-positives
          // with e.g. external defaultProps declarations.
          if (isDefaultProp) {
            markDefaultPropsAsUnresolved(component);
          }

          return;
        }

        addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));

        return;
      }

      // e.g.:
      // MyComponent.propTypes.baz = React.PropTypes.string;
      if (node.parent.type === 'MemberExpression' && node.parent.parent &&
        node.parent.parent.type === 'AssignmentExpression') {
        addDefaultPropsToComponent(component, [{
          name: node.parent.property.name,
          node: node.parent.parent
        }]);
      }
    },

    // e.g.:
    // class Hello extends React.Component {
    //   static get defaultProps() {
    //     return {
    //       name: 'Dean'
    //     };
    //   }
    //   render() {
    //     return <div>Hello {this.props.name}</div>;
    //   }
    // }
    MethodDefinition: function(node) {
      if (!node.static || node.kind !== 'get') {
        return;
      }

      if (!propsUtil.isDefaultPropsDeclaration(node)) {
        return;
      }

      // find component this propTypes/defaultProps belongs to
      const component = components.get(utils.getParentES6Component());
      if (!component) {
        return;
      }

      const returnStatement = utils.findReturnStatement(node);
      if (!returnStatement) {
        return;
      }

      const expression = resolveNodeValue(returnStatement.argument);
      if (!expression || expression.type !== 'ObjectExpression') {
        return;
      }

      addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
    },

    // e.g.:
    // class Greeting extends React.Component {
    //   render() {
    //     return (
    //       <h1>Hello, {this.props.foo} {this.props.bar}</h1>
    //     );
    //   }
    //   static defaultProps = {
    //     foo: 'bar',
    //     bar: 'baz'
    //   };
    // }
    ClassProperty: function(node) {
      if (!(node.static && node.value)) {
        return;
      }

      const propName = astUtil.getPropertyName(node);
      const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';

      if (!isDefaultProp) {
        return;
      }

      // find component this propTypes/defaultProps belongs to
      const component = components.get(utils.getParentES6Component());
      if (!component) {
        return;
      }

      const expression = resolveNodeValue(node.value);
      if (!expression || expression.type !== 'ObjectExpression') {
        return;
      }

      addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
    },

    // e.g.:
    // React.createClass({
    //   render: function() {
    //     return <div>{this.props.foo}</div>;
    //   },
    //   getDefaultProps: function() {
    //     return {
    //       foo: 'default'
    //     };
    //   }
    // });
    ObjectExpression: function(node) {
      // find component this propTypes/defaultProps belongs to
      const component = utils.isES5Component(node) && components.get(node);
      if (!component) {
        return;
      }

      // Search for the proptypes declaration
      node.properties.forEach(property => {
        if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
          return;
        }

        const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);

        if (isDefaultProp && property.value.type === 'FunctionExpression') {
          const returnStatement = utils.findReturnStatement(property);
          if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
            return;
          }

          addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
        }
      });
    }
  };
};
