Source: inputs.js

import * as bootstrap from 'bootstrap';

/**
 * Retrieves and validates all input values from the form.
 *
 * Combines DOM input extraction with validation to ensure all required fields
 * meet specified criteria. If the inputs are valid, the collected data is returned;
 * otherwise, it returns `null`.
 *
 * @returns {Object|null} An object containing validated form inputs or null if invalid.
 */
export function getInputs() {
  const inputs = getDOMInputs();
  const valid = validateInputs(inputs);
  return valid ? inputs : null;
}

/**
 * Extracts values from DOM elements representing the form inputs.
 *
 * Collects various input types, including text, dropdowns, checkboxes, and dynamically
 * selected formats. Converts specific fields to standardized formats (e.g., uppercase,
 * float values) to maintain consistency.
 *
 * @returns {Object} An object containing raw input values from the DOM.
 */
function getDOMInputs() {
  return {
    // Dropdowns
    mode: document.querySelector('input[name="mode"]:checked').value,

    // Text and number inputs
    yourCallsign: document
      .getElementById('yourCallsign')
      .value.trim()
      .toUpperCase(),
    yourName: document.getElementById('yourName').value.trim(),
    yourState: document.getElementById('yourState').value.trim().toUpperCase(), // Convert to uppercase for consistency
    yourSpeed: parseInt(document.getElementById('yourSpeed').value, 10),
    yourSidetone: parseInt(document.getElementById('yourSidetone').value, 10),
    // convert volume to a float between 0 and 1
    yourVolume: parseFloat(document.getElementById('yourVolume').value) / 100,
    maxStations: parseInt(document.getElementById('maxStations').value, 10),
    minSpeed: parseInt(document.getElementById('minSpeed').value, 10),
    maxSpeed: parseInt(document.getElementById('maxSpeed').value, 10),
    minTone: parseInt(document.getElementById('minTone').value, 10),
    maxTone: parseInt(document.getElementById('maxTone').value, 10),
    // convert volumes to a float between 0 and 1
    minVolume: parseFloat(document.getElementById('minVolume').value) / 100,
    maxVolume: parseFloat(document.getElementById('maxVolume').value) / 100,
    minWait: parseFloat(document.getElementById('minWait').value),
    maxWait: parseFloat(document.getElementById('maxWait').value),

    // Checkboxes & Radio
    usOnly: document.getElementById('usOnly')
      ? document.getElementById('usOnly').checked
      : false,
    qrn: document.querySelector('input[name="qrn"]:checked').value,
    qsb: document.getElementById('qsb').checked,
    qsbPercentage: parseInt(document.getElementById('qsbPercentage').value, 10),

    // Farnsworth inputs
    enableFarnsworth: document.getElementById('enableFarnsworth')
      ? document.getElementById('enableFarnsworth').checked
      : false,
    farnsworthSpeed: document.getElementById('farnsworthSpeed')
      ? parseInt(document.getElementById('farnsworthSpeed').value, 10)
      : null,

    // Formats (callsign formats are gathered dynamically)
    formats: getSelectedFormats(),

    // Cut number inputs
    enableCutNumbers: document.getElementById('enableCutNumbers')
      ? document.getElementById('enableCutNumbers').checked
      : false,
    cutNumbers: getSelectedCutNumbers(),
  };
}

// Add event listeners to clear invalid states when user types
document.querySelectorAll('input, select, textarea').forEach((el) => {
  el.addEventListener('input', () => {
    clearFieldInvalid(el.id);
  });
});

/**
 * Validates the collected form inputs and ensures their logical consistency.
 *
 * Performs checks for required fields, numerical range constraints, and mode-specific
 * requirements. Marks invalid fields visually and expands the relevant sections
 * of the form for easier user correction.
 *
 * @param {Object} inputs - The collected input data to validate.
 * @returns {boolean} True if all inputs are valid; false otherwise.
 */
function validateInputs(inputs) {
  let isValid = true;

  clearAllInvalidStates();

  if (!inputs.yourCallsign) {
    markFieldInvalid('yourCallsign', 'Your callsign is required.');
    openAccordionSection('collapseYourStationSettings');
    isValid = false;
  }
  if (!inputs.yourName && inputs.mode === 'sst') {
    markFieldInvalid('yourName', 'Your name is required for SST mode.');
    openAccordionSection('collapseYourStationSettings');
    isValid = false;
  }
  if (!inputs.yourState && inputs.mode === 'sst') {
    markFieldInvalid('yourState', 'Your state is required for SST mode.');
    openAccordionSection('collapseYourStationSettings');
    isValid = false;
  }
  if (!inputs.yourName && inputs.mode === 'cwt') {
    markFieldInvalid('yourName', 'Your name is required for CWT mode.');
    openAccordionSection('collapseYourStationSettings');
    isValid = false;
  }

  if (inputs.minSpeed > inputs.maxSpeed) {
    markFieldInvalid(
      'minSpeed',
      'Minimum Speed cannot be greater than Maximum Speed!'
    );
    openAccordionSection('collapseRespondingStationSettings');
    isValid = false;
  }

  if (inputs.minVolume > inputs.maxVolume) {
    markFieldInvalid(
      'minVolume',
      'Minimum Volume cannot be greater than Maximum Volume!'
    );
    openAccordionSection('collapseRespondingStationSettings');
    isValid = false;
  }

  if (inputs.minSpeed > inputs.maxSpeed) {
    markFieldInvalid(
      'minSpeed',
      'Minimum Speed cannot be greater than Maximum Speed!'
    );
    openAccordionSection('collapseRespondingStationSettings');
    isValid = false;
  }

  return isValid;
}

/**
 * Marks a specific input field as invalid and displays an error message.
 *
 * Adds a CSS class for invalid state and updates the associated error message
 * within a `.invalid-feedback` element if present.
 *
 * @param {string} inputId - The ID of the input field to mark as invalid.
 * @param {string} errorMessage - The error message to display.
 */
function markFieldInvalid(inputId, errorMessage) {
  const input = document.getElementById(inputId);
  if (!input) return;

  input.classList.add('is-invalid');

  // If there's an associated invalid-feedback element, update its text
  const feedback = input.parentElement.querySelector('.invalid-feedback');
  if (feedback) {
    feedback.textContent = errorMessage;
  }
}

/**
 * Clears the invalid state from a specific input field.
 *
 * Removes the CSS class for invalid state and resets any associated error message.
 *
 * @param {string} inputId - The ID of the input field to clear.
 */
function clearFieldInvalid(inputId) {
  const input = document.getElementById(inputId);
  if (!input) return;

  input.classList.remove('is-invalid');
}

/**
 * Clears the invalid state from all form fields.
 *
 * Targets all elements with the `.is-invalid` class and removes it to reset
 * the visual state of the form.
 */
export function clearAllInvalidStates() {
  // Target all elements with the .is-invalid class
  document
    .querySelectorAll('.is-invalid')
    .forEach((el) => el.classList.remove('is-invalid'));
}

/**
 * Programmatically opens an accordion section.
 *
 * Ensures that the specified accordion section is visible by checking its current
 * state and toggling it if necessary. Leverages Bootstrap's `Collapse` API.
 *
 * @param {string} sectionId - The ID of the accordion section to open.
 */
function openAccordionSection(sectionId) {
  const section = document.getElementById(sectionId);
  if (section && !section.classList.contains('show')) {
    // Programmatically toggle the collapse
    let bsCollapse = bootstrap.Collapse.getInstance(section);
    if (!bsCollapse) {
      bsCollapse = new bootstrap.Collapse(section, { toggle: false });
    }
    bsCollapse.show();
  }
}

/**
 * Collects the selected callsign formats from the form.
 *
 * Checks the state of specific checkboxes to determine the selected formats
 * and returns them as an array. Useful for dynamically gathering user preferences
 * for callsign generation.
 *
 * @returns {string[]} An array of selected callsign formats.
 */
function getSelectedFormats() {
  const formats = [];
  if (document.getElementById('1x1').checked) formats.push('1x1');
  if (document.getElementById('1x2').checked) formats.push('1x2');
  if (document.getElementById('1x3').checked) formats.push('1x3');
  if (document.getElementById('2x1').checked) formats.push('2x1');
  if (document.getElementById('2x2').checked) formats.push('2x2');
  if (document.getElementById('2x3').checked) formats.push('2x3');
  return formats;
}

/**
 * Collects the selected cut-number mappings from the form.
 *
 * For each digit the user has chosen to "cut," we store an entry in the returned
 * object that maps that digit to the corresponding letter. For example, if the user
 * checked "T/0" in the UI, then the returned object might include { '0': 'T' }.
 *
 * @example
 * // Suppose checkboxes for T/0 and N/9 are selected.
 * const cutMap = getSelectedCutNumbers();
 * // cutMap -> { '0': 'T', '9': 'N' }
 *
 * // You can then easily replace digits in a string:
 * const original = '80091';
 * const replaced = original.replace(/\d/g, digit => cutMap[digit] || digit);
 * // replaced -> '8TTN1'
 *
 * @returns {Object<string, string>} A dictionary mapping each selected digit
 * to its cut letter. Digits not selected are omitted.
 */
function getSelectedCutNumbers() {
  const cutMap = {};

  if (document.getElementById('cutT')?.checked) {
    cutMap['0'] = 'T';
  }
  if (document.getElementById('cutA')?.checked) {
    cutMap['1'] = 'A';
  }
  if (document.getElementById('cutU')?.checked) {
    cutMap['2'] = 'U';
  }
  if (document.getElementById('cutV')?.checked) {
    cutMap['3'] = 'V';
  }
  if (document.getElementById('cutE')?.checked) {
    cutMap['5'] = 'E';
  }
  if (document.getElementById('cutG')?.checked) {
    cutMap['7'] = 'G';
  }
  if (document.getElementById('cutD')?.checked) {
    cutMap['8'] = 'D';
  }
  if (document.getElementById('cutN')?.checked) {
    cutMap['9'] = 'N';
  }

  return cutMap;
}