const REPLACED = Symbol("replaced");
const h = require("./helpers");

module.exports = t => {
  function mergeNestedIfs(path) {
    const consequent = path.get("consequent");
    const alternate = path.get("alternate");

    // not nested if
    if (!consequent.isIfStatement()) return;

    // there are no alternate nodes in both the if statements (nested)
    if (alternate.node || consequent.get("alternate").node) return;

    const test = path.get("test");
    test.replaceWith(
      t.logicalExpression("&&", test.node, consequent.get("test").node)
    );

    consequent.replaceWith(t.clone(consequent.get("consequent").node));
  }

  // No alternate, make into a guarded expression
  function toGuardedExpression(path) {
    const { node } = path;
    if (
      node.consequent &&
      !node.alternate &&
      node.consequent.type === "ExpressionStatement"
    ) {
      let 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 REPLACED;
    }
  }

  // both consequent and alternate are expressions, turn into ternary
  function toTernary(path) {
    const { node } = path;
    if (
      t.isExpressionStatement(node.consequent) &&
      t.isExpressionStatement(node.alternate)
    ) {
      path.replaceWith(
        t.conditionalExpression(
          node.test,
          node.consequent.expression,
          node.alternate.expression
        )
      );
      return REPLACED;
    }
  }

  // consequent and alternate are return -- conditional.
  function toConditional(path) {
    const { node } = path;
    if (
      t.isReturnStatement(node.consequent) &&
      t.isReturnStatement(node.alternate)
    ) {
      if (!node.consequent.argument && !node.alternate.argument) {
        path.replaceWith(t.expressionStatement(node.test));
        return REPLACED;
      }

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

  // There is nothing after this If block. And one or both
  // of the consequent and alternate are either expression statment
  // or return statements.
  function toReturn(path) {
    const { node } = path;

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

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

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

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

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

        // 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 || h.VOID_0(t),
                h.VOID_0(t)
              )
            )
          );
          return REPLACED;
        }
      }

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

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

    let 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()
    ) {
      const nextArg = next.node.argument || h.VOID_0(t);
      next.remove();
      path.replaceWith(
        t.returnStatement(
          t.conditionalExpression(
            node.test,
            node.consequent.argument || h.VOID_0(t),
            nextArg
          )
        )
      );
      return REPLACED;
    }

    // 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()
    ) {
      const 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 REPLACED;
      }

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

  // Remove else for if-return
  function removeUnnecessaryElse(path) {
    const { node } = path;
    const consequent = path.get("consequent");
    const alternate = path.get("alternate");

    if (
      consequent.node &&
      alternate.node &&
      (consequent.isReturnStatement() ||
        (consequent.isBlockStatement() &&
          t.isReturnStatement(
            consequent.node.body[consequent.node.body.length - 1]
          ))) &&
      // don't hoist declarations
      // TODO: validate declarations after fixing scope issues
      (alternate.isBlockStatement()
        ? !alternate
            .get("body")
            .some(
              stmt =>
                stmt.isVariableDeclaration({ kind: "let" }) ||
                stmt.isVariableDeclaration({ kind: "const" })
            )
        : true)
    ) {
      path.insertAfter(
        alternate.isBlockStatement()
          ? alternate.node.body.map(el => t.clone(el))
          : t.clone(alternate.node)
      );
      node.alternate = null;
      return REPLACED;
    }
  }

  function runTransforms(path) {
    // ordered
    const transforms = [
      toGuardedExpression,
      toTernary,
      toConditional,
      toReturn,
      removeUnnecessaryElse
    ];

    // run each of the replacement till we replace something
    // which is identified by the Symbol(REPLACED) that each of the
    // functions return when they replace something
    for (const transform of transforms) {
      if (transform(path) === REPLACED) {
        break;
      }
    }
  }

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

    if (!node.alternate) {
      return;
    }

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

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

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

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

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

    let ret;
    let test;
    const exprs = [];
    const statements = node.consequent.body;

    for (let 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);

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

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

    path.replaceWith(t.ifStatement(replacement, ret, null));
  }

  return {
    mergeNestedIfs,
    simplify: runTransforms,
    switchConsequent,
    conditionalReturnToGuards
  };
};
