/**
 * 行情区左侧画线工具(canvas 叠加层,坐标与 Lightweight Charts 对齐).
 */
(function () {
  const STORAGE_PREFIX = "hubMarketDraw:";
  const HIT_PX = 8;
  const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 0.886, 1];
  const FIB_LINE_COLORS = {
    0: "#787b86",
    0.236: "#f23645",
    0.382: "#e6b422",
    0.5: "#5d606b",
    0.618: "#d97706",
    0.786: "#26a69a",
    0.886: "#9c27b0",
    1: "#11734b",
  };
  const FIB_ZONE_FILLS = [
    { top: 1, bot: 0.886, fill: "rgba(156, 39, 176, 0.14)" },
    { top: 0.886, bot: 0.786, fill: "rgba(38, 166, 154, 0.14)" },
    { top: 0.786, bot: 0.618, fill: "rgba(0, 188, 212, 0.14)" },
    { top: 0.618, bot: 0.5, fill: "rgba(244, 143, 177, 0.16)" },
    { top: 0.5, bot: 0.382, fill: "rgba(120, 123, 134, 0.12)" },
    { top: 0.382, bot: 0.236, fill: "rgba(255, 183, 77, 0.16)" },
    { top: 0.236, bot: 0, fill: "rgba(242, 54, 69, 0.12)" },
  ];
  const DRAG_TOOLS = new Set(["trend", "rect", "range", "fib"]);
  const ONE_SHOT_TOOLS = new Set([
    "hline", "cross", "channel", "rect", "brush", "range", "text", "fib", "trend", "path", "erase",
  ]);
  const MIN_DRAG_PX = 6;

  const TOOL_LABELS = {
    cursor: "光标",
    hline: "水平线",
    cross: "十字线",
    channel: "平行通道",
    rect: "矩形",
    brush: "画笔",
    range: "价格测距",
    text: "文字",
    fib: "斐波那契",
    trend: "趋势线",
    path: "折线",
    erase: "删除选中",
    clear: "清除全部",
  };

  let chart = null;
  let series = null;
  let hostEl = null;
  let mainEl = null;
  let canvasEl = null;
  let toolbarEl = null;
  let viewKey = "";
  let activeTool = "cursor";
  let drawings = [];
  let draft = null;
  let selectedId = null;
  let redrawRaf = 0;
  let unsubRange = null;
  let getCandlesFn = null;
  let brushPointerId = null;
  let dragActive = false;
  let dragStartPx = null;
  let pathPreviewPt = null;
  let menuEl = null;
  let unsubClick = null;
  let mainBound = false;
  let tradingDaySplitEnabled = false;
  const BJ_OFFSET_SEC = 8 * 60 * 60;

  function uid() {
    return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
  }

  function storageKey() {
    return STORAGE_PREFIX + (viewKey || "default");
  }

  function loadDrawings() {
    try {
      const raw = localStorage.getItem(storageKey());
      if (!raw) return [];
      const arr = JSON.parse(raw);
      return Array.isArray(arr) ? arr : [];
    } catch (_) {
      return [];
    }
  }

  function saveDrawings() {
    try {
      localStorage.setItem(storageKey(), JSON.stringify(drawings));
    } catch (_) {}
  }

  function setChartInteraction(enabled) {
    if (!chart) return;
    const on = !!enabled;
    chart.applyOptions({
      handleScroll: {
        mouseWheel: on,
        pressedMouseMove: on,
        horzTouchDrag: on,
        vertTouchDrag: false,
      },
      handleScale: {
        axisPressedMouseMove: on,
        mouseWheel: on,
        pinch: on,
      },
    });
  }

  function syncCanvasSize() {
    if (!canvasEl || !hostEl) return;
    const w = hostEl.clientWidth;
    const h = hostEl.clientHeight;
    if (w < 1 || h < 1) return;
    const dpr = window.devicePixelRatio || 1;
    canvasEl.width = Math.floor(w * dpr);
    canvasEl.height = Math.floor(h * dpr);
    canvasEl.style.width = w + "px";
    canvasEl.style.height = h + "px";
    const ctx = canvasEl.getContext("2d");
    if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  function getCandles() {
    if (typeof getCandlesFn === "function") {
      const rows = getCandlesFn();
      return Array.isArray(rows) ? rows : [];
    }
    return [];
  }

  function timeToX(time) {
    if (!chart || time == null) return null;
    try {
      const x = chart.timeScale().timeToCoordinate(time);
      return x == null || !Number.isFinite(x) ? null : x;
    } catch (_) {
      return null;
    }
  }

  function priceToY(price) {
    if (!series || price == null || !Number.isFinite(Number(price))) return null;
    try {
      const y = series.priceToCoordinate(Number(price));
      return y == null || !Number.isFinite(y) ? null : y;
    } catch (_) {
      return null;
    }
  }

  function xToTime(x) {
    if (!chart) return null;
    try {
      const direct = chart.timeScale().coordinateToTime(x);
      if (direct != null) return direct;
    } catch (_) {}
    const candles = getCandles();
    if (!candles.length) return null;
    let bestTime = candles[0].time;
    let bestDist = Infinity;
    candles.forEach(function (c) {
      const cx = timeToX(c.time);
      if (cx == null) return;
      const d = Math.abs(cx - x);
      if (d < bestDist) {
        bestDist = d;
        bestTime = c.time;
      }
    });
    return bestTime;
  }

  function yToPrice(y) {
    if (!series) return null;
    try {
      const direct = series.coordinateToPrice(y);
      if (direct != null && Number.isFinite(Number(direct))) return Number(direct);
    } catch (_) {}
    const candles = getCandles();
    if (!candles.length) return null;
    let lo = null;
    let hi = null;
    candles.forEach(function (c) {
      const vals = [c.low, c.high, c.open, c.close];
      vals.forEach(function (v) {
        const n = Number(v);
        if (!Number.isFinite(n)) return;
        if (lo == null || n < lo) lo = n;
        if (hi == null || n > hi) hi = n;
      });
    });
    if (lo == null || hi == null) return null;
    const yLo = priceToY(lo);
    const yHi = priceToY(hi);
    if (yLo == null || yHi == null || Math.abs(yHi - yLo) < 1e-6) return (lo + hi) / 2;
    const ratio = (y - yLo) / (yHi - yLo);
    return lo + (hi - lo) * ratio;
  }

  function xyToPoint(x, y) {
    if (!chart || !series) return null;
    const time = xToTime(x);
    const price = yToPrice(y);
    if (time == null || price == null || !Number.isFinite(price)) return null;
    return { time: time, price: price };
  }

  function clientToLocal(ev) {
    const rect = (hostEl || canvasEl).getBoundingClientRect();
    return { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
  }

  function mountCanvasOverlay() {
    if (!canvasEl || !hostEl) return;
    if (canvasEl.parentElement !== hostEl) {
      hostEl.appendChild(canvasEl);
    }
    canvasEl.style.position = "absolute";
    canvasEl.style.top = "0";
    canvasEl.style.left = "0";
    canvasEl.style.width = "100%";
    canvasEl.style.height = "100%";
  }

  function scheduleRedraw() {
    if (redrawRaf) cancelAnimationFrame(redrawRaf);
    redrawRaf = requestAnimationFrame(function () {
      redrawRaf = 0;
      redraw();
    });
  }

  function strokeStyle(selected) {
    return selected ? "#f59e0b" : "#60a5fa";
  }

  function formatPrice(p) {
    const n = Number(p);
    if (!Number.isFinite(n)) return "—";
    const a = Math.abs(n);
    if (a >= 10000) return n.toFixed(2);
    if (a >= 1) return n.toFixed(4);
    return n.toFixed(6);
  }

  function signedPrice(n) {
    const s = formatPrice(Math.abs(n));
    return n < 0 ? "-" + s : s;
  }

  function estimateTickSize(price) {
    const a = Math.abs(Number(price)) || 1;
    if (a >= 10000) return 0.01;
    if (a >= 100) return 0.01;
    if (a >= 1) return 0.0001;
    if (a >= 0.01) return 0.000001;
    return 0.0000001;
  }

  function tickCount(diff, refPrice) {
    const step = estimateTickSize(refPrice);
    if (!step) return 0;
    return Math.round(diff / step);
  }

  function fibPriceAt(top, bot, lv) {
    return bot + (top - bot) * (1 - lv);
  }

  function drawHandle(ctx, x, y, large, color) {
    if (x == null || y == null) return;
    const r = large ? 6 : 4.5;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fillStyle = "#ffffff";
    ctx.fill();
    ctx.strokeStyle = color || "#2962ff";
    ctx.lineWidth = large ? 2 : 1.5;
    ctx.stroke();
  }

  function roundBadge(ctx, x, y, text) {
    ctx.font = "11px sans-serif";
    const padX = 8;
    const padY = 5;
    const tw = ctx.measureText(text).width;
    const bw = tw + padX * 2;
    const bh = 20;
    const left = x - bw / 2;
    const top = y - bh / 2;
    ctx.fillStyle = "rgba(30, 58, 138, 0.92)";
    ctx.beginPath();
    const r = 4;
    ctx.moveTo(left + r, top);
    ctx.lineTo(left + bw - r, top);
    ctx.quadraticCurveTo(left + bw, top, left + bw, top + r);
    ctx.lineTo(left + bw, top + bh - r);
    ctx.quadraticCurveTo(left + bw, top + bh, left + bw - r, top + bh);
    ctx.lineTo(left + r, top + bh);
    ctx.quadraticCurveTo(left, top + bh, left, top + bh - r);
    ctx.lineTo(left, top + r);
    ctx.quadraticCurveTo(left, top, left + r, top);
    ctx.closePath();
    ctx.fill();
    ctx.fillStyle = "#f8fafc";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(text, x, y + 1);
    ctx.textAlign = "left";
    ctx.textBaseline = "alphabetic";
  }

  function isDragTool(tool) {
    return DRAG_TOOLS.has(tool);
  }

  function cancelDraft() {
    draft = null;
    dragActive = false;
    dragStartPx = null;
    pathPreviewPt = null;
    scheduleRedraw();
  }

  function returnToCursorIfOneShot() {
    if (ONE_SHOT_TOOLS.has(activeTool)) {
      setActiveTool("cursor");
    }
  }

  function drawLine(ctx, x1, y1, x2, y2, selected) {
    if (x1 == null || y1 == null || x2 == null || y2 == null) return;
    ctx.beginPath();
    ctx.strokeStyle = strokeStyle(selected);
    ctx.lineWidth = selected ? 2 : 1.5;
    ctx.setLineDash([]);
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
  }

  function drawHLine(ctx, y, w, selected) {
    if (y == null) return;
    drawLine(ctx, 0, y, w, y, selected);
  }

  function drawVLine(ctx, x, h, selected) {
    if (x == null) return;
    drawLine(ctx, x, 0, x, h, selected);
  }

  function utcSecToBjParts(utcSec) {
    const d = new Date((Number(utcSec) + BJ_OFFSET_SEC) * 1000);
    return {
      y: d.getUTCFullYear(),
      m: d.getUTCMonth(),
      d: d.getUTCDate(),
      h: d.getUTCHours(),
    };
  }

  function collectTradingDayBoundaries(candles) {
    if (!candles.length) return [];
    const minT = Number(candles[0].time);
    const maxT = Number(candles[candles.length - 1].time);
    const minP = utcSecToBjParts(minT);
    const maxP = utcSecToBjParts(maxT);
    const out = [];
    let curMs = Date.UTC(minP.y, minP.m, minP.d) - 86400000;
    const endMs = Date.UTC(maxP.y, maxP.m, maxP.d) + 2 * 86400000;
    while (curMs <= endMs) {
      const boundary = Math.floor(curMs / 1000);
      if (boundary >= minT - 3600 && boundary <= maxT + 3600) {
        if (!out.length || out[out.length - 1] !== boundary) {
          out.push(boundary);
        }
      }
      curMs += 86400000;
    }
    return out;
  }

  function drawTradingDaySplits(ctx, w, h) {
    if (!tradingDaySplitEnabled || !chart) return;
    const candles = getCandles();
    if (!candles.length) return;
    const boundaries = collectTradingDayBoundaries(candles);
    if (!boundaries.length) return;
    ctx.save();
    ctx.strokeStyle = "#3b82f6";
    ctx.lineWidth = 1;
    ctx.setLineDash([5, 4]);
    boundaries.forEach(function (t) {
      const x = timeToX(t);
      if (x == null || !Number.isFinite(x) || x < -2 || x > w + 2) return;
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, h);
      ctx.stroke();
    });
    ctx.setLineDash([]);
    ctx.restore();
  }

  function drawRect(ctx, x1, y1, x2, y2, selected) {
    if (x1 == null || y1 == null || x2 == null || y2 == null) return;
    const l = Math.min(x1, x2);
    const t = Math.min(y1, y2);
    const rw = Math.abs(x2 - x1);
    const rh = Math.abs(y2 - y1);
    ctx.strokeStyle = strokeStyle(selected);
    ctx.lineWidth = selected ? 2 : 1.5;
    ctx.setLineDash([]);
    ctx.strokeRect(l, t, rw, rh);
    ctx.fillStyle = selected ? "rgba(245,158,11,0.08)" : "rgba(96,165,250,0.06)";
    ctx.fillRect(l, t, rw, rh);
  }

  function drawBrush(ctx, pts, selected) {
    if (!pts || pts.length < 2) return;
    ctx.beginPath();
    ctx.strokeStyle = strokeStyle(selected);
    ctx.lineWidth = selected ? 2.5 : 2;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    let started = false;
    pts.forEach(function (p) {
      const x = timeToX(p.time);
      const y = priceToY(p.price);
      if (x == null || y == null) return;
      if (!started) {
        ctx.moveTo(x, y);
        started = true;
      } else {
        ctx.lineTo(x, y);
      }
    });
    if (started) ctx.stroke();
  }

  function drawFib(ctx, p1, p2, w, selected) {
    if (!p1 || !p2) return;
    const top = Math.max(p1.price, p2.price);
    const bot = Math.min(p1.price, p2.price);
    const x1 = timeToX(p1.time);
    const x2 = timeToX(p2.time);
    const y1 = priceToY(p1.price);
    const y2 = priceToY(p2.price);
    const yTop = priceToY(top);
    const yBot = priceToY(bot);
    if (x1 == null || x2 == null || yTop == null || yBot == null || y1 == null || y2 == null) return;
    const left = Math.min(x1, x2);
    const right = Math.max(x1, x2);
    const span = Math.max(right - left, 48);
    const drawRight = left + span;

    FIB_ZONE_FILLS.forEach(function (zone) {
      const yA = priceToY(fibPriceAt(top, bot, zone.top));
      const yB = priceToY(fibPriceAt(top, bot, zone.bot));
      if (yA == null || yB == null) return;
      const zt = Math.min(yA, yB);
      const zb = Math.max(yA, yB);
      ctx.fillStyle = zone.fill;
      ctx.fillRect(left, zt, drawRight - left, Math.max(zb - zt, 1));
    });

    ctx.beginPath();
    ctx.strokeStyle = "rgba(120, 123, 134, 0.7)";
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
    ctx.setLineDash([]);

    ctx.strokeStyle = selected ? "#f59e0b" : "#787b86";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(left, yTop);
    ctx.lineTo(left, yBot);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(drawRight, yTop);
    ctx.lineTo(drawRight, yBot);
    ctx.stroke();

    let lastLabelY = -9999;
    FIB_LEVELS.forEach(function (lv) {
      const price = fibPriceAt(top, bot, lv);
      const y = priceToY(price);
      if (y == null) return;
      const lineColor = FIB_LINE_COLORS[lv] || "#787b86";
      ctx.beginPath();
      ctx.strokeStyle = lineColor;
      ctx.lineWidth = 1;
      ctx.setLineDash(lv === 0 || lv === 1 ? [] : [5, 4]);
      ctx.moveTo(left, y);
      ctx.lineTo(drawRight, y);
      ctx.stroke();
      if (Math.abs(y - lastLabelY) < 13) return;
      lastLabelY = y;
      const lvLabel = lv === 1 || lv === 0 ? String(lv) : String(lv);
      ctx.fillStyle = lineColor;
      ctx.font = "10px sans-serif";
      ctx.fillText(lvLabel + " (" + formatPrice(price) + ")", drawRight + 6, y + 3);
    });
    ctx.setLineDash([]);
    if (selected) {
      drawHandle(ctx, x1, y1, false, "#2962ff");
      drawHandle(ctx, x2, y2, false, "#2962ff");
    }
  }

  function drawRange(ctx, p1, p2, selected) {
    const x1 = timeToX(p1.time);
    const x2 = timeToX(p2.time);
    const y1 = priceToY(p1.price);
    const y2 = priceToY(p2.price);
    if (x1 == null || x2 == null || y1 == null || y2 == null) return;
    const left = Math.min(x1, x2);
    const right = Math.max(x1, x2);
    const top = Math.min(y1, y2);
    const bot = Math.max(y1, y2);
    const boxW = Math.max(right - left, 10);
    const boxH = Math.max(bot - top, 6);
    const borderColor = selected ? "#f59e0b" : "#1e293b";

    ctx.fillStyle = selected ? "rgba(245,158,11,0.18)" : "rgba(45, 212, 191, 0.22)";
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1;
    ctx.setLineDash([]);
    ctx.fillRect(left, top, boxW, boxH);
    ctx.strokeRect(left, top, boxW, boxH);

    const midX = left + boxW / 2;
    ctx.beginPath();
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1;
    ctx.moveTo(midX, top);
    ctx.lineTo(midX, bot);
    ctx.stroke();
    const arrowY = bot + 2;
    ctx.beginPath();
    ctx.fillStyle = borderColor;
    ctx.moveTo(midX, arrowY + 7);
    ctx.lineTo(midX - 5, arrowY);
    ctx.lineTo(midX + 5, arrowY);
    ctx.closePath();
    ctx.fill();

    const diff = p2.price - p1.price;
    const pct = p1.price ? (diff / p1.price) * 100 : 0;
    const ticks = tickCount(diff, p1.price);
    const tickStr = ticks < 0 ? String(ticks) : ticks > 0 ? "+" + ticks : "0";
    const badgeText =
      signedPrice(diff) + " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%) " + tickStr;
    roundBadge(ctx, midX, bot + 22, badgeText);

    drawHandle(ctx, left, top, false, "#2962ff");
    drawHandle(ctx, right, bot, false, "#2962ff");
  }

  function drawText(ctx, p, text, selected) {
    const x = timeToX(p.time);
    const y = priceToY(p.price);
    if (x == null || y == null) return;
    ctx.font = "12px sans-serif";
    ctx.fillStyle = strokeStyle(selected);
    ctx.fillText(String(text || ""), x + 4, y - 4);
  }

  function parallelOffset(p1, p2, p3) {
    const x1 = timeToX(p1.time);
    const y1 = priceToY(p1.price);
    const x2 = timeToX(p2.time);
    const y2 = priceToY(p2.price);
    const x3 = timeToX(p3.time);
    const y3 = priceToY(p3.price);
    if (x1 == null || y1 == null || x2 == null || y2 == null || x3 == null || y3 == null) {
      return 0;
    }
    const dx = x2 - x1;
    const dy = y2 - y1;
    const len = Math.hypot(dx, dy) || 1;
    const nx = -dy / len;
    const ny = dx / len;
    return (x3 - x1) * nx + (y3 - y1) * ny;
  }

  function drawChannel(ctx, p1, p2, offset, w, selected) {
    const x1 = timeToX(p1.time);
    const y1 = priceToY(p1.price);
    const x2 = timeToX(p2.time);
    const y2 = priceToY(p2.price);
    if (x1 == null || y1 == null || x2 == null || y2 == null) return;
    const dx = x2 - x1;
    const dy = y2 - y1;
    const len = Math.hypot(dx, dy) || 1;
    const nx = -dy / len;
    const ny = dx / len;
    const ox = nx * offset;
    const oy = ny * offset;
    drawLine(ctx, x1, y1, x2, y2, selected);
    drawLine(ctx, x1 + ox, y1 + oy, x2 + ox, y2 + oy, selected);
    ctx.fillStyle = selected ? "rgba(245,158,11,0.06)" : "rgba(96,165,250,0.05)";
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x2 + ox, y2 + oy);
    ctx.lineTo(x1 + ox, y1 + oy);
    ctx.closePath();
    ctx.fill();
  }

  function drawPath(ctx, pts, selected, previewPt) {
    if (!pts || !pts.length) return;
    const color = strokeStyle(selected);
    const coords = [];
    pts.forEach(function (p) {
      const x = timeToX(p.time);
      const y = priceToY(p.price);
      if (x != null && y != null) coords.push({ x: x, y: y });
    });
    if (coords.length < 1) return;

    ctx.beginPath();
    ctx.strokeStyle = color;
    ctx.lineWidth = selected ? 2 : 1.5;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.setLineDash([]);
    ctx.moveTo(coords[0].x, coords[0].y);
    for (let i = 1; i < coords.length; i++) {
      ctx.lineTo(coords[i].x, coords[i].y);
    }
    if (coords.length > 1) ctx.stroke();

    if (previewPt) {
      const px = timeToX(previewPt.time);
      const py = priceToY(previewPt.price);
      const last = coords[coords.length - 1];
      if (px != null && py != null && last) {
        ctx.beginPath();
        ctx.strokeStyle = color;
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 4]);
        ctx.moveTo(last.x, last.y);
        ctx.lineTo(px, py);
        ctx.stroke();
        ctx.setLineDash([]);
        drawHandle(ctx, px, py, true, color);
      }
    }

    coords.forEach(function (c, idx) {
      const isLast = idx === coords.length - 1;
      drawHandle(ctx, c.x, c.y, isLast && !!previewPt, color);
    });
  }

  function renderDrawing(ctx, d, w, h, selected, previewPt) {
    const pts = d.points || [];
    if (!pts.length) return;
    switch (d.type) {
      case "hline":
        drawHLine(ctx, priceToY(pts[0].price), w, selected);
        break;
      case "cross":
        drawHLine(ctx, priceToY(pts[0].price), w, selected);
        drawVLine(ctx, timeToX(pts[0].time), h, selected);
        break;
      case "trend":
        if (pts.length >= 2) {
          drawLine(
            ctx,
            timeToX(pts[0].time),
            priceToY(pts[0].price),
            timeToX(pts[1].time),
            priceToY(pts[1].price),
            selected
          );
        }
        break;
      case "channel":
        if (pts.length >= 3) {
          drawChannel(ctx, pts[0], pts[1], d.offset || 0, w, selected);
        } else if (pts.length === 2) {
          drawLine(
            ctx,
            timeToX(pts[0].time),
            priceToY(pts[0].price),
            timeToX(pts[1].time),
            priceToY(pts[1].price),
            selected
          );
        }
        break;
      case "rect":
        if (pts.length >= 2) {
          drawRect(
            ctx,
            timeToX(pts[0].time),
            priceToY(pts[0].price),
            timeToX(pts[1].time),
            priceToY(pts[1].price),
            selected
          );
        }
        break;
      case "brush":
        drawBrush(ctx, pts, selected);
        break;
      case "range":
        if (pts.length >= 2) drawRange(ctx, pts[0], pts[1], selected);
        break;
      case "text":
        drawText(ctx, pts[0], d.text, selected);
        break;
      case "fib":
        if (pts.length >= 2) drawFib(ctx, pts[0], pts[1], w, selected);
        break;
      case "path":
        drawPath(ctx, pts, selected, previewPt || null);
        break;
      default:
        break;
    }
  }

  function redraw() {
    if (!canvasEl) return;
    syncCanvasSize();
    const ctx = canvasEl.getContext("2d");
    if (!ctx) return;
    const w = hostEl.clientWidth;
    const h = hostEl.clientHeight;
    ctx.clearRect(0, 0, w, h);
    drawTradingDaySplits(ctx, w, h);
    drawings.forEach(function (d) {
      if (d.hidden) ctx.globalAlpha = 0.14;
      renderDrawing(ctx, d, w, h, d.id === selectedId);
      if (d.hidden) ctx.globalAlpha = 1;
    });
    if (draft) {
      const preview = draft.type === "path" ? pathPreviewPt : null;
      renderDrawing(ctx, draft, w, h, true, preview);
    }
    if (activeTool === "cursor" && selectedId) {
      const sel = getDrawingById(selectedId);
      if (sel) drawSelectionOverlay(ctx, sel);
    }
  }

  function commitDrawing(d) {
    if (!d || !d.type) return;
    d.id = uid();
    drawings.push(d);
    selectedId = d.id;
    draft = null;
    pathPreviewPt = null;
    saveDrawings();
    scheduleRedraw();
    returnToCursorIfOneShot();
  }

  function finishPath() {
    if (draft && draft.type === "path" && draft.points.length > 1) {
      commitDrawing({ type: "path", points: draft.points.slice() });
    } else {
      cancelDraft();
    }
  }

  function pointsNeeded(tool) {
    if (tool === "hline" || tool === "cross" || tool === "text") return 1;
    if (tool === "trend" || tool === "rect" || tool === "range" || tool === "fib") return 2;
    if (tool === "channel") return 3;
    return 0;
  }

  function getDrawingById(id) {
    for (let i = 0; i < drawings.length; i++) {
      if (drawings[i].id === id) return drawings[i];
    }
    return null;
  }

  function pickDrawingAt(x, y) {
    for (let i = drawings.length - 1; i >= 0; i--) {
      if (hitTestDrawing(drawings[i], x, y)) return drawings[i];
    }
    return null;
  }

  function selectDrawing(id) {
    selectedId = id || null;
    scheduleRedraw();
  }

  function deselectDrawing() {
    if (!selectedId) return;
    selectedId = null;
    hideContextMenu();
    scheduleRedraw();
  }

  function removeDrawing(id, opts) {
    if (!id) return;
    const force = !!(opts && opts.force);
    const d = getDrawingById(id);
    if (!d) return;
    if (d.locked && !force) return;
    drawings = drawings.filter(function (item) {
      return item.id !== id;
    });
    if (selectedId === id) selectedId = null;
    hideContextMenu();
    saveDrawings();
    scheduleRedraw();
  }

  function removeSelectedDrawing() {
    if (!selectedId) return;
    removeDrawing(selectedId);
  }

  function cloneDrawing(id) {
    const src = getDrawingById(id);
    if (!src || src.locked) return;
    const copy = JSON.parse(JSON.stringify(src));
    copy.id = uid();
    copy.locked = false;
    const candles = getCandles();
    const timeStep = candles.length > 1 ? Math.abs(candles[1].time - candles[0].time) : 60;
    copy.points = (copy.points || []).map(function (p, idx) {
      return {
        time: p.time + timeStep * (idx + 1),
        price: p.price * 1.001,
      };
    });
    if (copy.text) copy.text = String(copy.text);
    drawings.push(copy);
    selectedId = copy.id;
    saveDrawings();
    scheduleRedraw();
  }

  function toggleDrawingLock(id) {
    const d = getDrawingById(id);
    if (!d) return;
    d.locked = !d.locked;
    saveDrawings();
    scheduleRedraw();
  }

  function toggleDrawingHide(id) {
    const d = getDrawingById(id);
    if (!d) return;
    d.hidden = !d.hidden;
    if (d.hidden && selectedId === id) selectedId = null;
    hideContextMenu();
    saveDrawings();
    scheduleRedraw();
  }

  function isTypingTarget(el) {
    if (!el) return false;
    const tag = (el.tagName || "").toUpperCase();
    return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || !!el.isContentEditable;
  }

  function ensureContextMenu() {
    if (menuEl) return menuEl;
    menuEl = document.createElement("div");
    menuEl.id = "market-draw-menu";
    menuEl.className = "market-draw-menu hidden";
    menuEl.setAttribute("role", "menu");
    document.body.appendChild(menuEl);
    menuEl.addEventListener("click", function (ev) {
      const btn = ev.target.closest("[data-action]");
      if (!btn || btn.disabled) return;
      const action = btn.getAttribute("data-action");
      const id = menuEl._targetId;
      if (!id) return;
      ev.preventDefault();
      ev.stopPropagation();
      if (action === "clone") cloneDrawing(id);
      else if (action === "toggle-lock") toggleDrawingLock(id);
      else if (action === "toggle-hide") toggleDrawingHide(id);
      else if (action === "remove") removeDrawing(id, { force: true });
      if (action !== "remove" && action !== "toggle-hide") {
        const d = getDrawingById(id);
        if (d) showContextMenu(menuEl._clientX, menuEl._clientY, d);
        else hideContextMenu();
      }
    });
    document.addEventListener("pointerdown", function (ev) {
      if (!menuEl || menuEl.classList.contains("hidden")) return;
      if (!menuEl.contains(ev.target)) hideContextMenu();
    });
    return menuEl;
  }

  function hideContextMenu() {
    if (!menuEl) return;
    menuEl.classList.add("hidden");
    menuEl._targetId = null;
  }

  function showContextMenu(clientX, clientY, d) {
    if (!d) return;
    const menu = ensureContextMenu();
    const label = TOOL_LABELS[d.type] || d.type;
    const locked = !!d.locked;
    menu.innerHTML =
      '<div class="market-draw-menu-head">' +
      label +
      "</div>" +
      '<button type="button" class="market-draw-menu-item" data-action="clone"' +
      (locked ? " disabled" : "") +
      ">克隆</button>" +
      '<button type="button" class="market-draw-menu-item" data-action="toggle-lock">' +
      (locked ? "解锁" : "锁定") +
      "</button>" +
      '<button type="button" class="market-draw-menu-item" data-action="toggle-hide">' +
      (d.hidden ? "显示" : "隐藏") +
      "</button>" +
      '<hr class="market-draw-menu-sep" />' +
      '<button type="button" class="market-draw-menu-item is-danger" data-action="remove">移除 <span class="market-draw-menu-kbd">Del</span></button>';
    menu.classList.remove("hidden");
    menu._targetId = d.id;
    menu._clientX = clientX;
    menu._clientY = clientY;
    menu.style.visibility = "hidden";
    menu.style.left = "0px";
    menu.style.top = "0px";
    const mw = menu.offsetWidth;
    const mh = menu.offsetHeight;
    const pad = 8;
    let left = clientX;
    let top = clientY;
    if (left + mw > window.innerWidth - pad) left = window.innerWidth - mw - pad;
    if (top + mh > window.innerHeight - pad) top = window.innerHeight - mh - pad;
    if (left < pad) left = pad;
    if (top < pad) top = pad;
    menu.style.left = left + "px";
    menu.style.top = top + "px";
    menu.style.visibility = "";
  }

  function drawSelectionOverlay(ctx, d) {
    if (!d) return;
    const pts = d.points || [];
    const handleColor = d.locked ? "#94a3b8" : "#2962ff";
    pts.forEach(function (p, idx) {
      const x = timeToX(p.time);
      const y = priceToY(p.price);
      if (x == null || y == null) return;
      const large = d.type === "path" && idx === pts.length - 1;
      drawHandle(ctx, x, y, large, handleColor);
    });
    if ((d.type === "trend" || d.type === "channel") && pts.length >= 2) {
      const x1 = timeToX(pts[0].time);
      const y1 = priceToY(pts[0].price);
      const x2 = timeToX(pts[1].time);
      const y2 = priceToY(pts[1].price);
      if (x1 != null && y1 != null && x2 != null && y2 != null) {
        const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
        const text = angle.toFixed(2) + "°";
        ctx.font = "11px sans-serif";
        const tw = ctx.measureText(text).width;
        const bx = x2 + 10;
        const by = y2 - 8;
        ctx.fillStyle = "rgba(15, 23, 42, 0.88)";
        ctx.fillRect(bx - 4, by - 12, tw + 8, 18);
        ctx.fillStyle = "#f8fafc";
        ctx.fillText(text, bx, by);
      }
    }
    if (d.locked) {
      const anchor = pts[0];
      if (!anchor) return;
      const ax = timeToX(anchor.time);
      const ay = priceToY(anchor.price);
      if (ax == null || ay == null) return;
      ctx.font = "10px sans-serif";
      ctx.fillStyle = "#94a3b8";
      ctx.fillText("已锁定", ax + 8, ay - 8);
    }
  }

  function tryEraseAt(x, y) {
    const d = pickDrawingAt(x, y);
    if (!d || d.locked) return false;
    removeDrawing(d.id);
    return true;
  }

  function onPointerDown(ev) {
    if (activeTool === "cursor" || activeTool === "clear") return;
    if (!chart || !series || !canvasEl) return;
    const loc = clientToLocal(ev);
    if (activeTool === "erase") {
      if (tryEraseAt(loc.x, loc.y)) {
        returnToCursorIfOneShot();
      }
      ev.preventDefault();
      return;
    }
    const pt = xyToPoint(loc.x, loc.y);
    if (!pt) return;
    ev.preventDefault();
    ev.stopPropagation();
    const capturePointer = activeTool === "brush" || isDragTool(activeTool);
    if (capturePointer) {
      try {
        canvasEl.setPointerCapture(ev.pointerId);
        brushPointerId = ev.pointerId;
      } catch (_) {}
    }

    if (activeTool === "brush") {
      draft = { type: "brush", points: [pt] };
      return;
    }
    if (activeTool === "path") {
      if (!draft || draft.type !== "path") {
        draft = { type: "path", points: [pt] };
        pathPreviewPt = pt;
      } else {
        const last = draft.points[draft.points.length - 1];
        const lx = timeToX(last.time);
        const ly = priceToY(last.price);
        const cx = timeToX(pt.time);
        const cy = priceToY(pt.price);
        if (
          lx != null &&
          ly != null &&
          cx != null &&
          cy != null &&
          Math.hypot(cx - lx, cy - ly) > 4
        ) {
          draft.points.push(pt);
        }
        pathPreviewPt = pt;
      }
      scheduleRedraw();
      return;
    }
    if (activeTool === "channel") {
      if (!draft || draft.type !== "channel") {
        draft = { type: "channel", points: [pt] };
      } else if (draft.points.length === 1) {
        draft.points.push(pt);
      } else if (draft.points.length === 2) {
        draft.points.push(pt);
        draft.offset = parallelOffset(draft.points[0], draft.points[1], draft.points[2]);
        commitDrawing(draft);
      }
      scheduleRedraw();
      return;
    }
    if (isDragTool(activeTool)) {
      dragActive = true;
      dragStartPx = { x: loc.x, y: loc.y };
      draft = { type: activeTool, points: [pt, pt] };
      scheduleRedraw();
      return;
    }

    if (activeTool === "text") {
      const text = window.prompt("输入标注文字", "");
      if (text && String(text).trim()) {
        commitDrawing({ type: "text", points: [pt], text: String(text).trim() });
      }
      return;
    }
    if (pointsNeeded(activeTool) === 1) {
      commitDrawing({ type: activeTool, points: [pt] });
    }
  }

  function onPointerMove(ev) {
    const loc = clientToLocal(ev);
    if (activeTool === "brush" && draft && draft.type === "brush") {
      const pt = xyToPoint(loc.x, loc.y);
      if (!pt) return;
      const last = draft.points[draft.points.length - 1];
      const lx = timeToX(last.time);
      const ly = priceToY(last.price);
      const cx = timeToX(pt.time);
      const cy = priceToY(pt.price);
      if (lx != null && ly != null && cx != null && cy != null && Math.hypot(cx - lx, cy - ly) > 2) {
        draft.points.push(pt);
        scheduleRedraw();
      }
      ev.preventDefault();
      return;
    }
    if (dragActive && draft && isDragTool(draft.type)) {
      const pt = xyToPoint(loc.x, loc.y);
      if (!pt) return;
      draft.points[1] = pt;
      scheduleRedraw();
      ev.preventDefault();
      return;
    }
    if (activeTool === "path" && draft && draft.type === "path") {
      const pt = xyToPoint(loc.x, loc.y);
      if (!pt) return;
      pathPreviewPt = pt;
      scheduleRedraw();
      ev.preventDefault();
    }
  }

  function onPointerUp(ev) {
    const loc = clientToLocal(ev);
    if (brushPointerId != null) {
      try {
        canvasEl.releasePointerCapture(brushPointerId);
      } catch (_) {}
      brushPointerId = null;
    }
    if (dragActive && draft && isDragTool(draft.type)) {
      const dist = dragStartPx
        ? Math.hypot(loc.x - dragStartPx.x, loc.y - dragStartPx.y)
        : 0;
      dragActive = false;
      dragStartPx = null;
      const p1 = draft.points[0];
      const p2 = draft.points[1];
      if (
        dist >= MIN_DRAG_PX &&
        p1 &&
        p2 &&
        (p1.price !== p2.price || p1.time !== p2.time)
      ) {
        commitDrawing({ type: draft.type, points: [p1, p2] });
      } else {
        cancelDraft();
      }
      return;
    }
    if (activeTool === "brush" && draft && draft.type === "brush" && draft.points.length > 1) {
      commitDrawing(draft);
    }
  }

  function onDblClick(ev) {
    if (activeTool === "path" && draft && draft.type === "path") {
      finishPath();
      ev.preventDefault();
    }
  }

  function onContextMenu(ev) {
    if (activeTool === "path" && draft && draft.type === "path") {
      ev.preventDefault();
      ev.stopPropagation();
      finishPath();
    }
  }

  function onMainContextMenu(ev) {
    if (!hostEl) return;
    if (activeTool === "path" && draft && draft.type === "path") return;
    if (draft || dragActive) return;
    const rect = hostEl.getBoundingClientRect();
    const x = ev.clientX - rect.left;
    const y = ev.clientY - rect.top;
    const d = pickDrawingAt(x, y);
    if (!d) {
      hideContextMenu();
      return;
    }
    ev.preventDefault();
    selectDrawing(d.id);
    showContextMenu(ev.clientX, ev.clientY, d);
  }

  function distSeg(px, py, x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    if (dx === 0 && dy === 0) return Math.hypot(px - x1, py - y1);
    const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy)));
    const nx = x1 + t * dx;
    const ny = y1 + t * dy;
    return Math.hypot(px - nx, py - ny);
  }

  function hitTestDrawing(d, x, y) {
    const pts = d.points || [];
    if (!pts.length) return false;
    const w = hostEl.clientWidth;
    const h = hostEl.clientHeight;
    switch (d.type) {
      case "hline": {
        const ly = priceToY(pts[0].price);
        return ly != null && Math.abs(y - ly) <= HIT_PX;
      }
      case "cross": {
        const ly = priceToY(pts[0].price);
        const lx = timeToX(pts[0].time);
        return (
          (ly != null && Math.abs(y - ly) <= HIT_PX) ||
          (lx != null && Math.abs(x - lx) <= HIT_PX)
        );
      }
      case "trend":
      case "channel":
        if (pts.length >= 2) {
          const x1 = timeToX(pts[0].time);
          const y1 = priceToY(pts[0].price);
          const x2 = timeToX(pts[1].time);
          const y2 = priceToY(pts[1].price);
          if (x1 != null && y1 != null && x2 != null && y2 != null) {
            if (distSeg(x, y, x1, y1, x2, y2) <= HIT_PX) return true;
          }
        }
        return false;
      case "path":
      case "brush":
        if (pts.length >= 2) {
          for (let i = 1; i < pts.length; i++) {
            const x1 = timeToX(pts[i - 1].time);
            const y1 = priceToY(pts[i - 1].price);
            const x2 = timeToX(pts[i].time);
            const y2 = priceToY(pts[i].price);
            if (x1 != null && y1 != null && x2 != null && y2 != null) {
              if (distSeg(x, y, x1, y1, x2, y2) <= HIT_PX) return true;
            }
          }
        }
        return false;
      case "text": {
        const tx = timeToX(pts[0].time);
        const ty = priceToY(pts[0].price);
        return tx != null && ty != null && Math.hypot(x - tx, y - ty) <= 14;
      }
      case "range":
      case "fib":
      case "rect":
        if (pts.length >= 2) {
          const x1 = timeToX(pts[0].time);
          const y1 = priceToY(pts[0].price);
          const x2 = timeToX(pts[1].time);
          const y2 = priceToY(pts[1].price);
          if (x1 == null || y1 == null || x2 == null || y2 == null) return false;
          const l = Math.min(x1, x2) - HIT_PX;
          const r = Math.max(x1, x2) + HIT_PX;
          const t = Math.min(y1, y2) - HIT_PX;
          const b = Math.max(y1, y2) + HIT_PX;
          return x >= l && x <= r && y >= t && y <= b;
        }
        return false;
      default:
        return false;
    }
  }

  function syncCanvasPointerMode() {
    if (!canvasEl) return;
    const drawing = activeTool !== "cursor";
    canvasEl.classList.toggle("is-drawing", drawing);
    canvasEl.style.pointerEvents = drawing ? "auto" : "none";
  }

  function setActiveTool(tool) {
    if (!TOOL_LABELS[tool]) return;
    if (tool === "clear") {
      if (drawings.length && window.confirm("清除当前图表上的全部画线?")) {
        drawings = [];
        selectedId = null;
        draft = null;
        saveDrawings();
        scheduleRedraw();
      }
      return;
    }
    if (tool === "erase") {
      if (selectedId) {
        drawings = drawings.filter(function (d) {
          return d.id !== selectedId;
        });
        selectedId = null;
        saveDrawings();
        scheduleRedraw();
      }
      return;
    }
    activeTool = tool;
    dragActive = false;
    dragStartPx = null;
    pathPreviewPt = null;
    draft = null;
    if (toolbarEl) {
      toolbarEl.querySelectorAll("[data-tool]").forEach(function (btn) {
        btn.classList.toggle("is-active", btn.getAttribute("data-tool") === tool);
      });
    }
    syncCanvasPointerMode();
    setChartInteraction(activeTool === "cursor");
    scheduleRedraw();
  }

  function bindToolbar() {
    if (!toolbarEl) return;
    toolbarEl.querySelectorAll("[data-tool]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        setActiveTool(btn.getAttribute("data-tool") || "cursor");
      });
    });
  }

  let canvasBound = false;

  function bindCanvas() {
    if (!canvasEl || canvasBound) return;
    canvasBound = true;
    canvasEl.addEventListener("pointerdown", onPointerDown);
    canvasEl.addEventListener("pointermove", onPointerMove);
    canvasEl.addEventListener("pointerup", onPointerUp);
    canvasEl.addEventListener("pointercancel", onPointerUp);
    canvasEl.addEventListener("dblclick", onDblClick);
    canvasEl.addEventListener("contextmenu", onContextMenu);
    document.addEventListener("keydown", onDrawKeydown);
  }

  function onDrawKeydown(ev) {
    const page = document.getElementById("page-market");
    if (!page || page.classList.contains("hidden")) return;
    if (isTypingTarget(ev.target)) return;
    if (ev.key === "Escape") {
      if (draft || dragActive) {
        cancelDraft();
        return;
      }
      if (!menuEl || menuEl.classList.contains("hidden")) {
        deselectDrawing();
      } else {
        hideContextMenu();
      }
      return;
    }
    if (ev.key === "Enter" && activeTool === "path" && draft && draft.type === "path") {
      finishPath();
      ev.preventDefault();
      return;
    }
    if (
      (ev.key === "Delete" || ev.key === "Backspace") &&
      activeTool === "cursor" &&
      selectedId
    ) {
      const d = getDrawingById(selectedId);
      if (d && !d.locked) {
        removeSelectedDrawing();
        ev.preventDefault();
      }
    }
  }

  function bindChartClick() {
    if (!chart || typeof chart.subscribeClick !== "function") return;
    if (unsubClick) {
      try {
        unsubClick();
      } catch (_) {}
      unsubClick = null;
    }
    unsubClick = chart.subscribeClick(function (param) {
      if (activeTool !== "cursor" || !param || !param.point) return;
      hideContextMenu();
      const d = pickDrawingAt(param.point.x, param.point.y);
      if (d) selectDrawing(d.id);
      else deselectDrawing();
    });
  }

  function bindMainEl() {
    if (!mainEl || mainBound) return;
    mainBound = true;
    mainEl.addEventListener("contextmenu", onMainContextMenu);
  }

  function attach(opts) {
    chart = opts.chart || null;
    series = opts.series || null;
    hostEl = opts.hostEl || null;
    mainEl = opts.mainEl || null;
    canvasEl = opts.canvasEl || null;
    toolbarEl = opts.toolbarEl || null;
    getCandlesFn = opts.getCandles || null;
    mountCanvasOverlay();
    bindToolbar();
    bindCanvas();
    bindMainEl();
    bindChartClick();
    setActiveTool("cursor");
    if (chart && chart.timeScale) {
      if (unsubRange) {
        try {
          unsubRange();
        } catch (_) {}
      }
      unsubRange = chart.timeScale().subscribeVisibleLogicalRangeChange(function () {
        scheduleRedraw();
      });
    }
    scheduleRedraw();
  }

  function setViewKey(key) {
    viewKey = key || "";
    drawings = loadDrawings();
    selectedId = null;
    dragActive = false;
    dragStartPx = null;
    pathPreviewPt = null;
    draft = null;
    scheduleRedraw();
  }

  function destroy() {
    if (unsubRange) {
      try {
        unsubRange();
      } catch (_) {}
      unsubRange = null;
    }
    if (unsubClick) {
      try {
        unsubClick();
      } catch (_) {}
      unsubClick = null;
    }
    hideContextMenu();
    setChartInteraction(true);
  }

  function setTradingDaySplit(enabled) {
    tradingDaySplitEnabled = !!enabled;
    scheduleRedraw();
  }

  window.HubChartDraw = {
    attach: attach,
    setViewKey: setViewKey,
    setTradingDaySplit: setTradingDaySplit,
    resize: scheduleRedraw,
    redraw: scheduleRedraw,
    destroy: destroy,
  };
})();
