/**
 * Non-side-effect class export for `<qr-code-ui>`.
 *
 * Importing this file gives you the class without auto-registering the
 * tag. Useful for test isolation, subclassing with tag-name override,
 * or selective composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/qr-code`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <qr-code-ui value="https://example.com" size="200"></qr-code-ui>
 *
 * Two modes:
 *   A. Auto-encode (default) — set `[value]`, built-in encoder runs.
 *   B. BYO matrix — set `[matrix]` (JSON 2-D 0/1 array), bypass the encoder.
 *
 * Matrix wins when both are set. Empty value + empty matrix renders
 * nothing.
 *
 * @see ./qr-encoder.js for the encoder implementation.
 */

import { UIElement } from '../../core/element.js';
import { encodeQR, matrixToSVG } from './qr-encoder.js';

export class UIQRCode extends UIElement {
  static properties = {
    value:           { type: String, default: '',       reflect: true },
    matrix:          { type: String, default: '',       reflect: true },
    size:            { type: Number, default: 200,      reflect: true },
    errorCorrection: { type: String, default: 'M',      reflect: true, attribute: 'error-correction' },
    margin:          { type: Number, default: 4,        reflect: true },
    color:           { type: String, default: '',       reflect: true },
    background:      { type: String, default: '',       reflect: true },
    label:           { type: String, default: 'QR code', reflect: true },
  };

  static template = () => null;

  #lastSig = '';

  #parseMatrix(raw) {
    if (!raw) return null;
    try {
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed) || parsed.length === 0) return null;
      if (!Array.isArray(parsed[0])) return null;
      // Coerce to 0/1
      return parsed.map((row) => row.map((v) => (v ? 1 : 0)));
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn('[qr-code-ui] invalid [matrix] JSON:', e.message);
      return null;
    }
  }

  #resolveMatrix() {
    // BYO matrix wins
    const byo = this.#parseMatrix(this.matrix);
    if (byo) {
      this.removeAttribute('data-error');
      return byo;
    }
    if (!this.value) {
      this.removeAttribute('data-error');
      return null;
    }
    try {
      const result = encodeQR(this.value, { errorCorrection: this.errorCorrection || 'M' });
      this.removeAttribute('data-error');
      return result.matrix;
    } catch (e) {
      this.setAttribute('data-error', '');
      // eslint-disable-next-line no-console
      console.warn(`[qr-code-ui] encode failed for value (${this.value?.length || 0} chars at ECC=${this.errorCorrection}): ${e.message}. Lower ECC, shorten input, or supply a precomputed [matrix].`);
      return null;
    }
  }

  connected() {
    super.connected();
    this.setAttribute('role', 'img');
    if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', this.label || 'QR code');
  }

  render() {
    // Cheap idempotence: compute a signature of inputs, skip re-render
    // if nothing changed (encoding is non-trivial — version selection +
    // RS ECC + 8 mask evaluations).
    const sig = `${this.value}|${this.matrix}|${this.size}|${this.errorCorrection}|${this.margin}|${this.color}|${this.background}`;
    if (sig === this.#lastSig && this.querySelector('svg')) return;
    this.#lastSig = sig;

    // Keep aria-label fresh from prop
    if (this.label) this.setAttribute('aria-label', this.label);

    const matrix = this.#resolveMatrix();
    if (!matrix) {
      this.innerHTML = '';
      return;
    }

    const svg = matrixToSVG(matrix, {
      cellSize: 8, // arbitrary internal unit; host CSS controls actual size via [size]
      margin: Number.isFinite(this.margin) ? this.margin : 4,
      // Hardcoded fallback to black-on-white — the scannable convention.
      // Theme-aware `currentColor` produces light-on-dark in dark mode,
      // which most phone cameras (iPhone Camera in particular) refuse to
      // decode. Consumers wanting brand chrome must set [color] +
      // [background] explicitly; the bare default ALWAYS scans.
      color: this.color || '#000000',
      background: this.background || '#ffffff',
    });
    this.innerHTML = svg;

    // Lock the visual size on the SVG root (CSS host dims would also
    // work, but explicit width/height on the SVG keeps the box clear
    // when the host is laid out as inline content).
    const svgEl = this.querySelector('svg');
    if (svgEl && Number.isFinite(this.size)) {
      svgEl.setAttribute('width', String(this.size));
      svgEl.setAttribute('height', String(this.size));
    }
  }
}
