'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

exports.default = function (_ref) {
  var t = _ref.types;
  var template = _ref.template;

  function matchesPatterns(path, patterns) {
    return !!(0, _find2.default)(patterns, function (pattern) {
      return t.isIdentifier(path.node, { name: pattern }) || path.matchesPattern(pattern);
    });
  }

  function isReactLikeClass(node) {
    return !!(0, _find2.default)(node.body.body, function (classMember) {
      return t.isClassMethod(classMember) && t.isIdentifier(classMember.key, { name: 'render' });
    });
  }

  function isReactLikeComponentObject(node) {
    return t.isObjectExpression(node) && !!(0, _find2.default)(node.properties, function (objectMember) {
      return (t.isObjectProperty(objectMember) || t.isObjectMethod(objectMember)) && (t.isIdentifier(objectMember.key, { name: 'render' }) || t.isStringLiteral(objectMember.key, { value: 'render' }));
    });
  }

  // `foo({ displayName: 'NAME' });` => 'NAME'
  function getDisplayName(node) {
    var property = (0, _find2.default)(node.arguments[0].properties, function (node) {
      return node.key.name === 'displayName';
    });
    return property && property.value.value;
  }

  function hasParentFunction(path) {
    return !!path.findParent(function (parentPath) {
      return parentPath.isFunction();
    });
  }

  // wrapperFunction("componentId")(node)
  function wrapComponent(node, componentId, wrapperFunctionId) {
    return t.callExpression(t.callExpression(wrapperFunctionId, [t.stringLiteral(componentId)]), [node]);
  }

  // `{ name: foo }` => Node { type: "ObjectExpression", properties: [...] }
  function toObjectExpression(object) {
    var properties = Object.keys(object).map(function (key) {
      return t.objectProperty(t.identifier(key), object[key]);
    });

    return t.objectExpression(properties);
  }

  var wrapperFunctionTemplate = template('\n    function WRAPPER_FUNCTION_ID(ID_PARAM) {\n      return function(COMPONENT_PARAM) {\n        return EXPRESSION;\n      };\n    }\n  ');

  var VISITED_KEY = 'react-transform-' + Date.now();

  var componentVisitor = {
    Class: function Class(path) {
      if (path.node[VISITED_KEY] || !matchesPatterns(path.get('superClass'), this.superClasses) || !isReactLikeClass(path.node)) {
        return;
      }

      path.node[VISITED_KEY] = true;

      var componentName = path.node.id && path.node.id.name || null;
      var componentId = componentName || path.scope.generateUid('component');
      var isInFunction = hasParentFunction(path);

      this.components.push({
        id: componentId,
        name: componentName,
        isInFunction: isInFunction
      });

      // Can't wrap ClassDeclarations
      var isStatement = t.isStatement(path.node);
      var expression = t.toExpression(path.node);

      // wrapperFunction("componentId")(node)
      var wrapped = wrapComponent(expression, componentId, this.wrapperFunctionId);
      var constId = void 0;

      if (isStatement) {
        // wrapperFunction("componentId")(class Foo ...) => const Foo = wrapperFunction("componentId")(class Foo ...)
        constId = t.identifier(componentName || componentId);
        wrapped = t.variableDeclaration('const', [t.variableDeclarator(constId, wrapped)]);
      }

      if (t.isExportDefaultDeclaration(path.parent)) {
        path.parentPath.insertBefore(wrapped);
        path.parent.declaration = constId;
      } else {
        path.replaceWith(wrapped);
      }
    },
    CallExpression: function CallExpression(path) {
      if (path.node[VISITED_KEY] || !matchesPatterns(path.get('callee'), this.factoryMethods) || !isReactLikeComponentObject(path.node.arguments[0])) {
        return;
      }

      path.node[VISITED_KEY] = true;

      // `foo({ displayName: 'NAME' });` => 'NAME'
      var componentName = getDisplayName(path.node);
      var componentId = componentName || path.scope.generateUid('component');
      var isInFunction = hasParentFunction(path);

      this.components.push({
        id: componentId,
        name: componentName,
        isInFunction: isInFunction
      });

      path.replaceWith(wrapComponent(path.node, componentId, this.wrapperFunctionId));
    }
  };

  var ReactTransformBuilder = function () {
    function ReactTransformBuilder(file, options) {
      _classCallCheck(this, ReactTransformBuilder);

      this.file = file;
      this.program = file.path;
      this.options = this.normalizeOptions(options);

      // @todo: clean this shit up
      this.configuredTransformsIds = [];
    }

    _createClass(ReactTransformBuilder, [{
      key: 'normalizeOptions',
      value: function normalizeOptions(options) {
        return {
          factoryMethods: options.factoryMethods || ['React.createClass'],
          superClasses: options.superClasses || ['React.Component', 'Component'],
          transforms: options.transforms.map(function (opts) {
            return {
              transform: opts.transform,
              locals: opts.locals || [],
              imports: opts.imports || []
            };
          })
        };
      }
    }, {
      key: 'build',
      value: function build() {
        var componentsDeclarationId = this.file.scope.generateUidIdentifier('components');
        var wrapperFunctionId = this.file.scope.generateUidIdentifier('wrapComponent');

        var components = this.collectAndWrapComponents(wrapperFunctionId);

        if (!components.length) {
          return;
        }

        var componentsDeclaration = this.initComponentsDeclaration(componentsDeclarationId, components);
        var configuredTransforms = this.initTransformers(componentsDeclarationId);
        var wrapperFunction = this.initWrapperFunction(wrapperFunctionId);

        var body = this.program.node.body;

        body.unshift(wrapperFunction);
        configuredTransforms.reverse().forEach(function (node) {
          return body.unshift(node);
        });
        body.unshift(componentsDeclaration);
      }

      /**
       * const Foo = _wrapComponent('Foo')(class Foo extends React.Component {});
       * ...
       * const Bar = _wrapComponent('Bar')(React.createClass({
       *   displayName: 'Bar'
       * }));
       */

    }, {
      key: 'collectAndWrapComponents',
      value: function collectAndWrapComponents(wrapperFunctionId) {
        var components = [];

        this.file.path.traverse(componentVisitor, {
          wrapperFunctionId: wrapperFunctionId,
          components: components,
          factoryMethods: this.options.factoryMethods,
          superClasses: this.options.superClasses,
          currentlyInFunction: false
        });

        return components;
      }

      /**
       * const _components = {
       *   Foo: {
       *     displayName: "Foo"
       *   }
       * };
       */

    }, {
      key: 'initComponentsDeclaration',
      value: function initComponentsDeclaration(componentsDeclarationId, components) {
        var uniqueId = 0;

        var props = components.map(function (component) {
          var componentId = component.id;
          var componentProps = [];

          if (component.name) {
            componentProps.push(t.objectProperty(t.identifier('displayName'), t.stringLiteral(component.name)));
          }

          if (component.isInFunction) {
            componentProps.push(t.objectProperty(t.identifier('isInFunction'), t.booleanLiteral(true)));
          }

          var objectKey = void 0;

          if (t.isValidIdentifier(componentId)) {
            objectKey = t.identifier(componentId);
          } else {
            objectKey = t.stringLiteral(componentId);
          }

          return t.objectProperty(objectKey, t.objectExpression(componentProps));
        });

        return t.variableDeclaration('const', [t.variableDeclarator(componentsDeclarationId, t.objectExpression(props))]);
      }

      /**
       * import _transformLib from "transform-lib";
       * ...
       * const _transformLib2 = _transformLib({
       *   filename: "filename",
       *   components: _components,
       *   locals: [],
       *   imports: []
       * });
       */

    }, {
      key: 'initTransformers',
      value: function initTransformers(componentsDeclarationId) {
        var _this = this;

        return this.options.transforms.map(function (transform) {
          var transformName = transform.transform;
          var transformImportId = _this.file.addImport(transformName, 'default', transformName);

          var transformLocals = transform.locals.map(function (local) {
            return t.identifier(local);
          });

          var transformImports = transform.imports.map(function (importName) {
            return _this.file.addImport(importName, 'default', importName);
          });

          var configuredTransformId = _this.file.scope.generateUidIdentifier(transformName);
          var configuredTransform = t.variableDeclaration('const', [t.variableDeclarator(configuredTransformId, t.callExpression(transformImportId, [toObjectExpression({
            filename: t.stringLiteral(_this.file.opts.filename),
            components: componentsDeclarationId,
            locals: t.arrayExpression(transformLocals),
            imports: t.arrayExpression(transformImports)
          })]))]);

          _this.configuredTransformsIds.push(configuredTransformId);

          return configuredTransform;
        });
      }

      /**
       * function _wrapComponent(id) {
       *   return function (Component) {
       *     return _transformLib2(Component, id);
       *   };
       * }
       */

    }, {
      key: 'initWrapperFunction',
      value: function initWrapperFunction(wrapperFunctionId) {
        var idParam = t.identifier('id');
        var componentParam = t.identifier('Component');

        var expression = this.configuredTransformsIds.reverse().reduce(function (memo, transformId) {
          return t.callExpression(transformId, [memo, idParam]);
        }, componentParam);

        return wrapperFunctionTemplate({
          WRAPPER_FUNCTION_ID: wrapperFunctionId,
          ID_PARAM: idParam,
          COMPONENT_PARAM: componentParam,
          EXPRESSION: expression
        });
      }
    }], [{
      key: 'validateOptions',
      value: function validateOptions(options) {
        return (typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object' && Array.isArray(options.transforms);
      }
    }, {
      key: 'assertValidOptions',
      value: function assertValidOptions(options) {
        if (!ReactTransformBuilder.validateOptions(options)) {
          throw new Error('babel-plugin-react-transform requires that you specify options ' + 'in .babelrc or from the Babel Node API, and that it is an object ' + 'with a transforms property which is an array.');
        }
      }
    }]);

    return ReactTransformBuilder;
  }();

  return {
    visitor: {
      Program: function Program(path, _ref2) {
        var file = _ref2.file;
        var opts = _ref2.opts;

        ReactTransformBuilder.assertValidOptions(opts);
        var builder = new ReactTransformBuilder(file, opts);
        builder.build();
      }
    }
  };
};

var _find = require('lodash/find');

var _find2 = _interopRequireDefault(_find);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }