Source: duplicate_util.js

/**
 * Utilities for handling entity duplication and name conflict resolution.
 *
 * @license BSD, see LICENSE.md.
 */

import {validateNumericInputs} from "ui_editor_util";
import {
  Application,
  DefinitionalStanza,
  SimulationScenario,
  Substance,
} from "ui_translator_components";

/**
 * Result of name conflict resolution.
 */
class NameConflictResolution {
  /**
   * Create a new NameConflictResolution.
   *
   * @param {string} originalName - The original name that was requested.
   * @param {string} resolvedName - The final name after conflict resolution.
   */
  constructor(originalName, resolvedName) {
    const self = this;
    self._originalName = originalName;
    self._resolvedName = resolvedName;
  }

  /**
   * Check if the name was changed during conflict resolution.
   *
   * @returns {boolean} True if the name was changed, false otherwise.
   */
  getNameChanged() {
    const self = this;
    return self._originalName !== self._resolvedName;
  }

  /**
   * Get the final resolved name.
   *
   * @returns {string} The resolved name.
   */
  getNewName() {
    const self = this;
    return self._resolvedName;
  }
}

/**
 * Resolves name conflicts by appending incrementing numbers until finding a unique name.
 *
 * @param {string} baseName - The initial desired name.
 * @param {Set<string>} existingNames - Set of existing names to avoid conflicts with.
 * @returns {NameConflictResolution} A resolution object with the result.
 */
function resolveNameConflict(baseName, existingNames) {
  if (!existingNames.has(baseName)) {
    return new NameConflictResolution(baseName, baseName);
  }

  let counter = 1;
  let candidate = `${baseName} (${counter})`;

  while (existingNames.has(candidate)) {
    counter++;
    candidate = `${baseName} (${counter})`;
  }

  return new NameConflictResolution(baseName, candidate);
}

/**
 * Resolves substance name conflicts with special handling for effective substance names.
 * This function handles the combination of substance and equipment model names.
 *
 * @param {string} baseName - The initial desired substance name.
 * @param {Set<string>} existingNames - Set of existing substance names to avoid conflicts with.
 * @returns {NameConflictResolution} A resolution object with the result.
 */
function resolveSubstanceNameConflict(baseName, existingNames) {
  return resolveNameConflict(baseName, existingNames);
}

/**
 * Presenter for managing entity duplication dialog functionality.
 */
class DuplicateEntityPresenter {
  /**
   * Creates a new DuplicateEntityPresenter.
   *
   * @param {Function} getCodeObj - Callback to get the current code object.
   * @param {Function} onCodeObjUpdate - Callback when code object is updated.
   */
  constructor(getCodeObj, onCodeObjUpdate) {
    const self = this;
    self._getCodeObj = getCodeObj;
    self._onCodeObjUpdate = onCodeObjUpdate;
    self._dialog = document.getElementById("duplicate-entity-dialog");
    self._duplicateLink = document.querySelector(".duplicate-entity-link");
    self._setupDialog();
  }

  /**
   * Set up dialog event handlers and dynamic behavior.
   *
   * @private
   */
  _setupDialog() {
    const self = this;
    self._setupDuplicateLink();
    self._setupEntityType();
    self._setupSourceEntity();
    self._setupEquipmentModel();
    self._setupSaveButton();
    self._setupCancelButton();
  }

  /**
   * Set up duplicate link click handler.
   *
   * @private
   */
  _setupDuplicateLink() {
    const self = this;
    self._duplicateLink.addEventListener("click", (event) => {
      event.preventDefault();
      self._refreshEntityDropdown();
      self._dialog.showModal();
    });
  }

  /**
   * Set up entity type change handler.
   *
   * @private
   */
  _setupEntityType() {
    const self = this;
    const entityTypeInput = self._dialog.querySelector(".duplicate-entity-type-input");
    entityTypeInput.addEventListener("change", () => {
      self._refreshEntityDropdown();
      self._updateNewNameSuggestion();
      self._toggleSubstanceSpecificFields();
    });
  }

  /**
   * Set up source entity change handler.
   *
   * @private
   */
  _setupSourceEntity() {
    const self = this;
    const sourceEntityInput = self._dialog.querySelector(".duplicate-source-entity-input");
    sourceEntityInput.addEventListener("change", () => {
      self._updateNewNameSuggestion();
      const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
      if (entityType === "substance") {
        self._refreshApplicationDropdown();
      }
    });
  }

  /**
   * Set up equipment model change handler.
   *
   * @private
   */
  _setupEquipmentModel() {
    const self = this;
    const equipmentModelInput = self._dialog.querySelector(".duplicate-equipment-model-input");
    equipmentModelInput.addEventListener("input", () => {
      self._updateNewNameSuggestion();
    });
  }

  /**
   * Set up save button click handler.
   *
   * @private
   */
  _setupSaveButton() {
    const self = this;
    const saveButton = self._dialog.querySelector(".save-button");
    saveButton.addEventListener("click", (event) => {
      event.preventDefault();
      self._duplicateEntity();
    });
  }

  /**
   * Set up cancel button click handler.
   *
   * @private
   */
  _setupCancelButton() {
    const self = this;
    const cancelButton = self._dialog.querySelector(".cancel-button");
    cancelButton.addEventListener("click", (event) => {
      event.preventDefault();
      self._dialog.close();
    });
  }

  /**
   * Refresh the source entity dropdown based on selected entity type.
   *
   * @private
   */
  _refreshEntityDropdown() {
    const self = this;
    const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
    const sourceDropdown = self._dialog.querySelector(".duplicate-source-entity-input");
    const codeObj = self._getCodeObj();

    // Clear existing options
    sourceDropdown.innerHTML = "";

    const entityMappers = {
      application: () => codeObj.getApplications().map((app) => ({
        name: app.getName(),
        value: app.getName(),
      })),
      policy: () => codeObj.getPolicies().map((policy) => ({
        name: policy.getName(),
        value: policy.getName(),
      })),
      simulation: () => codeObj.getScenarios().map((scenario) => ({
        name: scenario.getName(),
        value: scenario.getName(),
      })),
      substance: () => codeObj.getSubstances().map((substance) => ({
        name: substance.getName(),
        value: substance.getName(),
        application: self._findSubstanceApplication(codeObj, substance.getName()),
      })),
    };

    const mapper = entityMappers[entityType];
    const entities = mapper ? mapper() : [];

    // Add options to dropdown
    if (entities.length === 0) {
      const option = document.createElement("option");
      option.value = "";
      option.textContent = `No ${entityType}s available`;
      option.disabled = true;
      sourceDropdown.appendChild(option);
    } else {
      entities.forEach((entity) => {
        const option = document.createElement("option");
        option.value = entity.value;
        option.textContent = entity.name;
        sourceDropdown.appendChild(option);
      });
    }
  }

  /**
   * Update the new name suggestion based on selected source entity.
   *
   * @private
   */
  _updateNewNameSuggestion() {
    const self = this;
    const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
    const sourceEntity = self._dialog.querySelector(".duplicate-source-entity-input").value;
    const newNameInput = self._dialog.querySelector(".duplicate-new-name-input");
    const equipmentModel = self._dialog.querySelector(".duplicate-equipment-model-input").value;

    if (sourceEntity && sourceEntity !== "") {
      if (entityType === "substance" && equipmentModel && equipmentModel.trim() !== "") {
        newNameInput.value = `${sourceEntity} - ${equipmentModel.trim()}`;
      } else {
        newNameInput.value = `${sourceEntity} Copy`;
      }
    }
  }

  /**
   * Execute the entity duplication operation.
   *
   * @private
   */
  _duplicateEntity() {
    const self = this;
    const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
    const sourceEntityName = self._dialog.querySelector(".duplicate-source-entity-input").value;
    const newName = self._dialog.querySelector(".duplicate-new-name-input").value.trim();

    // Validation
    if (!sourceEntityName) {
      alert("Please select a source entity to duplicate.");
      return;
    }

    if (!newName) {
      alert("Please enter a name for the new entity.");
      return;
    }

    const codeObj = self._getCodeObj();

    try {
      const duplicators = {
        application: () => self._duplicateApplication(codeObj, sourceEntityName, newName),
        policy: () => self._duplicatePolicy(codeObj, sourceEntityName, newName),
        simulation: () => self._duplicateSimulation(codeObj, sourceEntityName, newName),
        substance: () => self._duplicateSubstance(codeObj, sourceEntityName, newName),
      };

      const duplicator = duplicators[entityType];
      if (!duplicator) {
        throw new Error(`Unknown entity type: ${entityType}`);
      }
      duplicator();

      if (!self._validateBeforeUpdate(entityType)) {
        return; // User cancelled validation, keep dialog open
      }

      self._onCodeObjUpdate(codeObj);
      self._dialog.close();
    } catch (error) {
      console.error("Error duplicating entity:", error);
      alert(`Error duplicating ${entityType}: ${error.message}`);
    }
  }

  /**
   * Validate the duplicate dialog before updating code object.
   * Runs validation checks that may show confirmation dialogs.
   *
   * @param {string} entityType - The type of entity being duplicated
   * @returns {boolean} True if validation passes or user confirms, false if user cancels
   * @private
   */
  _validateBeforeUpdate(entityType) {
    const self = this;

    // Validate numeric inputs and get user confirmation for potentially invalid values
    if (!validateNumericInputs(self._dialog, entityType)) {
      return false; // User cancelled numeric input validation
    }

    // For simulations, also check duration
    if (entityType === "simulation") {
      if (!validateSimulationDuration(self._dialog)) {
        return false; // User cancelled simulation duration validation
      }
    }

    return true;
  }

  /**
   * Duplicate an application with deep copy of all substances and commands.
   *
   * @param {Program} codeObj - The program object to modify
   * @param {string} sourceAppName - Name of source application
   * @param {string} newName - Name for the duplicated application
   * @private
   */
  _duplicateApplication(codeObj, sourceAppName, newName) {
    const self = this;
    const sourceApp = codeObj.getApplication(sourceAppName);

    if (!sourceApp) {
      throw new Error(`Application "${sourceAppName}" not found`);
    }

    // Deep copy substances with all their commands
    const duplicatedSubstances = sourceApp.getSubstances().map((substance) => {
      return self._deepCopySubstance(substance);
    });

    // Create new application with copied substances
    const newApplication = new Application(
      newName,
      duplicatedSubstances,
      sourceApp._isModification,
      sourceApp._isCompatible,
    );

    codeObj.addApplication(newApplication);
  }

  /**
   * Duplicate a policy with deep copy of all applications and commands.
   *
   * @param {Program} codeObj - The program object to modify
   * @param {string} sourcePolicyName - Name of source policy
   * @param {string} newName - Name for the duplicated policy
   * @private
   */
  _duplicatePolicy(codeObj, sourcePolicyName, newName) {
    const self = this;
    const sourcePolicy = codeObj.getPolicy(sourcePolicyName);

    if (!sourcePolicy) {
      throw new Error(`Policy "${sourcePolicyName}" not found`);
    }

    // Deep copy applications within the policy
    const duplicatedApplications = sourcePolicy.getApplications().map((app) => {
      const duplicatedSubstances = app.getSubstances().map((substance) => {
        return self._deepCopySubstance(substance);
      });

      return new Application(
        app.getName(),
        duplicatedSubstances,
        app._isModification,
        app._isCompatible,
      );
    });

    // Create new policy stanza
    const newPolicy = new DefinitionalStanza(
      newName,
      duplicatedApplications,
      sourcePolicy._isCompatible,
    );

    codeObj.insertPolicy(null, newPolicy);
  }

  /**
   * Duplicate a simulation scenario.
   *
   * @param {Program} codeObj - The program object to modify
   * @param {string} sourceSimName - Name of source simulation
   * @param {string} newName - Name for the duplicated simulation
   * @private
   */
  _duplicateSimulation(codeObj, sourceSimName, newName) {
    const self = this;
    const sourceSimulation = codeObj.getScenario(sourceSimName);

    if (!sourceSimulation) {
      throw new Error(`Simulation "${sourceSimName}" not found`);
    }

    const newSimulation = new SimulationScenario(
      newName,
      self._copyPolicyArray(sourceSimulation),
      sourceSimulation.getYearStart(),
      sourceSimulation.getYearEnd(),
      sourceSimulation._isCompatible,
    );

    codeObj.insertScenario(null, newSimulation);
  }

  /**
   * Create a copy of the policy names array from a simulation scenario.
   *
   * @param {SimulationScenario} sourceSimulation - The source simulation to copy from
   * @returns {Array<string>} A new array containing copied policy names
   * @private
   */
  _copyPolicyArray(sourceSimulation) {
    const self = this;
    return [...sourceSimulation.getPolicyNames()];
  }

  /**
   * Show/hide substance-specific fields (equipment model and application selection)
   * based on entity type selection.
   *
   * @private
   */
  _toggleSubstanceSpecificFields() {
    const self = this;
    const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
    const equipmentSection = self._dialog.querySelector(".equipment-model-section");
    const applicationSection = self._dialog.querySelector(".application-selection-section");

    if (entityType === "substance") {
      equipmentSection.style.display = "block";
      applicationSection.style.display = "block";
      self._refreshApplicationDropdown();
    } else {
      equipmentSection.style.display = "none";
      applicationSection.style.display = "none";
      self._dialog.querySelector(".duplicate-equipment-model-input").value = "";
      self._dialog.querySelector(".duplicate-target-application-input").innerHTML = "";
    }
  }

  /**
   * Find which application contains a specific substance.
   *
   * @param {Program} codeObj - The program object
   * @param {string} substanceName - Name of substance to find
   * @returns {string} Application name containing the substance
   * @private
   */
  _findSubstanceApplication(codeObj, substanceName) {
    const self = this;
    const applications = codeObj.getApplications();
    for (const app of applications) {
      if (app.getSubstances().some((sub) => sub.getName() === substanceName)) {
        return app.getName();
      }
    }
    return null;
  }

  /**
   * Refresh the application dropdown with available applications.
   *
   * @private
   */
  _refreshApplicationDropdown() {
    const self = this;
    const applicationDropdown = self._dialog.querySelector(".duplicate-target-application-input");
    const sourceEntityName = self._dialog.querySelector(".duplicate-source-entity-input").value;
    const codeObj = self._getCodeObj();

    // Clear existing options
    applicationDropdown.innerHTML = "";

    const applications = codeObj.getApplications().map((app) => ({
      name: app.getName(),
      value: app.getName(),
    }));

    if (applications.length === 0) {
      const option = document.createElement("option");
      option.value = "";
      option.textContent = "No applications available";
      option.disabled = true;
      applicationDropdown.appendChild(option);
      return;
    }

    // Find source substance's application for default selection
    const sourceApplicationName = self._findSubstanceApplication(codeObj, sourceEntityName);

    // Add application options
    applications.forEach((app) => {
      const option = document.createElement("option");
      option.value = app.value;
      option.textContent = app.name;
      // Select source application by default
      if (app.value === sourceApplicationName) {
        option.selected = true;
      }
      applicationDropdown.appendChild(option);
    });
  }

  /**
   * Duplicate a substance with optional equipment model for compound naming.
   *
   * @param {Program} codeObj - The program object to modify
   * @param {string} sourceSubstanceName - Name of source substance
   * @param {string} newName - Name for the duplicated substance (may be compound)
   * @private
   */
  _duplicateSubstance(codeObj, sourceSubstanceName, newName) {
    const self = this;

    // Get selected target application
    const targetApplicationName = self._dialog.querySelector(
      ".duplicate-target-application-input",
    ).value;
    if (!targetApplicationName) {
      throw new Error("No applications available for substance duplication");
    }

    // Find target application
    const targetApplication = codeObj.getApplications().find(
      (app) => app.getName() === targetApplicationName,
    );
    if (!targetApplication) {
      throw new Error(`Target application "${targetApplicationName}" not found`);
    }

    // Find the source substance across all applications
    let sourceSubstance = null;
    for (const app of codeObj.getApplications()) {
      const substance = app.getSubstances().find((sub) => sub.getName() === sourceSubstanceName);
      if (substance) {
        sourceSubstance = substance;
        break;
      }
    }

    if (!sourceSubstance) {
      throw new Error(`Substance "${sourceSubstanceName}" not found`);
    }

    // Check for duplicate names within target application (not globally)
    const existingSubstancesInTarget = targetApplication.getSubstances();
    if (existingSubstancesInTarget.some((sub) => sub.getName() === newName)) {
      throw new Error(
        `Substance "${newName}" already exists in application "${targetApplicationName}"`,
      );
    }

    // Deep copy the substance with new name
    const duplicatedSubstance = self._deepCopySubstance(sourceSubstance);
    duplicatedSubstance._name = newName;

    // Add to the selected target application
    targetApplication.insertSubstance(null, duplicatedSubstance);
  }

  /**
   * Deep copy a substance with all its commands and properties.
   *
   * @param {Substance} sourceSubstance - The substance to copy
   * @returns {Substance} Deep copied substance
   * @private
   */
  _deepCopySubstance(sourceSubstance) {
    const self = this;
    // Commands are immutable, so we can share references
    const copiedCharges = sourceSubstance.getInitialCharges();
    const copiedLimits = sourceSubstance.getLimits();
    const copiedChanges = sourceSubstance.getChanges();
    const copiedEqualsGhg = sourceSubstance.getEqualsGhg();
    const copiedEqualsKwh = sourceSubstance.getEqualsKwh();
    const copiedRecharges = sourceSubstance.getRecharges();
    const copiedRecycles = sourceSubstance.getRecycles();
    const copiedReplaces = sourceSubstance.getReplaces();
    const copiedRetire = sourceSubstance.getRetire();
    const copiedSetVals = sourceSubstance.getSetVals();
    const copiedEnables = sourceSubstance.getEnables();
    const copiedAssumeMode = sourceSubstance.getAssumeMode();

    // Create new substance with copied commands (matching constructor parameter order)
    return new Substance(
      sourceSubstance.getName(),
      copiedCharges,
      copiedLimits,
      copiedChanges,
      copiedEqualsGhg,
      copiedEqualsKwh,
      copiedRecharges,
      copiedRecycles,
      copiedReplaces,
      copiedRetire,
      copiedSetVals,
      copiedEnables,
      sourceSubstance._isModification,
      sourceSubstance._isCompatible,
      copiedAssumeMode,
    );
  }
}

export {
  NameConflictResolution,
  resolveNameConflict,
  resolveSubstanceNameConflict,
  DuplicateEntityPresenter,
};