import { declare } from "@babel/helper-plugin-utils";
import syntaxPartialApplication from "@babel/plugin-syntax-partial-application";
import { types as t } from "@babel/core";

export default declare(api => {
  api.assertVersion(7);

  /**
   * a function to figure out if a call expression has
   * ArgumentPlaceholder as one of its arguments
   * @param node a callExpression node
   * @returns boolean
   */
  function hasArgumentPlaceholder(node) {
    return node.arguments.some(arg => t.isArgumentPlaceholder(arg));
  }

  function unwrapArguments(node, scope) {
    const init = [];
    for (let i = 0; i < node.arguments.length; i++) {
      if (
        !t.isArgumentPlaceholder(node.arguments[i]) &&
        !t.isImmutable(node.arguments[i])
      ) {
        const id = scope.generateUidIdentifierBasedOnNode(
          node.arguments[i],
          "param",
        );
        scope.push({ id });
        if (t.isSpreadElement(node.arguments[i])) {
          init.push(
            t.assignmentExpression(
              "=",
              t.cloneNode(id),
              t.arrayExpression([t.spreadElement(node.arguments[i].argument)]),
            ),
          );
          node.arguments[i].argument = t.cloneNode(id);
        } else {
          init.push(
            t.assignmentExpression("=", t.cloneNode(id), node.arguments[i]),
          );
          node.arguments[i] = t.cloneNode(id);
        }
      }
    }
    return init;
  }

  function replacePlaceholders(node, scope) {
    const placeholders = [];
    const args = [];

    node.arguments.forEach(arg => {
      if (t.isArgumentPlaceholder(arg)) {
        const id = scope.generateUid("_argPlaceholder");
        placeholders.push(t.identifier(id));
        args.push(t.identifier(id));
      } else {
        args.push(arg);
      }
    });
    return [placeholders, args];
  }

  return {
    name: "proposal-partial-application",
    inherits: syntaxPartialApplication,

    visitor: {
      CallExpression(path) {
        if (!hasArgumentPlaceholder(path.node)) {
          return;
        }
        const { node, scope } = path;
        const functionLVal = path.scope.generateUidIdentifierBasedOnNode(
          node.callee,
        );
        const sequenceParts = [];
        const argsInitializers = unwrapArguments(node, scope);
        const [placeholdersParams, args] = replacePlaceholders(node, scope);

        scope.push({ id: functionLVal });

        if (node.callee.type === "MemberExpression") {
          const receiverLVal = path.scope.generateUidIdentifierBasedOnNode(
            node.callee.object,
          );
          scope.push({ id: receiverLVal });
          sequenceParts.push(
            t.assignmentExpression(
              "=",
              t.cloneNode(receiverLVal),
              node.callee.object,
            ),
            t.assignmentExpression(
              "=",
              t.cloneNode(functionLVal),
              t.memberExpression(
                receiverLVal,
                node.callee.property,
                false,
                false,
              ),
            ),
            ...argsInitializers,
            t.functionExpression(
              node.callee.property,
              placeholdersParams,
              t.blockStatement(
                [
                  t.returnStatement(
                    t.callExpression(
                      t.memberExpression(
                        functionLVal,
                        t.identifier("call"),
                        false,
                        false,
                      ),
                      [receiverLVal, ...args],
                    ),
                  ),
                ],
                [],
              ),
              false,
              false,
            ),
          );
        } else {
          sequenceParts.push(
            t.assignmentExpression("=", t.cloneNode(functionLVal), node.callee),
            ...argsInitializers,
            t.functionExpression(
              node.callee,
              placeholdersParams,
              t.blockStatement(
                [t.returnStatement(t.callExpression(functionLVal, args))],
                [],
              ),
              false,
              false,
            ),
          );
        }
        path.replaceWith(t.sequenceExpression(sequenceParts));
      },
    },
  };
});
