"use strict";

function replaceArray(t, path) {
  const { node } = path;
  // arguments is taken :(
  const constructorArgs = path.get("arguments");
  if (
    t.isIdentifier(node.callee, { name: "Array" }) &&
    !path.scope.getBinding("Array")
  ) {
    if (constructorArgs.length === 0) {
      // Array() -> []
      path.replaceWith(t.arrayExpression([]));
    } else if (constructorArgs.length === 1) {
      const arg = constructorArgs[0];
      const result = arg.evaluate();

      if (result.confident) {
        if (typeof result.value === "number") {
          if (
            result.value >= 0 &&
            result.value <= 6 &&
            result.value % 1 === 0
          ) {
            // "Array(7)" is shorter than "[,,,,,,,]"
            path.replaceWith(t.arrayExpression(Array(result.value).fill(null)));
          } else {
            dropNewIfPresent();
          }
        } else {
          // Array("Asdf"), Array(true), Array(false)
          path.replaceWith(t.arrayExpression([t.valueToNode(result.value)]));
        }
      } else {
        const transformables = [
          "ArrayExpression",
          "ObjectExpression",
          "FunctionExpression",
          "ArrowFunctionExpression",
          "ClassExpression"
        ];
        if (transformables.indexOf(arg.node.type) !== -1) {
          // Array([]), Array({})
          // Array(()=>{}), Array(class{}), Array(function(){})
          path.replaceWith(t.arrayExpression([arg.node]));
        } else {
          // Array(x); Array(a.b);
          dropNewIfPresent();
        }
      }
    } else {
      // Array(2,3), Array(a,b) => [2,3], [a,b]
      path.replaceWith(t.arrayExpression(node.arguments));
    }
    return true;
  }

  function dropNewIfPresent() {
    if (path.isNewExpression()) {
      path.replaceWith(t.callExpression(node.callee, node.arguments));
    }
  }
}

function replaceObject(t, path) {
  const { node } = path;
  if (
    t.isIdentifier(node.callee, { name: "Object" }) &&
    !path.scope.getBinding("Object")
  ) {
    const isVoid0 = require("babel-helper-is-void-0")(t);
    const arg = node.arguments[0];
    const binding =
      arg && t.isIdentifier(arg) && path.scope.getBinding(arg.name);

    // Object() -> {}
    if (node.arguments.length === 0) {
      path.replaceWith(t.objectExpression([]));

      // Object([]) -> []
    } else if (arg.type === "ArrayExpression" || t.isFunctionExpression(arg)) {
      path.replaceWith(arg);

      // Object(null) -> {}
    } else if (
      isVoid0(arg) ||
      arg.name === "undefined" ||
      arg.type === "NullLiteral" ||
      (arg.type === "ObjectExpression" && arg.properties.length === 0)
    ) {
      path.replaceWith(t.objectExpression([]));

      // Object(localFn) -> localFn
    } else if (binding && binding.path.isFunction()) {
      path.replaceWith(arg);

      // Object({a:b}) -> {a:b}
    } else if (arg.type === "ObjectExpression") {
      path.replaceWith(arg);

      // new Object(a) -> Object(a)
    } else if (node.type === "NewExpression") {
      path.replaceWith(t.callExpression(node.callee, node.arguments));
    }
    return true;
  }
}

function defaults({
  boolean = true,
  number = true,
  string = true,
  array = true,
  object = true
} = {}) {
  return {
    boolean,
    number,
    string,
    array,
    object
  };
}

module.exports = function({ types: t }) {
  return {
    name: "minify-type-constructors",
    visitor: {
      CallExpression(path) {
        const { node } = path;
        const opts = defaults(this.opts);

        // Boolean(foo) -> !!foo
        if (
          opts.boolean &&
          t.isIdentifier(node.callee, { name: "Boolean" }) &&
          node.arguments.length === 1 &&
          !path.scope.getBinding("Boolean")
        ) {
          path.replaceWith(
            t.unaryExpression(
              "!",
              t.unaryExpression("!", node.arguments[0], true),
              true
            )
          );
          return;
        }

        // Number(foo) -> +foo
        if (
          opts.number &&
          t.isIdentifier(node.callee, { name: "Number" }) &&
          node.arguments.length === 1 &&
          !path.scope.getBinding("Number")
        ) {
          path.replaceWith(t.unaryExpression("+", node.arguments[0], true));
          return;
        }

        // String(foo) -> foo + ''
        if (
          opts.string &&
          t.isIdentifier(node.callee, { name: "String" }) &&
          node.arguments.length === 1 &&
          !path.scope.getBinding("String")
        ) {
          path.replaceWith(
            t.binaryExpression("+", node.arguments[0], t.stringLiteral(""))
          );
          return;
        }

        // Array() -> []
        if (opts.array && replaceArray(t, path)) {
          return;
        }

        // Object() -> {}
        if (opts.object && replaceObject(t, path)) {
          return;
        }
      },
      NewExpression(path) {
        const opts = defaults(this.opts);

        // new Array() -> []
        if (opts.array && replaceArray(t, path)) {
          return;
        }

        // new Object() -> {}
        if (opts.object && replaceObject(t, path)) {
          return;
        }
      }
    }
  };
};
