import './style.css';

const chartPalette = ['#2563eb', '#0f766e', '#f97316', '#7c3aed', '#dc2626', '#0891b2', '#ca8a04'];
const chartState = new WeakMap();
const chartInstances = new WeakMap();
let chartJsLoader = null;

const parseData = (widget) => {
  const state = chartState.get(widget) || {};
  const raw = widget?.dataset?.chart || '';

  if (state.rawChartData === raw && state.parsedData) {
    return state.parsedData;
  }

  try {
    const parsedData = JSON.parse(raw || '{}');
    chartState.set(widget, { ...state, rawChartData: raw, parsedData });
    return parsedData;
  } catch (error) {
    chartState.set(widget, { ...state, rawChartData: raw, parsedData: null });
    return null;
  }
};

const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const easeOutCubic = (value) => 1 - ((1 - value) ** 3);
const lerp = (from, to, amount) => from + ((to - from) * amount);

const rgbaFrom = (color, alpha) => {
  if (!color) {
    return `rgba(37, 99, 235, ${alpha})`;
  }

  if (color.startsWith('rgba') || color.startsWith('rgb')) {
    return color;
  }

  const hex = color.replace('#', '');
  if (hex.length !== 6) {
    return color;
  }

  const intValue = Number.parseInt(hex, 16);
  const r = (intValue >> 16) & 255;
  const g = (intValue >> 8) & 255;
  const b = intValue & 255;
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};

const getSegmentColor = (data, index) => {
  if (data.datasets[index] && data.datasets[index].stroke) {
    return data.datasets[index].stroke;
  }

  return chartPalette[index % chartPalette.length];
};

const getHoverMix = (hoverState, key) => clamp(Number(hoverState?.[key] || 0), 0, 1);

const getDatasetHoverMix = (hoverState, datasetIndex) => {
  const prefix = `dataset-${datasetIndex}-`;
  return Object.entries(hoverState || {}).reduce((maxValue, [key, value]) => (
    key.startsWith(prefix) ? Math.max(maxValue, clamp(Number(value || 0), 0, 1)) : maxValue
  ), 0);
};

const getHoverOpacity = (data, hoverAmount, hasActiveHover) => {
  if (!hasActiveHover) {
    return 1;
  }

  const dimmedOpacity = data.hoverEmphasis === 'strong' ? 0.22 : 0.45;
  return lerp(dimmedOpacity, 1, clamp(hoverAmount, 0, 1));
};

const renderLegendItems = (widget, items) => {
  const legend = widget.querySelector('[data-legend]');
  if (!legend) {
    return;
  }

  const signature = JSON.stringify(items);
  const state = chartState.get(widget) || {};
  if (state.legendSignature === signature) {
    return;
  }

  legend.replaceChildren();
  items.forEach((item) => {
    const legendItem = document.createElement('span');
    legendItem.className = 'calio-fancy-chart-legend-item';

    const dot = document.createElement('span');
    dot.className = 'calio-fancy-chart-legend-dot';
    dot.style.setProperty('--legend-color', String(item.color || ''));

    const label = document.createElement('span');
    label.textContent = String(item.label || '');

    legendItem.append(dot, label);
    legend.appendChild(legendItem);
  });

  chartState.set(widget, { ...state, legendSignature: signature });
};

const drawLegend = (widget, data) => {
  const items = [];
  if (data.type === 'pie' || data.type === 'donut') {
    data.labels.forEach((label, index) => {
      items.push({ label, color: getSegmentColor(data, index) });
    });
  } else {
    data.datasets.forEach((dataset) => {
      items.push({ label: dataset.label, color: dataset.stroke || '#2563eb' });
    });
  }

  renderLegendItems(widget, items);
};

const getCanvasMetrics = (canvas) => {
  const rect = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  const width = Math.max(Math.round(rect.width), 1);
  const height = Math.max(Math.round(rect.height), 1);

  const context = canvas.getContext('2d');
  const targetWidth = width * ratio;
  const targetHeight = height * ratio;

  if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
    canvas.width = targetWidth;
    canvas.height = targetHeight;
  }

  context.setTransform(ratio, 0, 0, ratio, 0, 0);

  return { context, width, height };
};

const shouldUseChartJs = (data) => data && data.engine === 'chartjs';

const getWidgetState = (widget) => chartState.get(widget) || {};

const setWidgetState = (widget, nextState) => {
  const previous = getWidgetState(widget);
  chartState.set(widget, { ...previous, ...nextState });
};

const drawAxes = (ctx, chart, options) => {
  if (options.showGrid) {
    ctx.save();
    ctx.strokeStyle = options.gridColor;
    ctx.lineWidth = 1;
    for (let index = 0; index <= 4; index += 1) {
      const y = chart.top + ((chart.height / 4) * index);
      ctx.beginPath();
      ctx.moveTo(chart.left, y);
      ctx.lineTo(chart.right, y);
      ctx.stroke();
    }
    ctx.restore();
  }

  if (options.showAxes) {
    ctx.save();
    ctx.strokeStyle = options.axisColor;
    ctx.lineWidth = 1.25;
    ctx.beginPath();
    ctx.moveTo(chart.left, chart.top);
    ctx.lineTo(chart.left, chart.bottom);
    ctx.lineTo(chart.right, chart.bottom);
    ctx.stroke();
    ctx.restore();
  }
};

const drawTargetLine = (ctx, chart, options, maxValue) => {
  if (!options.showTargetLine || maxValue <= 0) {
    return;
  }

  const value = Number(options.targetValue || 0);
  if (value <= 0) {
    return;
  }

  const y = chart.bottom - ((value / maxValue) * (chart.height - 6));
  ctx.save();
  ctx.strokeStyle = options.targetColor;
  ctx.lineWidth = 1.5;
  ctx.setLineDash([6, 6]);
  ctx.beginPath();
  ctx.moveTo(chart.left, y);
  ctx.lineTo(chart.right, y);
  ctx.stroke();
  ctx.restore();

  if (options.targetLabel) {
    ctx.save();
    ctx.fillStyle = options.targetLabelColor;
    ctx.font = '12px Georgia, serif';
    ctx.textAlign = 'right';
    ctx.fillText(`${options.targetLabel}: ${value}`, chart.right, y - 8);
    ctx.restore();
  }
};

const resolveLabelIndex = (labels, match) => labels.findIndex((label) => String(label).toLowerCase() === String(match).toLowerCase());

const drawRangeHighlight = (ctx, chart, options, labels) => {
  if (!options.showRangeHighlight || !labels || labels.length === 0) {
    return;
  }

  const startIndex = resolveLabelIndex(labels, options.rangeStartLabel);
  const endIndex = resolveLabelIndex(labels, options.rangeEndLabel);
  if (startIndex < 0 || endIndex < 0) {
    return;
  }

  const safeStart = Math.min(startIndex, endIndex);
  const safeEnd = Math.max(startIndex, endIndex);
  const labelCount = Math.max(labels.length, 1);
  const step = labelCount > 1 ? chart.width / (labelCount - 1) : chart.width;
  const left = chart.left + (safeStart * step) - (step / 2);
  const right = chart.left + (safeEnd * step) + (step / 2);

  ctx.save();
  ctx.fillStyle = options.rangeColor;
  ctx.fillRect(left, chart.top, right - left, chart.height);
  ctx.restore();

  if (options.rangeLabel) {
    ctx.save();
    ctx.fillStyle = options.rangeLabelColor;
    ctx.font = '12px Georgia, serif';
    ctx.textAlign = 'left';
    ctx.fillText(options.rangeLabel, left + 8, chart.top + 16);
    ctx.restore();
  }
};

const drawEventMarkers = (ctx, chart, options, labels) => {
  if (!options.showEventMarkers || !Array.isArray(options.markers) || options.markers.length === 0) {
    return;
  }

  const labelCount = Math.max(labels.length, 1);
  const step = labelCount > 1 ? chart.width / (labelCount - 1) : chart.width;

  options.markers.forEach((marker) => {
    const index = resolveLabelIndex(labels, marker.labelMatch);
    if (index < 0) {
      return;
    }

    const x = chart.left + (index * step);
    const toneColor = marker.tone === 'success'
      ? '#15803d'
      : (marker.tone === 'warning' ? '#d97706' : options.markerColor);

    ctx.save();
    ctx.strokeStyle = toneColor;
    ctx.lineWidth = 1.5;
    ctx.setLineDash([3, 4]);
    ctx.beginPath();
    ctx.moveTo(x, chart.top);
    ctx.lineTo(x, chart.bottom);
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.fillStyle = toneColor;
    ctx.font = '12px Georgia, serif';
    ctx.textAlign = 'left';
    ctx.fillText(marker.title, x + 6, chart.top + 14);
    if (marker.note) {
      ctx.fillStyle = options.markerLabelColor;
      ctx.font = '11px Georgia, serif';
      ctx.fillText(marker.note, x + 6, chart.top + 28);
    }
    ctx.restore();
  });
};

const drawBarChart = (ctx, data, metrics, options, progress, hoverKey, hoverState) => {
  const hotspots = [];
  const chart = {
    left: 46,
    top: 18,
    right: metrics.width - 18,
    bottom: metrics.height - 34,
  };
  chart.width = chart.right - chart.left;
  chart.height = chart.bottom - chart.top;

  const values = data.datasets.flatMap((dataset) => dataset.values);
  const maxValue = Math.max(...values, 1);
  const groupCount = data.labels.length || 1;
  const seriesCount = data.datasets.length || 1;
  const groupWidth = chart.width / groupCount;
  const innerGap = 8;
  const barWidth = Math.max((groupWidth - innerGap) / seriesCount, 10);
  const hasActiveHover = typeof hoverKey === 'string';

  drawRangeHighlight(ctx, chart, options, data.labels);
  drawAxes(ctx, chart, options);
  drawTargetLine(ctx, chart, options, maxValue);
  drawEventMarkers(ctx, chart, options, data.labels);

  ctx.save();
  ctx.font = '12px Georgia, serif';
  ctx.fillStyle = options.valueColor;
  ctx.textAlign = 'center';

  data.labels.forEach((label, labelIndex) => {
    const baseX = chart.left + (groupWidth * labelIndex);
    const labelX = baseX + (groupWidth / 2);
    ctx.globalAlpha = 1;
    ctx.fillText(label, labelX, metrics.height - 10);

    data.datasets.forEach((dataset, datasetIndex) => {
      const key = `dataset-${datasetIndex}-label-${labelIndex}`;
      const value = Number(dataset.values[labelIndex] || 0);
      const barHeight = clamp((value / maxValue) * (chart.height - 6) * progress, 0, chart.height - 6);
      const x = baseX + 4 + (datasetIndex * barWidth);
      const y = chart.bottom - barHeight;
      const width = Math.max(barWidth - 6, 6);
      const hoverAmount = getHoverMix(hoverState, key);
      const hoverLift = hoverAmount * 8;
      const hoverExpand = hoverAmount * 4;
      const drawX = x - (hoverExpand / 2);
      const drawY = y - hoverLift;
      const drawWidth = width + hoverExpand;
      const drawHeight = barHeight + hoverLift;

      ctx.save();
      ctx.globalAlpha = getHoverOpacity(data, hoverAmount, hasActiveHover);
      ctx.fillStyle = dataset.stroke || '#2563eb';
      ctx.beginPath();
      ctx.roundRect(drawX, drawY, drawWidth, drawHeight, 8 + (hoverAmount * 2));
      ctx.fill();
      ctx.restore();

      if (options.showValues) {
        ctx.save();
        ctx.globalAlpha = getHoverOpacity(data, hoverAmount, hasActiveHover);
        ctx.fillStyle = options.valueColor;
        ctx.fillText(String(value), drawX + (drawWidth / 2), drawY - 8);
        ctx.restore();
      }

      hotspots.push({
        key,
        x: drawX,
        y: drawY,
        width: drawWidth,
        height: drawHeight,
        label: `${dataset.label}: ${value}`,
        color: dataset.stroke || '#2563eb',
      });
    });
  });

  ctx.restore();
  return hotspots;
};

const drawLineLikeChart = (ctx, data, metrics, options, progress, hoverKey, hoverState, isArea) => {
  const hotspots = [];
  const chart = {
    left: 36,
    top: 18,
    right: metrics.width - 18,
    bottom: metrics.height - 34,
  };
  chart.width = chart.right - chart.left;
  chart.height = chart.bottom - chart.top;

  const values = data.datasets.flatMap((dataset) => dataset.values);
  const maxValue = Math.max(...values, 1);
  const xStep = data.labels.length > 1 ? chart.width / (data.labels.length - 1) : chart.width;
  const hasActiveHover = typeof hoverKey === 'string';

  drawRangeHighlight(ctx, chart, options, data.labels);
  drawAxes(ctx, chart, options);
  drawTargetLine(ctx, chart, options, maxValue);
  drawEventMarkers(ctx, chart, options, data.labels);

  ctx.save();
  ctx.font = '12px Georgia, serif';
  ctx.fillStyle = options.valueColor;
  ctx.textAlign = 'center';
  data.labels.forEach((label, index) => {
    ctx.fillText(label, chart.left + (index * xStep), metrics.height - 10);
  });
  ctx.restore();

  data.datasets.forEach((dataset, datasetIndex) => {
    const points = data.labels.map((label, index) => {
      const value = Number(dataset.values[index] || 0);
      const x = chart.left + (index * xStep);
      const targetY = chart.bottom - ((value / maxValue) * (chart.height - 6));
      const y = chart.bottom - ((chart.bottom - targetY) * progress);
      return { x, y, value, key: `dataset-${datasetIndex}-label-${index}` };
    });
    const datasetHoverAmount = getDatasetHoverMix(hoverState, datasetIndex);

    if (isArea) {
      ctx.save();
      ctx.globalAlpha = getHoverOpacity(data, datasetHoverAmount, hasActiveHover);
      ctx.beginPath();
      points.forEach((point, index) => {
        if (index === 0) {
          ctx.moveTo(point.x, point.y);
        } else {
          ctx.lineTo(point.x, point.y);
        }
      });
      ctx.lineTo(points[points.length - 1].x, chart.bottom);
      ctx.lineTo(points[0].x, chart.bottom);
      ctx.closePath();
      ctx.fillStyle = dataset.fill || rgbaFrom(dataset.stroke, 0.2);
      ctx.fill();
      ctx.restore();
    }

    ctx.save();
    ctx.globalAlpha = getHoverOpacity(data, datasetHoverAmount, hasActiveHover);
    ctx.beginPath();
    points.forEach((point, index) => {
      if (index === 0) {
        ctx.moveTo(point.x, point.y);
      } else {
        ctx.lineTo(point.x, point.y);
      }
    });
    ctx.lineWidth = lerp(3, 4.5, datasetHoverAmount);
    ctx.strokeStyle = dataset.stroke || '#2563eb';
    ctx.stroke();
    ctx.restore();

    if (options.showPoints) {
      points.forEach((point) => {
        const pointHoverAmount = getHoverMix(hoverState, point.key);
        const visualHover = Math.max(pointHoverAmount, datasetHoverAmount * 0.65);
        ctx.save();
        ctx.globalAlpha = getHoverOpacity(data, visualHover, hasActiveHover);
        ctx.beginPath();
        ctx.arc(point.x, point.y, lerp(4.5, 6.8, visualHover), 0, Math.PI * 2);
        ctx.fillStyle = dataset.point || '#ffffff';
        ctx.fill();
        ctx.lineWidth = lerp(2, 2.8, visualHover);
        ctx.strokeStyle = dataset.stroke || '#2563eb';
        ctx.stroke();
        ctx.restore();

        if (options.showValues) {
          ctx.save();
          ctx.globalAlpha = getHoverOpacity(data, visualHover, hasActiveHover);
          ctx.fillStyle = options.valueColor;
          ctx.font = '12px Georgia, serif';
          ctx.textAlign = 'center';
          ctx.fillText(String(point.value), point.x, point.y - (12 + (visualHover * 3)));
          ctx.restore();
        }

        hotspots.push({
          key: point.key,
          x: point.x,
          y: point.y,
          radius: 12,
          label: `${dataset.label}: ${point.value}`,
          color: dataset.stroke || '#2563eb',
        });
      });
    }
  });

  return hotspots;
};

const drawMixedChart = (ctx, data, metrics, options, progress, hoverKey, hoverState) => {
  const barDatasets = data.datasets.filter((dataset) => (dataset.renderAs || 'auto') !== 'line');
  const lineDatasets = data.datasets.filter((dataset) => (dataset.renderAs || 'auto') === 'line');
  const mixedData = { ...data };
  let hotspots = [];

  if (barDatasets.length > 0) {
    hotspots = hotspots.concat(drawBarChart(ctx, { ...mixedData, datasets: barDatasets }, metrics, options, progress, hoverKey, hoverState));
  } else {
    const chart = {
      left: 46,
      top: 18,
      right: metrics.width - 18,
      bottom: metrics.height - 34,
    };
    chart.width = chart.right - chart.left;
    chart.height = chart.bottom - chart.top;
    drawAxes(ctx, chart, options);
  }

  if (lineDatasets.length > 0) {
    hotspots = hotspots.concat(drawLineLikeChart(ctx, { ...mixedData, datasets: lineDatasets }, metrics, options, progress, hoverKey, hoverState, false));
  }

  return hotspots;
};

const drawRadarChart = (ctx, data, metrics, options, progress, hoverKey, hoverState) => {
  const hotspots = [];
  const centerX = metrics.width / 2;
  const centerY = metrics.height / 2;
  const radius = Math.min(metrics.width, metrics.height) * 0.34;
  const stepCount = Math.max(data.labels.length, 3);
  const angleStep = (Math.PI * 2) / stepCount;
  const values = data.datasets.flatMap((dataset) => dataset.values);
  const maxValue = Math.max(...values, 1);
  const hasActiveHover = typeof hoverKey === 'string';

  if (options.showGrid) {
    ctx.save();
    ctx.strokeStyle = options.gridColor;
    ctx.lineWidth = 1;
    for (let ring = 1; ring <= 5; ring += 1) {
      const ringRadius = (radius / 5) * ring;
      ctx.beginPath();
      data.labels.forEach((label, index) => {
        const angle = (-Math.PI / 2) + (angleStep * index);
        const x = centerX + (Math.cos(angle) * ringRadius);
        const y = centerY + (Math.sin(angle) * ringRadius);
        if (index === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      });
      ctx.closePath();
      ctx.stroke();
    }
    ctx.restore();
  }

  if (options.showAxes) {
    ctx.save();
    ctx.strokeStyle = options.axisColor;
    data.labels.forEach((label, index) => {
      const angle = (-Math.PI / 2) + (angleStep * index);
      ctx.beginPath();
      ctx.moveTo(centerX, centerY);
      ctx.lineTo(centerX + (Math.cos(angle) * radius), centerY + (Math.sin(angle) * radius));
      ctx.stroke();
    });
    ctx.restore();
  }

  ctx.save();
  ctx.font = '12px Georgia, serif';
  ctx.fillStyle = options.valueColor;
  ctx.textAlign = 'center';
  data.labels.forEach((label, index) => {
    const angle = (-Math.PI / 2) + (angleStep * index);
    const labelX = centerX + (Math.cos(angle) * (radius + 18));
    const labelY = centerY + (Math.sin(angle) * (radius + 18));
    ctx.fillText(label, labelX, labelY);
  });
  ctx.restore();

  data.datasets.forEach((dataset, datasetIndex) => {
    const datasetHoverAmount = getDatasetHoverMix(hoverState, datasetIndex);
    const points = data.labels.map((label, index) => {
      const value = Number(dataset.values[index] || 0);
      const angle = (-Math.PI / 2) + (angleStep * index);
      const pointRadius = ((value / maxValue) * radius) * progress;
      const x = centerX + (Math.cos(angle) * pointRadius);
      const y = centerY + (Math.sin(angle) * pointRadius);
      return { x, y, value, key: `dataset-${datasetIndex}-label-${index}` };
    });

    ctx.save();
    ctx.globalAlpha = getHoverOpacity(data, datasetHoverAmount, hasActiveHover);
    ctx.beginPath();
    points.forEach((point, index) => {
      if (index === 0) {
        ctx.moveTo(point.x, point.y);
      } else {
        ctx.lineTo(point.x, point.y);
      }
    });
    ctx.closePath();
    ctx.fillStyle = dataset.fill || rgbaFrom(dataset.stroke, 0.16);
    ctx.fill();
    ctx.lineWidth = lerp(2.5, 3.8, datasetHoverAmount);
    ctx.strokeStyle = dataset.stroke || '#2563eb';
    ctx.stroke();
    ctx.restore();

    if (options.showPoints) {
      points.forEach((point) => {
        const pointHoverAmount = getHoverMix(hoverState, point.key);
        const visualHover = Math.max(pointHoverAmount, datasetHoverAmount * 0.65);
        ctx.save();
        ctx.globalAlpha = getHoverOpacity(data, visualHover, hasActiveHover);
        ctx.beginPath();
        ctx.arc(point.x, point.y, lerp(4.5, 6.8, visualHover), 0, Math.PI * 2);
        ctx.fillStyle = dataset.point || '#ffffff';
        ctx.fill();
        ctx.lineWidth = lerp(2, 2.8, visualHover);
        ctx.strokeStyle = dataset.stroke || '#2563eb';
        ctx.stroke();
        ctx.restore();

        if (options.showValues) {
          ctx.save();
          ctx.globalAlpha = getHoverOpacity(data, visualHover, hasActiveHover);
          ctx.fillStyle = options.valueColor;
          ctx.font = '12px Georgia, serif';
          ctx.textAlign = 'center';
          ctx.fillText(String(point.value), point.x, point.y - (12 + (visualHover * 3)));
          ctx.restore();
        }

        hotspots.push({
          key: point.key,
          x: point.x,
          y: point.y,
          radius: 12,
          label: `${dataset.label}: ${point.value}`,
          color: dataset.stroke || '#2563eb',
        });
      });
    }
  });

  return hotspots;
};

const drawPolarAreaChart = (ctx, data, metrics, options, progress, hoverKey, hoverState) => {
  const hotspots = [];
  const source = data.datasets[0] || { values: [], stroke: '#2563eb' };
  const values = data.labels.map((label, index) => Number(source.values[index] || 0));
  const maxValue = Math.max(...values, 1);
  const centerX = metrics.width / 2;
  const centerY = metrics.height / 2;
  const radius = Math.min(metrics.width, metrics.height) * 0.36;
  const angleStep = (Math.PI * 2) / Math.max(data.labels.length, 1);
  const hasActiveHover = typeof hoverKey === 'string';
  let startAngle = -Math.PI / 2;

  if (options.showGrid) {
    ctx.save();
    ctx.strokeStyle = options.gridColor;
    for (let ring = 1; ring <= 4; ring += 1) {
      ctx.beginPath();
      ctx.arc(centerX, centerY, (radius / 4) * ring, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.restore();
  }

  values.forEach((value, index) => {
    const key = `segment-${index}`;
    const endAngle = startAngle + angleStep;
    const segmentRadius = ((value / maxValue) * radius) * progress;
    const color = getSegmentColor(data, index);
    const hoverAmount = getHoverMix(hoverState, key);

    ctx.save();
    ctx.globalAlpha = getHoverOpacity(data, hoverAmount, hasActiveHover);
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, segmentRadius + (hoverAmount * 8), startAngle, endAngle);
    ctx.closePath();
    ctx.fillStyle = color;
    ctx.fill();
    ctx.restore();

    if (options.showValues) {
      const midAngle = startAngle + (angleStep / 2);
      const labelRadius = segmentRadius + 18 + (hoverAmount * 4);
      ctx.save();
      ctx.fillStyle = options.valueColor;
      ctx.font = '12px Georgia, serif';
      ctx.textAlign = 'center';
      ctx.fillText(String(value), centerX + (Math.cos(midAngle) * labelRadius), centerY + (Math.sin(midAngle) * labelRadius));
      ctx.restore();
    }

    hotspots.push({
      key,
      centerX,
      centerY,
      radius: segmentRadius + 8,
      startAngle,
      endAngle,
      label: `${data.labels[index] || `Item ${index + 1}`}: ${value}`,
      color,
    });

    startAngle = endAngle;
  });

  return hotspots;
};

const drawPieLikeChart = (ctx, data, metrics, options, progress, hoverKey, hoverState, isDonut) => {
  const hotspots = [];
  const source = data.datasets[0] || { values: [], stroke: '#2563eb' };
  const values = data.labels.map((label, index) => Number(source.values[index] || 0));
  const total = values.reduce((sum, value) => sum + value, 0) || 1;
  const centerX = metrics.width / 2;
  const centerY = metrics.height / 2;
  const radius = Math.min(metrics.width, metrics.height) * (isDonut ? 0.29 : 0.34);
  const hasActiveHover = typeof hoverKey === 'string';
  let startAngle = -Math.PI / 2;

  values.forEach((value, index) => {
    const key = `segment-${index}`;
    const angle = ((value / total) * Math.PI * 2) * progress;
    const endAngle = startAngle + angle;
    const color = getSegmentColor(data, index) || source.stroke || '#2563eb';
    const hoverAmount = getHoverMix(hoverState, key);
    const outerRadius = radius + (hoverAmount * 8);

    ctx.save();
    ctx.globalAlpha = getHoverOpacity(data, hoverAmount, hasActiveHover);
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle);
    ctx.closePath();
    ctx.fillStyle = color;
    ctx.fill();
    ctx.restore();

    if (options.showValues && progress > 0.85) {
      const midAngle = startAngle + (angle / 2);
      const labelRadius = outerRadius * lerp(0.72, 0.78, hoverAmount);
      ctx.save();
      ctx.fillStyle = '#ffffff';
      ctx.font = '12px Georgia, serif';
      ctx.textAlign = 'center';
      ctx.fillText(String(value), centerX + (Math.cos(midAngle) * labelRadius), centerY + (Math.sin(midAngle) * labelRadius));
      ctx.restore();
    }

    hotspots.push({
      key,
      centerX,
      centerY,
      radius: outerRadius,
      startAngle,
      endAngle,
      label: `${data.labels[index] || `Item ${index + 1}`}: ${value}`,
      color,
    });

    startAngle = endAngle;
  });

  if (isDonut) {
    ctx.beginPath();
    ctx.arc(centerX, centerY, radius * 0.56, 0, Math.PI * 2);
    ctx.fillStyle = '#ffffff';
    ctx.fill();
  }

  return hotspots;
};

const getHoverHit = (hotspots, pointer, type) => {
  if (!pointer) {
    return null;
  }

  if (type === 'pie' || type === 'donut') {
    return hotspots.find((hotspot) => {
      const dx = pointer.x - hotspot.centerX;
      const dy = pointer.y - hotspot.centerY;
      const distance = Math.sqrt((dx * dx) + (dy * dy));
      let angle = Math.atan2(dy, dx);
      if (angle < -Math.PI / 2) {
        angle += Math.PI * 2;
      }

      return distance <= hotspot.radius && angle >= hotspot.startAngle && angle <= hotspot.endAngle;
    }) || null;
  }

  return hotspots.find((hotspot) => {
    if (typeof hotspot.radius === 'number') {
      const dx = pointer.x - hotspot.x;
      const dy = pointer.y - hotspot.y;
      return Math.sqrt((dx * dx) + (dy * dy)) <= hotspot.radius;
    }

    return pointer.x >= hotspot.x &&
      pointer.x <= hotspot.x + hotspot.width &&
      pointer.y >= hotspot.y &&
      pointer.y <= hotspot.y + hotspot.height;
  }) || null;
};

const renderTooltip = (widget, hit, pointer) => {
  const tooltip = widget.querySelector('[data-tooltip]');
  if (!tooltip) {
    return;
  }

  const state = getWidgetState(widget);
  if (state.tooltipHideTimer) {
    clearTimeout(state.tooltipHideTimer);
    setWidgetState(widget, { tooltipHideTimer: null });
  }

  if (!hit || !pointer) {
    tooltip.classList.remove('is-visible');
    const tooltipHideTimer = window.setTimeout(() => {
      tooltip.hidden = true;
      setWidgetState(widget, { tooltipHideTimer: null });
    }, 140);
    setWidgetState(widget, { tooltipHideTimer });
    return;
  }

  const tooltipSignature = `${hit.color}|${hit.label}`;
  if (state.tooltipSignature !== tooltipSignature) {
    const dot = document.createElement('span');
    dot.className = 'calio-fancy-chart-tooltip-dot';
    dot.style.setProperty('--tooltip-color', String(hit.color || ''));

    const label = document.createElement('span');
    label.textContent = String(hit.label || '');

    tooltip.replaceChildren(dot, label);
    setWidgetState(widget, { tooltipSignature });
  }

  const currentX = typeof state.tooltipX === 'number' ? state.tooltipX : (pointer.x + 18);
  const currentY = typeof state.tooltipY === 'number' ? state.tooltipY : (pointer.y + 18);
  const nextX = lerp(currentX, pointer.x + 18, 0.32);
  const nextY = lerp(currentY, pointer.y + 18, 0.32);

  tooltip.hidden = false;
  tooltip.classList.add('is-visible');
  tooltip.style.transform = `translate3d(${nextX}px, ${nextY}px, 0)`;
  setWidgetState(widget, { tooltipX: nextX, tooltipY: nextY });
};

const loadChartJs = async () => {
  if (!chartJsLoader) {
    chartJsLoader = import('chart.js').then((module) => {
      const {
        ArcElement,
        BarController,
        BarElement,
        CategoryScale,
        Chart,
        Filler,
        Legend,
        LineController,
        LineElement,
        PolarAreaController,
        LinearScale,
        PointElement,
        RadarController,
        RadialLinearScale,
        Tooltip,
      } = module;

      Chart.register(
        ArcElement,
        BarController,
        BarElement,
        CategoryScale,
        Filler,
        Legend,
        LineController,
        LineElement,
        PolarAreaController,
        LinearScale,
        PointElement,
        RadarController,
        RadialLinearScale,
        Tooltip
      );

      return Chart;
    });
  }

  return chartJsLoader;
};

const renderCustomChart = (widget, progress = 1, pointer = null) => {
  const data = parseData(widget);
  const canvas = widget.querySelector('.calio-fancy-chart-canvas');
  if (!data || !canvas || !Array.isArray(data.labels) || !Array.isArray(data.datasets) || data.datasets.length === 0) {
    return;
  }

  const rect = canvas.getBoundingClientRect();
  if (rect.width < 2 || rect.height < 2) {
    return;
  }

  const existing = chartInstances.get(widget);
  if (existing) {
    existing.destroy();
    chartInstances.delete(widget);
  }

  const styles = getComputedStyle(widget.querySelector('.calio-fancy-chart-canvas-wrap') || widget);
  const options = {
    showGrid: Boolean(data.showGrid) && !['pie', 'donut'].includes(data.type),
    showAxes: Boolean(data.showAxes) && !['pie', 'donut', 'polar'].includes(data.type),
    showPoints: Boolean(data.showPoints),
    showValues: Boolean(data.showValues),
    showTargetLine: Boolean(data.showTargetLine) && ['bar', 'line', 'area', 'mixed'].includes(data.type),
    targetValue: Number(data.targetValue || 0),
    targetLabel: data.targetLabel || 'Target',
    showRangeHighlight: Boolean(data.showRangeHighlight) && ['bar', 'line', 'area', 'mixed'].includes(data.type),
    rangeStartLabel: data.rangeStartLabel || '',
    rangeEndLabel: data.rangeEndLabel || '',
    rangeLabel: data.rangeLabel || '',
    showEventMarkers: Boolean(data.showEventMarkers) && ['bar', 'line', 'area', 'mixed'].includes(data.type),
    markers: Array.isArray(data.markers) ? data.markers : [],
    gridColor: styles.getPropertyValue('--calio-chart-grid').trim() || 'rgba(148, 163, 184, 0.18)',
    axisColor: styles.getPropertyValue('--calio-chart-axis').trim() || 'rgba(148, 163, 184, 0.55)',
    valueColor: styles.getPropertyValue('--calio-chart-value').trim() || '#0f172a',
    targetColor: styles.getPropertyValue('--calio-chart-target').trim() || '#dc2626',
    targetLabelColor: styles.getPropertyValue('--calio-chart-target-label').trim() || '#dc2626',
    rangeColor: styles.getPropertyValue('--calio-chart-range').trim() || 'rgba(37, 99, 235, 0.08)',
    rangeLabelColor: styles.getPropertyValue('--calio-chart-range-label').trim() || '#2563eb',
    markerColor: styles.getPropertyValue('--calio-chart-marker').trim() || '#0f172a',
    markerLabelColor: styles.getPropertyValue('--calio-chart-marker-label').trim() || '#0f172a',
  };

  const metrics = getCanvasMetrics(canvas);
  const ctx = metrics.context;
  ctx.clearRect(0, 0, metrics.width, metrics.height);

  let hotspots = [];
  const activeState = getWidgetState(widget);
  const hoverKey = activeState.hoverKey || null;
  const hoverState = activeState.hoverMix || {};

  if (data.type === 'bar') {
    hotspots = drawBarChart(ctx, data, metrics, options, progress, hoverKey, hoverState);
  } else if (data.type === 'line') {
    hotspots = drawLineLikeChart(ctx, data, metrics, options, progress, hoverKey, hoverState, false);
  } else if (data.type === 'area') {
    hotspots = drawLineLikeChart(ctx, data, metrics, options, progress, hoverKey, hoverState, true);
  } else if (data.type === 'mixed') {
    hotspots = drawMixedChart(ctx, data, metrics, options, progress, hoverKey, hoverState);
  } else if (data.type === 'pie') {
    hotspots = drawPieLikeChart(ctx, data, metrics, options, progress, hoverKey, hoverState, false);
  } else if (data.type === 'donut') {
    hotspots = drawPieLikeChart(ctx, data, metrics, options, progress, hoverKey, hoverState, true);
  } else if (data.type === 'radar') {
    hotspots = drawRadarChart(ctx, data, metrics, options, progress, hoverKey, hoverState);
  } else if (data.type === 'polar') {
    hotspots = drawPolarAreaChart(ctx, data, metrics, options, progress, hoverKey, hoverState);
  }

  drawLegend(widget, data);

  const currentHit = getHoverHit(hotspots, pointer, data.type);
  const { nextHoverMix, needsAnimationFrame } = updateHoverMixState(hotspots, hoverState, currentHit ? currentHit.key : null);
  const nextState = {
    hotspots,
    hoverKey: currentHit ? currentHit.key : null,
    hoverMix: nextHoverMix,
    pointer,
    lastProgress: progress,
  };
  setWidgetState(widget, nextState);

  if (currentHit && activeState.hoverKey !== nextState.hoverKey) {
    scheduleRender(widget, progress, pointer);
    return;
  }

  renderTooltip(widget, data.showTooltip ? currentHit : null, pointer);

  if (needsAnimationFrame) {
    scheduleRender(widget, progress, pointer);
  }
};

const buildChartJsData = (data) => {
  if (data.type === 'pie' || data.type === 'donut' || data.type === 'polar') {
    const source = data.datasets[0] || { values: [], label: 'Series' };

    return {
      labels: data.labels,
      datasets: [{
        label: source.label || 'Series',
        data: data.labels.map((label, index) => Number(source.values[index] || 0)),
        backgroundColor: data.labels.map((label, index) => getSegmentColor(data, index)),
        borderColor: '#ffffff',
        borderWidth: 2,
        hoverOffset: data.hoverEmphasis === 'strong' ? 18 : 10,
      }],
    };
  }

  return {
    labels: data.labels,
    datasets: data.datasets.map((dataset) => ({
      type: data.type === 'mixed'
        ? (((dataset.renderAs || 'auto') === 'line') ? 'line' : 'bar')
        : undefined,
      label: dataset.label,
      data: data.labels.map((label, index) => Number(dataset.values[index] || 0)),
      borderColor: dataset.stroke || '#2563eb',
      backgroundColor: (data.type === 'bar' || (data.type === 'mixed' && (dataset.renderAs || 'auto') !== 'line'))
        ? (dataset.fill || rgbaFrom(dataset.stroke, 0.75))
        : (dataset.fill || rgbaFrom(dataset.stroke, 0.18)),
      pointBackgroundColor: dataset.point || '#ffffff',
      pointBorderColor: dataset.stroke || '#2563eb',
      pointRadius: data.showPoints ? 4 : 0,
      pointHoverRadius: data.hoverEmphasis === 'strong' ? 7 : 6,
      borderWidth: data.hoverEmphasis === 'strong' ? 3 : 2.5,
      tension: 0.36,
      fill: data.type === 'area' || data.type === 'radar',
      hoverBackgroundColor: dataset.stroke || '#2563eb',
    })),
  };
};

const createChartJsPlugins = (widget, data) => {
  const htmlLegend = {
    id: 'calioHtmlLegend',
    afterUpdate(chart) {
      if (data.legendPosition === 'hidden') {
        renderLegendItems(widget, []);
        return;
      }

      if (data.type === 'pie' || data.type === 'donut' || data.type === 'polar') {
        const colors = chart.data.datasets[0]?.backgroundColor || [];
        renderLegendItems(widget, chart.data.labels.map((label, index) => ({ label, color: colors[index] || getSegmentColor(data, index) })));
        return;
      }

      renderLegendItems(widget, chart.data.datasets.map((dataset) => ({ label: dataset.label, color: dataset.borderColor || '#2563eb' })));
    },
  };

  const valueLabels = {
    id: 'calioValueLabels',
    afterDatasetsDraw(chart) {
      if (!data.showValues) {
        return;
      }

      const { ctx } = chart;
      ctx.save();
      ctx.font = '12px Georgia, serif';
      ctx.fillStyle = chart.options.plugins.calioValueColor || '#0f172a';
      ctx.textAlign = 'center';

      chart.data.datasets.forEach((dataset, datasetIndex) => {
        const meta = chart.getDatasetMeta(datasetIndex);
        meta.data.forEach((element, index) => {
          const rawValue = dataset.data[index];
          if (rawValue === null || rawValue === undefined) {
            return;
          }

          if (data.type === 'pie' || data.type === 'donut' || data.type === 'polar') {
            const position = element.tooltipPosition();
            ctx.fillStyle = '#ffffff';
            ctx.fillText(String(rawValue), position.x, position.y);
          } else {
            const position = element.tooltipPosition();
            ctx.fillStyle = chart.options.plugins.calioValueColor || '#0f172a';
            ctx.fillText(String(rawValue), position.x, position.y - 12);
          }
        });
      });

      ctx.restore();
    },
  };

  const targetLine = {
    id: 'calioTargetLine',
    afterDraw(chart) {
      if (!data.showTargetLine || !['bar', 'line', 'area', 'mixed'].includes(data.type)) {
        return;
      }

      const yScale = chart.scales.y;
      if (!yScale) {
        return;
      }

      const targetValue = Number(data.targetValue || 0);
      if (targetValue <= 0) {
        return;
      }

      const y = yScale.getPixelForValue(targetValue);
      const { ctx, chartArea } = chart;

      ctx.save();
      ctx.strokeStyle = chart.options.plugins.calioTargetColor || '#dc2626';
      ctx.lineWidth = 1.5;
      ctx.setLineDash([6, 6]);
      ctx.beginPath();
      ctx.moveTo(chartArea.left, y);
      ctx.lineTo(chartArea.right, y);
      ctx.stroke();
      ctx.restore();

      if (data.targetLabel) {
        ctx.save();
        ctx.fillStyle = chart.options.plugins.calioTargetLabelColor || '#dc2626';
        ctx.font = '12px Georgia, serif';
        ctx.textAlign = 'right';
        ctx.fillText(`${data.targetLabel}: ${targetValue}`, chartArea.right, y - 8);
        ctx.restore();
      }
    },
  };

  const rangesAndMarkers = {
    id: 'calioRangesAndMarkers',
    afterDraw(chart) {
      if (!['bar', 'line', 'area', 'mixed'].includes(data.type)) {
        return;
      }

      const xScale = chart.scales.x;
      const yScale = chart.scales.y;
      if (!xScale || !yScale) {
        return;
      }

      const { ctx, chartArea } = chart;

      if (data.showRangeHighlight && data.rangeStartLabel && data.rangeEndLabel) {
        const startIndex = resolveLabelIndex(data.labels, data.rangeStartLabel);
        const endIndex = resolveLabelIndex(data.labels, data.rangeEndLabel);
        if (startIndex >= 0 && endIndex >= 0) {
          const safeStart = Math.min(startIndex, endIndex);
          const safeEnd = Math.max(startIndex, endIndex);
          const left = xScale.getPixelForValue(safeStart) - ((xScale.getPixelForValue(Math.min(safeStart + 1, data.labels.length - 1)) - xScale.getPixelForValue(safeStart)) / 2 || 20);
          const right = xScale.getPixelForValue(safeEnd) + ((xScale.getPixelForValue(safeEnd) - xScale.getPixelForValue(Math.max(safeEnd - 1, 0))) / 2 || 20);

          ctx.save();
          ctx.fillStyle = chart.options.plugins.calioRangeColor || 'rgba(37, 99, 235, 0.08)';
          ctx.fillRect(left, chartArea.top, right - left, chartArea.bottom - chartArea.top);
          if (data.rangeLabel) {
            ctx.fillStyle = chart.options.plugins.calioRangeLabelColor || '#2563eb';
            ctx.font = '12px Georgia, serif';
            ctx.textAlign = 'left';
            ctx.fillText(data.rangeLabel, left + 8, chartArea.top + 16);
          }
          ctx.restore();
        }
      }

      if (data.showEventMarkers && Array.isArray(data.markers)) {
        data.markers.forEach((marker) => {
          const index = resolveLabelIndex(data.labels, marker.labelMatch);
          if (index < 0) {
            return;
          }

          const x = xScale.getPixelForValue(index);
          const toneColor = marker.tone === 'success'
            ? '#15803d'
            : (marker.tone === 'warning' ? '#d97706' : (chart.options.plugins.calioMarkerColor || '#0f172a'));

          ctx.save();
          ctx.strokeStyle = toneColor;
          ctx.lineWidth = 1.5;
          ctx.setLineDash([3, 4]);
          ctx.beginPath();
          ctx.moveTo(x, chartArea.top);
          ctx.lineTo(x, chartArea.bottom);
          ctx.stroke();
          ctx.fillStyle = toneColor;
          ctx.font = '12px Georgia, serif';
          ctx.textAlign = 'left';
          ctx.fillText(marker.title, x + 6, chartArea.top + 14);
          if (marker.note) {
            ctx.fillStyle = chart.options.plugins.calioMarkerLabelColor || '#0f172a';
            ctx.font = '11px Georgia, serif';
            ctx.fillText(marker.note, x + 6, chartArea.top + 28);
          }
          ctx.restore();
        });
      }
    },
  };

  return [htmlLegend, valueLabels, targetLine, rangesAndMarkers];
};

const renderChartJs = async (widget) => {
  const data = parseData(widget);
  const canvas = widget.querySelector('.calio-fancy-chart-canvas');
  if (!data || !canvas) {
    return;
  }

  const rect = canvas.getBoundingClientRect();
  if (rect.width < 2 || rect.height < 2) {
    return;
  }

  const state = getWidgetState(widget);
  renderTooltip(widget, null, null);
  setWidgetState(widget, { ...state, hoverKey: null, pointer: null, lastProgress: 1 });

  const existing = chartInstances.get(widget);
  if (existing) {
    existing.destroy();
  }

  const Chart = await loadChartJs();

  const wrap = widget.querySelector('.calio-fancy-chart-canvas-wrap') || widget;
  const styles = getComputedStyle(wrap);
  const chartType = data.type === 'donut'
    ? 'doughnut'
    : (data.type === 'area'
      ? 'line'
      : (data.type === 'polar'
        ? 'polarArea'
        : (data.type === 'mixed' ? 'bar' : data.type)));
  const sharedValueColor = styles.getPropertyValue('--calio-chart-value').trim() || '#0f172a';
  const sharedAxisColor = styles.getPropertyValue('--calio-chart-axis').trim() || 'rgba(148, 163, 184, 0.55)';
  const sharedGridColor = styles.getPropertyValue('--calio-chart-grid').trim() || 'rgba(148, 163, 184, 0.18)';
  const sharedTargetColor = styles.getPropertyValue('--calio-chart-target').trim() || '#dc2626';
  const sharedTargetLabelColor = styles.getPropertyValue('--calio-chart-target-label').trim() || '#dc2626';
  const scales = (data.type === 'pie' || data.type === 'donut' || data.type === 'polar')
    ? {}
    : (data.type === 'radar'
      ? {
        r: {
          beginAtZero: true,
          ticks: {
            display: false,
            color: sharedValueColor,
          },
          angleLines: {
            color: sharedAxisColor,
            display: Boolean(data.showAxes),
          },
          grid: {
            color: sharedGridColor,
            display: Boolean(data.showGrid),
          },
          pointLabels: {
            color: sharedValueColor,
            font: { size: 12 },
          },
        },
      }
      : {
        x: {
          display: Boolean(data.showAxes),
          grid: { display: false },
          ticks: { color: sharedValueColor },
          border: { color: sharedAxisColor },
        },
        y: {
          display: Boolean(data.showAxes),
          beginAtZero: true,
          grid: {
            display: Boolean(data.showGrid),
            color: sharedGridColor,
          },
          ticks: { color: sharedValueColor },
          border: { color: sharedAxisColor },
        },
      });
  const config = {
    type: chartType,
    data: buildChartJsData(data),
    options: {
      responsive: true,
      maintainAspectRatio: false,
      animation: {
        duration: data.animate ? Math.max(200, Number(data.animationDuration || 900)) : 0,
      },
      interaction: {
        mode: (data.type === 'pie' || data.type === 'donut') ? 'nearest' : 'index',
        intersect: data.type === 'bar',
      },
      plugins: {
        legend: { display: false },
        tooltip: { enabled: Boolean(data.showTooltip) },
        calioValueColor: sharedValueColor,
        calioTargetColor: sharedTargetColor,
        calioTargetLabelColor: sharedTargetLabelColor,
        calioRangeColor: styles.getPropertyValue('--calio-chart-range').trim() || 'rgba(37, 99, 235, 0.08)',
        calioRangeLabelColor: styles.getPropertyValue('--calio-chart-range-label').trim() || '#2563eb',
        calioMarkerColor: styles.getPropertyValue('--calio-chart-marker').trim() || '#0f172a',
        calioMarkerLabelColor: styles.getPropertyValue('--calio-chart-marker-label').trim() || '#0f172a',
      },
      scales,
      elements: data.type === 'radar' ? {
        line: { borderWidth: 2.5 },
      } : {},
      cutout: data.type === 'donut' ? '56%' : undefined,
    },
    plugins: createChartJsPlugins(widget, data),
  };

  const instance = new Chart(canvas.getContext('2d'), config);
  chartInstances.set(widget, instance);
};

const renderEngine = async (widget, progress = 1, pointer = null) => {
  const data = parseData(widget);
  if (!data) {
    return;
  }

  if (shouldUseChartJs(data)) {
    await renderChartJs(widget);
    return;
  }

  renderCustomChart(widget, progress, pointer);
};

const scheduleRender = (widget, progress = null, pointer = null) => {
  const state = getWidgetState(widget);
  const nextProgress = progress ?? state.lastProgress ?? 1;
  const nextPointer = pointer === undefined ? (state.pointer ?? null) : pointer;

  if (state.renderFrame) {
    cancelAnimationFrame(state.renderFrame);
  }

  const renderFrame = requestAnimationFrame(() => {
    setWidgetState(widget, { renderFrame: null, pointer: nextPointer, lastProgress: nextProgress });
    renderEngine(widget, nextProgress, nextPointer);
  });

  setWidgetState(widget, { renderFrame, pointer: nextPointer, lastProgress: nextProgress });
};

const getHitFromState = (widget, pointer, chartType) => {
  const state = getWidgetState(widget);
  if (!pointer || !Array.isArray(state.hotspots)) {
    return null;
  }

  return getHoverHit(state.hotspots, pointer, chartType);
};

const updateHoverMixState = (hotspots, previousHoverMix, activeKey) => {
  const nextHoverMix = {};
  let needsAnimationFrame = false;

  hotspots.forEach((hotspot) => {
    const key = hotspot.key;
    const previous = clamp(Number(previousHoverMix?.[key] || 0), 0, 1);
    const target = activeKey === key ? 1 : 0;
    const next = Math.abs(target - previous) < 0.03 ? target : lerp(previous, target, 0.28);

    if (next > 0.001 || target === 1) {
      nextHoverMix[key] = next;
    }

    if (Math.abs(next - target) > 0.03) {
      needsAnimationFrame = true;
    }
  });

  return { nextHoverMix, needsAnimationFrame };
};

const animateChart = (widget) => {
  const data = parseData(widget);
  if (!data) {
    return;
  }

  if (shouldUseChartJs(data)) {
    scheduleRender(widget, 1, null);
    return;
  }

  if (!data.animate) {
    scheduleRender(widget, 1, null);
    return;
  }

  const startedAt = performance.now();
  const duration = Math.max(200, Number(data.animationDuration || 900));

  const step = (now) => {
    const rawProgress = clamp((now - startedAt) / duration, 0, 1);
    scheduleRender(widget, easeOutCubic(rawProgress), null);

    if (rawProgress < 1) {
      requestAnimationFrame(step);
    }
  };

  requestAnimationFrame(step);
};

const initFancyChart = (widget) => {
  if (!widget) {
    return;
  }

  if (widget.dataset.calioFancyChartInit === 'yes') {
    scheduleRender(widget, 1, null);
    return;
  }

  widget.dataset.calioFancyChartInit = 'yes';
  chartState.set(widget, { lastProgress: 1, hoverKey: null, hoverMix: {}, pointer: null, renderFrame: null });
  animateChart(widget);

  const canvas = widget.querySelector('.calio-fancy-chart-canvas');
  if (!canvas) {
    return;
  }

  canvas.addEventListener('mousemove', (event) => {
    const data = parseData(widget);
    if (!data || shouldUseChartJs(data)) {
      return;
    }

    const rect = canvas.getBoundingClientRect();
    const pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top };
    const state = getWidgetState(widget);
    const currentHit = getHitFromState(widget, pointer, data.type);
    const currentKey = currentHit ? currentHit.key : null;

    if (state.hoverKey === currentKey && Array.isArray(state.hotspots)) {
      setWidgetState(widget, { pointer });
      renderTooltip(widget, data.showTooltip ? currentHit : null, pointer);
      return;
    }

    scheduleRender(widget, 1, pointer);
  });

  canvas.addEventListener('mouseleave', () => {
    const data = parseData(widget);
    if (!data || shouldUseChartJs(data)) {
      renderTooltip(widget, null, null);
      return;
    }

    setWidgetState(widget, { hoverKey: null, pointer: null });
    scheduleRender(widget, 1, null);
  });

  if ('ResizeObserver' in window) {
    const observer = new ResizeObserver(() => {
      setWidgetState(widget, { hoverKey: null, pointer: null });
      scheduleRender(widget, 1, null);
    });
    observer.observe(widget);
    setWidgetState(widget, { resizeObserver: observer });
  } else {
    window.addEventListener('resize', () => scheduleRender(widget, 1, null));
  }

  const exportButton = widget.querySelector('.calio-fancy-chart-export');
  if (exportButton) {
    exportButton.addEventListener('click', () => {
      const canvasElement = widget.querySelector('.calio-fancy-chart-canvas');
      if (!canvasElement) {
        return;
      }

      const title = widget.querySelector('.calio-fancy-chart-title')?.textContent?.trim() || 'chart';
      const safeTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'chart';
      const link = document.createElement('a');
      link.href = canvasElement.toDataURL('image/png');
      link.download = `${safeTitle}.png`;
      document.body.appendChild(link);
      link.click();
      link.remove();
    });
  }
};

const bootstrapFancyCharts = (scope = document) => {
  const root = scope instanceof window.jQuery ? scope[0] : scope;
  if (!root || !root.querySelectorAll) {
    return;
  }

  root.querySelectorAll('.calio-fancy-chart').forEach((widget) => {
    initFancyChart(widget);
    requestAnimationFrame(() => scheduleRender(widget, 1, null));
  });
};

document.addEventListener('DOMContentLoaded', () => {
  bootstrapFancyCharts(document);
});

let fancyChartHookBound = false;
const bindElementorFancyCharts = () => {
  if (fancyChartHookBound) {
    return true;
  }

  if (!(window.elementorFrontend && window.elementorFrontend.hooks)) {
    return false;
  }

  window.elementorFrontend.hooks.addAction('frontend/element_ready/global', (scope) => {
    bootstrapFancyCharts(scope);
  });
  fancyChartHookBound = true;
  return true;
};

const bootFancyChartsNow = () => {
  bootstrapFancyCharts(document);
};

if (window.elementorFrontend && window.elementorFrontend.hooks) {
  bindElementorFancyCharts();
  bootFancyChartsNow();
} else if (window.jQuery) {
  window.jQuery(window).on('elementor/frontend/init', () => {
    bindElementorFancyCharts();
    bootFancyChartsNow();
  });
}

if (!bindElementorFancyCharts()) {
  let fancyChartAttempts = 0;
  const fancyChartInterval = window.setInterval(() => {
    fancyChartAttempts += 1;
    if (bindElementorFancyCharts()) {
      bootFancyChartsNow();
      window.clearInterval(fancyChartInterval);
      return;
    }

    if (fancyChartAttempts >= 40) {
      window.clearInterval(fancyChartInterval);
    }
  }, 250);
}
