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

/**
 * ApexCharts BoxCandleStick Class responsible for drawing both Stacked Columns and Bars.
 *
 * @module BoxCandleStick
 **/

class BoxCandleStick extends Bar {
  /**
   * @param {any[]} series
   * @param {string} ctype
   * @param {number} seriesIndex
   */
  // @ts-ignore -- BoxCandleStick.draw has an extra ctype param compared to Bar.draw
  draw(series, ctype, seriesIndex) {
    const w = this.w
    const graphics = new Graphics(this.w)
    const type = w.globals.comboCharts ? ctype : w.config.chart.type
    const fill = new Fill(this.w)

    this.candlestickOptions = this.w.config.plotOptions.candlestick
    this.boxOptions = this.w.config.plotOptions.boxPlot
    this.isHorizontal = w.config.plotOptions.bar.horizontal
    this.isOHLC =
      this.candlestickOptions && this.candlestickOptions.type === 'ohlc'

    this.coreUtils = new CoreUtils(this.w)
    series = this.coreUtils.getLogSeries(series)
    this.series = series
    this.yRatio = this.coreUtils.getLogYRatios(this.yRatio)

    this.barHelpers.initVariables(series)

    const ret = graphics.group({
      class: `apexcharts-${type}-series apexcharts-plot-series`,
    })

    for (let i = 0; i < series.length; i++) {
      this.isBoxPlot =
        w.config.chart.type === 'boxPlot' ||
        /** @type {Record<string,any>} */ (w.config.series[i]).type === 'boxPlot'

      /** @type {any} */
      let x
      /** @type {any} */
      let y

      const yArrj = [] // hold y values of current iterating series
      const xArrj = [] // hold x values of current iterating series

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

      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,
        barHeight,
        yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
        zeroW, // zeroW is the baseline where 0 meets x axis
        x: initX,
        barWidth,
        xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
        zeroH, // zeroH is the baseline where 0 meets y axis
      } = initPositions

      y = initY
      x = initX

      xArrj.push(x + (barWidth ?? 0) / 2)

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

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

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

        let paths = /** @type {any} */ (null)
        const pathsParams = {
          indexes: {
            i,
            j,
            realIndex,
            translationsIndex,
          },
          x,
          y,
          strokeWidth,
          elSeries,
        }

        if (this.isHorizontal) {
          paths = this.drawHorizontalBoxPaths({
            ...pathsParams,
            yDivision,
            barHeight,
            zeroW,
          })
        } else {
          paths = this.drawVerticalBoxPaths({
            ...pathsParams,
            xDivision,
            barWidth,
            zeroH,
          })
        }

        y = paths.y
        x = paths.x

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

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

        // push current X
        if (j > 0) {
          xArrj.push(x + (barWidth ?? 0) / 2)
        }

        yArrj.push(y)

        /**
         * @param {string} pathTo
         * @param {number} pi
         */
        paths.pathTo.forEach(
          (/** @type {any} */ pathTo, /** @type {any} */ pi) => {
            const lineFill =
              !this.isBoxPlot && this.candlestickOptions.wick.useFillColor
                ? paths.color[pi]
                : w.globals.stroke.colors[i]

            const pathFill = fill.fillPath({
              seriesNumber: realIndex,
              dataPointIndex: j,
              color: paths.color[pi],
              value: series[i][j],
            })

            this.renderSeries({
              realIndex,
              pathFill,
              lineFill,
              j,
              i,
              pathFrom: paths.pathFrom,
              pathTo,
              strokeWidth,
              elSeries,
              x,
              y,
              series,
              columnGroupIndex,
              barHeight,
              barWidth,
              elDataLabelsWrap,
              elGoalsMarkers,
              visibleSeries: this.visibleI,
              type: w.config.chart.type,
            })
          },
        )
      }

      // push all x val arrays into main xArr
      w.globals.seriesXvalues[realIndex] = xArrj
      w.globals.seriesYvalues[realIndex] = yArrj

      ret.add(elSeries)
    }

    return ret
  }

  /** @param {{indexes: any, x: any, xDivision: any, barWidth: any, zeroH: any, strokeWidth: any}} opts */
  drawVerticalBoxPaths({
    indexes,
    x,
    xDivision,
    barWidth,
    zeroH,
    strokeWidth,
  }) {
    const w = this.w
    const graphics = new Graphics(this.w)

    const i = indexes.i
    const j = indexes.j

    const { colors: candleColors } = w.config.plotOptions.candlestick
    const { colors: boxColors } = this.boxOptions
    const realIndex = indexes.realIndex

    /**
     * @param {string} color
     */
    const getColor = (color) =>
      Array.isArray(color) ? color[realIndex] : color

    const colorPos = getColor(candleColors.upward)
    const colorNeg = getColor(candleColors.downward)

    const yRatio = this.yRatio[indexes.translationsIndex]

    const ohlc = this.getOHLCValue(realIndex, j)
    let l1 = zeroH
    let l2 = zeroH

    let color = ohlc.o < ohlc.c ? [colorPos] : [colorNeg]

    if (this.isBoxPlot) {
      color = [getColor(boxColors.lower), getColor(boxColors.upper)]
    }

    let y1 = Math.min(ohlc.o, ohlc.c)
    let y2 = Math.max(ohlc.o, ohlc.c)
    let m = ohlc.m

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

    const barXPosition = x + barWidth * this.visibleI

    if (
      typeof /** @type {any} */ (this.series)[i]?.[j] === 'undefined' ||
      /** @type {any} */ (this.series)[i]?.[j] === null
    ) {
      y1 = zeroH
      y2 = zeroH
    } else {
      y1 = zeroH - y1 / yRatio
      y2 = zeroH - y2 / yRatio
      l1 = zeroH - ohlc.h / yRatio
      l2 = zeroH - ohlc.l / yRatio
      m = zeroH - ohlc.m / yRatio
    }

    let pathTo
    let pathFrom = graphics.move(barXPosition + barWidth / 2, y1)
    if (w.globals.previousPaths.length > 0) {
      pathFrom = this.getPreviousPath(realIndex, j)
    }

    if (this.isOHLC) {
      const centerX = barXPosition + barWidth / 2
      const openY = zeroH - ohlc.o / yRatio
      const closeY = zeroH - ohlc.c / yRatio

      pathTo = [
        graphics.move(centerX, l1) +
          graphics.line(centerX, l2) +
          graphics.move(centerX, openY) +
          graphics.line(barXPosition, openY) +
          graphics.move(centerX, closeY) +
          graphics.line(barXPosition + barWidth, closeY),
      ]
    } else if (this.isBoxPlot) {
      pathTo = [
        graphics.move(barXPosition, y1) +
          graphics.line(barXPosition + barWidth / 2, y1) +
          graphics.line(barXPosition + barWidth / 2, l1) +
          graphics.line(barXPosition + barWidth / 4, l1) +
          graphics.line(barXPosition + barWidth - barWidth / 4, l1) +
          graphics.line(barXPosition + barWidth / 2, l1) +
          graphics.line(barXPosition + barWidth / 2, y1) +
          graphics.line(barXPosition + barWidth, y1) +
          graphics.line(barXPosition + barWidth, m) +
          graphics.line(barXPosition, m) +
          graphics.line(barXPosition, y1 + strokeWidth / 2),
        graphics.move(barXPosition, m) +
          graphics.line(barXPosition + barWidth, m) +
          graphics.line(barXPosition + barWidth, y2) +
          graphics.line(barXPosition + barWidth / 2, y2) +
          graphics.line(barXPosition + barWidth / 2, l2) +
          graphics.line(barXPosition + barWidth - barWidth / 4, l2) +
          graphics.line(barXPosition + barWidth / 4, l2) +
          graphics.line(barXPosition + barWidth / 2, l2) +
          graphics.line(barXPosition + barWidth / 2, y2) +
          graphics.line(barXPosition, y2) +
          graphics.line(barXPosition, m) +
          'z',
      ]
    } else {
      // Regular candlestick
      pathTo = [
        graphics.move(barXPosition, y2) +
          graphics.line(barXPosition + barWidth / 2, y2) +
          graphics.line(barXPosition + barWidth / 2, l1) +
          graphics.line(barXPosition + barWidth / 2, y2) +
          graphics.line(barXPosition + barWidth, y2) +
          graphics.line(barXPosition + barWidth, y1) +
          graphics.line(barXPosition + barWidth / 2, y1) +
          graphics.line(barXPosition + barWidth / 2, l2) +
          graphics.line(barXPosition + barWidth / 2, y1) +
          graphics.line(barXPosition, y1) +
          graphics.line(barXPosition, y2 - strokeWidth / 2),
      ]
    }

    pathFrom = pathFrom + graphics.move(barXPosition, y1)

    if (!w.axisFlags.isXNumeric) {
      x = x + xDivision
    }

    return {
      pathTo,
      pathFrom,
      x,
      y: y2,
      goalY: this.barHelpers.getGoalValues(
        'y',
        /** @type {any} */ (null),
        zeroH,
        i,
        j,
        indexes.translationsIndex,
      ),
      barXPosition,
      color,
    }
  }

  /** @param {{indexes: any, y: any, yDivision: any, barHeight: any, zeroW: any, strokeWidth: any}} opts */
  drawHorizontalBoxPaths({
    indexes,
    y,
    yDivision,
    barHeight,
    zeroW,
    strokeWidth,
  }) {
    const w = this.w
    const graphics = new Graphics(this.w)

    const i = indexes.i
    const j = indexes.j
    const realIndex = indexes.realIndex

    const { colors: candleColors } = w.config.plotOptions.candlestick
    const { colors: boxColors } = this.boxOptions

    /**
     * @param {string} color
     */
    const getColor = (color) =>
      Array.isArray(color) ? color[realIndex] : color

    const yRatio = this.invertedYRatio
    const ohlc = this.getOHLCValue(realIndex, j)

    let color =
      ohlc.o < ohlc.c
        ? [getColor(candleColors.upward)]
        : [getColor(candleColors.downward)]

    if (this.isBoxPlot) {
      color = [getColor(boxColors.lower), getColor(boxColors.upper)]
    }

    let l1 = zeroW
    let l2 = zeroW

    let x1 = Math.min(ohlc.o, ohlc.c)
    let x2 = Math.max(ohlc.o, ohlc.c)
    let m = ohlc.m

    if (w.axisFlags.isXNumeric) {
      y =
        (w.seriesData.seriesX[realIndex][j] - w.globals.minX) /
          this.invertedXRatio -
        barHeight / 2
    }

    const barYPosition = y + barHeight * this.visibleI

    if (
      typeof /** @type {any} */ (this.series)[i]?.[j] === 'undefined' ||
      /** @type {any} */ (this.series)[i]?.[j] === null
    ) {
      x1 = zeroW
      x2 = zeroW
    } else {
      x1 = zeroW + x1 / yRatio
      x2 = zeroW + x2 / yRatio
      l1 = zeroW + ohlc.h / yRatio
      l2 = zeroW + ohlc.l / yRatio
      m = zeroW + ohlc.m / yRatio
    }

    let pathFrom = graphics.move(x1, barYPosition + barHeight / 2)
    if (w.globals.previousPaths.length > 0) {
      pathFrom = this.getPreviousPath(realIndex, j)
    }

    const pathTo = [
      graphics.move(x1, barYPosition) +
        graphics.line(x1, barYPosition + barHeight / 2) +
        graphics.line(l1, barYPosition + barHeight / 2) +
        graphics.line(l1, barYPosition + barHeight / 2 - barHeight / 4) +
        graphics.line(l1, barYPosition + barHeight / 2 + barHeight / 4) +
        graphics.line(l1, barYPosition + barHeight / 2) +
        graphics.line(x1, barYPosition + barHeight / 2) +
        graphics.line(x1, barYPosition + barHeight) +
        graphics.line(m, barYPosition + barHeight) +
        graphics.line(m, barYPosition) +
        graphics.line(x1 + strokeWidth / 2, barYPosition),
      graphics.move(m, barYPosition) +
        graphics.line(m, barYPosition + barHeight) +
        graphics.line(x2, barYPosition + barHeight) +
        graphics.line(x2, barYPosition + barHeight / 2) +
        graphics.line(l2, barYPosition + barHeight / 2) +
        graphics.line(l2, barYPosition + barHeight - barHeight / 4) +
        graphics.line(l2, barYPosition + barHeight / 4) +
        graphics.line(l2, barYPosition + barHeight / 2) +
        graphics.line(x2, barYPosition + barHeight / 2) +
        graphics.line(x2, barYPosition) +
        graphics.line(m, barYPosition) +
        'z',
    ]

    pathFrom = pathFrom + graphics.move(x1, barYPosition)

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

    return {
      pathTo,
      pathFrom,
      x: x2,
      y,
      goalX: this.barHelpers.getGoalValues(
        'x',
        zeroW,
        /** @type {any} */ (null),
        i,
        j,
        0,
      ),
      barYPosition,
      color,
    }
  }

  /**
   * @param {number} i
   * @param {number} j
   */
  getOHLCValue(i, j) {
    const w = this.w
    const coreUtils = this.coreUtils
    /**
     * @param {any[]} arr
     */
    const getCandleVal = (arr) =>
      arr[i] && arr[i][j] != null
        ? /** @type {any} */ (coreUtils).getLogValAtSeriesIndex(arr[i][j], i)
        : 0

    const h = getCandleVal(w.candleData.seriesCandleH)
    const o = getCandleVal(w.candleData.seriesCandleO)
    const m = getCandleVal(w.candleData.seriesCandleM)
    const c = getCandleVal(w.candleData.seriesCandleC)
    const l = getCandleVal(w.candleData.seriesCandleL)

    // BoxPlot data arrives as [min, q1, median, q3, max] and is stored in
    // H=min, O=q1, M=median, C=q3, L=max — remap to OHLC semantics:
    // o=q1(O), h=min(H), m=median(M), l=q3(C), c=max(L)
    return {
      o: this.isBoxPlot ? h : o,
      h: this.isBoxPlot ? o : h,
      m,
      l: this.isBoxPlot ? c : l,
      c: this.isBoxPlot ? l : c,
    }
  }
}

export default BoxCandleStick
