// @ts-check
import Bar from './Bar'
import Graphics from '../modules/Graphics'
import Series from '../modules/Series'
import Utils from '../utils/Utils'

/**
 * ApexCharts RangeBar Class responsible for drawing Range/Timeline Bars.
 *
 * @module RangeBar
 **/

class RangeBar extends Bar {
  /**
   * @param {any[]} series
   * @param {number} seriesIndex
   */
  draw(series, seriesIndex) {
    const w = this.w
    const graphics = new Graphics(this.w)

    this.rangeBarOptions = this.w.config.plotOptions.rangeBar

    this.series = series
    this.seriesRangeStart = w.rangeData.seriesRangeStart
    this.seriesRangeEnd = w.rangeData.seriesRangeEnd

    this.barHelpers.initVariables(series)

    const ret = graphics.group({
      class: 'apexcharts-rangebar-series apexcharts-plot-series',
    })

    for (let i = 0; i < series.length; i++) {
      let x, y

      const realIndex = w.globals.comboCharts
        ? /** @type {any} */ (seriesIndex)[i]
        : i
      const { columnGroupIndex } = this.barHelpers.getGroupIndex(realIndex)

      // el to which series will be drawn
      const elSeries = graphics.group({
        class: `apexcharts-series`,
        seriesName: Utils.escapeString(w.seriesData.seriesNames[realIndex]),
        rel: i + 1,
        'data:realIndex': realIndex,
      })

      Series.addCollapsedClassToSeries(this.w, elSeries, realIndex)

      if (series[i].length > 0) {
        this.visibleI = this.visibleI + 1
      }

      let translationsIndex = 0
      if (this.yRatio.length > 1) {
        this.yaxisIndex = /** @type {any} */ (
          w.globals.seriesYAxisReverseMap[realIndex]
        )[0]
        translationsIndex = realIndex
      }

      const initPositions = this.barHelpers.initialPositions(realIndex)
      const {
        y: initY,
        zeroW, // zeroW is the baseline where 0 meets x axis
        x: initX,
        zeroH, // zeroH is the baseline where 0 meets y axis
      } = initPositions
      let barWidth = initPositions.barWidth ?? 0
      let barHeight = initPositions.barHeight ?? 0
      const yDivision = initPositions.yDivision ?? 0
      const xDivision = initPositions.xDivision ?? 0

      y = initY
      x = initX

      // eldatalabels
      const elDataLabelsWrap = graphics.group({
        class: 'apexcharts-datalabels',
        'data:realIndex': realIndex,
      })

      const elGoalsMarkers = graphics.group({
        class: 'apexcharts-rangebar-goals-markers',
      })

      for (let j = 0; j < w.globals.dataPoints; j++) {
        const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)

        const y1 = this.seriesRangeStart[i][j]
        const y2 = this.seriesRangeEnd[i][j]

        let paths = /** @type {any} */ (null)
        let barXPosition = null
        let barYPosition = null
        const params = { x, y, strokeWidth, elSeries }

        let seriesLen = this.seriesLen
        if (w.config.plotOptions.bar.rangeBarGroupRows) {
          seriesLen = 1
        }

        if (
          typeof /** @type {Record<string,any>} */ (w.config.series[i]).data?.[j] ===
          'undefined'
        ) {
          // no data exists for further indexes, hence we need to get out the innr loop.
          // As we are iterating over total datapoints, there is a possiblity the series might not have data for j index
          break
        }

        if (this.isHorizontal) {
          barYPosition = y + barHeight * /** @type {any} */ (this).visibleI

          const srty = (yDivision - barHeight * seriesLen) / 2

          if (/** @type {Record<string,any>} */ (w.config.series[i]).data?.[j]?.x) {
            const positions = this.detectOverlappingBars({
              i,
              j,
              barYPosition,
              srty,
              barHeight,
              yDivision,
              initPositions,
            })

            barHeight = positions.barHeight
            barYPosition = positions.barYPosition
          }

          paths = this.drawRangeBarPaths({
            indexes: { i, j, realIndex },
            barHeight,
            barYPosition,
            zeroW,
            yDivision,
            y1,
            y2,
            ...params,
          })

          barWidth = paths.barWidth
        } else {
          if (w.axisFlags.isXNumeric) {
            x =
              (w.seriesData.seriesX[i][j] - w.globals.minX) / this.xRatio -
              barWidth / 2
          }

          barXPosition = x + barWidth * /** @type {any} */ (this).visibleI

          const srtx = (xDivision - barWidth * seriesLen) / 2

          if (/** @type {Record<string,any>} */ (w.config.series[i]).data?.[j]?.x) {
            const positions = this.detectOverlappingBars({
              i,
              j,
              barXPosition,
              srtx,
              barWidth,
              xDivision,
              initPositions,
            })

            barWidth = positions.barWidth
            barXPosition = positions.barXPosition
          }

          paths = this.drawRangeColumnPaths({
            indexes: { i, j, realIndex, translationsIndex },
            barWidth,
            barXPosition,
            zeroH,
            xDivision,
            ...params,
          })

          barHeight = paths.barHeight
        }

        const barGoalLine = this.barHelpers.drawGoalLine({
          barXPosition: paths.barXPosition,
          barYPosition,
          goalX: paths.goalX,
          goalY: paths.goalY,
          barHeight,
          barWidth,
        })

        if (barGoalLine) {
          elGoalsMarkers.add(barGoalLine)
        }

        y = paths.y
        x = paths.x

        const pathFill = this.barHelpers.getPathFillColor(
          series,
          i,
          j,
          realIndex,
        )

        this.renderSeries({
          realIndex,
          pathFill: pathFill.color,
          lineFill: pathFill.useRangeColor
            ? pathFill.color
            : w.globals.stroke.colors[realIndex],
          j,
          i,
          x,
          y,
          y1,
          y2,
          pathFrom: paths.pathFrom,
          pathTo: paths.pathTo,
          strokeWidth,
          elSeries,
          series,
          barHeight,
          barWidth,
          barXPosition,
          barYPosition,
          columnGroupIndex,
          elDataLabelsWrap,
          elGoalsMarkers,
          visibleSeries: this.visibleI,
          type: 'rangebar',
        })
      }

      ret.add(elSeries)
    }

    return ret
  }

  /** @param {{ i?: any, j?: any, barYPosition?: any, barXPosition?: any, srty?: any, srtx?: any, barHeight?: any, barWidth?: any, yDivision?: any, xDivision?: any, initPositions?: any }} opts */
  detectOverlappingBars({
    i,
    j,
    barYPosition,
    barXPosition,
    srty,
    srtx,
    barHeight,
    barWidth,
    yDivision,
    xDivision,
    initPositions,
  }) {
    const w = this.w
    let overlaps = []
    const rangeName = /** @type {Record<string,any>} */ (w.config.series[i]).data?.[j]
      ?.rangeName

    const x = /** @type {Record<string,any>} */ (w.config.series[i]).data?.[j]?.x
    const labelX = Array.isArray(x) ? x.join(' ') : x

    const rowIndex = w.labelData.labels
      /**
       * @param {any} _
       */
      .map((_) => (Array.isArray(_) ? _.join(' ') : _))
      .indexOf(labelX)
    /**
     * @param {any} tx
     */
    const overlappedIndex = w.rangeData.seriesRange[i].findIndex(
      (/** @type {any} */ tx) => tx.x === labelX && tx.overlaps?.size > 0,
    )

    if (this.isHorizontal) {
      if (w.config.plotOptions.bar.rangeBarGroupRows) {
        barYPosition = srty + yDivision * rowIndex
      } else {
        barYPosition = srty + barHeight * this.visibleI + yDivision * rowIndex
      }

      if (overlappedIndex > -1 && !w.config.plotOptions.bar.rangeBarOverlap) {
        overlaps = Array.from(
          /** @type {any} */ (w.rangeData.seriesRange[i][overlappedIndex])
            .overlaps,
        )

        if (overlaps.indexOf(rangeName) > -1) {
          barHeight = initPositions.barHeight / overlaps.length

          barYPosition =
            barHeight * this.visibleI +
            (yDivision * (100 - parseInt(this.barOptions.barHeight, 10))) /
              100 /
              2 +
            barHeight * (this.visibleI + overlaps.indexOf(rangeName)) +
            yDivision * rowIndex
        }
      }
    } else {
      if (rowIndex > -1 && !w.labelData.timescaleLabels.length) {
        if (w.config.plotOptions.bar.rangeBarGroupRows) {
          barXPosition = srtx + xDivision * rowIndex
        } else {
          barXPosition = srtx + barWidth * this.visibleI + xDivision * rowIndex
        }
      }

      if (overlappedIndex > -1 && !w.config.plotOptions.bar.rangeBarOverlap) {
        overlaps = Array.from(
          /** @type {any} */ (w.rangeData.seriesRange[i][overlappedIndex])
            .overlaps,
        )

        if (overlaps.indexOf(rangeName) > -1) {
          barWidth = initPositions.barWidth / overlaps.length

          barXPosition =
            barWidth * this.visibleI +
            (xDivision * (100 - parseInt(this.barOptions.barWidth, 10))) /
              100 /
              2 +
            barWidth * (this.visibleI + overlaps.indexOf(rangeName)) +
            xDivision * rowIndex
        }
      }
    }

    return {
      barYPosition,
      barXPosition,
      barHeight,
      barWidth,
    }
  }

  /** @param {{indexes: any, x: any, xDivision: any, barWidth: any, barXPosition: any, zeroH: any}} opts */
  drawRangeColumnPaths({
    indexes,
    x,
    xDivision,
    barWidth,
    barXPosition,
    zeroH,
  }) {
    const w = this.w

    const { i, j, realIndex, translationsIndex } = indexes

    const yRatio = this.yRatio[translationsIndex]

    const range = this.getRangeValue(realIndex, j)

    let y1 = Math.min(range.start, range.end)
    let y2 = Math.max(range.start, range.end)

    if (
      typeof /** @type {any} */ (this.series)[i]?.[j] === 'undefined' ||
      /** @type {any} */ (this.series)[i]?.[j] === null
    ) {
      y1 = zeroH
    } else {
      y1 = zeroH - y1 / yRatio
      y2 = zeroH - y2 / yRatio
    }
    const barHeight = Math.abs(y2 - y1)

    const paths = this.barHelpers.getColumnPaths({
      barXPosition,
      barWidth,
      y1,
      y2,
      strokeWidth: this.strokeWidth,
      series: this.seriesRangeEnd,
      realIndex: realIndex,
      i: realIndex,
      j,
      w,
    })

    if (!w.axisFlags.isXNumeric) {
      x = x + xDivision
    } else {
      const xForNumericXAxis = this.getBarXForNumericXAxis({
        x,
        j,
        realIndex,
        barWidth,
      })
      x = xForNumericXAxis.x
      barXPosition = xForNumericXAxis.barXPosition
    }

    return {
      pathTo: paths.pathTo,
      pathFrom: paths.pathFrom,
      barHeight,
      x,
      y: range.start < 0 && range.end < 0 ? y1 : y2,
      goalY: this.barHelpers.getGoalValues(
        'y',
        /** @type {any} */ (null),
        zeroH,
        i,
        j,
        translationsIndex,
      ),
      barXPosition,
    }
  }

  /**
   * @param {number} val
   */
  preventBarOverflow(val) {
    const w = this.w

    if (val < 0) {
      val = 0
    }
    if (val > w.layout.gridWidth) {
      val = w.layout.gridWidth
    }

    return val
  }

  /** @param {{indexes: any, y: any, y1: any, y2: any, yDivision: any, barHeight: any, barYPosition: any, zeroW: any}} opts */
  drawRangeBarPaths({
    indexes,
    y,
    y1,
    y2,
    yDivision,
    barHeight,
    barYPosition,
    zeroW,
  }) {
    const w = this.w

    const { realIndex, j } = indexes

    const x1 = this.preventBarOverflow(zeroW + y1 / this.invertedYRatio)
    const x2 = this.preventBarOverflow(zeroW + y2 / this.invertedYRatio)

    const range = this.getRangeValue(realIndex, j)

    const barWidth = Math.abs(x2 - x1)

    const paths = this.barHelpers.getBarpaths({
      barYPosition,
      barHeight,
      x1,
      x2,
      strokeWidth: this.strokeWidth,
      series: this.seriesRangeEnd,
      i: realIndex,
      realIndex,
      j,
      w,
    })

    if (!w.axisFlags.isXNumeric) {
      y = y + yDivision
    }

    return {
      pathTo: paths.pathTo,
      pathFrom: paths.pathFrom,
      barWidth,
      x: range.start < 0 && range.end < 0 ? x1 : x2,
      goalX: this.barHelpers.getGoalValues(
        'x',
        zeroW,
        /** @type {any} */ (null),
        realIndex,
        j,
        0,
      ),
      y,
    }
  }

  /**
   * @param {number} i
   * @param {number} j
   */
  getRangeValue(i, j) {
    const w = this.w
    return {
      start: w.rangeData.seriesRangeStart[i][j],
      end: w.rangeData.seriesRangeEnd[i][j],
    }
  }
}

export default RangeBar
