// ============ 31-DAY CHARTS ============
//
// A temperature and a consumption are read differently, so they are drawn
// differently: a temperature is a curve with the day's low and high behind
// it, a consumption is a bar per day that can be added up by eye. Both draw
// their own axes with units, say what is being read, and let the pointer pick
// a day.
//
// The SVG on screen is what gets exported, so "download the graph" can never
// disagree with what was seen.

const { useState: useStateC, useMemo: useMemoC, useRef: useRefC } = React;

const CH_COLOURS = ["var(--accent)", "var(--warm)", "#7C6BD6", "#C9A227"];

/** Redraws the component when the language changes: dates and figures follow it. */
function useLangRedraw() {
  const [, bump] = useStateC(0);
  React.useEffect(() => {
    const on = () => bump((n) => n + 1);
    window.addEventListener("st-lang", on);
    return () => window.removeEventListener("st-lang", on);
  }, []);
}

const chartLocale = () => (window.STI18n ? STI18n.locale() : "fr-FR");
const dayLabel = (d) => new Date(d).toLocaleDateString(chartLocale(), { day: "2-digit", month: "short" });
const fullDay = (d) => new Date(d).toLocaleDateString(chartLocale(), { weekday: "short", day: "2-digit", month: "long" });

// An axis people can read: 1, 2, 2.5 or 5 times a power of ten.
function niceMax(v) {
  if (!isFinite(v) || v <= 0) return 1;
  const exp = Math.floor(Math.log10(v));
  const base = Math.pow(10, exp);
  const n = v / base;
  const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10;
  return step * base;
}

function niceFloor(v) {
  if (!isFinite(v)) return 0;
  return Math.floor(v);
}

// Every day between the first reading and today, so a gap in the data reads
// as a gap rather than being closed up by the line.
//
// The server cuts days at UTC midnight and the browser counts them from local
// midnight: keyed naively, every day of a European reader lands on the one
// before, and today's reading vanishes. A day is keyed by its calendar date,
// read half a day in so either midnight gives the same date.
function dayKey(at) {
  const d = new Date(new Date(at).getTime() + 12 * 3600 * 1000);
  return d.toISOString().slice(0, 10);
}
function localKey(at) {
  const p = (n) => String(n).padStart(2, "0");
  return `${at.getFullYear()}-${p(at.getMonth() + 1)}-${p(at.getDate())}`;
}

function calendar(points, days) {
  const end = new Date();
  end.setHours(0, 0, 0, 0);
  const out = [];
  const byDay = new Map();
  for (const p of points || []) byDay.set(dayKey(p.day), p);
  for (let i = days - 1; i >= 0; i--) {
    const at = new Date(end.getFullYear(), end.getMonth(), end.getDate() - i);
    const key = localKey(at);
    out.push({ at, key, ...(byDay.get(key) || {}) });
  }
  return out;
}

function Gridlines({ plot, ticks, scale, unit, side }) {
  const x = side === "right" ? plot.x + plot.w : plot.x;
  return React.createElement(React.Fragment, null,
    ticks.map((t, i) => React.createElement("g", { key: i },
      side === "left" && React.createElement("line", {
        x1: plot.x, x2: plot.x + plot.w, y1: scale(t), y2: scale(t),
        stroke: "var(--line)", strokeWidth: 1,
      }),
      React.createElement("text", {
        x: side === "right" ? x + 7 : x - 7, y: scale(t) + 4,
        textAnchor: side === "right" ? "start" : "end",
        fill: "var(--ink-3)", fontSize: 11, fontFamily: "var(--mono)",
      }, t + (i === ticks.length - 1 ? " " + unit : ""))
    )));
}

function DayAxis({ plot, cal, xOf }) {
  const every = Math.max(1, Math.round(cal.length / 6));
  return cal.map((d, i) => (i % every === 0 || i === cal.length - 1)
    ? React.createElement("text", {
        key: i, x: xOf(i), y: plot.y + plot.h + 18, textAnchor: "middle",
        fill: "var(--ink-3)", fontSize: 11, fontFamily: "var(--mono)",
      }, dayLabel(d.at))
    : null);
}

// A transparent column per day catches the pointer: no maths on mousemove,
// and it works with a finger as well as a mouse.
function HoverColumns({ plot, cal, xOf, step, onPick }) {
  return cal.map((d, i) => React.createElement("rect", {
    key: i, x: xOf(i) - step / 2, y: plot.y, width: step, height: plot.h,
    fill: "transparent", onMouseEnter: () => onPick(i), onTouchStart: () => onPick(i),
  }));
}

/** Temperature and humidity: a band for the day's low and high, a line for the average. */
function TempChart({ points, days, height = 260, svgRef }) {
  useLangRedraw();
  const [hover, setHover] = useStateC(null);
  const cal = useMemoC(() => calendar(points, days), [points, days]);
  const W = 760, H = height;
  const plot = { x: 52, y: 16, w: W - 52 - 54, h: H - 16 - 34 };
  const withTemp = cal.filter((d) => d.temp != null);

  const lo = withTemp.length ? Math.min(...withTemp.map((d) => (d.tempMin != null ? d.tempMin : d.temp))) : 0;
  const hi = withTemp.length ? Math.max(...withTemp.map((d) => (d.tempMax != null ? d.tempMax : d.temp))) : 30;
  const tMin = niceFloor(lo - 1), tMax = Math.ceil(hi + 1);
  const yT = (v) => plot.y + plot.h - ((v - tMin) / Math.max(1, tMax - tMin)) * plot.h;
  const yH = (v) => plot.y + plot.h - (v / 100) * plot.h;
  const step = plot.w / Math.max(1, cal.length - 1);
  const xOf = (i) => plot.x + i * step;

  const ticks = [tMin, Math.round((tMin + tMax) / 2), tMax];
  const humTicks = [0, 50, 100];
  const line = (key, y) => cal.map((d, i) => (d[key] == null ? null : `${xOf(i)},${y(d[key])}`))
    .filter(Boolean).join(" ");
  const band = (() => {
    const top = cal.map((d, i) => (d.tempMax == null ? null : `${xOf(i)},${yT(d.tempMax)}`)).filter(Boolean);
    const bottom = cal.map((d, i) => (d.tempMin == null ? null : `${xOf(i)},${yT(d.tempMin)}`)).filter(Boolean).reverse();
    return top.length > 1 ? top.concat(bottom).join(" ") : "";
  })();
  const picked = hover != null ? cal[hover] : null;

  return React.createElement("div", { className: "mchart" },
    React.createElement("svg", {
      ref: svgRef, viewBox: `0 0 ${W} ${H}`, className: "mchart-svg",
      onMouseLeave: () => setHover(null), role: "img",
    },
      React.createElement(Gridlines, { plot, ticks, scale: yT, unit: "°C", side: "left" }),
      React.createElement(Gridlines, { plot, ticks: humTicks, scale: yH, unit: "%", side: "right" }),
      band && React.createElement("polygon", { points: band, fill: "var(--accent)", opacity: 0.14 }),
      React.createElement("polyline", {
        points: line("hum", yH), fill: "none", stroke: "var(--ink-3)", strokeWidth: 1.6,
        strokeDasharray: "4 4", strokeLinejoin: "round",
      }),
      React.createElement("polyline", {
        points: line("temp", yT), fill: "none", stroke: "var(--accent)", strokeWidth: 2.4,
        strokeLinejoin: "round", strokeLinecap: "round",
      }),
      React.createElement(DayAxis, { plot, cal, xOf }),
      picked && picked.temp != null && React.createElement("g", null,
        React.createElement("line", {
          x1: xOf(hover), x2: xOf(hover), y1: plot.y, y2: plot.y + plot.h,
          stroke: "var(--ink-3)", strokeWidth: 1,
        }),
        React.createElement("circle", { cx: xOf(hover), cy: yT(picked.temp), r: 4, fill: "var(--accent)" })),
      React.createElement(HoverColumns, { plot, cal, xOf, step, onPick: setHover })
    ),
    picked && React.createElement("div", { className: "mchart-tip" },
      React.createElement("b", null, fullDay(picked.at)), " · ",
      picked.temp == null
        ? "aucun relevé"
        : `${picked.temp} °C (${picked.tempMin} – ${picked.tempMax}) · ${picked.hum == null ? "—" : picked.hum + " %"}`)
  );
}

// Figures on the axis and in the tooltip, in the reader's own notation.
const fmtAxis = (v, digits) => Number(v).toLocaleString(chartLocale(), { maximumFractionDigits: digits == null ? 2 : digits });

/**
 * Energy or volume: one bar per day and per channel, so days can be compared
 * and added up. The two clamps of a meter are separate circuits, so they
 * stand side by side rather than on top of each other. `factor` converts the
 * stored figure for display (m³ to litres).
 */
function EnergyChart({ series, days, unit, field: fieldIn, factor = 1, height = 260, svgRef }) {
  useLangRedraw();
  const [hover, setHover] = useStateC(null);
  const cals = useMemoC(() => series.map((s) => calendar(s.points, days).map((d) => {
    const f = fieldIn || (unit === "m³" ? "volume" : "kwh");
    return d[f] == null ? d : { ...d, [f]: +(d[f] * factor).toFixed(3) };
  })), [series, days, factor, fieldIn, unit]);
  const field = fieldIn || (unit === "m³" ? "volume" : "kwh");
  const W = 760, H = height;
  const plot = { x: 52, y: 16, w: W - 52 - 18, h: H - 16 - 34 };
  const cal = cals[0] || [];
  const all = cals.flat().map((d) => d[field]).filter((v) => v != null);
  const top = niceMax(all.length ? Math.max(...all) : 1);
  const y = (v) => plot.y + plot.h - (v / top) * plot.h;
  const step = plot.w / Math.max(1, cal.length);
  const xOf = (i) => plot.x + i * step + step / 2;
  const bw = Math.max(2, (step * 0.68) / Math.max(1, series.length));
  const ticks = [0, top / 2, top].map((t) => +t.toFixed(2));
  const picked = hover != null ? cals.map((c) => c[hover]) : null;

  return React.createElement("div", { className: "mchart" },
    React.createElement("svg", {
      ref: svgRef, viewBox: `0 0 ${W} ${H}`, className: "mchart-svg",
      onMouseLeave: () => setHover(null), role: "img",
    },
      React.createElement(Gridlines, { plot, ticks, scale: y, unit, side: "left" }),
      cals.map((c, si) => c.map((d, i) => (d[field] == null || d[field] <= 0 ? null : React.createElement("rect", {
        key: `${si}-${i}`,
        x: xOf(i) - (bw * series.length) / 2 + si * bw,
        y: y(d[field]), width: bw - 1, height: Math.max(1, plot.y + plot.h - y(d[field])),
        fill: CH_COLOURS[si % CH_COLOURS.length], rx: 2,
        opacity: hover == null || hover === i ? 1 : 0.45,
      })))),
      React.createElement(DayAxis, { plot, cal, xOf }),
      React.createElement(HoverColumns, { plot, cal, xOf, step, onPick: setHover })
    ),
    picked && React.createElement("div", { className: "mchart-tip" },
      React.createElement("b", null, fullDay(cal[hover].at)), " · ",
      picked.map((d, i) => `${series[i].label || ""} ${d && d[field] != null ? fmtAxis(d[field]) + " " + unit : "—"}`.trim()).join(" · "))
  );
}

/**
 * Daily figures that are levels rather than amounts — a power, a voltage — as
 * lines over the 31 days, with an optional band between a low and a high.
 * Each line: { label, points, field, colour, dashed, bandLo, bandHi }.
 */
function LevelChart({ lines, days, unit, digits = 0, height = 260, svgRef, floor }) {
  useLangRedraw();
  const [hover, setHover] = useStateC(null);
  const cals = useMemoC(() => lines.map((l) => calendar(l.points, days)), [lines, days]);
  const W = 760, H = height;
  const plot = { x: 58, y: 16, w: W - 58 - 18, h: H - 16 - 34 };
  const cal = cals[0] || [];
  const values = [];
  lines.forEach((l, i) => cals[i].forEach((d) => {
    [l.field, l.bandLo, l.bandHi].forEach((f) => { if (f && d[f] != null) values.push(d[f]); });
  }));
  let lo = floor != null ? floor : (values.length ? Math.min(...values) : 0);
  let hi = values.length ? Math.max(...values) : 1;
  if (floor == null) {
    const pad = Math.max((hi - lo) * 0.15, hi * 0.005, 0.5);
    lo = Math.floor(lo - pad); hi = Math.ceil(hi + pad);
  } else {
    hi = niceMax(hi);
  }
  const y = (v) => plot.y + plot.h - ((v - lo) / Math.max(1e-9, hi - lo)) * plot.h;
  const step = plot.w / Math.max(1, cal.length - 1);
  const xOf = (i) => plot.x + i * step;
  const ticks = [lo, (lo + hi) / 2, hi].map((t) => +t.toFixed(digits));
  const path = (c, f) => c.map((d, i) => (d[f] == null ? null : `${xOf(i)},${y(d[f])}`)).filter(Boolean).join(" ");
  const band = (c, a, b) => {
    const top = c.map((d, i) => (d[b] == null ? null : `${xOf(i)},${y(d[b])}`)).filter(Boolean);
    const bottom = c.map((d, i) => (d[a] == null ? null : `${xOf(i)},${y(d[a])}`)).filter(Boolean).reverse();
    return top.length > 1 ? top.concat(bottom).join(" ") : "";
  };
  const picked = hover != null ? cals.map((c) => c[hover]) : null;

  return React.createElement("div", { className: "mchart" },
    React.createElement("svg", {
      ref: svgRef, viewBox: `0 0 ${W} ${H}`, className: "mchart-svg",
      onMouseLeave: () => setHover(null), role: "img",
    },
      React.createElement(Gridlines, { plot, ticks: ticks.map((t) => t), scale: y, unit, side: "left" }),
      lines.map((l, i) => l.bandLo && l.bandHi && React.createElement("polygon", {
        key: "b" + i, points: band(cals[i], l.bandLo, l.bandHi), fill: l.colour, opacity: 0.14,
      })),
      lines.map((l, i) => React.createElement("polyline", {
        key: "l" + i, points: path(cals[i], l.field), fill: "none", stroke: l.colour,
        strokeWidth: l.dashed ? 1.6 : 2.4, strokeDasharray: l.dashed ? "4 4" : null,
        strokeLinejoin: "round", strokeLinecap: "round",
      })),
      React.createElement(DayAxis, { plot, cal, xOf }),
      hover != null && React.createElement("line", {
        x1: xOf(hover), x2: xOf(hover), y1: plot.y, y2: plot.y + plot.h, stroke: "var(--ink-3)", strokeWidth: 1,
      }),
      React.createElement(HoverColumns, { plot, cal, xOf, step, onPick: setHover })
    ),
    picked && React.createElement("div", { className: "mchart-tip" },
      React.createElement("b", null, fullDay(cal[hover].at)), " · ",
      lines.map((l, i) => {
        const d = picked[i] || {};
        const v = d[l.field];
        const range = l.bandLo && d[l.bandLo] != null ? ` (${fmtAxis(d[l.bandLo], digits)} – ${fmtAxis(d[l.bandHi], digits)})` : "";
        return `${l.label} ${v == null ? "—" : fmtAxis(v, digits) + " " + unit}${range}`;
      }).join(" · "))
  );
}

// ─── downloads ──────────────────────────────────────────────────────────────

function saveBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// The page's colours are CSS variables, which mean nothing outside the page:
// they are resolved to real colours before the drawing leaves the browser.
function exportChartPng(svg, filename) {
  if (!svg) return;
  const root = getComputedStyle(document.documentElement);
  const solve = (v) => v.replace(/var\((--[\w-]+)\)/g, (_, name) => root.getPropertyValue(name).trim() || "#333");
  const clone = svg.cloneNode(true);
  clone.querySelectorAll("*").forEach((el) => {
    ["fill", "stroke", "font-family"].forEach((attr) => {
      const v = el.getAttribute(attr);
      if (v && v.includes("var(")) el.setAttribute(attr, solve(v));
    });
  });
  clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  const box = svg.viewBox.baseVal;
  const w = box.width || 760, h = box.height || 260;
  const svgText = new XMLSerializer().serializeToString(clone);
  const img = new Image();
  img.onload = () => {
    const scale = 2;
    const canvas = document.createElement("canvas");
    canvas.width = w * scale;
    canvas.height = h * scale;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = solve("var(--surface)") || "#fff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    canvas.toBlob((b) => b && saveBlob(b, filename));
  };
  img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgText);
}

window.TempChart = TempChart;
window.EnergyChart = EnergyChart;
window.LevelChart = LevelChart;
window.STChart = { useLangRedraw, saveBlob, exportChartPng, calendar, dayKey, localKey, dayLabel, fullDay, niceMax, CH_COLOURS };
