const FIXED_PRIVATE_TEXT = "*****";

let hidden = false;
const effects = /** @type {RedactionEffect[]} */ ([]);

/**
 * @typedef {"exact" | "fixed"} RedactionMode
 *
 * @typedef {Object} RedactionEffect
 * @property {HTMLElement} element
 * @property {() => void} sync
 */

function isHidden() {
  return hidden;
}

/**
 * @param {string} value
 */
function createText(value) {
  return [...value].map((character) => {
    return character === " " ? " " : "*";
  }).join("");
}

/**
 * @param {string} value
 * @param {RedactionMode} mode
 */
function mask(value, mode) {
  return mode === "fixed" ? FIXED_PRIVATE_TEXT : createText(value);
}

/**
 * @param {HTMLElement} element
 * @param {() => void} sync
 */
function addEffect(element, sync) {
  effects.push({ element, sync });
  sync();
}

/**
 * @param {HTMLElement} element
 * @param {string} value
 * @param {RedactionMode} [mode]
 */
function setValue(element, value, mode = "exact") {
  addEffect(element, () => {
    element.textContent = hidden ? mask(value, mode) : value;
  });
}

/**
 * @param {HTMLElement} element
 * @param {string} value
 */
function setTitle(element, value) {
  addEffect(element, () => {
    element.title = hidden ? createText(value) : value;
  });
}

/**
 * @param {HTMLElement} element
 * @param {string} value
 * @param {(text: string) => void} render
 */
function setAddress(element, value, render) {
  addEffect(element, () => {
    render(hidden ? createText(value) : value);
  });
}

/**
 * @param {HTMLInputElement | HTMLTextAreaElement} input
 */
function setInput(input) {
  addEffect(input, () => {
    if (input instanceof HTMLTextAreaElement) {
      input.style.setProperty("-webkit-text-security", hidden ? "disc" : "");
    } else {
      input.type = hidden ? "password" : "text";
    }
  });
}

/**
 * @template {keyof HTMLElementTagNameMap} Tag
 * @param {Tag} tag
 * @param {string} value
 * @param {RedactionMode} [mode]
 */
function createValue(tag, value, mode = "exact") {
  const element = document.createElement(tag);

  setValue(element, value, mode);

  return element;
}

function sync() {
  for (let index = effects.length - 1; index >= 0; index -= 1) {
    const effect = effects[index];

    if (!effect.element.isConnected) {
      effects.splice(index, 1);
    } else {
      effect.sync();
    }
  }
}

/**
 * @param {HTMLButtonElement} button
 */
function syncButton(button) {
  button.textContent = hidden ? "Reveal" : "Privacy";
  button.setAttribute("aria-pressed", hidden ? "true" : "false");
}

/**
 * @param {HTMLButtonElement} button
 */
function toggle(button) {
  hidden = !hidden;
  sync();
  syncButton(button);
}

export const redaction = /** @type {const} */ ({
  isHidden,
  createText,
  addEffect,
  setValue,
  setTitle,
  setAddress,
  setInput,
  createValue,
  syncButton,
  toggle,
});
