/* Custom <input type="number"> stepper enhancer.
 *
 * The native browser spin buttons are tiny, OS-themed, and (on
 * Firefox/Linux) often look mismatched against the rest of the app.
 * This module wraps every <input type="number"> with class="form-control"
 * or "form-input" in a glass container and supplies our own up/down
 * buttons that call stepUp()/stepDown() on the native input — so any
 * existing change/input handler keeps working unchanged.
 *
 * Features:
 *   - Click ↑/↓ to step once.
 *   - Hold ↑/↓ to repeat (delay-then-accelerate).
 *   - Buttons auto-disable at min/max.
 *   - Mouse wheel over the input also steps (with shift = ×10).
 *   - Native spin buttons hidden via CSS (see forms.css).
 *
 * Opt-out: data-no-enhance on the input.
 */

(() => {
  const ENHANCED = Symbol('customNumberEnhanced');
  const ENHANCE_CLASSES = ['form-control', 'form-input'];

  class CustomNumber {
    constructor(input) {
      if (input[ENHANCED]) return;
      input[ENHANCED] = true;
      this.input = input;
      this.build();
      this.bind();
      this.refresh();
    }

    build() {
      this.wrapper = document.createElement('div');
      this.wrapper.className = 'custom-number';
      // Preserve inline width/maxWidth so layout-sensitive callers still work.
      if (this.input.style.maxWidth) this.wrapper.style.maxWidth = this.input.style.maxWidth;
      if (this.input.style.width && this.input.style.width !== '100%') {
        this.wrapper.style.width = this.input.style.width;
      }

      this.input.parentNode.insertBefore(this.wrapper, this.input);
      this.wrapper.appendChild(this.input);
      this.input.classList.add('custom-number-input');

      this.controls = document.createElement('div');
      this.controls.className = 'custom-number-controls';

      this.upBtn = document.createElement('button');
      this.upBtn.type = 'button';
      this.upBtn.className = 'custom-number-btn custom-number-up';
      this.upBtn.setAttribute('aria-label', 'Increase');
      this.upBtn.tabIndex = -1;
      this.upBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>';

      this.downBtn = document.createElement('button');
      this.downBtn.type = 'button';
      this.downBtn.className = 'custom-number-btn custom-number-down';
      this.downBtn.setAttribute('aria-label', 'Decrease');
      this.downBtn.tabIndex = -1;
      this.downBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';

      this.controls.append(this.upBtn, this.downBtn);
      this.wrapper.appendChild(this.controls);
    }

    refresh() {
      const v = this.numeric();
      const min = this.input.min === '' ? -Infinity : Number(this.input.min);
      const max = this.input.max === '' ? Infinity : Number(this.input.max);
      this.upBtn.disabled = !Number.isNaN(v) && v >= max;
      this.downBtn.disabled = !Number.isNaN(v) && v <= min;
      this.upBtn.classList.toggle('is-disabled', this.upBtn.disabled);
      this.downBtn.classList.toggle('is-disabled', this.downBtn.disabled);
      if (this.input.disabled) {
        this.wrapper.classList.add('is-disabled');
        this.upBtn.disabled = true;
        this.downBtn.disabled = true;
      } else {
        this.wrapper.classList.remove('is-disabled');
      }
    }

    numeric() {
      const raw = this.input.value;
      if (raw === '' || raw == null) return NaN;
      return Number(raw);
    }

    step(direction) {
      if (this.input.disabled) return;
      // If the input is empty, seed with min (or 0) on first step so
      // stepUp/stepDown have somewhere to go.
      if (this.input.value === '' || this.input.value == null) {
        const min = this.input.min === '' ? 0 : Number(this.input.min);
        this.input.value = String(min);
      }
      try {
        if (direction > 0) this.input.stepUp();
        else this.input.stepDown();
      } catch {
        // stepUp/stepDown throw if min/max prevents the step.
        return;
      }
      this.input.dispatchEvent(new Event('input', { bubbles: true }));
      this.input.dispatchEvent(new Event('change', { bubbles: true }));
      this.refresh();
    }

    holdStart(direction) {
      this.step(direction);
      let delay = 400;
      const tick = () => {
        this.step(direction);
        delay = Math.max(40, delay * 0.85);
        this._holdTimer = setTimeout(tick, delay);
      };
      this._holdTimer = setTimeout(tick, delay);
    }

    holdStop() {
      if (this._holdTimer) {
        clearTimeout(this._holdTimer);
        this._holdTimer = null;
      }
    }

    bind() {
      this.upBtn.addEventListener('mousedown', (e) => {
        e.preventDefault();
        if (this.upBtn.disabled) return;
        this.holdStart(1);
      });
      this.downBtn.addEventListener('mousedown', (e) => {
        e.preventDefault();
        if (this.downBtn.disabled) return;
        this.holdStart(-1);
      });
      ['mouseup', 'mouseleave', 'blur'].forEach(ev => {
        this.upBtn.addEventListener(ev, () => this.holdStop());
        this.downBtn.addEventListener(ev, () => this.holdStop());
      });
      document.addEventListener('mouseup', () => this.holdStop());

      // Keyboard support for the buttons.
      this.upBtn.addEventListener('click', (e) => e.preventDefault());
      this.downBtn.addEventListener('click', (e) => e.preventDefault());

      // Re-evaluate disabled state on any value change (typing, paste,
      // programmatic .value = ...).
      this.input.addEventListener('input', () => this.refresh());
      this.input.addEventListener('change', () => this.refresh());

      // Wheel-to-step when the input is focused (intentional, non-passive).
      this.input.addEventListener('wheel', (e) => {
        if (document.activeElement !== this.input) return;
        e.preventDefault();
        const factor = e.shiftKey ? 10 : 1;
        for (let i = 0; i < factor; i++) this.step(e.deltaY < 0 ? 1 : -1);
      }, { passive: false });

      // React to min/max/disabled attribute changes (config-shared.js
      // sometimes flips these after the field is mounted).
      this.attrObserver = new MutationObserver(() => this.refresh());
      this.attrObserver.observe(this.input, {
        attributes: true,
        attributeFilter: ['min', 'max', 'disabled', 'step']
      });
    }
  }

  function shouldEnhance(el) {
    if (!(el instanceof HTMLInputElement)) return false;
    if (el.type !== 'number') return false;
    if (el[ENHANCED]) return false;
    if (el.hasAttribute('data-no-enhance')) return false;
    return ENHANCE_CLASSES.some(c => el.classList.contains(c));
  }

  function enhanceAll(root = document) {
    const sel = ENHANCE_CLASSES.map(c => `input[type="number"].${c}`).join(',');
    root.querySelectorAll(sel).forEach(el => {
      if (shouldEnhance(el)) new CustomNumber(el);
    });
  }

  function start() {
    enhanceAll();
    const observer = new MutationObserver((mutations) => {
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (node.nodeType !== 1) continue;
          if (shouldEnhance(node)) {
            new CustomNumber(node);
          } else {
            enhanceAll(node);
          }
        }
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start);
  } else {
    start();
  }

  window.CustomNumber = { enhance: enhanceAll, _Class: CustomNumber };
})();
