"use strict";

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

var PatternMatch = require("./pattern-match");

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

  var flipExpressions = require("babel-helper-flip-expressions")(t);
  var toMultipleSequenceExpressions = require("babel-helper-to-multiple-sequence-expressions")(t);

  var VOID_0 = t.unaryExpression("void", t.numericLiteral(0), true);
  var condExprSeen = Symbol("condExprSeen");
  var seqExprSeen = Symbol("seqExprSeen");
  var shouldRevisit = Symbol("shouldRevisit");

  // Types as symbols for comparisions
  var types = {};
  t.TYPES.forEach(function (type) {
    types[type] = Symbol.for(type);
  });
  var isNodeOfType = function isNodeOfType(node, typeSymbol) {
    if (typeof typeSymbol !== "symbol") return false;
    return t["is" + Symbol.keyFor(typeSymbol)](node);
  };

  // small abstractions
  var not = function not(node) {
    return t.unaryExpression("!", node);
  };
  var notnot = function notnot(node) {
    return not(not(node));
  };
  var or = function or(a, b) {
    return t.logicalExpression("||", a, b);
  };
  var and = function and(a, b) {
    return t.logicalExpression("&&", a, b);
  };

  var operators = new Set(["+", "-", "*", "%", "<<", ">>", ">>>", "&", "|", "^", "/", "**"]);

  var updateOperators = new Set(["+", "-"]);

  function areArraysEqual(arr1, arr2) {
    return arr1.every(function (value, index) {
      return String(value) === String(arr2[index]);
    });
  }

  function getName(node) {
    if (node.type === "ThisExpression") {
      return "this";
    }
    if (node.type === "Super") {
      return "super";
    }
    if (node.type === "NullLiteral") {
      return "null";
    }
    // augment identifiers so that they don't match
    // string/number literals
    // but still match against each other
    return node.name ? node.name + "_" : node.value /* Literal */;
  }

  function getPropNames(path) {
    if (!path.isMemberExpression()) {
      return;
    }

    var obj = path.get("object");

    var prop = path.get("property");
    var propNames = [getName(prop.node)];

    while (obj.type === "MemberExpression") {
      var node = obj.get("property").node;
      if (node) {
        propNames.push(getName(node));
      }
      obj = obj.get("object");
    }
    propNames.push(getName(obj.node));

    return propNames;
  }
  var OP_AND = function OP_AND(input) {
    return input === "&&";
  };
  var OP_OR = function OP_OR(input) {
    return input === "||";
  };

  return {
    name: "minify-simplify",
    visitor: {
      Statement: {
        exit(path) {
          if (path.node[shouldRevisit]) {
            delete path.node[shouldRevisit];
            path.visit();
          }
        }
      },

      // CallExpression(path) {
      // const { node } = path;

      /* (function() {})() -> !function() {}()
      There is a bug in babel in printing this. Disabling for now.
      if (t.isFunctionExpression(node.callee) &&
          (t.isExpressionStatement(parent) ||
           (t.isSequenceExpression(parent) && parent.expressions[0] === node))
      ) {
        path.replaceWith(
          t.callExpression(
            t.unaryExpression("!", node.callee, true),
            node.arguments
          )
        );
        return;
      }*/
      // },

      UnaryExpression: {
        enter: [

        // Demorgans.
        function (path) {
          var node = path.node;


          if (node.operator !== "!" || flipExpressions.hasSeen(node)) {
            return;
          }

          var expr = node.argument;

          // We need to make sure that the return type will always be boolean.
          if (!(t.isLogicalExpression(expr) || t.isConditionalExpression(expr) || t.isBinaryExpression(expr))) {
            return;
          }
          if (t.isBinaryExpression(expr) && t.COMPARISON_BINARY_OPERATORS.indexOf(expr.operator) === -1) {
            return;
          }

          if (flipExpressions.shouldFlip(expr, 1)) {
            var newNode = flipExpressions.flip(expr);
            path.replaceWith(newNode);
          }
        },

        // !(a, b, c) -> a, b, !c
        function (path) {
          var node = path.node;


          if (node.operator !== "!") {
            return;
          }

          if (!t.isSequenceExpression(node.argument)) {
            return;
          }

          var seq = node.argument.expressions;
          var expr = seq[seq.length - 1];
          seq[seq.length - 1] = t.unaryExpression("!", expr, true);
          path.replaceWith(node.argument);
        },

        // !(a ? b : c) -> a ? !b : !c
        function (path) {
          var node = path.node;


          if (node.operator !== "!") {
            return;
          }

          if (!t.isConditional(node.argument)) {
            return;
          }

          var cond = node.argument;
          cond.alternate = t.unaryExpression("!", cond.alternate, true);
          cond.consequent = t.unaryExpression("!", cond.consequent, true);
          path.replaceWith(node.argument);
        }]
      },

      LogicalExpression: {
        exit(path) {
          // cache of path.evaluate()
          var evaluateMemo = new Map();

          var TRUTHY = function TRUTHY(input) {
            // !NaN and !undefined are truthy
            // separate check here as they are considered impure by babel
            if (input.isUnaryExpression() && input.get("argument").isIdentifier()) {
              if (input.node.argument.name === "NaN" || input.node.argument.name === "undefined") {
                return true;
              }
            }
            var evalResult = input.evaluate();
            evaluateMemo.set(input, evalResult);
            return evalResult.confident && input.isPure() && evalResult.value;
          };

          var FALSY = function FALSY(input) {
            // NaN and undefined are falsy
            // separate check here as they are considered impure by babel
            if (input.isIdentifier()) {
              if (input.node.name === "NaN" || input.node.name === "undefined") {
                return true;
              }
            }
            var evalResult = input.evaluate();
            evaluateMemo.set(input, evalResult);
            return evalResult.confident && input.isPure() && !evalResult.value;
          };

          var EX = types.Expression;

          // Convention:
          // [left, operator, right, handler(leftNode, rightNode)]

          var matcher = new PatternMatch([[TRUTHY, OP_AND, EX, function (l, r) {
            return r;
          }], [FALSY, OP_AND, EX, function (l) {
            return l;
          }], [TRUTHY, OP_OR, EX, function (l) {
            return l;
          }], [FALSY, OP_OR, EX, function (l, r) {
            return r;
          }]]);

          var left = path.get("left");
          var right = path.get("right");
          var operator = path.node.operator;

          var result = matcher.match([left, operator, right], isPatternMatchesPath);

          if (result.match) {
            // here we are sure that left.evaluate is always confident becuase
            // it satisfied one of TRUTHY/FALSY paths
            var value = void 0;
            if (evaluateMemo.has(left)) {
              value = evaluateMemo.get(left).value;
            } else {
              value = left.evaluate().value;
            }
            path.replaceWith(result.value(t.valueToNode(value), right.node));
          }
        }
      },

      AssignmentExpression(path) {

        var rightExpr = path.get("right");
        var leftExpr = path.get("left");

        if (path.node.operator !== "=") {
          return;
        }

        var canBeUpdateExpression = rightExpr.get("right").isNumericLiteral() && rightExpr.get("right").node.value === 1 && updateOperators.has(rightExpr.node.operator);

        if (leftExpr.isMemberExpression()) {

          var leftPropNames = getPropNames(leftExpr);
          var rightPropNames = getPropNames(rightExpr.get("left"));

          if (!leftPropNames || leftPropNames.indexOf(undefined) > -1 || !rightPropNames || rightPropNames.indexOf(undefined) > -1 || !operators.has(rightExpr.node.operator) || !areArraysEqual(leftPropNames, rightPropNames)) {
            return;
          }
        } else {
          if (!rightExpr.isBinaryExpression() || !operators.has(rightExpr.node.operator) || leftExpr.node.name !== rightExpr.node.left.name) {
            return;
          }
        }

        var newExpression = void 0;

        // special case x=x+1 --> ++x
        if (canBeUpdateExpression) {
          newExpression = t.updateExpression(rightExpr.node.operator + rightExpr.node.operator, t.clone(leftExpr.node), true /* prefix */);
        } else {
          newExpression = t.assignmentExpression(rightExpr.node.operator + "=", t.clone(leftExpr.node), t.clone(rightExpr.node.right));
        }

        path.replaceWith(newExpression);
      },

      ConditionalExpression: {
        enter: [
        // !foo ? 'foo' : 'bar' -> foo ? 'bar' : 'foo'
        // foo !== 'lol' ? 'foo' : 'bar' -> foo === 'lol' ? 'bar' : 'foo'
        function flipIfOrConditional(path) {
          var node = path.node;

          if (!path.get("test").isLogicalExpression()) {
            flipNegation(node);
            return;
          }

          if (flipExpressions.shouldFlip(node.test)) {
            node.test = flipExpressions.flip(node.test);
            var _ref2 = [node.consequent, node.alternate];
            node.alternate = _ref2[0];
            node.consequent = _ref2[1];
          }
        }, function simplifyPatterns(path) {
          var test = path.get("test");
          var consequent = path.get("consequent");
          var alternate = path.get("alternate");

          var EX = types.Expression,
              LE = types.LogicalExpression;

          // Convention:
          // ===============
          // for each pattern [test, consequent, alternate, handler(expr, cons, alt)]

          var matcher = new PatternMatch([[LE, true, false, function (e) {
            return e;
          }], [EX, true, false, function (e) {
            return notnot(e);
          }], [EX, false, true, function (e) {
            return not(e);
          }], [LE, true, EX, function (e, c, a) {
            return or(e, a);
          }], [EX, true, EX, function (e, c, a) {
            return or(notnot(e), a);
          }], [EX, false, EX, function (e, c, a) {
            return and(not(e), a);
          }], [EX, EX, true, function (e, c) {
            return or(not(e), c);
          }], [LE, EX, false, function (e, c) {
            return and(e, c);
          }], [EX, EX, false, function (e, c) {
            return and(notnot(e), c);
          }]]);

          var result = matcher.match([test, consequent, alternate], isPatternMatchesPath);

          if (result.match) {
            path.replaceWith(result.value(test.node, consequent.node, alternate.node));
          }
        }],

        exit: [
        // a ? x = foo : b ? x = bar : x = baz;
        // x = a ? foo : b ? bar : baz;
        function (topPath) {
          if (!topPath.parentPath.isExpressionStatement() && !topPath.parentPath.isSequenceExpression()) {
            return;
          }

          var mutations = [];
          var firstLeft = null;
          var operator = null;
          function visit(path) {
            if (path.isConditionalExpression()) {
              var _bail = visit(path.get("consequent"));
              if (_bail) {
                return true;
              }
              _bail = visit(path.get("alternate"));
              return _bail;
            }

            if (operator == null) {
              operator = path.node.operator;
            } else if (path.node.operator !== operator) {
              return true;
            }

            if (!path.isAssignmentExpression() || !(path.get("left").isIdentifier() || path.get("left").isMemberExpression())) {
              return true;
            }

            var left = path.get("left").node;
            if (firstLeft == null) {
              firstLeft = left;
            } else if (!t.isNodesEquivalent(left, firstLeft)) {
              return true;
            }

            mutations.push(function () {
              return path.replaceWith(path.get("right").node);
            });
          }

          var bail = visit(topPath);
          if (bail) {
            return;
          }

          mutations.forEach(function (f) {
            return f();
          });
          topPath.replaceWith(t.assignmentExpression(operator, firstLeft, topPath.node));
        },

        // bar ? void 0 : void 0
        // (bar, void 0)
        // TODO: turn this into general equiv check
        function (path) {
          var node = path.node;

          if (isVoid0(node.consequent) && isVoid0(node.alternate)) {
            path.replaceWith(t.sequenceExpression([path.node.test, VOID_0]));
          }
        },

        // bar ? void 0 : foo ? void 0 : <etc>
        // bar || foo : void 0
        // TODO: turn this into general equiv check
        function (path) {
          var node = path.node;


          if (node[condExprSeen] || !isVoid0(node.consequent)) {
            return;
          }

          node[condExprSeen] = true;

          var tests = [node.test];
          var mutations = [];
          var alt = void 0;

          var _loop = function _loop(next) {
            next.node[condExprSeen] = true;
            alt = next.node.alternate;

            if (isVoid0(next.node.consequent)) {
              tests.push(next.node.test);
              mutations.push(function () {
                return next.remove();
              });
            } else {
              alt = next.node;
              return "break";
            }
          };

          for (var next = path.get("alternate"); next.isConditionalExpression(); next = next.get("alternate")) {
            var _ret = _loop(next);

            if (_ret === "break") break;
          }

          if (tests.length === 1) {
            return;
          }

          var test = tests.reduce(function (expr, curTest) {
            return t.logicalExpression("||", expr, curTest);
          });

          path.replaceWith(t.conditionalExpression(test, VOID_0, alt));
        }]
      },

      // concat
      VariableDeclaration: {
        enter: [
        // Put vars with no init at the top.
        function (path) {
          var node = path.node;


          if (node.declarations.length < 2) {
            return;
          }

          var inits = [];
          var empty = [];
          var _iteratorNormalCompletion = true;
          var _didIteratorError = false;
          var _iteratorError = undefined;

          try {
            for (var _iterator = node.declarations[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
              var decl = _step.value;

              if (!decl.init) {
                empty.push(decl);
              } else {
                inits.push(decl);
              }
            }

            // This is based on exprimintation but there is a significant
            // imrpovement when we place empty vars at the top in smaller
            // files. Whereas it hurts in larger files.
          } catch (err) {
            _didIteratorError = true;
            _iteratorError = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion && _iterator.return) {
                _iterator.return();
              }
            } finally {
              if (_didIteratorError) {
                throw _iteratorError;
              }
            }
          }

          if (this.fitsInSlidingWindow) {
            node.declarations = empty.concat(inits);
          } else {
            node.declarations = inits.concat(empty);
          }
        }]
      },

      Function: {
        exit(path) {
          earlyReturnTransform(path);

          if (!path.node[shouldRevisit]) {
            return;
          }

          delete path.node[shouldRevisit];
          path.visit();
        }
      },

      For: {
        enter: earlyContinueTransform,
        exit: earlyContinueTransform
      },

      ForStatement: {

        // Merge previous expressions in the init part of the for.
        enter(path) {
          var node = path.node;

          if (!path.inList || node.init && !t.isExpression(node.init)) {
            return;
          }

          var prev = path.getSibling(path.key - 1);
          var consumed = false;
          if (prev.isVariableDeclaration()) {
            var referencedOutsideLoop = false;

            // we don't care if vars are referenced outside the loop as they are fn scope
            if (prev.node.kind === "let" || prev.node.kind === "const") {
              var ids = Object.keys(prev.getBindingIdentifiers());

              idloop: for (var i = 0; i < ids.length; i++) {
                var refs = prev.scope.bindings[ids[i]].referencePaths;
                for (var j = 0; j < refs.length; j++) {
                  if (!isAncestor(path, refs[j])) {
                    referencedOutsideLoop = true;
                    break idloop;
                  }
                }
              }
            }

            if (!node.init && !referencedOutsideLoop) {
              node.init = prev.node;
              consumed = true;
            }
          } else if (prev.isExpressionStatement()) {
            var expr = prev.node.expression;
            if (node.init) {
              if (t.isSequenceExpression(expr)) {
                expr.expressions.push(node.init);
                node.init = expr;
              } else {
                node.init = t.sequenceExpression([expr, node.init]);
              }
            } else {
              node.init = expr;
            }
            consumed = true;
          }
          if (consumed) {
            prev.remove();
          }
        },

        exit(path) {
          var node = path.node;

          if (!node.test) {
            return;
          }

          if (!path.get("body").isBlockStatement()) {
            var bodyNode = path.get("body").node;
            if (!t.isIfStatement(bodyNode)) {
              return;
            }

            if (t.isBreakStatement(bodyNode.consequent, { label: null })) {
              node.test = t.logicalExpression("&&", node.test, t.unaryExpression("!", bodyNode.test, true));
              node.body = bodyNode.alternate || t.emptyStatement();
              return;
            }

            if (t.isBreakStatement(bodyNode.alternate, { label: null })) {
              node.test = t.logicalExpression("&&", node.test, bodyNode.test);
              node.body = bodyNode.consequent || t.emptyStatement();
              return;
            }

            return;
          }

          var statements = node.body.body;
          var exprs = [];
          var ifStatement = null;
          var breakAt = null;
          var i = 0;
          for (var statement; statement = statements[i]; i++) {
            if (t.isIfStatement(statement)) {
              if (t.isBreakStatement(statement.consequent, { label: null })) {
                ifStatement = statement;
                breakAt = "consequent";
              } else if (t.isBreakStatement(statement.alternate, { label: null })) {
                ifStatement = statement;
                breakAt = "alternate";
              }
              break;
            }

            // A statement appears before the break statement then bail.
            if (!t.isExpressionStatement(statement)) {
              return;
            }

            exprs.push(statement.expression);
          }

          if (!ifStatement) {
            return;
          }

          var rest = [];

          if (breakAt = "consequent") {
            if (t.isBlockStatement(ifStatement.alternate)) {
              rest.push.apply(rest, _toConsumableArray(ifStatement.alternate.body));
            } else if (ifStatement.alternate) {
              rest.push(ifStatement.alternate);
            }
          } else {
            if (t.isBlockStatement(ifStatement.consequent)) {
              rest.push.apply(rest, _toConsumableArray(ifStatement.consequent.body));
            } else if (ifStatement.consequent) {
              rest.push(ifStatement.consequent);
            }
          }

          rest.push.apply(rest, _toConsumableArray(statements.slice(i + 1)));

          var test = breakAt === "consequent" ? t.unaryExpression("!", ifStatement.test, true) : ifStatement.test;
          var expr = void 0;
          if (exprs.length === 1) {
            expr = t.sequenceExpression([exprs[0], test]);
          } else if (exprs.length) {
            exprs.push(test);
            expr = t.sequenceExpression(exprs);
          } else {
            expr = test;
          }

          node.test = t.logicalExpression("&&", node.test, expr);
          if (rest.length === 1) {
            node.body = rest[0];
          } else if (rest.length) {
            node.body = t.blockStatement(rest);
          } else {
            node.body = t.emptyStatement();
          }
        }
      },

      Program(path) {
        // An approximation of the resultant gzipped code after minification
        this.fitsInSlidingWindow = path.getSource().length / 10 < 33000;

        var node = path.node;

        var statements = toMultipleSequenceExpressions(node.body);
        if (!statements.length) {
          return;
        }
        node.body = statements;
      },

      BlockStatement: {
        enter(path) {
          var node = path.node,
              parent = path.parent;


          var top = [];
          var bottom = [];

          for (var i = 0; i < node.body.length; i++) {
            var bodyNode = node.body[i];
            if (t.isFunctionDeclaration(bodyNode)) {
              top.push(bodyNode);
            } else {
              bottom.push(bodyNode);
            }
          }

          var statements = top.concat(toMultipleSequenceExpressions(bottom));

          if (!statements.length) {
            return;
          }

          if (statements.length > 1 || needsBlock(node, parent) || node.directives) {
            node.body = statements;
            return;
          }

          if (statements.length) {
            path.replaceWith(statements[0]);
            return;
          }
        },

        exit(path) {
          var node = path.node,
              parent = path.parent;


          if (needsBlock(node, parent)) {
            return;
          }

          if (node.body.length === 1) {
            path.get("body")[0].inList = false;
            path.replaceWith(node.body[0]);
            return;
          }

          if (node.body.length === 0) {
            path.replaceWith(t.emptyStatement());
            return;
          }

          // Check if oppurtinties to merge statements are available.
          var statements = node.body;
          if (!statements.length) {
            return;
          }

          var _iteratorNormalCompletion2 = true;
          var _didIteratorError2 = false;
          var _iteratorError2 = undefined;

          try {
            for (var _iterator2 = statements[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
              var statement = _step2.value;

              if (!t.isExpressionStatement(statement)) {
                return;
              }
            }
          } catch (err) {
            _didIteratorError2 = true;
            _iteratorError2 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion2 && _iterator2.return) {
                _iterator2.return();
              }
            } finally {
              if (_didIteratorError2) {
                throw _iteratorError2;
              }
            }
          }

          path.visit();
        }
      },

      ThrowStatement: createPrevExpressionEater("throw"),

      // Try to merge previous statements into a sequence
      ReturnStatement: {
        enter: [createPrevExpressionEater("return"),

        // Remove return if last statement with no argument.
        // Replace return with `void` argument with argument.
        function (path) {
          var node = path.node;


          if (!path.parentPath.parentPath.isFunction() || path.getSibling(path.key + 1).node) {
            return;
          }

          if (!node.argument) {
            path.remove();
            return;
          }

          if (t.isUnaryExpression(node.argument, { operator: "void" })) {
            path.replaceWith(node.argument.argument);
          }
        }]
      },

      // turn blocked ifs into single statements
      IfStatement: {
        exit: [
        // Merge nested if statements if possible
        function (_ref3) {
          var node = _ref3.node;

          if (!t.isIfStatement(node.consequent)) {
            return;
          }

          if (node.alternate || node.consequent.alternate) {
            return;
          }

          node.test = t.logicalExpression("&&", node.test, node.consequent.test);
          node.consequent = node.consequent.consequent;
        }, function (path) {
          var node = path.node;

          // No alternate, make into a guarded expression

          if (node.consequent && !node.alternate && node.consequent.type === "ExpressionStatement") {
            var op = "&&";
            if (t.isUnaryExpression(node.test, { operator: "!" })) {
              node.test = node.test.argument;
              op = "||";
            }

            path.replaceWith(t.expressionStatement(t.logicalExpression(op, node.test, node.consequent.expression)));
            return;
          }

          // Easy, both are expressions, turn into ternary
          if (t.isExpressionStatement(node.consequent) && t.isExpressionStatement(node.alternate)) {
            path.replaceWith(t.conditionalExpression(node.test, node.consequent.expression, node.alternate.expression));
            return;
          }

          // Easy: consequent and alternate are return -- conditional.
          if (t.isReturnStatement(node.consequent) && t.isReturnStatement(node.alternate)) {
            if (!node.consequent.argument && !node.alternate.argument) {
              path.replaceWith(t.expressionStatement(node.test));
              return;
            }

            path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || VOID_0, node.alternate.argument || VOID_0)));
            return;
          }

          // There is nothing after this block. And one or both
          // of the consequent and alternate are either expression statment
          // or return statements.
          if (!path.getSibling(path.key + 1).node && path.parentPath && path.parentPath.parentPath && path.parentPath.parentPath.isFunction()) {
            // Only the consequent is a return, void the alternate.
            if (t.isReturnStatement(node.consequent) && t.isExpressionStatement(node.alternate)) {
              if (!node.consequent.argument) {
                path.replaceWith(t.expressionStatement(t.logicalExpression("||", node.test, node.alternate.expression)));
                return;
              }

              path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || VOID_0, t.unaryExpression("void", node.alternate.expression, true))));
              return;
            }

            // Only the alternate is a return, void the consequent.
            if (t.isReturnStatement(node.alternate) && t.isExpressionStatement(node.consequent)) {
              if (!node.alternate.argument) {
                path.replaceWith(t.expressionStatement(t.logicalExpression("&&", node.test, node.consequent.expression)));
                return;
              }

              path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, t.unaryExpression("void", node.consequent.expression, true), node.alternate.argument || VOID_0)));
              return;
            }

            if (t.isReturnStatement(node.consequent) && !node.alternate) {
              if (!node.consequent.argument) {
                path.replaceWith(t.expressionStatement(node.test));
                return;
              }

              // This would only be worth it if the previous statement was an if
              // because then we may merge to create a conditional.
              if (path.getSibling(path.key - 1).isIfStatement()) {
                path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || VOID_0, VOID_0)));
                return;
              }
            }

            if (t.isReturnStatement(node.alternate) && !node.consequent) {
              if (!node.alternate.argument) {
                path.replaceWith(t.expressionStatement(node.test));
                return;
              }

              // Same as above.
              if (path.getSibling(path.key - 1).isIfStatement()) {
                path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.alternate.argument || VOID_0, VOID_0)));
                return;
              }
            }
          }

          var next = path.getSibling(path.key + 1);

          // If the next satatement(s) is an if statement and we can simplify that
          // to potentailly be an expression (or a return) then this will make it
          // easier merge.
          if (next.isIfStatement()) {
            next.pushContext(path.context);
            next.visit();
            next.popContext();
            next = path.getSibling(path.key + 1);
          }

          // Some other visitor might have deleted our node. OUR NODE ;_;
          if (!path.node) {
            return;
          }

          // No alternate but the next statement is a return
          // also turn into a return conditional
          if (t.isReturnStatement(node.consequent) && !node.alternate && next.isReturnStatement()) {
            var nextArg = next.node.argument || VOID_0;
            next.remove();
            path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument || VOID_0, nextArg)));
            return;
          }

          // Next is the last expression, turn into a return while void'ing the exprs
          if (path.parentPath && path.parentPath.parentPath && path.parentPath.parentPath.isFunction() && !path.getSibling(path.key + 2).node && t.isReturnStatement(node.consequent) && !node.alternate && next.isExpressionStatement()) {
            var nextExpr = next.node.expression;
            next.remove();
            if (node.consequent.argument) {
              path.replaceWith(t.returnStatement(t.conditionalExpression(node.test, node.consequent.argument, t.unaryExpression("void", nextExpr, true))));
              return;
            }

            path.replaceWith(t.logicalExpression("||", node.test, nextExpr));
            return;
          }

          if (node.consequent && node.alternate && (t.isReturnStatement(node.consequent) || t.isBlockStatement(node.consequent) && t.isReturnStatement(node.consequent.body[node.consequent.body.length - 1]))) {
            path.insertAfter(t.isBlockStatement(node.alternate) ? node.alternate.body : node.alternate);
            node.alternate = null;
            return;
          }
        },

        // If the consequent is if and the altenrate is not then
        // switch them out. That way we know we don't have to print
        // a block.x
        function (path) {
          var node = path.node;


          if (!node.alternate) {
            return;
          }

          if (!t.isIfStatement(node.consequent)) {
            return;
          }

          if (t.isIfStatement(node.alternate)) {
            return;
          }

          node.test = t.unaryExpression("!", node.test, true);
          var _ref4 = [node.consequent, node.alternate];
          node.alternate = _ref4[0];
          node.consequent = _ref4[1];
        },

        // Make if statements with conditional returns in the body into
        // an if statement that guards the rest of the block.
        function (path) {
          var node = path.node;


          if (!path.inList || !path.get("consequent").isBlockStatement() || node.alternate) {
            return;
          }

          var ret = void 0;
          var test = void 0;
          var exprs = [];
          var statements = node.consequent.body;

          for (var i = 0, statement; statement = statements[i]; i++) {
            if (t.isExpressionStatement(statement)) {
              exprs.push(statement.expression);
            } else if (t.isIfStatement(statement)) {
              if (i < statements.length - 1) {
                // This isn't the last statement. Bail.
                return;
              }
              if (statement.alternate) {
                return;
              }
              if (!t.isReturnStatement(statement.consequent)) {
                return;
              }
              ret = statement.consequent;
              test = statement.test;
            } else {
              return;
            }
          }

          if (!test || !ret) {
            return;
          }

          exprs.push(test);

          var expr = exprs.length === 1 ? exprs[0] : t.sequenceExpression(exprs);

          var replacement = t.logicalExpression("&&", node.test, expr);

          path.replaceWith(t.ifStatement(replacement, ret, null));
        }, createPrevExpressionEater("if")]
      },

      WhileStatement(path) {
        var node = path.node;

        path.replaceWith(t.forStatement(null, node.test, null, node.body));
      },

      ForInStatement: createPrevExpressionEater("for-in"),

      // Flatten sequence expressions.
      SequenceExpression: {
        exit(path) {
          if (path.node[seqExprSeen]) {
            return;
          }

          function flatten(node) {
            node[seqExprSeen] = true;
            var ret = [];
            var _iteratorNormalCompletion3 = true;
            var _didIteratorError3 = false;
            var _iteratorError3 = undefined;

            try {
              for (var _iterator3 = node.expressions[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
                var n = _step3.value;

                if (t.isSequenceExpression(n)) {
                  ret.push.apply(ret, _toConsumableArray(flatten(n)));
                } else {
                  ret.push(n);
                }
              }
            } catch (err) {
              _didIteratorError3 = true;
              _iteratorError3 = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion3 && _iterator3.return) {
                  _iterator3.return();
                }
              } finally {
                if (_didIteratorError3) {
                  throw _iteratorError3;
                }
              }
            }

            return ret;
          }

          path.node.expressions = flatten(path.node);
        }
      },

      SwitchCase(path) {
        var node = path.node;


        if (!node.consequent.length) {
          return;
        }

        node.consequent = toMultipleSequenceExpressions(node.consequent);
      },

      SwitchStatement: {
        exit: [

        // Convert switch statements with all returns in their cases
        // to return conditional.
        function (path) {
          var node = path.node;

          // Need to be careful of side-effects.

          if (!t.isIdentifier(node.discriminant)) {
            return;
          }

          if (!node.cases.length) {
            return;
          }

          var consTestPairs = [];
          var fallThru = [];
          var defaultRet = void 0;
          var _iteratorNormalCompletion4 = true;
          var _didIteratorError4 = false;
          var _iteratorError4 = undefined;

          try {
            for (var _iterator4 = node.cases[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
              var switchCase = _step4.value;

              if (switchCase.consequent.length > 1) {
                return;
              }

              var cons = switchCase.consequent[0];

              if (!switchCase.test) {
                if (!t.isReturnStatement(cons)) {
                  return;
                }
                defaultRet = cons;
                continue;
              }

              if (!switchCase.consequent.length) {
                if (fallThru.length) {
                  fallThru.push(switchCase.test);
                } else {
                  fallThru = [switchCase.test];
                }
                continue;
              }

              // TODO: can we void it?
              if (!t.isReturnStatement(cons)) {
                return;
              }

              var test = t.binaryExpression("===", node.discriminant, switchCase.test);
              if (fallThru.length) {
                test = fallThru.reduceRight(function (right, test) {
                  return t.logicalExpression("||", t.binaryExpression("===", node.discriminant, test), right);
                }, test);
                fallThru = [];
              }

              consTestPairs.push([test, cons.argument || VOID_0]);
            }

            // Bail if we have any remaining fallthrough
          } catch (err) {
            _didIteratorError4 = true;
            _iteratorError4 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion4 && _iterator4.return) {
                _iterator4.return();
              }
            } finally {
              if (_didIteratorError4) {
                throw _iteratorError4;
              }
            }
          }

          if (fallThru.length) {
            return;
          }

          // We need the default to be there to make sure there is an oppurtinity
          // not to return.
          if (!defaultRet) {
            if (path.inList) {
              var nextPath = path.getSibling(path.key + 1);
              if (nextPath.isReturnStatement()) {
                defaultRet = nextPath.node;
                nextPath.remove();
              } else if (!nextPath.node && path.parentPath.parentPath.isFunction()) {
                // If this is the last statement in a function we just fake a void return.
                defaultRet = t.returnStatement(VOID_0);
              } else {
                return;
              }
            } else {
              return;
            }
          }

          var cond = consTestPairs.reduceRight(function (alt, _ref5) {
            var _ref6 = _slicedToArray(_ref5, 2),
                test = _ref6[0],
                cons = _ref6[1];

            return t.conditionalExpression(test, cons, alt);
          }, defaultRet.argument || VOID_0);

          path.replaceWith(t.returnStatement(cond));

          // Maybe now we can merge with some previous switch statement.
          if (path.inList) {
            var prev = path.getSibling(path.key - 1);
            if (prev.isSwitchStatement()) {
              prev.visit();
            }
          }
        },

        // Convert switches into conditionals.
        function (path) {
          var node = path.node;

          // Need to be careful of side-effects.

          if (!t.isIdentifier(node.discriminant)) {
            return;
          }

          if (!node.cases.length) {
            return;
          }

          var exprTestPairs = [];
          var fallThru = [];
          var defaultExpr = void 0;
          var _iteratorNormalCompletion5 = true;
          var _didIteratorError5 = false;
          var _iteratorError5 = undefined;

          try {
            for (var _iterator5 = node.cases[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
              var switchCase = _step5.value;

              if (!switchCase.test) {
                if (switchCase.consequent.length !== 1) {
                  return;
                }
                if (!t.isExpressionStatement(switchCase.consequent[0])) {
                  return;
                }
                defaultExpr = switchCase.consequent[0].expression;
                continue;
              }

              if (!switchCase.consequent.length) {
                if (fallThru.length) {
                  fallThru.push(switchCase.test);
                } else {
                  fallThru = [switchCase.test];
                }
                continue;
              }

              var _switchCase$consequen = _slicedToArray(switchCase.consequent, 2),
                  cons = _switchCase$consequen[0],
                  breakStatement = _switchCase$consequen[1];

              if (switchCase === node.cases[node.cases.length - 1]) {
                if (breakStatement && !t.isBreakStatement(breakStatement)) {
                  return;
                }
              } else if (!t.isBreakStatement(breakStatement)) {
                return;
              }

              if (!t.isExpressionStatement(cons) || switchCase.consequent.length > 2) {
                return;
              }

              var test = t.binaryExpression("===", node.discriminant, switchCase.test);
              if (fallThru.length) {
                test = fallThru.reduceRight(function (right, test) {
                  return t.logicalExpression("||", t.binaryExpression("===", node.discriminant, test), right);
                }, test);
                fallThru = [];
              }

              exprTestPairs.push([test, cons.expression]);
            }
          } catch (err) {
            _didIteratorError5 = true;
            _iteratorError5 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion5 && _iterator5.return) {
                _iterator5.return();
              }
            } finally {
              if (_didIteratorError5) {
                throw _iteratorError5;
              }
            }
          }

          if (fallThru.length) {
            return;
          }

          var cond = exprTestPairs.reduceRight(function (alt, _ref7) {
            var _ref8 = _slicedToArray(_ref7, 2),
                test = _ref8[0],
                cons = _ref8[1];

            return t.conditionalExpression(test, cons, alt);
          }, defaultExpr || VOID_0);

          path.replaceWith(cond);
        }, function (path) {
          var node = path.node;


          if (!node.cases.length) {
            return;
          }

          var lastCase = path.get("cases")[node.cases.length - 1];
          if (!lastCase.node.consequent.length) {
            return;
          }

          var potentialBreak = lastCase.get("consequent")[lastCase.node.consequent.length - 1];
          if (t.isBreakStatement(potentialBreak) && potentialBreak.node.label === null) {
            potentialBreak.remove();
          }
        }, createPrevExpressionEater("switch")]
      }
    }
  };

  function flipNegation(node) {
    if (!node.consequent || !node.alternate) {
      return;
    }

    var test = node.test;
    var flip = false;

    if (t.isBinaryExpression(test)) {
      if (test.operator === "!==") {
        test.operator = "===";
        flip = true;
      }

      if (test.operator === "!=") {
        test.operator = "==";
        flip = true;
      }
    }

    if (t.isUnaryExpression(test, { operator: "!" })) {
      node.test = test.argument;
      flip = true;
    }

    if (flip) {
      var consequent = node.consequent;
      node.consequent = node.alternate;
      node.alternate = consequent;
    }
  }

  function needsBlock(node, parent) {
    return t.isFunction(parent) && node === parent.body || t.isTryStatement(parent) || t.isCatchClause(parent) || t.isSwitchStatement(parent) || isSingleBlockScopeDeclaration(node) && t.isIfStatement(parent);
  }

  function isSingleBlockScopeDeclaration(block) {
    return t.isBlockStatement(block) && block.body.length === 1 && (t.isVariableDeclaration(block.body[0], { kind: "let" }) || t.isVariableDeclaration(block.body[0], { kind: "const" }) || t.isFunctionDeclaration(block.body[0]));
  }

  function isVoid0(expr) {
    return expr === VOID_0 || t.isUnaryExpression(expr, { operator: "void" }) && t.isNumericLiteral(expr.argument, { value: 0 });
  }

  function earlyReturnTransform(path) {
    var node = path.node;


    if (!t.isBlockStatement(node.body)) {
      return;
    }

    for (var i = node.body.body.length; i >= 0; i--) {
      var statement = node.body.body[i];
      if (t.isIfStatement(statement) && !statement.alternate && t.isReturnStatement(statement.consequent) && !statement.consequent.argument) {
        genericEarlyExitTransform(path.get("body").get("body")[i]);
      }
    }
  }

  function earlyContinueTransform(path) {
    var node = path.node;


    if (!t.isBlockStatement(node.body)) {
      return;
    }

    for (var i = node.body.body.length; i >= 0; i--) {
      var statement = node.body.body[i];
      if (t.isIfStatement(statement) && !statement.alternate && t.isContinueStatement(statement.consequent) && !statement.consequent.label) {
        genericEarlyExitTransform(path.get("body").get("body")[i]);
      }
    }

    // We may have reduced the body to a single statement.
    if (node.body.body.length === 1) {
      path.get("body").replaceWith(node.body.body[0]);
    }
  }

  function genericEarlyExitTransform(path) {
    var node = path.node;


    var statements = path.container.slice(path.key + 1).filter(function (stmt) {
      return !t.isFunctionDeclaration(stmt);
    });

    if (!statements.length) {
      path.replaceWith(t.expressionStatement(node.test));
      return;
    }

    var test = node.test;
    if (t.isBinaryExpression(test) && test.operator === "!==") {
      test.operator = "===";
    } else if (t.isBinaryExpression(test) && test.operator === "!=") {
      test.operator = "==";
    } else if (t.isUnaryExpression(test, { operator: "!" })) {
      node.test = test.argument;
    } else {
      node.test = t.unaryExpression("!", node.test, true);
    }

    var l = statements.length;
    while (l-- > 0) {
      if (!t.isFunctionDeclaration(statements[l])) {
        path.getSibling(path.key + 1).remove();
      }
    }

    node.consequent = t.blockStatement(statements);

    // this should take care of removing the block
    path.visit();
  }

  function createPrevExpressionEater(keyword) {
    var key = void 0;
    switch (keyword) {
      case "switch":
        key = "discriminant";break;
      case "throw":
      case "return":
        key = "argument";break;
      case "if":
        key = "test";break;
      case "for-in":
        key = "right";break;
    }

    return function (path) {
      if (!path.inList) {
        return;
      }

      var node = path.node;

      var prev = path.getSibling(path.key - 1);
      if (!prev.isExpressionStatement()) {
        return;
      }

      var seq = prev.node.expression;
      if (node[key]) {
        if (t.isSequenceExpression(seq)) {
          seq.expressions.push(node[key]);
        } else {
          seq = t.sequenceExpression([seq, node[key]]);
        }
      } else {
        if (t.isSequenceExpression(seq)) {
          var lastExpr = seq.expressions[seq.expressions.length - 1];
          seq.expressions[seq.expressions.length - 1] = t.unaryExpression("void", lastExpr, true);
        } else {
          seq = t.unaryExpression("void", seq, true);
        }
      }

      if (seq) {
        node[key] = seq;
        prev.remove();

        // Since we were able to merge some stuff it's possible that this has opened
        // oppurtinties for other transforms to happen.
        // TODO: Look into changing the traversal order from bottom to up to avoid
        // having to revisit things.
        if (path.parentPath.parent) {
          path.parentPath.parent[shouldRevisit] = true;
        }
      }
    };
  }

  function isPatternMatchesPath(patternValue, inputPath) {
    if (Array.isArray(patternValue)) {
      for (var i = 0; i < patternValue.length; i++) {
        if (isPatternMatchesPath(patternValue[i], inputPath)) {
          return true;
        }
      }
      return false;
    }
    if (typeof patternValue === "function") {
      return patternValue(inputPath);
    }
    if (isNodeOfType(inputPath.node, patternValue)) return true;
    var evalResult = inputPath.evaluate();
    if (!evalResult.confident || !inputPath.isPure()) return false;
    return evalResult.value === patternValue;
  }

  // path1 -> path2
  // is path1 an ancestor of path2
  function isAncestor(path1, path2) {
    return !!path2.findParent(function (parent) {
      return parent === path1;
    });
  }
};