/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

import { replaceNode } from "./utils/ast";
import { isTopLevel } from "./utils/helpers";

import generate from "@babel/generator";
import * as t from "@babel/types";

function getAssignmentTarget(node, bindings) {
  if (t.isObjectPattern(node)) {
    for (const property of node.properties) {
      if (t.isRestElement(property)) {
        property.argument = getAssignmentTarget(property.argument, bindings);
      } else {
        property.value = getAssignmentTarget(property.value, bindings);
      }
    }

    return node;
  }

  if (t.isArrayPattern(node)) {
    for (const [i, element] of node.elements.entries()) {
      node.elements[i] = getAssignmentTarget(element, bindings);
    }

    return node;
  }

  if (t.isAssignmentPattern(node)) {
    node.left = getAssignmentTarget(node.left, bindings);

    return node;
  }

  if (t.isRestElement(node)) {
    node.argument = getAssignmentTarget(node.argument, bindings);

    return node;
  }

  if (t.isIdentifier(node)) {
    return bindings.includes(node.name)
      ? node
      : t.memberExpression(t.identifier("self"), node);
  }

  return node;
}

// translates new bindings `var a = 3` into `self.a = 3`
// and existing bindings `var a = 3` into `a = 3` for re-assignments
function globalizeDeclaration(node, bindings) {
  return node.declarations.map(declaration =>
    t.expressionStatement(
      t.assignmentExpression(
        "=",
        getAssignmentTarget(declaration.id, bindings),
        declaration.init || t.unaryExpression("void", t.numericLiteral(0))
      )
    )
  );
}

// translates new bindings `a = 3` into `self.a = 3`
// and keeps assignments the same for existing bindings.
function globalizeAssignment(node, bindings) {
  return t.assignmentExpression(
    node.operator,
    getAssignmentTarget(node.left, bindings),
    node.right
  );
}

export default function mapExpressionBindings(expression, ast, bindings = []) {
  let isMapped = false;
  let shouldUpdate = true;

  t.traverse(ast, (node, ancestors) => {
    const parent = ancestors[ancestors.length - 1];

    if (t.isWithStatement(node)) {
      shouldUpdate = false;
      return;
    }

    if (!isTopLevel(ancestors)) {
      return;
    }

    if (t.isAssignmentExpression(node)) {
      if (t.isIdentifier(node.left) || t.isPattern(node.left)) {
        const newNode = globalizeAssignment(node, bindings);
        isMapped = true;
        return replaceNode(ancestors, newNode);
      }

      return;
    }

    if (!t.isVariableDeclaration(node)) {
      return;
    }

    if (!t.isForStatement(parent.node)) {
      const newNodes = globalizeDeclaration(node, bindings);
      isMapped = true;
      replaceNode(ancestors, newNodes);
    }
  });

  if (!shouldUpdate || !isMapped) {
    return expression;
  }

  return generate(ast).code;
}
