/* dom.js — tiny DOM helpers used across the app. No framework. */
'use strict';

/** h('div.cls1.cls2', {attrs, on*, dataset, style}, ...children) */
export function h(spec, props, ...children) {
  const [tag, ...classes] = spec.split('.');
  const el = document.createElement(tag || 'div');
  if (classes.length) el.className = classes.join(' ');
  if (props) {
    for (const [k, v] of Object.entries(props)) {
      if (v == null) continue;
      if (k === 'class') el.className += (el.className ? ' ' : '') + v;
      else if (k === 'dataset') Object.assign(el.dataset, v);
      else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
      else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
      else if (k === 'html') el.innerHTML = v;
      else if (k in el && k !== 'list' && k !== 'form') { try { el[k] = v; } catch { el.setAttribute(k, v); } }
      else el.setAttribute(k, v);
    }
  }
  append(el, children);
  return el;
}

export function append(el, children) {
  for (const c of children.flat(9)) {
    if (c == null || c === false) continue;
    el.append(c instanceof Node ? c : document.createTextNode(String(c)));
  }
  return el;
}

export function clear(el) { el.textContent = ''; return el; }

export function frag(...children) {
  const f = document.createDocumentFragment();
  append(f, children);
  return f;
}

/** Simple event emitter mixin. */
export class Emitter {
  constructor() { this._ls = new Map(); }
  on(ev, fn) {
    if (!this._ls.has(ev)) this._ls.set(ev, new Set());
    this._ls.get(ev).add(fn);
    return () => this.off(ev, fn);
  }
  off(ev, fn) { this._ls.get(ev)?.delete(fn); }
  emit(ev, ...args) { this._ls.get(ev)?.forEach(fn => { try { fn(...args); } catch (e) { console.error(e); } }); }
}

export function debounce(fn, ms) {
  let t = null;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}

export function throttle(fn, ms) {
  let last = 0, t = null;
  return (...args) => {
    const now = Date.now();
    if (now - last >= ms) { last = now; fn(...args); }
    else { clearTimeout(t); t = setTimeout(() => { last = Date.now(); fn(...args); }, ms - (now - last)); }
  };
}

export function esc(s) {
  return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}

export function fmtInt(n) {
  if (n == null) return '—';
  if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
  if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(0) + 'k';
  return String(n);
}

export function fmtClockMs(ms, showTenths) {
  ms = Math.max(0, ms | 0);
  const m = Math.floor(ms / 60000), s = Math.floor((ms % 60000) / 1000);
  if (showTenths && ms < 20000) return `${s}.${Math.floor((ms % 1000) / 100)}`;
  if (m >= 60) return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
  return `${m}:${String(s).padStart(2, '0')}`;
}

/** Virtualized list: renders only visible rows. container must have fixed height + overflow auto. */
export class VirtualList {
  constructor(container, { rowHeight, render, overscan = 8 }) {
    this.c = container;
    this.rowHeight = rowHeight;
    this.render = render;
    this.overscan = overscan;
    this.items = [];
    this.spacer = h('div', { style: { position: 'relative', width: '100%' } });
    this.c.appendChild(this.spacer);
    this.c.addEventListener('scroll', () => this._paint(), { passive: true });
    this._ro = new ResizeObserver(() => this._paint());
    this._ro.observe(this.c);
    this._rendered = new Map();
  }
  setItems(items) {
    this.items = items;
    this.spacer.style.height = items.length * this.rowHeight + 'px';
    this._rendered.forEach(el => el.remove());
    this._rendered.clear();
    this._paint();
  }
  _paint() {
    const top = this.c.scrollTop, height = this.c.clientHeight;
    const first = Math.max(0, Math.floor(top / this.rowHeight) - this.overscan);
    const last = Math.min(this.items.length - 1, Math.ceil((top + height) / this.rowHeight) + this.overscan);
    for (const [i, el] of this._rendered) {
      if (i < first || i > last) { el.remove(); this._rendered.delete(i); }
    }
    for (let i = first; i <= last; i++) {
      if (this._rendered.has(i)) continue;
      const el = this.render(this.items[i], i);
      el.style.position = 'absolute';
      el.style.top = i * this.rowHeight + 'px';
      el.style.left = '0';
      el.style.right = '0';
      el.style.height = this.rowHeight + 'px';
      this.spacer.appendChild(el);
      this._rendered.set(i, el);
    }
  }
  scrollToIndex(i) {
    const y = i * this.rowHeight;
    if (y < this.c.scrollTop || y > this.c.scrollTop + this.c.clientHeight - this.rowHeight)
      this.c.scrollTop = Math.max(0, y - this.c.clientHeight / 2);
  }
  destroy() { this._ro.disconnect(); }
}
