"use strict";

module.exports = ({ types: t }) => {
  const flipExpressions = require("babel-helper-flip-expressions")(t);
  const toMultipleSequenceExpressions = require("babel-helper-to-multiple-sequence-expressions")(
    t
  );
  const ifStatement = require("./if-statement")(t);
  const conditionalExpression = require("./conditional-expression")(t);
  const logicalExpression = require("./logical-expression")(t);
  const assignmentExpression = require("./assignment-expression")(t);

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

  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) {
            const { node } = path;

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

            const 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)) {
              const newNode = flipExpressions.flip(expr);
              path.replaceWith(newNode);
            }
          },

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

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

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

            const seq = node.argument.expressions;
            const 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) {
            const { node } = path;

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

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

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

      BinaryExpression(path) {
        if (["!=", "=="].indexOf(path.node.operator) !== -1) {
          undefinedToNull(path.get("left"));
          undefinedToNull(path.get("right"));
        }
      },

      LogicalExpression: {
        exit: logicalExpression.simplifyPatterns
      },

      AssignmentExpression: assignmentExpression.simplify,

      ConditionalExpression: {
        enter: [
          // !foo ? 'foo' : 'bar' -> foo ? 'bar' : 'foo'
          // foo !== 'lol' ? 'foo' : 'bar' -> foo === 'lol' ? 'bar' : 'foo'
          function flipIfOrConditional(path) {
            const { node } = path;
            if (!path.get("test").isLogicalExpression()) {
              flipNegation(node);
              return;
            }

            if (flipExpressions.shouldFlip(node.test)) {
              node.test = flipExpressions.flip(node.test);
              [node.alternate, node.consequent] = [
                node.consequent,
                node.alternate
              ];
            }
          },

          conditionalExpression.simplifyPatterns
        ],

        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;
            }

            const mutations = [];
            let firstLeft = null;
            let operator = null;
            function visit(path) {
              if (path.isConditionalExpression()) {
                let 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;
              }

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

              mutations.push(() => path.replaceWith(path.get("right").node));
            }

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

            mutations.forEach(f => 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) {
            const { node } = path;
            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) {
            const { node } = path;

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

            node[condExprSeen] = true;

            const tests = [node.test];
            const mutations = [];
            let alt;
            for (
              let next = path.get("alternate");
              next.isConditionalExpression();
              next = next.get("alternate")
            ) {
              next.node[condExprSeen] = true;
              alt = next.node.alternate;

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

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

            const test = tests.reduce((expr, curTest) =>
              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) {
            const { node } = path;

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

            const inits = [];
            const empty = [];
            for (const decl of node.declarations) {
              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.
            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) {
          const { node } = path;
          if (!path.inList || (node.init && !t.isExpression(node.init))) {
            return;
          }

          const prev = path.getSibling(path.key - 1);
          let consumed = false;
          if (prev.isVariableDeclaration()) {
            let 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") {
              const ids = Object.keys(prev.getBindingIdentifiers());

              idloop: for (let i = 0; i < ids.length; i++) {
                const binding = prev.scope.bindings[ids[i]];
                // TODO
                // Temporary Fix
                // if there is no binding, we assume it is referenced outside
                // and deopt to avoid bugs
                if (!binding) {
                  referencedOutsideLoop = true;
                  break idloop;
                }
                const refs = binding.referencePaths;
                for (let 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()) {
            const 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) {
          const { node } = path;
          if (!node.test) {
            return;
          }

          if (!path.get("body").isBlockStatement()) {
            const 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;
          }

          const statements = node.body.body;
          const exprs = [];
          let ifStatement = null;
          let breakAt = null;
          let i = 0;
          for (let 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;
          }

          const rest = [];

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

          rest.push(...statements.slice(i + 1));

          const test =
            breakAt === "consequent"
              ? t.unaryExpression("!", ifStatement.test, true)
              : ifStatement.test;
          let expr;
          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;

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

      BlockStatement: {
        enter(path) {
          const { node, parent } = path;

          const top = [];
          const bottom = [];

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

          const 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) {
          const { node, parent } = path;

          if (
            t.isArrowFunctionExpression(parent) &&
            node.body.length === 1 &&
            t.isReturnStatement(node.body[0]) &&
            node.body[0].argument !== null
          ) {
            path.replaceWith(node.body[0].argument);
            return;
          }

          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.
          const statements = node.body;
          if (!statements.length) {
            return;
          }

          for (const statement of statements) {
            if (!t.isExpressionStatement(statement)) {
              return;
            }
          }

          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) {
            const { node } = path;

            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: [
          ifStatement.mergeNestedIfs,
          ifStatement.simplify,
          ifStatement.switchConsequent,
          ifStatement.conditionalReturnToGuards,
          createPrevExpressionEater("if")
        ]
      },

      WhileStatement(path) {
        const { node } = path;
        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;
            const ret = [];
            for (const n of node.expressions) {
              if (t.isSequenceExpression(n)) {
                ret.push(...flatten(n));
              } else {
                ret.push(n);
              }
            }
            return ret;
          }

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

      SwitchCase(path) {
        const { node } = path;

        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) {
            const { node } = path;

            // Need to be careful of side-effects.
            if (!t.isIdentifier(node.discriminant)) {
              return;
            }

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

            const consTestPairs = [];
            let fallThru = [];
            let defaultRet;
            for (const switchCase of node.cases) {
              if (switchCase.consequent.length > 1) {
                return;
              }

              const cons = switchCase.consequent[0];

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

              if (!switchCase.consequent.length) {
                fallThru.push(switchCase.test);
                continue;
              }

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

              let test = t.binaryExpression(
                "===",
                node.discriminant,
                switchCase.test
              );

              if (fallThru.length && !defaultRet) {
                test = fallThru.reduceRight(
                  (right, test) =>
                    t.logicalExpression(
                      "||",
                      t.binaryExpression("===", node.discriminant, test),
                      right
                    ),
                  test
                );
              }
              fallThru = [];

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

            // Bail if we have any remaining fallthrough
            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) {
                const 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;
              }
            }

            const cond = consTestPairs.reduceRight(
              (alt, [test, cons]) => 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) {
              const prev = path.getSibling(path.key - 1);
              if (prev.isSwitchStatement()) {
                prev.visit();
              }
            }
          },

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

            // Need to be careful of side-effects.
            if (!t.isIdentifier(node.discriminant)) {
              return;
            }

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

            const exprTestPairs = [];
            let fallThru = [];
            let defaultExpr;
            for (const switchCase of node.cases) {
              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) {
                fallThru.push(switchCase.test);
                continue;
              }

              const [cons, breakStatement] = switchCase.consequent;
              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;
              }

              let test = t.binaryExpression(
                "===",
                node.discriminant,
                switchCase.test
              );
              if (fallThru.length && !defaultExpr) {
                test = fallThru.reduceRight(
                  (right, test) =>
                    t.logicalExpression(
                      "||",
                      t.binaryExpression("===", node.discriminant, test),
                      right
                    ),
                  test
                );
              }
              fallThru = [];

              exprTestPairs.push([test, cons.expression]);
            }

            if (fallThru.length) {
              return;
            }

            const cond = exprTestPairs.reduceRight(
              (alt, [test, cons]) => t.conditionalExpression(test, cons, alt),
              defaultExpr || VOID_0
            );

            path.replaceWith(cond);
          },

          function(path) {
            const { node } = path;

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

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

            const 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;
    }

    const test = node.test;
    let 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) {
      const 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) || t.isLoop(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) {
    const block = path.get("body");

    if (!block.isBlockStatement()) {
      return;
    }

    const body = block.get("body");

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

  function earlyContinueTransform(path) {
    const block = path.get("body");

    if (!block.isBlockStatement()) {
      return;
    }

    let body = block.get("body");

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

    // because we might have folded or removed statements
    body = block.get("body");

    // We may have reduced the body to a single statement.
    if (body.length === 1 && !needsBlock(block.node, path.node)) {
      block.replaceWith(body[0].node);
    }
  }

  function genericEarlyExitTransform(path) {
    const { node } = path;

    const statements = path.parentPath
      .get(path.listKey)
      .slice(path.key + 1)
      .filter(stmt => !stmt.isFunctionDeclaration());

    // deopt for any block scoped bindings
    // issue#399
    const deopt = !statements.every(stmt => {
      if (
        !(
          stmt.isVariableDeclaration({ kind: "let" }) ||
          stmt.isVariableDeclaration({ kind: "const" })
        )
      ) {
        return true;
      }
      const ids = Object.keys(stmt.getBindingIdentifiers());
      for (const id of ids) {
        const binding = path.scope.getBinding(id);

        // TODO
        // Temporary Fix
        // if there is no binding, we assume it is referenced outside
        // and deopt to avoid bugs
        if (!binding) {
          return false;
        }

        const refs = [...binding.referencePaths, ...binding.constantViolations];
        for (const ref of refs) {
          if (!ref.isIdentifier()) return false;
          const fnParent = ref.getFunctionParent();

          // TODO
          // Usage of scopes and bindings in simplify plugin results in
          // undefined bindings because scope changes are not updated in the
          // scope tree. So, we deopt whenever we encounter one such issue
          // and not perform the transformation
          if (!fnParent) {
            return false;
          }
          if (fnParent.scope !== path.scope) return false;
        }
      }

      return true;
    });

    if (deopt) {
      path.visit();
      return false;
    }

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

    const 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);
    }

    path
      .get("consequent")
      .replaceWith(
        t.blockStatement(statements.map(stmt => t.clone(stmt.node)))
      );

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

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

  function createPrevExpressionEater(keyword) {
    let key;
    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;
      }

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

      let 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)) {
          const 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;
        }
      }
    };
  }

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

  function isPureVoid(path) {
    return path.isUnaryExpression({ operator: "void" }) && path.isPure();
  }

  function isGlobalUndefined(path) {
    return (
      path.isIdentifier({ name: "undefined" }) &&
      !path.scope.getBinding("undefined")
    );
  }

  function undefinedToNull(path) {
    if (isGlobalUndefined(path) || isPureVoid(path)) {
      path.replaceWith(t.nullLiteral());
    }
  }
};
