// @ts-check
import Labels from './Labels'
import Position from './Position'
import Marker from './Marker'
import Intersect from './Intersect'
import AxesTooltip from './AxesTooltip'
import { BrowserAPIs } from '../../ssr/BrowserAPIs.js'
import Graphics from '../Graphics'
import Series from '../Series'
import XAxis from './../axes/XAxis'
import Utils from './Utils'

/**
 * ApexCharts Core Tooltip Class to handle the tooltip generation.
 *
 * @module Tooltip
 **/

export default class Tooltip {
  /**
   * @param {import('../../types/internal').ChartStateW} w
   * @param {import('../../types/internal').ChartContext} ctx
   */
  constructor(w, ctx) {
    this.w = w
    this.ctx = ctx // needed: getGroupedCharts, getSyncedCharts, fireEvent, XAxis instantiation

    this.tConfig = w.config.tooltip

    this.tooltipUtil = new Utils(this)
    this.tooltipLabels = new Labels(this)
    this.tooltipPosition = new Position(this)
    this.marker = new Marker(this)
    this.intersect = new Intersect(this)
    this.axesTooltip = new AxesTooltip(this)
    this.showOnIntersect = this.tConfig.intersect
    this.showTooltipTitle = this.tConfig.x.show
    this.fixedTooltip = this.tConfig.fixed.enabled
    /** @type {HTMLElement | null} */
    this.xaxisTooltip = null
    /** @type {HTMLElement | null} */
    this.xaxisTooltipText = null
    /** @type {HTMLElement | null} */
    this.yaxisTooltip = null
    /** @type {HTMLElement[] | null} */
    this.yaxisTooltipText = null
    /** @type {HTMLElement[] | null} */
    this.yaxisTTEls = null
    /** @type {number} */
    this.xaxisOffY = 0
    /** @type {number} */
    this.yaxisOffX = 0
    /** @type {number} */
    this.xcrosshairsWidth = 0
    /** @type {Element | null} */
    this.ycrosshairs = null
    /** @type {Element | null} */
    this.ycrosshairsHidden = null
    /** @type {Element | null} */
    this.tooltip = null
    /** @type {any} */
    this.e = null
    this.isBarShared = !w.globals.isBarHorizontal && this.tConfig.shared
    this.lastHoverTime = Date.now()
    this.dimensionUpdateScheduled = false

    // Properties set in drawTooltip() / create() / event handlers
    /** @type {import('../../types/internal').XYRatios | null} */
    this.xyRatios = null
    /** @type {boolean} */
    this.isXAxisTooltipEnabled = false
    /** @type {boolean[]} */
    this.yaxisTooltips = []
    /** @type {any} */
    this.allTooltipSeriesGroups = []
    /** @type {any} */
    this.xAxisTicksPositions = null
    /** @type {number} */
    this.dataPointsDividedHeight = 0
    /** @type {number} */
    this.dataPointsDividedWidth = 0
    /** @type {HTMLElement | null} */
    this.tooltipTitle = null
    /** @type {NodeListOf<Element> | null} */
    this.legendLabels = null
    /** @type {any} */
    this.ttItems = null
    /** @type {DOMRect | null} */
    this.seriesBound = null
    /** @type {ReturnType<typeof setTimeout> | undefined} */
    this.seriesHoverTimeout = undefined
    /** @type {number} */
    this.clientX = 0
    /** @type {number} */
    this.clientY = 0
    /** @type {number} */
    this.barSeriesHeight = 0
    /** @type {{ x: number, y: number, ttWidth: number, ttHeight: number }} */
    this.tooltipRect = { x: 0, y: 0, ttWidth: 0, ttHeight: 0 }
  }

  setupDimensionCache() {
    const w = this.w
    const tooltipEl = this.getElTooltip()

    if (!tooltipEl) return

    // Initial dimension cache
    this.updateDimensionCache()

    // Setup ResizeObserver for automatic dimension updates
    if (typeof ResizeObserver !== 'undefined' && !w.globals.resizeObserver) {
      w.globals.resizeObserver = new ResizeObserver(() => {
        if (!this.dimensionUpdateScheduled) {
          this.dimensionUpdateScheduled = true
          requestAnimationFrame(() => {
            this.updateDimensionCache()
            this.dimensionUpdateScheduled = false
          })
        }
      })
      w.globals.resizeObserver.observe(tooltipEl)
    }
  }

  updateDimensionCache() {
    const w = this.w
    const tooltipEl = this.getElTooltip()

    if (!tooltipEl) return

    const rect = tooltipEl.getBoundingClientRect()
    w.globals.dimensionCache.tooltip = /** @type {any} */ ({
      width: rect.width,
      height: rect.height,
      lastUpdate: Date.now(),
    })
  }

  getCachedDimensions() {
    const w = this.w

    // Return cached dimensions if available and fresh (< 1 second old)
    if (w.globals.dimensionCache.tooltip) {
      const cache = /** @type {Record<string,any>} */ (
        w.globals.dimensionCache.tooltip
      )
      const age = Date.now() - cache.lastUpdate

      if (age < 1000) {
        return {
          ttWidth: cache.width,
          ttHeight: cache.height,
        }
      }
    }

    // Fallback to live measurement and update cache
    this.updateDimensionCache()
    const cache = /** @type {Record<string,any>} */ (
      w.globals.dimensionCache.tooltip
    )
    return cache
      ? {
          ttWidth: cache.width,
          ttHeight: cache.height,
        }
      : { ttWidth: 0, ttHeight: 0 }
  }

  /**
   * @param {{ w: import('../../types/internal').ChartStateW }} [ctx]
   * @returns {HTMLElement | null}
   */
  getElTooltip(ctx) {
    if (!ctx) ctx = this
    if (!ctx.w.dom.baseEl) return null

    return /** @type {HTMLElement | null} */ (
      ctx.w.dom.baseEl.querySelector('.apexcharts-tooltip')
    )
  }

  getElXCrosshairs() {
    return this.w.dom.baseEl.querySelector('.apexcharts-xcrosshairs')
  }

  getElGrid() {
    return this.w.dom.baseEl.querySelector('.apexcharts-grid')
  }

  /**
   * @param {import('../../types/internal').XYRatios} xyRatios
   */
  drawTooltip(xyRatios) {
    const w = this.w
    this.xyRatios = xyRatios
    this.isXAxisTooltipEnabled =
      w.config.xaxis.tooltip.enabled && w.globals.axisCharts
    /**
     * @param {number} y
     */
    this.yaxisTooltips = w.config.yaxis.map((y) => {
      return y.show && y.tooltip.enabled && w.globals.axisCharts ? true : false
    })
    /** @type {any} */
    this.allTooltipSeriesGroups = []

    if (!w.globals.axisCharts) {
      this.showTooltipTitle = false
    }

    // Remove any existing tooltip element to avoid duplicates on re-draws
    // (e.g. fastUpdate path calls drawTooltip without full DOM teardown)
    const existingTooltip = this.getElTooltip()
    if (existingTooltip?.parentNode) {
      existingTooltip.parentNode.removeChild(existingTooltip)
    }
    this.tooltipTitle = null

    const tooltipEl = BrowserAPIs.createElementNS(
      'http://www.w3.org/1999/xhtml',
      'div',
    )
    tooltipEl.classList.add('apexcharts-tooltip')
    if (w.config.tooltip.cssClass) {
      tooltipEl.classList.add(w.config.tooltip.cssClass)
    }
    tooltipEl.classList.add(`apexcharts-theme-${this.tConfig.theme || 'light'}`)

    // accessibility attributes
    if (
      w.config.chart.accessibility.enabled &&
      w.config.chart.accessibility.announcements.enabled
    ) {
      tooltipEl.setAttribute('role', 'tooltip')
      tooltipEl.setAttribute('aria-live', 'polite')
      tooltipEl.setAttribute('aria-atomic', 'true')
      tooltipEl.setAttribute('aria-hidden', 'true')
    }

    w.dom.elWrap.appendChild(tooltipEl)

    if (w.globals.axisCharts) {
      this.axesTooltip.drawXaxisTooltip()
      this.axesTooltip.drawYaxisTooltip()
      this.axesTooltip.setXCrosshairWidth()
      this.axesTooltip.handleYCrosshair()

      const xAxis = new XAxis(this.w, this.ctx, undefined)
      this.xAxisTicksPositions = xAxis.getXAxisTicksPositions()
    }

    // we forcefully set intersect true for these conditions
    if (
      (w.globals.comboCharts ||
        this.tConfig.intersect ||
        w.config.chart.type === 'rangeBar') &&
      !this.tConfig.shared
    ) {
      this.showOnIntersect = true
    }

    if (w.config.markers.size === 0 || w.globals.markers.largestSize === 0) {
      // when user don't want to show points all the time, but only on when hovering on series
      this.marker.drawDynamicPoints()
    }

    // no visible series, exit
    if (w.globals.collapsedSeries.length === w.seriesData.series.length) return

    this.dataPointsDividedHeight = w.layout.gridHeight / w.globals.dataPoints
    this.dataPointsDividedWidth = w.layout.gridWidth / w.globals.dataPoints

    if (this.showTooltipTitle) {
      this.tooltipTitle = BrowserAPIs.createElementNS(
        'http://www.w3.org/1999/xhtml',
        'div',
      )
      this.tooltipTitle.classList.add('apexcharts-tooltip-title')
      this.tooltipTitle.style.fontFamily =
        this.tConfig.style.fontFamily || w.config.chart.fontFamily
      this.tooltipTitle.style.fontSize = this.tConfig.style.fontSize
      tooltipEl.appendChild(this.tooltipTitle)
    }

    let ttItemsCnt = w.seriesData.series.length // whether shared or not, default is shared
    if ((w.globals.xyCharts || w.globals.comboCharts) && this.tConfig.shared) {
      if (!this.showOnIntersect) {
        ttItemsCnt = w.seriesData.series.length
      } else {
        ttItemsCnt = 1
      }
    }

    this.legendLabels = w.dom.baseEl.querySelectorAll('.apexcharts-legend-text')

    this.ttItems = this.createTTElements(ttItemsCnt)
    this.addSVGEvents()

    this.setupDimensionCache()
  }

  /**
   * @param {number} ttItemsCnt
   */
  createTTElements(ttItemsCnt) {
    const w = this.w
    /** @type {any[]} */
    const ttItems = []

    const tooltipEl = this.getElTooltip()
    if (!tooltipEl) return ttItems
    for (let i = 0; i < ttItemsCnt; i++) {
      const gTxt = BrowserAPIs.createElementNS(
        'http://www.w3.org/1999/xhtml',
        'div',
      )

      gTxt.classList.add(
        'apexcharts-tooltip-series-group',
        `apexcharts-tooltip-series-group-${i}`,
      )
      gTxt.style.order = String(
        w.config.tooltip.inverseOrder ? ttItemsCnt - i : i + 1,
      )

      const point = BrowserAPIs.createElementNS(
        'http://www.w3.org/1999/xhtml',
        'span',
      )
      point.classList.add('apexcharts-tooltip-marker')

      if (w.config.tooltip.fillSeriesColor) {
        point.style.backgroundColor = w.globals.colors[i]
      } else {
        point.style.color = w.globals.colors[i]
      }

      const mShape = w.config.markers.shape
      let shape = mShape
      if (Array.isArray(mShape)) {
        shape = mShape[i]
      }

      point.setAttribute('shape', shape)
      gTxt.appendChild(point)

      const gYZ = BrowserAPIs.createElementNS(
        'http://www.w3.org/1999/xhtml',
        'div',
      )
      gYZ.classList.add('apexcharts-tooltip-text')

      gYZ.style.fontFamily =
        this.tConfig.style.fontFamily || w.config.chart.fontFamily
      gYZ.style.fontSize = this.tConfig.style.fontSize
      ;['y', 'goals', 'z'].forEach((g) => {
        const gValText = BrowserAPIs.createElementNS(
          'http://www.w3.org/1999/xhtml',
          'div',
        )
        gValText.classList.add(`apexcharts-tooltip-${g}-group`)

        const txtLabel = BrowserAPIs.createElementNS(
          'http://www.w3.org/1999/xhtml',
          'span',
        )
        txtLabel.classList.add(`apexcharts-tooltip-text-${g}-label`)
        gValText.appendChild(txtLabel)

        const txtValue = BrowserAPIs.createElementNS(
          'http://www.w3.org/1999/xhtml',
          'span',
        )
        txtValue.classList.add(`apexcharts-tooltip-text-${g}-value`)
        gValText.appendChild(txtValue)

        gYZ.appendChild(gValText)
      })

      gTxt.appendChild(gYZ)

      tooltipEl.appendChild(gTxt)

      ttItems.push(gTxt)
    }

    return ttItems
  }

  addSVGEvents() {
    const w = this.w
    const type = w.config.chart.type
    const tooltipEl = this.getElTooltip()
    if (!tooltipEl) return

    const commonBar = !!(
      type === 'bar' ||
      type === 'candlestick' ||
      type === 'boxPlot' ||
      type === 'rangeBar'
    )

    const chartWithmarkers =
      type === 'area' ||
      type === 'line' ||
      type === 'scatter' ||
      type === 'bubble' ||
      type === 'radar'

    const hoverArea = w.dom.Paper.node

    const elGrid = this.getElGrid()
    if (elGrid) {
      this.seriesBound = elGrid.getBoundingClientRect()
    }

    /** @type {any[]} */
    const tooltipY = []
    /** @type {any[]} */
    const tooltipX = []

    const seriesHoverParams = {
      hoverArea,
      elGrid,
      tooltipEl,
      tooltipY,
      tooltipX,
      ttItems: this.ttItems,
    }

    let points

    if (w.globals.axisCharts) {
      if (chartWithmarkers) {
        points = w.dom.baseEl.querySelectorAll(
          ".apexcharts-series[data\\:longestSeries='true'] .apexcharts-marker",
        )
      } else if (commonBar) {
        points = w.dom.baseEl.querySelectorAll(
          '.apexcharts-series .apexcharts-bar-area, .apexcharts-series .apexcharts-candlestick-area, .apexcharts-series .apexcharts-boxPlot-area, .apexcharts-series .apexcharts-rangebar-area',
        )
      } else if (type === 'heatmap' || type === 'treemap') {
        points = w.dom.baseEl.querySelectorAll(
          '.apexcharts-series .apexcharts-heatmap, .apexcharts-series .apexcharts-treemap',
        )
      }

      if (points && points.length) {
        for (let p = 0; p < points.length; p++) {
          tooltipY.push(points[p].getAttribute('cy'))
          tooltipX.push(points[p].getAttribute('cx'))
        }
      }
    }

    const validSharedChartTypes =
      (w.globals.xyCharts && !this.showOnIntersect) ||
      (w.globals.comboCharts && !this.showOnIntersect) ||
      (commonBar && this.tooltipUtil.hasBars() && this.tConfig.shared)

    if (validSharedChartTypes) {
      this.addPathsEventListeners([hoverArea], seriesHoverParams)
    } else if (
      (commonBar && !w.globals.comboCharts) ||
      (chartWithmarkers && this.showOnIntersect)
    ) {
      this.addDatapointEventsListeners(seriesHoverParams)
    } else if (
      !w.globals.axisCharts ||
      type === 'heatmap' ||
      type === 'treemap'
    ) {
      const seriesAll = w.dom.baseEl.querySelectorAll('.apexcharts-series')
      this.addPathsEventListeners(seriesAll, seriesHoverParams)
    }

    if (this.showOnIntersect) {
      const lineAreaPoints = w.dom.baseEl.querySelectorAll(
        '.apexcharts-line-series .apexcharts-marker, .apexcharts-area-series .apexcharts-marker',
      )
      if (lineAreaPoints.length > 0) {
        // if we find any lineSeries, addEventListeners for them
        this.addPathsEventListeners(lineAreaPoints, seriesHoverParams)
      }

      // combo charts may have bars, so add event listeners here too
      if (this.tooltipUtil.hasBars() && !this.tConfig.shared) {
        this.addDatapointEventsListeners(seriesHoverParams)
      }
    }
  }

  drawFixedTooltipRect() {
    const w = this.w

    const tooltipEl = this.getElTooltip()
    if (!tooltipEl) return { x: 0, y: 0, ttWidth: 0, ttHeight: 0 }

    const tooltipRect = tooltipEl.getBoundingClientRect()

    const ttWidth = tooltipRect.width + 10
    const ttHeight = tooltipRect.height + 10
    let x = this.tConfig.fixed.offsetX
    let y = this.tConfig.fixed.offsetY

    const fixed = this.tConfig.fixed.position.toLowerCase()

    if (fixed.indexOf('right') > -1) {
      x = x + w.globals.svgWidth - ttWidth + 10
    }
    if (fixed.indexOf('bottom') > -1) {
      y = y + w.globals.svgHeight - ttHeight - 10
    }

    tooltipEl.style.left = x + 'px'
    tooltipEl.style.top = y + 'px'

    return {
      x,
      y,
      ttWidth,
      ttHeight,
    }
  }

  /**
   * @param {Record<string, any>} seriesHoverParams
   */
  addDatapointEventsListeners(seriesHoverParams) {
    const w = this.w
    const points = w.dom.baseEl.querySelectorAll(
      '.apexcharts-series-markers .apexcharts-marker, .apexcharts-bar-area, .apexcharts-candlestick-area, .apexcharts-boxPlot-area, .apexcharts-rangebar-area',
    )
    this.addPathsEventListeners(points, seriesHoverParams)
  }

  /**
   * @param {any} paths
   * @param {Record<string, any>} opts
   */
  addPathsEventListeners(paths, opts) {
    const self = this

    for (let p = 0; p < paths.length; p++) {
      const extendedOpts = {
        paths: paths[p],
        tooltipEl: opts.tooltipEl,
        tooltipY: opts.tooltipY,
        tooltipX: opts.tooltipX,
        elGrid: opts.elGrid,
        hoverArea: opts.hoverArea,
        ttItems: opts.ttItems,
      }

      const events = [
        'mousemove',
        'mouseup',
        'touchmove',
        'mouseout',
        'touchend',
      ]

      events.map((ev) => {
        return paths[p].addEventListener(
          ev,
          self.onSeriesHover.bind(self, extendedOpts),
          { capture: false, passive: true },
        )
      })
    }
  }

  /*
   ** Check to see if the tooltips should be updated based on a mouse / touch event
   * @param {Record<string, any>} opt
   * @param {Event} e
   */
  /** @param {Record<string, any>} opt @param {any} e */
  onSeriesHover(opt, e) {
    // If a user is moving their mouse quickly, don't bother updating the tooltip every single frame

    const targetDelay = 20
    const timeSinceLastUpdate = Date.now() - this.lastHoverTime
    if (timeSinceLastUpdate >= targetDelay) {
      // The tooltip was last updated over 100ms ago - redraw it even if the user is still moving their
      // mouse so they get some feedback that their moves are being registered
      this.seriesHover(opt, e)
    } else {
      // The tooltip was last updated less than 100ms ago
      // Cancel any other delayed draw, so we don't show stale data
      clearTimeout(this.seriesHoverTimeout)

      // Schedule the next draw so that it happens about 100ms after the last update
      this.seriesHoverTimeout = setTimeout(() => {
        this.seriesHover(opt, e)
      }, targetDelay - timeSinceLastUpdate)
    }
  }

  /*
   ** The actual series hover function
   * @param {Record<string, any>} opt
   * @param {Event} e
   */
  /** @param {Record<string, any>} opt @param {any} e */
  seriesHover(opt, e) {
    this.lastHoverTime = Date.now()
    let chartGroups = []
    const w = this.w

    // if user has more than one charts in group, we need to sync
    if (w.config.chart.group) {
      chartGroups = this.ctx.getGroupedCharts()
    }

    if (
      w.globals.axisCharts &&
      ((w.globals.minX === -Infinity && w.globals.maxX === Infinity) ||
        w.globals.dataPoints === 0)
    ) {
      return
    }

    if (chartGroups.length) {
      /**
       * @param {Record<string, any>} ch
       */
      chartGroups.forEach((ch) => {
        const tooltipEl = this.getElTooltip(ch)

        const newOpts = {
          paths: opt.paths,
          tooltipEl,
          tooltipY: opt.tooltipY,
          tooltipX: opt.tooltipX,
          elGrid: opt.elGrid,
          hoverArea: opt.hoverArea,
          ttItems: ch.w.globals.tooltip.ttItems,
        }

        // all the charts should have the same minX and maxX (same xaxis) for multiple tooltips to work correctly
        if (
          ch.w.globals.minX === this.w.globals.minX &&
          ch.w.globals.maxX === this.w.globals.maxX
        ) {
          ch.w.globals.tooltip.seriesHoverByContext({
            chartCtx: ch,
            ttCtx: ch.w.globals.tooltip,
            opt: newOpts,
            e,
          })
        }
      })
    } else {
      this.seriesHoverByContext({
        chartCtx: this.ctx,
        ttCtx: this.w.globals.tooltip,
        opt,
        e,
      })
    }
  }

  /** @param {{chartCtx: any, ttCtx: any, opt: any, e: any}} opts */
  seriesHoverByContext({ chartCtx, ttCtx, opt, e }) {
    const w = chartCtx.w
    const tooltipEl = this.getElTooltip(chartCtx)

    if (!tooltipEl) return

    // use cached dimensions instead of live getBoundingClientRect
    const cachedDims = ttCtx.getCachedDimensions()
    ttCtx.tooltipRect = {
      x: 0,
      y: 0,
      ttWidth: cachedDims.ttWidth,
      ttHeight: cachedDims.ttHeight,
    }
    ttCtx.e = e

    // highlight the current hovered bars
    if (
      ttCtx.tooltipUtil.hasBars() &&
      !w.globals.comboCharts &&
      !ttCtx.isBarShared
    ) {
      if (this.tConfig.onDatasetHover.highlightDataSeries) {
        const series = new Series(chartCtx.w)
        series.toggleSeriesOnHover(e, e.target.parentNode)
      }
    }

    if (w.globals.axisCharts) {
      ttCtx.axisChartsTooltips({
        e,
        opt,
        tooltipRect: ttCtx.tooltipRect,
      })
    } else {
      // non-plot charts i.e pie/donut/circle
      ttCtx.nonAxisChartsTooltips({
        e,
        opt,
        tooltipRect: ttCtx.tooltipRect,
      })
    }

    if (ttCtx.fixedTooltip) {
      ttCtx.drawFixedTooltipRect()
    }
  }

  // tooltip handling for line/area/bar/columns/scatter
  /** @param {{e: any, opt: any}} opts */
  axisChartsTooltips({ e, opt }) {
    const w = this.w
    let x, y

    const seriesBound = opt.elGrid.getBoundingClientRect()

    const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX
    const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY

    this.clientY = clientY
    this.clientX = clientX

    w.interact.capturedSeriesIndex = -1
    w.interact.capturedDataPointIndex = -1

    if (
      clientY < seriesBound.top ||
      clientY > seriesBound.top + seriesBound.height
    ) {
      this.handleMouseOut(opt)
      return
    }

    if (
      Array.isArray(this.tConfig.enabledOnSeries) &&
      !w.config.tooltip.shared
    ) {
      const index = parseInt(opt.paths.getAttribute('index'), 10)
      if (this.tConfig.enabledOnSeries.indexOf(index) < 0) {
        this.handleMouseOut(opt)
        return
      }
    }

    const tooltipEl = this.getElTooltip()
    if (!tooltipEl) return
    const xcrosshairs = this.getElXCrosshairs()

    let syncedCharts = []
    if (w.config.chart.group) {
      // we need to fallback to sticky tooltip in case charts are synced
      syncedCharts = this.ctx.getSyncedCharts()
    }

    const isStickyTooltip =
      w.globals.xyCharts ||
      (w.config.chart.type === 'bar' &&
        !w.globals.isBarHorizontal &&
        this.tooltipUtil.hasBars() &&
        this.tConfig.shared) ||
      (w.globals.comboCharts && this.tooltipUtil.hasBars())

    if (
      e.type === 'mousemove' ||
      e.type === 'touchmove' ||
      e.type === 'mouseup'
    ) {
      // there is no series to hover over
      if (
        w.globals.collapsedSeries.length +
          w.globals.ancillaryCollapsedSeries.length ===
        w.seriesData.series.length
      ) {
        return
      }

      if (xcrosshairs !== null) {
        xcrosshairs.classList.add('apexcharts-active')
      }

      const hasYAxisTooltip = this.yaxisTooltips?.filter(
        (/** @type {any} */ b) => {
          return b === true
        },
      )
      // ycrosshairs is set dynamically during drawTooltip()
      const _yc = /** @type {any} */ (this).ycrosshairs
      if (_yc !== null && hasYAxisTooltip?.length) {
        _yc.classList.add('apexcharts-active')
      }

      if (
        (isStickyTooltip && !this.showOnIntersect) ||
        syncedCharts.length > 1
      ) {
        this.handleStickyTooltip(e, clientX, clientY, opt)
      } else {
        if (
          w.config.chart.type === 'heatmap' ||
          w.config.chart.type === 'treemap'
        ) {
          const markerXY = this.intersect.handleHeatTreeTooltip({
            e,
            opt,
            x,
            y,
            type: w.config.chart.type,
          })
          x = markerXY.x
          y = markerXY.y

          tooltipEl.style.left = x + 'px'
          tooltipEl.style.top = y + 'px'
        } else {
          if (this.tooltipUtil.hasBars()) {
            this.intersect.handleBarTooltip({
              e,
              opt,
            })
          }

          if (this.tooltipUtil.hasMarkers(0)) {
            // intersect - line/area/scatter/bubble
            this.intersect.handleMarkerTooltip({
              e,
              opt,
              x,
              y,
            })
          }
        }
      }

      if (this.yaxisTooltips && this.yaxisTooltips.length) {
        for (let yt = 0; yt < w.config.yaxis.length; yt++) {
          this.axesTooltip.drawYaxisTooltipText(
            yt,
            clientY,
            /** @type {import('../../types/internal').XYRatios} */ (
              this.xyRatios
            ),
          )
        }
      }

      w.dom.baseEl.classList.add('apexcharts-tooltip-active')
      opt.tooltipEl.classList.add('apexcharts-active')
      if (
        w.config.chart.accessibility.enabled &&
        w.config.chart.accessibility.announcements.enabled
      ) {
        opt.tooltipEl.removeAttribute('aria-hidden')
      }
    } else if (e.type === 'mouseout' || e.type === 'touchend') {
      this.handleMouseOut(opt)
    }
  }

  // tooltip handling for pie/donuts
  /** @param {{e: any, opt: any, tooltipRect: any}} opts */
  nonAxisChartsTooltips({ e, opt, tooltipRect }) {
    const w = this.w
    const rel = opt.paths.getAttribute('rel')

    const tooltipEl = this.getElTooltip()
    if (!tooltipEl) return

    const seriesBound = w.dom.elWrap.getBoundingClientRect()

    if (e.type === 'mousemove' || e.type === 'touchmove') {
      w.dom.baseEl.classList.add('apexcharts-tooltip-active')
      tooltipEl.classList.add('apexcharts-active')
      if (
        w.config.chart.accessibility.enabled &&
        w.config.chart.accessibility.announcements.enabled
      ) {
        tooltipEl.removeAttribute('aria-hidden')
      }

      this.tooltipLabels.drawSeriesTexts({
        ttItems: opt.ttItems,
        i: parseInt(rel, 10) - 1,
        shared: false,
      })

      let x, y

      // opt.paths is the <g class="apexcharts-series"> group element;
      // data:cx / data:cy are set on the child <path> arc element inside it
      const arcPath = opt.paths.querySelector('path[data\\:cx]') || opt.paths

      if (
        w.config.tooltip.intersect &&
        arcPath.hasAttribute('data:cx') &&
        arcPath.hasAttribute('data:cy')
      ) {
        const svgBound = w.dom.Paper.node.getBoundingClientRect()
        x =
          svgBound.left -
          seriesBound.left +
          parseFloat(arcPath.getAttribute('data:cx')) -
          tooltipRect.ttWidth / 2
        y =
          svgBound.top -
          seriesBound.top +
          parseFloat(arcPath.getAttribute('data:cy')) -
          tooltipRect.ttHeight -
          10
      } else {
        x =
          (w.interact.clientX ?? 0) - seriesBound.left - tooltipRect.ttWidth / 2
        y =
          (w.interact.clientY ?? 0) -
          seriesBound.top -
          tooltipRect.ttHeight -
          10
      }

      tooltipEl.style.left = x + 'px'
      tooltipEl.style.top = y + 'px'

      if (w.config.legend.tooltipHoverFormatter) {
        const legendFormatter = w.config.legend.tooltipHoverFormatter

        const i = rel - 1
        const legendEl = /** @type {HTMLElement | undefined} */ (
          this.legendLabels?.[i]
        )
        if (!legendEl) return
        const legendName = legendEl.getAttribute('data:default-text')

        const text = legendFormatter(legendName, {
          seriesIndex: i,
          dataPointIndex: i,
          w,
        })

        legendEl.innerHTML = text
      }
    } else if (e.type === 'mouseout' || e.type === 'touchend') {
      tooltipEl.classList.remove('apexcharts-active')
      w.dom.baseEl.classList.remove('apexcharts-tooltip-active')
      if (w.config.legend.tooltipHoverFormatter) {
        this.legendLabels?.forEach((l) => {
          const defaultText = l.getAttribute('data:default-text')
          /** @type {HTMLElement} */ l.innerHTML = decodeURIComponent(
            defaultText ?? '',
          )
        })
      }
    }
  }

  /**
   * @param {Event} e
   * @param {number} clientX
   * @param {number} clientY
   * @param {Record<string, any>} opt
   */
  handleStickyTooltip(e, clientX, clientY, opt) {
    const w = this.w
    const capj = this.tooltipUtil.getNearestValues({
      context: this,
      hoverArea: opt.hoverArea,
      elGrid: opt.elGrid,
      clientX,
      clientY,
    })

    const j = capj.j
    let capturedSeries = capj.capturedSeries

    if (
      capturedSeries !== null &&
      w.globals.collapsedSeriesIndices.includes(capturedSeries ?? -1)
    )
      capturedSeries = null

    const bounds = opt.elGrid.getBoundingClientRect()
    if (capj.hoverX < 0 || capj.hoverX > bounds.width) {
      this.handleMouseOut(opt)
      return
    }

    if (capturedSeries !== null) {
      this.handleStickyCapturedSeries(e, capturedSeries ?? -1, opt, j ?? 0)
    } else {
      // couldn't capture any series. check if shared X is same,
      // if yes, draw a grouped tooltip
      if (this.tooltipUtil.isXoverlap(j ?? 0) || w.globals.isBarHorizontal) {
        const firstVisibleSeries = w.seriesData.series.findIndex(
          /**
           * @param {any} s
           * @param {number} i
           */
          (s, i) => !w.globals.collapsedSeriesIndices.includes(i),
        )
        this.create(e, this, firstVisibleSeries, j ?? 0, opt.ttItems)
      }
    }
  }

  /**
   * @param {Event} e
   * @param {number} capturedSeries
   * @param {Record<string, any>} opt
   * @param {number} j
   */
  handleStickyCapturedSeries(e, capturedSeries, opt, j) {
    const w = this.w
    if (!this.tConfig.shared) {
      const ignoreNull = w.seriesData.series[capturedSeries][j] === null
      if (ignoreNull) {
        this.handleMouseOut(opt)
        return
      }
    }

    if (typeof w.seriesData.series[capturedSeries][j] !== 'undefined') {
      if (
        this.tConfig.shared &&
        this.tooltipUtil.isXoverlap(j) &&
        this.tooltipUtil.isInitialSeriesSameLen()
      ) {
        this.create(e, this, capturedSeries, j, opt.ttItems)
      } else {
        this.create(e, this, capturedSeries, j, opt.ttItems, false)
      }
    } else {
      if (this.tooltipUtil.isXoverlap(j)) {
        const firstVisibleSeries = w.seriesData.series.findIndex(
          /**
           * @param {any} s
           * @param {number} i
           */
          (s, i) => !w.globals.collapsedSeriesIndices.includes(i),
        )
        this.create(e, this, firstVisibleSeries, j, opt.ttItems)
      }
    }
  }

  deactivateHoverFilter() {
    const w = this.w
    const graphics = new Graphics(this.w, this.ctx)

    const allPaths = w.dom.Paper.find(`.apexcharts-bar-area`)

    for (let b = 0; b < allPaths.length; b++) {
      graphics.pathMouseLeave(
        /** @type {any} */ (allPaths[b]),
        /** @type {any} */ (undefined),
      )
    }
  }

  /**
   * @param {Record<string, any>} opt
   */
  handleMouseOut(opt) {
    const w = this.w

    const xcrosshairs = this.getElXCrosshairs()
    w.dom.baseEl.classList.remove('apexcharts-tooltip-active')

    opt.tooltipEl.classList.remove('apexcharts-active')
    if (
      w.config.chart.accessibility.enabled &&
      w.config.chart.accessibility.announcements.enabled
    ) {
      opt.tooltipEl.setAttribute('aria-hidden', 'true')
    }
    this.deactivateHoverFilter()
    if (w.config.chart.type !== 'bubble') {
      this.marker.resetPointsSize()
    }
    if (xcrosshairs !== null) {
      xcrosshairs.classList.remove('apexcharts-active')
    }
    // ycrosshairs is set dynamically during drawTooltip()
    const _yc2 = /** @type {any} */ (this).ycrosshairs
    if (_yc2 !== null) {
      _yc2.classList.remove('apexcharts-active')
    }
    if (this.isXAxisTooltipEnabled) {
      this.xaxisTooltip?.classList.remove('apexcharts-active')
    }
    if (this.yaxisTooltips && this.yaxisTooltips.length) {
      if (this.yaxisTTEls === null) {
        this.yaxisTTEls = /** @type {HTMLElement[]} */ ([
          ...w.dom.baseEl.querySelectorAll('.apexcharts-yaxistooltip'),
        ])
      }
      for (let i = 0; i < this.yaxisTTEls.length; i++) {
        this.yaxisTTEls[i].classList.remove('apexcharts-active')
      }
    }

    if (w.config.legend.tooltipHoverFormatter) {
      this.legendLabels?.forEach((l) => {
        const defaultText = l.getAttribute('data:default-text')
        /** @type {HTMLElement} */ l.innerHTML = decodeURIComponent(
          defaultText ?? '',
        )
      })
    }
  }

  /**
   * @param {Event} e
   * @param {number} seriesIndex
   * @param {number} dataPointIndex
   */
  markerClick(e, seriesIndex, dataPointIndex) {
    const w = this.w
    if (typeof w.config.chart.events.markerClick === 'function') {
      w.config.chart.events.markerClick(e, this.ctx, {
        seriesIndex,
        dataPointIndex,
        w,
      })
    }
    this.ctx.events.fireEvent('markerClick', [
      e,
      this.ctx,
      { seriesIndex, dataPointIndex, w },
    ])
  }

  /**
   * @param {Event} e
   * @param {any} context
   * @param {number} capturedSeries
   * @param {number} j
   * @param {any} ttItems
   * @param {boolean | null} shared
   */
  create(e, context, capturedSeries, j, ttItems, shared = null) {
    const w = this.w
    const ttCtx = context

    if (e.type === 'mouseup') {
      this.markerClick(e, capturedSeries, j)
    }

    if (shared === null) shared = this.tConfig.shared

    const hasMarkers = this.tooltipUtil.hasMarkers(capturedSeries)

    const bars = this.tooltipUtil.getElBars()

    const handlePoints = () => {
      if (w.globals.markers.largestSize > 0) {
        ttCtx.marker.enlargePoints(j)
      } else {
        ttCtx.tooltipPosition.moveDynamicPointsOnHover(j)
      }
    }

    if (w.config.legend.tooltipHoverFormatter) {
      const legendFormatter = w.config.legend.tooltipHoverFormatter

      const els = /** @type {HTMLElement[]} */ (
        Array.from(this.legendLabels ?? [])
      )

      // reset all legend values first
      els.forEach((l) => {
        const legendName = l.getAttribute('data:default-text')
        l.innerHTML = decodeURIComponent(legendName ?? '')
      })

      // for irregular time series
      for (let i = 0; i < els.length; i++) {
        const l = els[i]
        const lsIndex = parseInt(l.getAttribute('i') ?? '', 10)
        const legendName = decodeURIComponent(
          l.getAttribute('data:default-text') ?? '',
        )

        const text = legendFormatter(legendName, {
          seriesIndex: shared ? lsIndex : capturedSeries,
          dataPointIndex: j,
          w,
        })

        if (!shared) {
          l.innerHTML = lsIndex === capturedSeries ? text : legendName
          if (capturedSeries === lsIndex) {
            break
          }
        } else {
          l.innerHTML =
            w.globals.collapsedSeriesIndices.indexOf(lsIndex) < 0
              ? text
              : legendName
        }
      }
    }

    const _rangeData = /** @type {any} */ (w.rangeData)
    const commonSeriesTextsParams = {
      ttItems,
      i: capturedSeries,
      j,
      ...(_rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1 !==
        undefined && {
        y1: _rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y1,
      }),
      ...(_rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y2 !==
        undefined && {
        y2: _rangeData.seriesRange?.[capturedSeries]?.[j]?.y[0]?.y2,
      }),
    }
    if (shared) {
      ttCtx.tooltipLabels.drawSeriesTexts({
        ...commonSeriesTextsParams,
        shared: this.showOnIntersect ? false : this.tConfig.shared,
      })

      if (hasMarkers) {
        handlePoints()
      } else if (this.tooltipUtil.hasBars()) {
        this.barSeriesHeight = this.tooltipUtil.getBarsHeight(
          /** @type {any[]} */ ([...bars]),
        )
        if (this.barSeriesHeight > 0) {
          // hover state, activate snap filter
          const graphics = new Graphics(this.w, this.ctx)
          const paths = w.dom.Paper.find(`.apexcharts-bar-area[j='${j}']`)

          // de-activate first
          this.deactivateHoverFilter()
          const points = ttCtx.tooltipUtil.getAllMarkers(true)

          if (points.length && !this.barSeriesHeight) {
            handlePoints()
          }

          ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)

          for (let b = 0; b < paths.length; b++) {
            graphics.pathMouseEnter(
              /** @type {any} */ (paths[b]),
              /** @type {any} */ (undefined),
            )
          }
        }
      }
    } else {
      ttCtx.tooltipLabels.drawSeriesTexts({
        shared: false,
        ...commonSeriesTextsParams,
      })

      if (this.tooltipUtil.hasBars()) {
        ttCtx.tooltipPosition.moveStickyTooltipOverBars(j, capturedSeries)
      }

      if (hasMarkers) {
        ttCtx.tooltipPosition.moveMarkers(capturedSeries, j)
      }
    }
  }
}
