Source: ui_translator.js

/**
 * Compiler and visitor for translating QubecTalk scripts.
 *
 * Provides the main compilation infrastructure for converting text-based
 * QubecTalk scripts into object representations. Uses ANTLR visitor pattern
 * to traverse the parse tree.
 *
 * @license BSD, see LICENSE.md
 */

import {EngineNumber} from "engine_number";
import {YearMatcher, ParsedYear} from "duration";
import {parseUnitValue} from "meta_serialization";
import {NumberParseUtil} from "number_parse_util";
import {
  formatEngineNumber,
  indentSingle,
  indent,
  buildAddCode,
  finalizeCodePieces,
  preprocessEachYearSyntax,
} from "ui_translator_util";
import {
  AboutStanza,
  Application,
  AssumeCommand,
  Command,
  DefinitionalStanza,
  IncompatibleCommand,
  LimitCommand,
  Program,
  RechargeCommand,
  RecycleCommand,
  ReplaceCommand,
  RetireCommand,
  SimulationScenario,
  SimulationStanza,
  Substance,
  SubstanceBuilder,
  SubstanceMetadata,
  SubstanceMetadataBuilder,
} from "ui_translator_components";

const toolkit = QubecTalk.getToolkit();

/**
 * Visitor compiling a QubecTalk program to JS objects describing the analysis.
 *
 * Visitor which attempts to compile a QubecTalk program to JS objects
 * describing the anlaysis or indication that the anlaysis cannot use the
 * simplified JS object format.
 */
class TranslatorVisitor extends toolkit.QubecTalkVisitor {
  /**
   * Create a new TranslatorVisitor with number parsing utilities.
   */
  constructor() {
    super();
    this.numberParser = new NumberParseUtil();
  }

  /**
   * Visit a number node and converts it to a numeric value.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {number} The parsed number value, accounting for sign.
   */
  visitNumber(ctx) {
    const self = this;

    const raw = ctx.getText();
    const signMultiplier = raw.includes("-") ? -1 : 1;
    const bodyRawText = ctx.getChild(ctx.getChildCount() - 1).getText();

    const result = self.numberParser.parseFlexibleNumber(bodyRawText);
    if (!result.isSuccess()) {
      throw new Error(`Failed to parse number in QubecTalk expression: ${result.getError()}`);
    }

    // Return an object with both the numeric value and original string
    // Use the full raw text which already includes the sign if present
    return {
      value: signMultiplier * result.getNumber(),
      originalString: raw,
    };
  }

  /**
   * Visit a string node and removes surrounding quotes.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The string value without quotes.
   */
  visitString(ctx) {
    const self = this;
    return self._getStringWithoutQuotes(ctx.getText());
  }

  /**
   * Visit a unit node and formats it by delegating to the appropriate child visitor.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted unit string.
   */
  visitUnit(ctx) {
    const self = this;
    return ctx.getChild(0).accept(self);
  }

  /**
   * Visit a volume unit node and returns its text.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The volume unit string (kg, mt, etc.).
   */
  visitVolumeUnit(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a temporal unit node and returns its text.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The temporal unit string (year, years, etc.).
   */
  visitTemporalUnit(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a relative unit node and formats it with proper spacing.
   *
   * Handles percent variants: %, % prior year, % current year, % current.
   * ANTLR's getText() concatenates tokens without spaces, so we reconstruct
   * the proper format here.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The properly formatted relative unit string.
   */
  visitRelativeUnit(ctx) {
    const self = this;
    const childCount = ctx.getChildCount();

    const isDefault = childCount === 1;
    const isExplicitCurrent = childCount === 2;
    const isExplicitYear = childCount === 3;

    if (isDefault) {
      return "%";
    } else if (isExplicitCurrent) {
      return "% current";
    } else if (isExplicitYear) {
      const secondToken = ctx.getChild(1).getText();
      return "% " + secondToken + " year";
    }

    // Fallback to getText() for unknown cases
    return ctx.getText();
  }

  /**
   * Visit a unit or ratio node and formats it appropriately.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted unit or ratio string.
   */
  visitUnitOrRatio(ctx) {
    const self = this;
    if (ctx.getChildCount() == 1) {
      return ctx.getChild(0).accept(self);
    } else {
      const numerator = ctx.getChild(0).accept(self);
      const denominator = ctx.getChild(2).accept(self);

      // Always use "/" format for consistency
      return numerator + " / " + denominator;
    }
  }

  /**
   * Visit a unit value node and creates an EngineNumber.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {EngineNumber} The value with its associated units.
   */
  visitUnitValue(ctx) {
    const self = this;

    const unitString = ctx.getChild(1).accept(self);
    const expressionContent = ctx.getChild(0).accept(self);

    // Handle the case where expressionContent might be an object with value and originalString
    if (typeof expressionContent === "object" && expressionContent.value !== undefined) {
      return new EngineNumber(
        expressionContent.value,
        unitString,
        self._coerceToString(expressionContent),
      );
    } else {
      return new EngineNumber(expressionContent, unitString);
    }
  }

  /**
   * Visit a simple expression node and processes its single child.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {*} The result of visiting the child expression.
   */
  visitSimpleExpression(ctx) {
    const self = this;
    return ctx.getChild(0).accept(self);
  }

  /**
   * Visit a condition expression node and format it.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted condition expression.
   */
  visitConditionExpression(ctx) {
    const self = this;

    const posExpression = self._coerceToString(ctx.pos.accept(self));
    const opFunc = ctx.op.text;
    const negExpression = self._coerceToString(ctx.neg.accept(self));

    return posExpression + " " + opFunc + " " + negExpression;
  }

  /**
   * Visit a logical expression node and format it.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted logical expression.
   */
  visitLogicalExpression(ctx) {
    const self = this;

    const leftExpression = self._coerceToString(ctx.left.accept(self));
    const opFunc = ctx.op.text;
    const rightExpression = self._coerceToString(ctx.right.accept(self));

    return leftExpression + " " + opFunc + " " + rightExpression;
  }

  /**
   * Visit a conditional expression node and format it.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted conditional expression.
   */
  visitConditionalExpression(ctx) {
    const self = this;

    const condition = self._coerceToString(ctx.cond.accept(self));
    const positive = self._coerceToString(ctx.pos.accept(self));
    const negative = self._coerceToString(ctx.neg.accept(self));

    return positive + " if " + condition + " else " + negative + " endif";
  }

  /**
   * Build an arithmetic expression.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {string} op - The operator to use.
   * @returns {string} The formatted arithmetic expression.
   */
  buildAirthmeticExpression(ctx, op) {
    const self = this;

    const priorExpression = self._coerceToString(ctx.getChild(0).accept(self));
    const afterExpression = self._coerceToString(ctx.getChild(2).accept(self));

    return priorExpression + " " + op + " " + afterExpression;
  }

  /**
   * Visit an addition expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted addition expression.
   */
  visitAdditionExpression(ctx) {
    const self = this;
    return self.buildAirthmeticExpression(ctx, ctx.op.text);
  }

  /**
   * Visit a multiplication expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted multiplication expression.
   */
  visitMultiplyExpression(ctx) {
    const self = this;
    return self.buildAirthmeticExpression(ctx, ctx.op.text);
  }

  /**
   * Visit a power expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The formatted power expression.
   */
  visitPowExpression(ctx) {
    const self = this;
    return self.buildAirthmeticExpression(ctx, "^");
  }

  /**
   * Visit a stream access expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The stream access text.
   */
  visitGetStream(ctx) {
    const self = this;
    return "get " + ctx.getChild(1).getText();
  }

  /**
   * Visit an indirect stream access expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The indirect stream access text.
   */
  visitGetStreamIndirect(ctx) {
    const self = this;
    return "get " + ctx.getChild(1).getText() + " of " + ctx.getChild(3).getText();
  }

  /**
   * Visit a stream conversion expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The stream conversion text.
   */
  visitGetStreamConversion(ctx) {
    const self = this;
    return "get " + ctx.getChild(1).getText() + " as " + ctx.getChild(3).getText();
  }

  /**
   * Visit a substance/application units stream access node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The stream access text.
   */
  visitGetStreamIndirectSubstanceAppUnits(ctx) {
    const self = this;
    const part1 = "get " + ctx.getChild(1).getText() + " of " + ctx.getChild(3).getText();
    const part2 = " as " + ctx.getChild(5).getText();
    return part1 + part2;
  }

  /**
   * Visit a minimum limit expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The minimum limit expression text.
   */
  visitLimitMinExpression(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a maximum limit expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The maximum limit expression text.
   */
  visitLimitMaxExpression(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a bounded limit expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The bounded limit expression text.
   */
  visitLimitBoundExpression(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a parenthesized expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The parenthesized expression text.
   */
  visitParenExpression(ctx) {
    const self = this;
    return "(" + self._coerceToString(ctx.getChild(1).accept(self)) + ")";
  }

  /**
   * Visit a normal distribution expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The normal distribution expression text.
   */
  visitDrawNormalExpression(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a uniform distribution expression node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The uniform distribution expression text.
   */
  visitDrawUniformExpression(ctx) {
    const self = this;
    return ctx.getText();
  }

  /**
   * Visit a simple identifier node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {string} The identifier text.
   */
  visitSimpleIdentifier(ctx) {
    const self = this;
    const identifier = ctx.getChild(0).getText();
    return identifier;
  }

  /**
   * Build a YearMatcher for a duration.
   *
   * @param {ParsedYear|null} minYear - Start year or null for unbounded
   * @param {ParsedYear|null} maxYear - End year or null for unbounded
   * @returns {YearMatcher} The year matcher object
   */
  buildDuring(minYear, maxYear) {
    const self = this;
    return new YearMatcher(minYear, maxYear);
  }

  /**
   * Visit a single year duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {YearMatcher} Year matcher for single year.
   */
  visitDuringSingleYear(ctx) {
    const self = this;
    const yearObj = ctx.target.accept(self);
    const parsedYear = new ParsedYear(yearObj.value, yearObj.originalString);
    return self.buildDuring(parsedYear, parsedYear);
  }

  /**
   * Visit a start year duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {YearMatcher} Year matcher starting from engine start.
   */
  visitDuringStart(ctx) {
    const self = this;
    const startYear = new ParsedYear("beginning");
    return self.buildDuring(startYear, startYear);
  }

  /**
   * Visit a year range duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {YearMatcher} Year matcher for range.
   */
  visitDuringRange(ctx) {
    const self = this;
    const lowerObj = ctx.lower.accept(self);
    const upperObj = ctx.upper.accept(self);
    const parsedLower = new ParsedYear(lowerObj.value, lowerObj.originalString);
    const parsedUpper = new ParsedYear(upperObj.value, upperObj.originalString);
    return self.buildDuring(parsedLower, parsedUpper);
  }

  /**
   * Visit a minimum year duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {YearMatcher} Year matcher with min bound only.
   */
  visitDuringWithMin(ctx) {
    const self = this;
    const lowerObj = ctx.lower.accept(self);
    const parsedLower = new ParsedYear(lowerObj.value, lowerObj.originalString);
    return self.buildDuring(parsedLower, null);
  }

  /**
   * Visit a maximum year duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {YearMatcher} Year matcher with max bound only.
   */
  visitDuringWithMax(ctx) {
    const self = this;
    const upperObj = ctx.upper.accept(self);
    const parsedUpper = new ParsedYear(upperObj.value, upperObj.originalString);
    return self.buildDuring(null, parsedUpper);
  }

  /**
   * Visit an "all years" duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Function} Function that returns null for unbounded.
   */
  visitDuringAll(ctx) {
    const self = this;
    return (engine) => null;
  }

  /**
   * Visit an about stanza node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {AboutStanza} New about stanza instance.
   */
  visitAboutStanza(ctx) {
    const self = this;
    return new AboutStanza();
  }

  /**
   * Visit a default stanza node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {DefinitionalStanza} New default stanza instance.
   */
  visitDefaultStanza(ctx) {
    const self = this;
    const numApplications = ctx.getChildCount() - 4;

    const appChildren = [];
    for (let i = 0; i < numApplications; i++) {
      appChildren.push(ctx.getChild(i + 2));
    }

    const applications = appChildren.map((x) => x.accept(self));
    const isCompatible = self._getChildrenCompatible(applications);
    return new DefinitionalStanza("default", applications, isCompatible);
  }

  /**
   * Visit a policy stanza node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {DefinitionalStanza} New policy stanza instance.
   */
  visitPolicyStanza(ctx) {
    const self = this;
    const policyName = self._getStringWithoutQuotes(ctx.name.getText());
    const numApplications = ctx.getChildCount() - 5;

    const appChildren = [];
    for (let i = 0; i < numApplications; i++) {
      appChildren.push(ctx.getChild(i + 3));
    }

    const applications = appChildren.map((x) => x.accept(self));
    const isCompatible = self._getChildrenCompatible(applications);
    return new DefinitionalStanza(policyName, applications, isCompatible);
  }

  /**
   * Visit a simulations stanza node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {SimulationStanza} New simulations stanza instance.
   */
  visitSimulationsStanza(ctx) {
    const self = this;
    const numApplications = ctx.getChildCount() - 4;

    const children = [];
    for (let i = 0; i < numApplications; i++) {
      children.push(ctx.getChild(i + 2));
    }

    const scenarios = children.map((x) => x.accept(self));
    const isCompatible = self._getChildrenCompatible(scenarios);
    return new SimulationStanza(scenarios, isCompatible);
  }

  /**
   * Visit an application definition node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Application} New application instance.
   */
  visitApplicationDef(ctx) {
    const self = this;
    return self._parseApplication(ctx, false);
  }

  /**
   * Visit a substance definition node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Substance} New substance instance.
   */
  visitSubstanceDef(ctx) {
    const self = this;
    return self._parseSubstance(ctx, false);
  }

  /**
   * Visit an application modification node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Application} New application instance.
   */
  visitApplicationMod(ctx) {
    const self = this;
    return self._parseApplication(ctx, true);
  }

  /**
   * Visit a substance modification node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Substance} New substance instance.
   */
  visitSubstanceMod(ctx) {
    const self = this;
    return self._parseSubstance(ctx, true);
  }

  /**
   * Visit a limit command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandAllYears(ctx) {
    const self = this;
    return self._buildLimit(ctx, null, null, null, null, "");
  }

  /**
   * Visit a limit command with displacement and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingAllYears(ctx) {
    const self = this;
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(5).getText());
    return self._buildLimit(ctx, null, displaceTarget, null, null, "");
  }

  /**
   * Visit a limit command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    return self._buildLimit(ctx, duration, null, null, null, "");
  }

  /**
   * Visit a limit command with displacement and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(5).getText());
    return self._buildLimit(ctx, duration, displaceTarget, null, null, "");
  }

  /**
   * Visit a limit command with displacement by volume and all years duration.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingByVolumeAllYears(ctx) {
    const self = this;
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(7).getText());
    return self._buildLimit(ctx, null, displaceTarget, null, null, "by volume");
  }

  /**
   * Visit a limit command with displacement by units and all years duration.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingByUnitsAllYears(ctx) {
    const self = this;
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(7).getText());
    return self._buildLimit(ctx, null, displaceTarget, null, null, "by units");
  }

  /**
   * Visit a limit command with displacement by volume and duration.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingByVolumeDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(7).getText());
    return self._buildLimit(ctx, duration, displaceTarget, null, null, "by volume");
  }

  /**
   * Visit a limit command with displacement by units and duration.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {LimitCommand} New limit command instance.
   */
  visitLimitCommandDisplacingByUnitsDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    const displaceTarget = self._getStringWithoutQuotes(ctx.getChild(7).getText());
    return self._buildLimit(ctx, duration, displaceTarget, null, null, "by units");
  }

  /**
   * Visit a change command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New change command instance.
   */
  visitChangeAllYears(ctx) {
    const self = this;
    return self._buildOperation(ctx, "change", null);
  }

  /**
   * Visit a change command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New change command instance.
   */
  visitChangeDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    return self._buildOperation(ctx, "change", duration);
  }

  /**
   * Visit a define var statement (user-defined variable) node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {IncompatibleCommand} Incompatibility marker for define var.
   */
  visitDefineVarStatement(ctx) {
    const self = this;
    return new IncompatibleCommand("define var");
  }

  /**
   * Visit an initial charge command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New initial charge command instance.
   */
  visitInitialChargeAllYears(ctx) {
    const self = this;
    return self._buildOperation(ctx, "initial charge", null);
  }

  /**
   * Visit an initial charge command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New initial charge command instance.
   */
  visitInitialChargeDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    return self._buildOperation(ctx, "initial charge", duration);
  }

  /**
   * Visit a recharge command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RechargeCommand} New recharge command instance.
   */
  visitRechargeAllYears(ctx) {
    const self = this;
    const population = ctx.population.accept(self);
    const volume = ctx.volume.accept(self);
    return new RechargeCommand(population, volume, null);
  }

  /**
   * Visit a recharge command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RechargeCommand} New recharge command instance.
   */
  visitRechargeDuration(ctx) {
    const self = this;
    const population = ctx.population.accept(self);
    const volume = ctx.volume.accept(self);
    const duration = ctx.duration.accept(self);
    return new RechargeCommand(population, volume, duration);
  }

  /**
   * Visit a recover command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New recover command instance.
   */
  visitRecoverAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    return new RecycleCommand(volume, yieldVal, null, "recharge");
  }

  /**
   * Visit a recover command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New recover command instance.
   */
  visitRecoverDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, "recharge");
  }


  /**
   * Visit a recover command with stage and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with stage.
   */
  visitRecoverStageAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const stage = ctx.stage.text;
    return new RecycleCommand(volume, yieldVal, null, stage);
  }

  /**
   * Visit a recover command with stage and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with stage and duration.
   */
  visitRecoverStageDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const stage = ctx.stage.text;
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, stage);
  }

  /**
   * Visit a recover command with explicit induction and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with induction.
   */
  visitRecoverInductionAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const inductionVal = ctx.inductionVal.accept(self);
    return new RecycleCommand(volume, yieldVal, null, "recharge", inductionVal);
  }

  /**
   * Visit a recover command with explicit induction and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with induction and duration.
   */
  visitRecoverInductionDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const inductionVal = ctx.inductionVal.accept(self);
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, "recharge", inductionVal);
  }

  /**
   * Visit a recover command with explicit induction, stage and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with induction and stage.
   */
  visitRecoverInductionStageAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const inductionVal = ctx.inductionVal.accept(self);
    const stage = ctx.stage.text;
    return new RecycleCommand(volume, yieldVal, null, stage, inductionVal);
  }

  /**
   * Visit a recover command with explicit induction, stage and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with induction, stage and duration.
   */
  visitRecoverInductionStageDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const inductionVal = ctx.inductionVal.accept(self);
    const stage = ctx.stage.text;
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, stage, inductionVal);
  }

  /**
   * Visit a recover command with default induction and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with default induction.
   */
  visitRecoverDefaultInductionAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    return new RecycleCommand(volume, yieldVal, null, "recharge", "default");
  }

  /**
   * Visit a recover command with default induction and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with default induction and duration.
   */
  visitRecoverDefaultInductionDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, "recharge", "default");
  }

  /**
   * Visit a recover command with default induction, stage and all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with default induction and stage.
   */
  visitRecoverDefaultInductionStageAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const stage = ctx.stage.text;
    return new RecycleCommand(volume, yieldVal, null, stage, "default");
  }

  /**
   * Visit a recover command with default induction, stage and duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RecycleCommand} New recycle command with default induction, stage and duration.
   */
  visitRecoverDefaultInductionStageDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const yieldVal = ctx.yieldVal.accept(self);
    const stage = ctx.stage.text;
    const duration = ctx.duration.accept(self);
    return new RecycleCommand(volume, yieldVal, duration, stage, "default");
  }

  /**
   * Visit a replace command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {ReplaceCommand} New replace command instance.
   */
  visitReplaceAllYears(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const source = ctx.target.getText();
    const destination = self._getStringWithoutQuotes(ctx.destination.getText());
    return new ReplaceCommand(volume, source, destination, null);
  }

  /**
   * Visit a replace command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {ReplaceCommand} New replace command instance.
   */
  visitReplaceDuration(ctx) {
    const self = this;
    const volume = ctx.volume.accept(self);
    const duration = ctx.duration.accept(self);
    const source = ctx.target.getText();
    const destination = self._getStringWithoutQuotes(ctx.destination.getText());
    return new ReplaceCommand(volume, source, destination, duration);
  }

  /**
   * Visit a retire command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RetireCommand} New retire command instance.
   */
  visitRetireAllYears(ctx) {
    const self = this;
    const volumeFuture = (ctx) => ctx.volume.accept(self);
    const value = volumeFuture(ctx);

    // Check if "with replacement" is present in the parsed context
    const withReplacement = ctx.getText().toLowerCase().includes("withreplacement");

    // Create retire-specific command
    return new RetireCommand(value, null, withReplacement);
  }

  /**
   * Visit a retire command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {RetireCommand} New retire command instance.
   */
  visitRetireDuration(ctx) {
    const self = this;
    const volumeFuture = (ctx) => ctx.volume.accept(self);
    const value = volumeFuture(ctx);
    const duration = ctx.duration.accept(self);

    // Check if "with replacement" is present in the parsed context
    const withReplacement = ctx.getText().toLowerCase().includes("withreplacement");

    // Create retire-specific command with duration
    return new RetireCommand(value, duration, withReplacement);
  }

  /**
   * Visit a set command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New set command instance.
   */
  visitSetAllYears(ctx) {
    const self = this;
    return self._buildOperation(ctx, "setVal", null);
  }

  /**
   * Visit a set command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New set command instance.
   */
  visitSetDuration(ctx) {
    const self = this;
    const duration = ctx.duration.accept(self);
    return self._buildOperation(ctx, "setVal", duration);
  }

  /**
   * Visit an equals command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New equals command instance.
   */
  visitEqualsAllYears(ctx) {
    const self = this;
    const targetFuture = (ctx) => null;
    return self._buildOperation(ctx, "equals", null, targetFuture);
  }

  /**
   * Visit an equals command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} New equals command instance.
   */
  visitEqualsDuration(ctx) {
    const self = this;
    const targetFuture = (ctx) => null;
    const duration = ctx.duration.accept(self);
    return self._buildOperation(ctx, "equals", duration, targetFuture);
  }

  /**
   * Visit an enable command with all years duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} Enable command.
   */
  visitEnableAllYears(ctx) {
    const self = this;
    const target = ctx.target.getText();
    return new Command("enable", target, null, null);
  }

  /**
   * Visit an enable command with duration node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Command} Enable command.
   */
  visitEnableDuration(ctx) {
    const self = this;
    const target = ctx.target.getText();
    const duration = ctx.duration.accept(self);
    return new Command("enable", target, null, duration);
  }

  /**
   * Visit an assume no command for all years.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeNoAllYears(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    return new AssumeCommand("no", stream, null);
  }

  /**
   * Visit an assume no command with duration.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeNoDuration(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    const duration = ctx.duration.accept(self);
    return new AssumeCommand("no", stream, duration);
  }

  /**
   * Visit an assume only recharge command for all years.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeOnlyRechargeAllYears(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    return new AssumeCommand("only recharge", stream, null);
  }

  /**
   * Visit an assume only recharge command with duration.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeOnlyRechargeDuration(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    const duration = ctx.duration.accept(self);
    return new AssumeCommand("only recharge", stream, duration);
  }

  /**
   * Visit an assume continued command for all years.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeContinuedAllYears(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    return new AssumeCommand("continued", stream, null);
  }

  /**
   * Visit an assume continued command with duration.
   *
   * @param {Object} ctx - Parse tree context.
   * @returns {AssumeCommand} Assume command with mode, stream, and duration.
   */
  visitAssumeContinuedDuration(ctx) {
    const self = this;
    const stream = ctx.target.getText();
    const duration = ctx.duration.accept(self);
    return new AssumeCommand("continued", stream, duration);
  }

  /**
   * Visit a base simulation node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {SimulationScenario} New simulation scenario instance.
   */
  visitBaseSimulation(ctx) {
    const self = this;
    const name = self._getStringWithoutQuotes(ctx.name.getText());
    const yearStart = ctx.start.getText();
    const yearEnd = ctx.end.getText();
    return new SimulationScenario(name, [], yearStart, yearEnd, true);
  }

  /**
   * Visit a policy simulation node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {SimulationScenario} New simulation scenario instance.
   */
  visitPolicySim(ctx) {
    const self = this;
    const name = self._getStringWithoutQuotes(ctx.name.getText());
    const numPolicies = Math.ceil((ctx.getChildCount() - 8) / 2);
    const yearStart = ctx.start.getText();
    const yearEnd = ctx.end.getText();

    const policies = [];
    for (let i = 0; i < numPolicies; i++) {
      const rawName = ctx.getChild(i * 2 + 3).getText();
      const nameNoQuotes = self._getStringWithoutQuotes(rawName);
      policies.push(nameNoQuotes);
    }

    return new SimulationScenario(name, policies, yearStart, yearEnd, true);
  }

  /**
   * Visit a base simulation with trials node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {IncompatibleCommand} Incompatibility marker for simulate with trials.
   */
  visitBaseSimulationTrials(ctx) {
    const self = this;
    return new IncompatibleCommand("simulate with trials");
  }

  /**
   * Visit a policy simulation with trials node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {IncompatibleCommand} Incompatibility marker for simulate with trials.
   */
  visitPolicySimTrials(ctx) {
    const self = this;
    return new IncompatibleCommand("simulate with trials");
  }

  /**
   * Visit a program node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {Program} New program instance.
   */
  visitProgram(ctx) {
    const self = this;

    const stanzasByName = new Map();
    const numStanzas = ctx.getChildCount();

    for (let i = 0; i < numStanzas; i++) {
      const newStanza = ctx.getChild(i).accept(self);
      if (newStanza !== undefined) {
        stanzasByName.set(newStanza.getName(), newStanza);
      }
    }

    if (!stanzasByName.has("default")) {
      return new Program([], [], [], true);
    }

    const applications = stanzasByName.get("default").getApplications();

    const allStanzaNames = Array.of(...stanzasByName.keys());
    const policies = allStanzaNames
      .filter((x) => x !== "default")
      .filter((x) => x !== "about")
      .filter((x) => x !== "simulations")
      .map((x) => stanzasByName.get(x));

    if (!stanzasByName.has("simulations")) {
      return new Program(applications, policies, [], true);
    }

    const scenarios = stanzasByName.get("simulations").getScenarios();

    const stanzas = Array.of(...stanzasByName.values());

    const isCompatible = self._getChildrenCompatible(stanzas);

    return new Program(applications, policies, scenarios, isCompatible);
  }

  /**
   * Visit a global statement node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {*} The result of visiting the child node.
   */
  visitGlobalStatement(ctx) {
    const self = this;
    return ctx.getChild(0).accept(self);
  }

  /**
   * Visit a substance statement node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @returns {*} The result of visiting the child node.
   */
  visitSubstanceStatement(ctx) {
    const self = this;
    return ctx.getChild(0).accept(self);
  }

  /**
   * Extract string value from a quoted string node, removing quotes.
   *
   * @param {string} target - The quoted string.
   * @returns {string} The string without quotes.
   * @private
   */
  _getStringWithoutQuotes(target) {
    const self = this;
    return target.substring(1, target.length - 1);
  }

  /**
   * Check compatibility of children nodes.
   *
   * @param {Array} children - Array of nodes to check.
   * @returns {boolean} True if all children are compatible, false otherwise.
   * @private
   */
  _getChildrenCompatible(children) {
    const self = this;
    return children.map((x) => x.getIsCompatible()).reduce((a, b) => a && b, true);
  }

  /**
   * Parse an application node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Application} New application instance.
   * @private
   */
  _parseApplication(ctx, isModification) {
    const self = this;
    const name = self._getStringWithoutQuotes(ctx.name.getText());
    const numApplications = ctx.getChildCount() - 5;

    const children = [];
    for (let i = 0; i < numApplications; i++) {
      children.push(ctx.getChild(i + 3));
    }

    const childrenParsed = children.map((x) => x.accept(self));
    const isCompatible = self._getChildrenCompatible(childrenParsed);

    return new Application(name, childrenParsed, isModification, isCompatible);
  }

  /**
   * Parse a substance node.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {boolean} isModification - Whether this is a modification.
   * @returns {Substance} New substance instance.
   * @private
   */
  _parseSubstance(ctx, isModification) {
    const self = this;
    const name = self._getStringWithoutQuotes(ctx.name.getText());
    const numChildren = ctx.getChildCount() - 5;

    const children = [];
    for (let i = 0; i < numChildren; i++) {
      children.push(ctx.getChild(i + 3));
    }

    const commands = children.map((x) => {
      return x.accept(self);
    });

    const builder = new SubstanceBuilder(name, isModification);

    commands.forEach((x) => {
      builder.addCommand(x);
    });

    // Check assume compatibility before building
    const assumeCommands = commands
      .filter((x) => x && x.getTypeName && x.getTypeName() === "assume");

    if (assumeCommands.length > 1) {
      // Multiple assumes not supported in UI
      builder.setIsCompatible(false);
    } else if (assumeCommands.length === 1) {
      const assume = assumeCommands[0];

      // Check if compatible with UI: single assume, for sales, no duration
      if (assume.getStream() !== "sales" || assume.getDuration() !== null) {
        builder.setIsCompatible(false);
      }
    }

    const isCompatibleRaw = commands.map((x) => x.getIsCompatible()).reduce((a, b) => a && b, true);

    const substance = builder.build(isCompatibleRaw);

    return substance;
  }

  /**
   * Build an operation command.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {string} typeName - Type name of the command.
   * @param {YearMatcher} duration - Duration of the command.
   * @param {Function} targetGetter - Function to get the target.
   * @param {Function} valueGetter - Function to get the value.
   * @returns {Command} New command instance.
   * @private
   */
  _buildOperation(ctx, typeName, duration, targetGetter, valueGetter) {
    const self = this;
    if (targetGetter === undefined || targetGetter === null) {
      targetGetter = (ctx) => ctx.target.getText();
    }
    const target = targetGetter(ctx);

    if (valueGetter === undefined || valueGetter === null) {
      valueGetter = (ctx) => ctx.value.accept(self);
    }
    const value = valueGetter(ctx);

    return new Command(typeName, target, value, duration);
  }

  /**
   * Build a limit command.
   *
   * @param {Object} ctx - The parse tree node context.
   * @param {YearMatcher} duration - Duration of the command.
   * @param {string} displaceTarget - Displacing target.
   * @param {Function} targetGetter - Function to get the target.
   * @param {Function} valueGetter - Function to get the value.
   * @param {string} displacingType - Type of displacement ("", "by volume", "by units").
   * @returns {LimitCommand} New limit command instance.
   * @private
   */
  _buildLimit(ctx, duration, displaceTarget, targetGetter, valueGetter, displacingType) {
    const self = this;
    const capType = ctx.getChild(0).getText();

    if (targetGetter === undefined || targetGetter === null) {
      targetGetter = (ctx) => ctx.target.getText();
    }
    const target = targetGetter(ctx);

    if (valueGetter === undefined || valueGetter === null) {
      valueGetter = (ctx) => ctx.value.accept(self);
    }
    const value = valueGetter(ctx);

    const finalDisplacingType = displacingType || "";

    return new LimitCommand(capType, target, value, duration, displaceTarget, finalDisplacingType);
  }

  /**
   * Check if we got a complex back and take it to a string.
   *
   * Some operations return a complex object but some operations throw away that information to
   * return to original string. This coerces to string.
   *
   * @param {Object} target - The value to coerce down to string.
   * @returns {String} Forced to string.
   */
  _coerceToString(target) {
    const self = this;
    if (target.originalString === undefined) {
      return target;
    } else {
      return target.originalString;
    }
  }
}

/**
 * Result of translating from QubecTalk script to UI editor objects.
 */
class TranslationResult {
  /**
   * Create a new record of a translation attempt.
   *
   * @param program The translated program as a lambda if successful or null if
   *     unsuccessful.
   * @param errors Any errors encountered or empty list if no errors.
   */
  constructor(program, errors) {
    const self = this;
    self._program = program;
    self._errors = errors;
  }

  /**
   * Get the program as an object.
   *
   * @returns The compiled program as an object or null if translation failed.
   */
  getProgram() {
    const self = this;
    return self._program;
  }

  /**
   * Get errors encountered in compiling the QubecTalk script.
   *
   * @returns Errors or empty list if no errors.
   */
  getErrors() {
    const self = this;
    return self._errors;
  }
}

/**
 * Compiler that translates QubecTalk code into object representation.
 *
 * Facade which parses QubecTalk scripts and converts them into objects which
 * represent the program structure for UI editor-compatiable objects. Detects
 * and reports syntax errors.
 */
class UiTranslatorCompiler {
  /**
   * Compiles QubecTalk code into an object representation.
   *
   * Parses the input code using ANTLR and translates it into objects
   * representing the program structure. Reports any syntax errors encountered.
   *
   * @param {string} input - The QubecTalk code to compile.
   * @returns {TranslationResult} Result containing either the compiled program
   *     object or any encountered errors.
   */
  compile(input) {
    const self = this;

    if (input.replaceAll("\n", "").replaceAll(" ", "") === "") {
      return new TranslationResult(null, []);
    }

    const errors = [];

    const preprocessedCode = preprocessEachYearSyntax(input);
    const chars = new toolkit.antlr4.InputStream(preprocessedCode);
    const lexer = new toolkit.QubecTalkLexer(chars);
    lexer.removeErrorListeners();
    lexer.addErrorListener({
      syntaxError: (recognizer, offendingSymbol, line, column, msg, err) => {
        const result = `(line ${line}, col ${column}): ${msg}`;
        errors.push(result);
      },
      reportAmbiguity: (recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs) => {
        errors.push(`Ambiguity detected at position ${startIndex}-${stopIndex}`);
      },
      reportAttemptingFullContext: (recognizer, dfa, startIndex, stopIndex,
        conflictingAlts, configs) => {
        errors.push(`Attempting full context at position ${startIndex}-${stopIndex}`);
      },
      reportContextSensitivity: (recognizer, dfa, startIndex, stopIndex, prediction, configs) => {
        errors.push(`Context sensitivity at position ${startIndex}-${stopIndex}`);
      },
    });

    const tokens = new toolkit.antlr4.CommonTokenStream(lexer);
    const parser = new toolkit.QubecTalkParser(tokens);
    parser.removeErrorListeners();
    parser.addErrorListener({
      syntaxError: (recognizer, offendingSymbol, line, column, msg, err) => {
        const result = `(line ${line}, col ${column}): ${msg}`;
        errors.push(result);
      },
      reportAmbiguity: (recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs) => {
        // Performance warning about ambiguous grammar, not an error
      },
      reportAttemptingFullContext: (recognizer, dfa, startIndex, stopIndex,
        conflictingAlts, configs) => {
        // Performance warning, not an error
      },
      reportContextSensitivity: (recognizer, dfa, startIndex, stopIndex, prediction, configs) => {
        // Performance warning, not an error
      },
    });

    const programUncompiled = parser.program();
    if (errors.length > 0) {
      return new TranslationResult(null, errors);
    }

    const program = programUncompiled.accept(new TranslatorVisitor());
    if (errors.length > 0) {
      return new TranslationResult(null, errors);
    }

    return new TranslationResult(program, errors);
  }
}

export {
  TranslatorVisitor,
  UiTranslatorCompiler,
};