import * as Acorn from "../../third_party/acorn/acorn.js";
import { ECMA_VERSION } from "./AcornTokenizer.js";
export function substituteExpression(expression, nameMap) {
  const replacements = computeSubstitution(expression, nameMap);
  return applySubstitution(expression, replacements);
}
function computeSubstitution(expression, nameMap) {
  const root = Acorn.parse(expression, { ecmaVersion: ECMA_VERSION, allowAwaitOutsideFunction: true, ranges: false });
  const scopeVariables = new ScopeVariableAnalysis();
  scopeVariables.processNode(root);
  const freeVariables = scopeVariables.getFreeVariables();
  const result = [];
  const allNames = scopeVariables.getAllNames();
  for (const rename of nameMap.values()) {
    allNames.add(rename);
  }
  function getNewName(base) {
    let i = 1;
    while (allNames.has(`${base}_${i}`)) {
      i++;
    }
    const newName = `${base}_${i}`;
    allNames.add(newName);
    return newName;
  }
  for (const [name, rename] of nameMap.entries()) {
    const defUse = freeVariables.get(name);
    if (!defUse) {
      continue;
    }
    const binders = [];
    for (const use of defUse) {
      result.push({
        from: name,
        to: rename,
        offset: use.offset,
        isShorthandAssignmentProperty: use.isShorthandAssignmentProperty
      });
      binders.push(...use.scope.findBinders(rename));
    }
    for (const binder of binders) {
      if (binder.definitionKind === DefinitionKind.Fixed) {
        throw new Error(`Cannot avoid capture of '${rename}'`);
      }
      const newName = getNewName(rename);
      for (const use of binder.uses) {
        result.push({
          from: rename,
          to: newName,
          offset: use.offset,
          isShorthandAssignmentProperty: use.isShorthandAssignmentProperty
        });
      }
    }
  }
  result.sort((l, r) => l.offset - r.offset);
  return result;
}
function applySubstitution(expression, replacements) {
  const accumulator = [];
  let last = 0;
  for (const r of replacements) {
    accumulator.push(expression.slice(last, r.offset));
    let replacement = r.to;
    if (r.isShorthandAssignmentProperty) {
      replacement = `${r.from}: ${r.to}`;
    }
    accumulator.push(replacement);
    last = r.offset + r.from.length;
  }
  accumulator.push(expression.slice(last));
  return accumulator.join("");
}
var DefinitionKind = /* @__PURE__ */ ((DefinitionKind2) => {
  DefinitionKind2[DefinitionKind2["None"] = 0] = "None";
  DefinitionKind2[DefinitionKind2["Let"] = 1] = "Let";
  DefinitionKind2[DefinitionKind2["Var"] = 2] = "Var";
  DefinitionKind2[DefinitionKind2["Fixed"] = 3] = "Fixed";
  return DefinitionKind2;
})(DefinitionKind || {});
class Scope {
  variables = /* @__PURE__ */ new Map();
  parent;
  constructor(parent) {
    this.parent = parent;
  }
  addVariable(name, offset, definitionKind, isShorthandAssignmentProperty) {
    const variable = this.variables.get(name);
    const use = { offset, scope: this, isShorthandAssignmentProperty };
    if (!variable) {
      this.variables.set(name, { definitionKind, uses: [use] });
      return;
    }
    if (variable.definitionKind === 0 /* None */) {
      variable.definitionKind = definitionKind;
    }
    variable.uses.push(use);
  }
  findBinders(name) {
    const result = [];
    let scope = this;
    while (scope !== null) {
      const defUse = scope.variables.get(name);
      if (defUse && defUse.definitionKind !== 0 /* None */) {
        result.push(defUse);
      }
      scope = scope.parent;
    }
    return result;
  }
  #mergeChildDefUses(name, defUses) {
    const variable = this.variables.get(name);
    if (!variable) {
      this.variables.set(name, defUses);
      return;
    }
    variable.uses.push(...defUses.uses);
    if (defUses.definitionKind === 2 /* Var */) {
      console.assert(variable.definitionKind !== 1 /* Let */);
      if (variable.definitionKind === 0 /* None */) {
        variable.definitionKind = defUses.definitionKind;
      }
    } else {
      console.assert(defUses.definitionKind === 0 /* None */);
    }
  }
  finalizeToParent(isFunctionScope) {
    if (!this.parent) {
      console.error("Internal error: wrong nesting in scope analysis.");
      throw new Error("Internal error");
    }
    const keysToRemove = [];
    for (const [name, defUse] of this.variables.entries()) {
      if (defUse.definitionKind === 0 /* None */ || defUse.definitionKind === 2 /* Var */ && !isFunctionScope) {
        this.parent.#mergeChildDefUses(name, defUse);
        keysToRemove.push(name);
      }
    }
    keysToRemove.forEach((k) => this.variables.delete(k));
  }
}
class ScopeVariableAnalysis {
  #rootScope = new Scope(null);
  #allNames = /* @__PURE__ */ new Set();
  #currentScope = this.#rootScope;
  processNode(node) {
    if (node === null) {
      return;
    }
    switch (node.type) {
      case "AwaitExpression":
        this.processNode(node.argument);
        break;
      case "ArrayExpression":
        node.elements.forEach((item) => this.processNode(item));
        break;
      case "ExpressionStatement":
        this.processNode(node.expression);
        break;
      case "Program":
        console.assert(this.#currentScope === this.#rootScope);
        node.body.forEach((item) => this.processNode(item));
        console.assert(this.#currentScope === this.#rootScope);
        break;
      case "ArrayPattern":
        node.elements.forEach((item) => this.processNode(item));
        break;
      case "ArrowFunctionExpression":
        this.#pushScope();
        node.params.forEach(this.#processNodeAsDefinition.bind(this, 2 /* Var */));
        this.processNode(node.body);
        this.#popScope(true);
        break;
      case "AssignmentExpression":
      case "AssignmentPattern":
      case "BinaryExpression":
      case "LogicalExpression":
        this.processNode(node.left);
        this.processNode(node.right);
        break;
      case "BlockStatement":
        this.#pushScope();
        node.body.forEach(this.processNode.bind(this));
        this.#popScope(false);
        break;
      case "CallExpression":
        this.processNode(node.callee);
        node.arguments.forEach(this.processNode.bind(this));
        break;
      case "VariableDeclaration": {
        const definitionKind = node.kind === "var" ? 2 /* Var */ : 1 /* Let */;
        node.declarations.forEach(this.#processVariableDeclarator.bind(this, definitionKind));
        break;
      }
      case "CatchClause":
        this.#pushScope();
        this.#processNodeAsDefinition(1 /* Let */, node.param);
        node.body.body.forEach(this.processNode.bind(this));
        this.#popScope(false);
        break;
      case "ClassBody":
        node.body.forEach(this.processNode.bind(this));
        break;
      case "ClassDeclaration":
        this.#processNodeAsDefinition(1 /* Let */, node.id);
        this.#pushScope();
        this.processNode(node.superClass ?? null);
        this.processNode(node.body);
        this.#popScope(false);
        break;
      case "ClassExpression":
        this.#pushScope();
        this.processNode(node.superClass ?? null);
        this.processNode(node.body);
        this.#popScope(false);
        break;
      case "ChainExpression":
        this.processNode(node.expression);
        break;
      case "ConditionalExpression":
        this.processNode(node.test);
        this.processNode(node.consequent);
        this.processNode(node.alternate);
        break;
      case "DoWhileStatement":
        this.processNode(node.body);
        this.processNode(node.test);
        break;
      case "ForInStatement":
      case "ForOfStatement":
        this.#pushScope();
        this.processNode(node.left);
        this.processNode(node.right);
        this.processNode(node.body);
        this.#popScope(false);
        break;
      case "ForStatement":
        this.#pushScope();
        this.processNode(node.init ?? null);
        this.processNode(node.test ?? null);
        this.processNode(node.update ?? null);
        this.processNode(node.body);
        this.#popScope(false);
        break;
      case "FunctionDeclaration":
        this.#processNodeAsDefinition(2 /* Var */, node.id);
        this.#pushScope();
        this.#addVariable("this", node.start, 3 /* Fixed */);
        this.#addVariable("arguments", node.start, 3 /* Fixed */);
        node.params.forEach(this.#processNodeAsDefinition.bind(this, 1 /* Let */));
        this.processNode(node.body);
        this.#popScope(true);
        break;
      case "FunctionExpression":
        this.#pushScope();
        this.#addVariable("this", node.start, 3 /* Fixed */);
        this.#addVariable("arguments", node.start, 3 /* Fixed */);
        node.params.forEach(this.#processNodeAsDefinition.bind(this, 1 /* Let */));
        this.processNode(node.body);
        this.#popScope(true);
        break;
      case "Identifier":
        this.#addVariable(node.name, node.start);
        break;
      case "IfStatement":
        this.processNode(node.test);
        this.processNode(node.consequent);
        this.processNode(node.alternate ?? null);
        break;
      case "LabeledStatement":
        this.processNode(node.body);
        break;
      case "MetaProperty":
        break;
      case "MethodDefinition":
        if (node.computed) {
          this.processNode(node.key);
        }
        this.processNode(node.value);
        break;
      case "NewExpression":
        this.processNode(node.callee);
        node.arguments.forEach(this.processNode.bind(this));
        break;
      case "MemberExpression":
        this.processNode(node.object);
        if (node.computed) {
          this.processNode(node.property);
        }
        break;
      case "ObjectExpression":
        node.properties.forEach(this.processNode.bind(this));
        break;
      case "ObjectPattern":
        node.properties.forEach(this.processNode.bind(this));
        break;
      case "PrivateIdentifier":
        break;
      case "PropertyDefinition":
        if (node.computed) {
          this.processNode(node.key);
        }
        this.processNode(node.value ?? null);
        break;
      case "Property":
        if (node.shorthand) {
          console.assert(node.value === node.key);
          console.assert(node.value.type === "Identifier");
          this.#addVariable(node.value.name, node.value.start, 0 /* None */, true);
        } else {
          if (node.computed) {
            this.processNode(node.key);
          }
          this.processNode(node.value);
        }
        break;
      case "RestElement":
        this.#processNodeAsDefinition(1 /* Let */, node.argument);
        break;
      case "ReturnStatement":
        this.processNode(node.argument ?? null);
        break;
      case "SequenceExpression":
        node.expressions.forEach(this.processNode.bind(this));
        break;
      case "SpreadElement":
        this.processNode(node.argument);
        break;
      case "SwitchCase":
        this.processNode(node.test ?? null);
        node.consequent.forEach(this.processNode.bind(this));
        break;
      case "SwitchStatement":
        this.processNode(node.discriminant);
        node.cases.forEach(this.processNode.bind(this));
        break;
      case "TaggedTemplateExpression":
        this.processNode(node.tag);
        this.processNode(node.quasi);
        break;
      case "TemplateLiteral":
        node.expressions.forEach(this.processNode.bind(this));
        break;
      case "ThisExpression":
        this.#addVariable("this", node.start);
        break;
      case "ThrowStatement":
        this.processNode(node.argument);
        break;
      case "TryStatement":
        this.processNode(node.block);
        this.processNode(node.handler ?? null);
        this.processNode(node.finalizer ?? null);
        break;
      case "WithStatement":
        this.processNode(node.object);
        this.processNode(node.body);
        break;
      case "YieldExpression":
        this.processNode(node.argument ?? null);
        break;
      case "UnaryExpression":
      case "UpdateExpression":
        this.processNode(node.argument);
        break;
      case "WhileStatement":
        this.processNode(node.test);
        this.processNode(node.body);
        break;
      case "BreakStatement":
      case "ContinueStatement":
      case "DebuggerStatement":
      case "EmptyStatement":
      case "Literal":
      case "Super":
      case "TemplateElement":
        break;
      case "ImportDeclaration":
      case "ImportDefaultSpecifier":
      case "ImportNamespaceSpecifier":
      case "ImportSpecifier":
      case "ImportExpression":
      case "ExportAllDeclaration":
      case "ExportDefaultDeclaration":
      case "ExportNamedDeclaration":
      case "ExportSpecifier":
        break;
      case "VariableDeclarator":
        console.error("Should not encounter VariableDeclarator in general traversal.");
        break;
    }
  }
  getFreeVariables() {
    const result = /* @__PURE__ */ new Map();
    for (const [name, defUse] of this.#rootScope.variables) {
      if (defUse.definitionKind !== 0 /* None */) {
        continue;
      }
      result.set(name, defUse.uses);
    }
    return result;
  }
  getAllNames() {
    return this.#allNames;
  }
  #pushScope() {
    this.#currentScope = new Scope(this.#currentScope);
  }
  #popScope(isFunctionContext) {
    if (this.#currentScope.parent === null) {
      console.error("Internal error: wrong nesting in scope analysis.");
      throw new Error("Internal error");
    }
    this.#currentScope.finalizeToParent(isFunctionContext);
    this.#currentScope = this.#currentScope.parent;
  }
  #addVariable(name, offset, definitionKind = 0 /* None */, isShorthandAssignmentProperty = false) {
    this.#allNames.add(name);
    this.#currentScope.addVariable(name, offset, definitionKind, isShorthandAssignmentProperty);
  }
  #processNodeAsDefinition(definitionKind, node) {
    if (node === null) {
      return;
    }
    switch (node.type) {
      case "ArrayPattern":
        node.elements.forEach(this.#processNodeAsDefinition.bind(this, definitionKind));
        break;
      case "AssignmentPattern":
        this.#processNodeAsDefinition(definitionKind, node.left);
        this.processNode(node.right);
        break;
      case "Identifier":
        this.#addVariable(node.name, node.start, definitionKind);
        break;
      case "MemberExpression":
        this.processNode(node.object);
        if (node.computed) {
          this.processNode(node.property);
        }
        break;
      case "ObjectPattern":
        node.properties.forEach(this.#processNodeAsDefinition.bind(this, definitionKind));
        break;
      case "Property":
        if (node.shorthand) {
          console.assert(node.value === node.key);
          console.assert(node.value.type === "Identifier");
          this.#addVariable(node.value.name, node.value.start, definitionKind, true);
        } else {
          if (node.computed) {
            this.processNode(node.key);
          }
          this.#processNodeAsDefinition(definitionKind, node.value);
        }
        break;
      case "RestElement":
        this.#processNodeAsDefinition(definitionKind, node.argument);
        break;
    }
  }
  #processVariableDeclarator(definitionKind, decl) {
    this.#processNodeAsDefinition(definitionKind, decl.id);
    this.processNode(decl.init ?? null);
  }
}
//# sourceMappingURL=Substitute.js.map
