/**
 * Non-side-effect class export for `<heatmap-ui>`.
 *
 * Importing this file gives you the class(es) 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/heatmap`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <heatmap-ui> — Grid-based heatmap / density / day-grid visualization.
 *
 * Attributes:
 *   type         — 'day-grid' | 'matrix' | 'density'   (default 'matrix')
 *   rows         — Number                              (default 7)
 *   cols         — Number                              (default 12)
 *   scale        — 'linear' | 'log' | 'quantile'       (default 'linear')
 *   data         — JSON string OR .data property [{ r, c, v, label? }]
 *   no-legend    — Boolean                              (default false — legend shown)
 *   color-scheme — 'accent' | 'success' | 'warning' | 'data-ramp'  (default 'data-ramp')
 *   start-date   — ISO date (for day-grid to label months)
 *   aspect       — 'square' | 'wide'                   (default 'wide')
 *
 * Events:
 *   cell-hover  — { detail: { r, c, v, label } }
 *   cell-click  — { detail: { r, c, v, label } }
 *
 * Slots:
 *   title — optional header title
 */

import { UIElement } from '../../core/element.js';

const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

export class UIHeatmap extends UIElement {
  static properties = {
    type:        { type: String,  default: 'matrix',    reflect: true },
    rows:        { type: Number,  default: 7,           reflect: true },
    cols:        { type: Number,  default: 12,          reflect: true },
    scale:       { type: String,  default: 'linear',    reflect: true },
    noLegend:    { type: Boolean, default: false,       reflect: true, attribute: 'no-legend' },
    colorScheme: { type: String,  default: 'data-ramp', reflect: true, attribute: 'color-scheme' },
    startDate:   { type: String,  default: '',          reflect: true, attribute: 'start-date' },
    aspect:      { type: String,  default: 'wide',      reflect: true },
  };

  static template = () => null;

  #data = [];
  #bound = false;

  set data(arr) {
    if (typeof arr === 'string') {
      try { arr = JSON.parse(arr); } catch { arr = []; }
    }
    this.#data = Array.isArray(arr) ? arr : [];
    this.#requestRender();
  }

  get data() { return this.#data; }

  connected() {
    this.setAttribute('role', 'img');
    // Parse data attribute if present
    const raw = this.getAttribute('data');
    if (raw && !this.#data.length) {
      try { this.#data = JSON.parse(raw); } catch { /* ignore */ }
    }
    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('pointerover', this.#onHover);
      this.addEventListener('pointermove', this.#onMove);
      this.addEventListener('pointerleave', this.#onLeave);
      this.addEventListener('click', this.#onClick);
    }
  }

  disconnected() {
    this.removeEventListener('pointerover', this.#onHover);
    this.removeEventListener('pointermove', this.#onMove);
    this.removeEventListener('pointerleave', this.#onLeave);
    this.removeEventListener('click', this.#onClick);
    this.#bound = false;
    if (this.#raf != null) {
      cancelAnimationFrame(this.#raf);
      this.#raf = null;
    }
  }

  #raf = null;
  #requestRender() {
    if (!this.isConnected || this.#raf) return;
    this.#raf = requestAnimationFrame(() => {
      this.#raf = null;
      this.render();
    });
  }

  // ── Bucket value to 0..4 based on scale + data min/max ──

  #buckets() {
    const values = this.#data.map((d) => Number(d.v) || 0);
    if (!values.length) return (_) => 0;
    let min = Math.min(...values);
    let max = Math.max(...values);
    if (min === max) return (_) => 4;
    const scale = this.scale;
    if (scale === 'log') {
      const lo = Math.log(Math.max(1, min));
      const hi = Math.log(Math.max(1, max));
      return (v) => {
        const l = Math.log(Math.max(1, Number(v) || 0));
        const t = (l - lo) / (hi - lo);
        return Math.min(4, Math.max(0, Math.round(t * 4)));
      };
    }
    if (scale === 'quantile') {
      const sorted = [...values].sort((a, b) => a - b);
      const q = (p) => sorted[Math.floor(p * (sorted.length - 1))];
      const breaks = [q(0.2), q(0.4), q(0.6), q(0.8)];
      return (v) => {
        const n = Number(v) || 0;
        if (n <= breaks[0]) return 0;
        if (n <= breaks[1]) return 1;
        if (n <= breaks[2]) return 2;
        if (n <= breaks[3]) return 3;
        return 4;
      };
    }
    // linear
    return (v) => {
      const n = Number(v) || 0;
      const t = (n - min) / (max - min);
      return Math.min(4, Math.max(0, Math.round(t * 4)));
    };
  }

  render() {
    const rows = Math.max(1, this.rows || 7);
    const cols = Math.max(1, this.cols || 12);

    // Build a map for fast lookup
    const map = new Map();
    for (const d of this.#data) {
      if (d && d.r != null && d.c != null) map.set(`${d.r},${d.c}`, d);
    }
    const bucketOf = this.#buckets();

    // ── Outer host layout: clear everything except <title> slot ──
    // Keep only slotted title children if present.
    const title = this.querySelector(':scope > [slot="title"]');
    this.replaceChildren();
    if (title) this.appendChild(title);

    // ── Optional month-labels row (day-grid mode) ──
    if (this.type === 'day-grid' && this.startDate) {
      const labels = document.createElement('div');
      labels.setAttribute('data-months', '');
      const start = new Date(this.startDate);
      if (!Number.isNaN(start.getTime())) {
        // For day-grid: rows=7 (days of week), cols=weeks. Label months at column
        // positions where the 1st of the month falls.
        const marks = [];
        for (let c = 0; c < cols; c++) {
          const d = new Date(start);
          d.setDate(d.getDate() + c * 7);
          if (c === 0 || d.getDate() <= 7) {
            marks.push({ col: c, label: MONTHS[d.getMonth()] });
          }
        }
        for (const m of marks) {
          const span = document.createElement('span');
          span.setAttribute('data-month-label', '');
          span.style.gridColumn = String(m.col + 1);
          span.textContent = m.label;
          labels.appendChild(span);
        }
      }
      // day-grid month labels align to the fixed-size cell columns.
      labels.style.gridTemplateColumns = `repeat(${cols}, var(--heatmap-cell-min-size, 0.75rem))`;
      this.appendChild(labels);
    }

    // ── Grid of cells ──
    // day-grid keeps FIXED cell tracks (host scrolls horizontally when the
    // 52-week grid exceeds the container); matrix/density use minmax(0,1fr)
    // so they fit-and-shrink to the container.
    const isDayGrid = this.type === 'day-grid';
    const cellTrack = isDayGrid ? 'var(--heatmap-cell-min-size, 0.75rem)' : 'minmax(0, 1fr)';
    const grid = document.createElement('div');
    grid.setAttribute('data-grid', '');
    grid.style.gridTemplateColumns = `repeat(${cols}, ${cellTrack})`;
    grid.style.gridTemplateRows = `repeat(${rows}, ${isDayGrid ? 'var(--heatmap-cell-min-size, 0.75rem)' : '1fr'})`;
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const d = map.get(`${r},${c}`);
        const cell = document.createElement('div');
        cell.setAttribute('data-cell', '');
        cell.setAttribute('role', 'gridcell');
        cell.dataset.r = String(r);
        cell.dataset.c = String(c);
        if (d != null) {
          const v = Number(d.v) || 0;
          const bucket = bucketOf(v);
          cell.dataset.bucket = String(bucket);
          cell.dataset.v = String(v);
          if (d.label) cell.setAttribute('aria-label', d.label);
        } else {
          cell.dataset.empty = '';
        }
        grid.appendChild(cell);
      }
    }
    this.appendChild(grid);

    // ── Legend ──
    if (!this.noLegend) {
      const legend = document.createElement('div');
      legend.setAttribute('data-legend', '');
      const less = document.createElement('span');
      less.setAttribute('data-legend-label', '');
      less.textContent = 'Less';
      legend.appendChild(less);
      for (let i = 0; i < 5; i++) {
        const swatch = document.createElement('span');
        swatch.setAttribute('data-legend-swatch', '');
        swatch.dataset.bucket = String(i);
        legend.appendChild(swatch);
      }
      const more = document.createElement('span');
      more.setAttribute('data-legend-label', '');
      more.textContent = 'More';
      legend.appendChild(more);
      this.appendChild(legend);
    }
  }

  #findCell(target) {
    return target?.closest?.('[data-cell]');
  }

  /* Compose the cell-level event with the canonical chart-* shape so that
     tooltip-ui[follows=pointer][for=this-heatmap] can render without caring
     whether the source is a chart or a heatmap. */
  #chartDetail(cell, event) {
    const r = Number(cell.dataset.r);
    const c = Number(cell.dataset.c);
    const v = Number(cell.dataset.v);
    const label = cell.getAttribute('aria-label') || '';
    return {
      r, c,
      label,
      value: Number.isFinite(v) ? v : null,
      pct: null,
      series: null,
      slot: 0,
      pointerX: event?.clientX ?? null,
      pointerY: event?.clientY ?? null,
    };
  }

  #hoveredCell = null;

  #onHover = (e) => {
    const cell = this.#findCell(e.target);
    if (!cell || !cell.dataset.v) return;
    this.#hoveredCell = cell;
    const detail = this.#chartDetail(cell, e);
    /* Legacy cell-specific shape — kept for back-compat. */
    this.dispatchEvent(new CustomEvent('cell-hover', { detail, bubbles: true }));
    /* Canonical chart-hover shape for tooltip-ui[follows=pointer]. */
    this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
  };

  #onMove = (e) => {
    const cell = this.#findCell(e.target);
    if (!cell || !cell.dataset.v) {
      if (this.#hoveredCell) {
        this.#hoveredCell = null;
        this.dispatchEvent(new CustomEvent('chart-leave', { bubbles: true }));
      }
      return;
    }
    if (cell !== this.#hoveredCell) {
      this.#hoveredCell = cell;
      const detail = this.#chartDetail(cell, e);
      this.dispatchEvent(new CustomEvent('cell-hover', { detail, bubbles: true }));
      this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
    } else {
      /* Pointer still inside the same cell — re-fire chart-hover so the
         pointer-follow tooltip can reposition without re-painting content. */
      const detail = this.#chartDetail(cell, e);
      this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
    }
  };

  #onLeave = () => {
    if (!this.#hoveredCell) return;
    this.#hoveredCell = null;
    this.dispatchEvent(new CustomEvent('chart-leave', { bubbles: true }));
  };

  #onClick = (e) => {
    const cell = this.#findCell(e.target);
    if (!cell || !cell.dataset.v) return;
    const detail = this.#chartDetail(cell, e);
    this.dispatchEvent(new CustomEvent('cell-click', { detail, bubbles: true }));
    this.dispatchEvent(new CustomEvent('chart-select', { detail, bubbles: true }));
  };
}
