import Utils from '../utils/Utils'
import DateTime from '../utils/DateTime'
import Scales from './Scales'

/**
 * Range is used to generates values between min and max.
 *
 * @module Range
 **/

class Range {
  constructor(ctx) {
    this.ctx = ctx
    this.w = ctx.w

    this.scales = new Scales(ctx)
  }

  init() {
    this.setYRange()
    this.setXRange()
    this.setZRange()
  }

  getMinYMaxY(
    startingIndex,
    lowestY = Number.MAX_VALUE,
    highestY = -Number.MAX_VALUE,
    len = null
  ) {
    const cnf = this.w.config
    const gl = this.w.globals
    let maxY = -Number.MAX_VALUE
    let minY = Number.MIN_VALUE

    if (len === null) {
      len = startingIndex + 1
    }
    const series = gl.series
    let seriesMin = series
    let seriesMax = series

    if (cnf.chart.type === 'candlestick') {
      seriesMin = gl.seriesCandleL
      seriesMax = gl.seriesCandleH
    } else if (cnf.chart.type === 'boxPlot') {
      seriesMin = gl.seriesCandleO
      seriesMax = gl.seriesCandleC
    } else if (gl.isRangeData) {
      seriesMin = gl.seriesRangeStart
      seriesMax = gl.seriesRangeEnd
    }

    for (let i = startingIndex; i < len; i++) {
      gl.dataPoints = Math.max(gl.dataPoints, series[i].length)

      if (gl.categoryLabels.length) {
        gl.dataPoints = gl.categoryLabels.filter(
          (label) => typeof label !== 'undefined'
        ).length
      }

      if (
        gl.labels.length &&
        cnf.xaxis.type !== 'datetime' &&
        gl.series.reduce((a, c) => a + c.length, 0) !== 0
      ) {
        // the condition cnf.xaxis.type !== 'datetime' fixes #3897 and #3905
        gl.dataPoints = Math.max(gl.dataPoints, gl.labels.length)
      }
      for (let j = 0; j < gl.series[i].length; j++) {
        let val = series[i][j]
        if (val !== null && Utils.isNumber(val)) {
          if (typeof seriesMax[i][j] !== 'undefined') {
            maxY = Math.max(maxY, seriesMax[i][j])
            lowestY = Math.min(lowestY, seriesMax[i][j])
          }
          if (typeof seriesMin[i][j] !== 'undefined') {
            lowestY = Math.min(lowestY, seriesMin[i][j])
            highestY = Math.max(highestY, seriesMin[i][j])
          }

          if (
            this.w.config.chart.type === 'candlestick' ||
            this.w.config.chart.type === 'boxPlot' ||
            this.w.config.chart.type !== 'rangeArea' ||
            this.w.config.chart.type !== 'rangeBar'
          ) {
            if (
              this.w.config.chart.type === 'candlestick' ||
              this.w.config.chart.type === 'boxPlot'
            ) {
              if (typeof gl.seriesCandleC[i][j] !== 'undefined') {
                maxY = Math.max(maxY, gl.seriesCandleO[i][j])
                maxY = Math.max(maxY, gl.seriesCandleH[i][j])
                maxY = Math.max(maxY, gl.seriesCandleL[i][j])
                maxY = Math.max(maxY, gl.seriesCandleC[i][j])
                if (this.w.config.chart.type === 'boxPlot') {
                  maxY = Math.max(maxY, gl.seriesCandleM[i][j])
                }
              }
            }

            // there is a combo chart and the specified series in not either candlestick, boxplot, or rangeArea/rangeBar; find the max there
            if (
              cnf.series[i].type &&
              (cnf.series[i].type !== 'candlestick' ||
                cnf.series[i].type !== 'boxPlot' ||
                cnf.series[i].type !== 'rangeArea' ||
                cnf.series[i].type !== 'rangeBar')
            ) {
              maxY = Math.max(maxY, gl.series[i][j])
              lowestY = Math.min(lowestY, gl.series[i][j])
            }

            highestY = maxY
          }

          if (
            gl.seriesGoals[i] &&
            gl.seriesGoals[i][j] &&
            Array.isArray(gl.seriesGoals[i][j])
          ) {
            gl.seriesGoals[i][j].forEach((g) => {
              if (minY !== Number.MIN_VALUE) {
                minY = Math.min(minY, g.value)
                lowestY = minY
              }
              maxY = Math.max(maxY, g.value)
              highestY = maxY
            })
          }

          if (Utils.isFloat(val)) {
            val = Utils.noExponents(val)
            gl.yValueDecimal = Math.max(
              gl.yValueDecimal,
              val.toString().split('.')[1].length
            )
          }
          if (minY > seriesMin[i][j] && seriesMin[i][j] < 0) {
            minY = seriesMin[i][j]
          }
        } else {
          gl.hasNullValues = true
        }
      }
    }

    if (
      cnf.chart.type === 'rangeBar' &&
      gl.seriesRangeStart.length &&
      gl.isBarHorizontal
    ) {
      minY = lowestY
    }

    if (cnf.chart.type === 'bar') {
      if (minY < 0 && maxY < 0) {
        // all negative values in a bar chart, hence make the max to 0
        maxY = 0
      }
      if (minY === Number.MIN_VALUE) {
        minY = 0
      }
    }

    return {
      minY,
      maxY,
      lowestY,
      highestY,
    }
  }

  setYRange() {
    let gl = this.w.globals
    let cnf = this.w.config
    gl.maxY = -Number.MAX_VALUE
    gl.minY = Number.MIN_VALUE

    let lowestYInAllSeries = Number.MAX_VALUE

    if (gl.isMultipleYAxis) {
      // we need to get minY and maxY for multiple y axis
      for (let i = 0; i < gl.series.length; i++) {
        const minYMaxYArr = this.getMinYMaxY(i, lowestYInAllSeries, null, i + 1)
        gl.minYArr.push(minYMaxYArr.minY)
        gl.maxYArr.push(minYMaxYArr.maxY)
        lowestYInAllSeries = minYMaxYArr.lowestY
      }
    }

    // and then, get the minY and maxY from all series
    const minYMaxY = this.getMinYMaxY(
      0,
      lowestYInAllSeries,
      null,
      gl.series.length
    )
    gl.minY = minYMaxY.minY
    gl.maxY = minYMaxY.maxY
    lowestYInAllSeries = minYMaxY.lowestY

    if (cnf.chart.stacked) {
      this._setStackedMinMax()
    }

    // if the numbers are too big, reduce the range
    // for eg, if number is between 100000-110000, putting 0 as the lowest value is not so good idea. So change the gl.minY for line/area/candlesticks/boxPlot
    if (
      cnf.chart.type === 'line' ||
      cnf.chart.type === 'area' ||
      cnf.chart.type === 'candlestick' ||
      cnf.chart.type === 'boxPlot' ||
      (cnf.chart.type === 'rangeBar' && !gl.isBarHorizontal)
    ) {
      if (
        gl.minY === Number.MIN_VALUE &&
        lowestYInAllSeries !== -Number.MAX_VALUE &&
        lowestYInAllSeries !== gl.maxY // single value possibility
      ) {
        let diff = gl.maxY - lowestYInAllSeries
        if (
          (lowestYInAllSeries >= 0 && lowestYInAllSeries <= 10) ||
          cnf.yaxis[0].min !== undefined ||
          cnf.yaxis[0].max !== undefined
        ) {
          // if minY is already 0/low value, we don't want to go negatives here - so this check is essential.
          diff = 0
        }

        gl.minY = lowestYInAllSeries - (diff * 5) / 100

        /* fix https://github.com/apexcharts/apexcharts.js/issues/614 */
        /* fix https://github.com/apexcharts/apexcharts.js/issues/968 */
        if (lowestYInAllSeries > 0 && gl.minY < 0) {
          gl.minY = 0
        }

        /* fix https://github.com/apexcharts/apexcharts.js/issues/426 */
        gl.maxY = gl.maxY + (diff * 5) / 100
      }
    }

    cnf.yaxis.forEach((yaxe, index) => {
      // override all min/max values by user defined values (y axis)
      if (yaxe.max !== undefined) {
        if (typeof yaxe.max === 'number') {
          gl.maxYArr[index] = yaxe.max
        } else if (typeof yaxe.max === 'function') {
          // fixes apexcharts.js/issues/2098
          gl.maxYArr[index] = yaxe.max(
            gl.isMultipleYAxis ? gl.maxYArr[index] : gl.maxY
          )
        }

        // gl.maxY is for single y-axis chart, it will be ignored in multi-yaxis
        gl.maxY = gl.maxYArr[index]
      }
      if (yaxe.min !== undefined) {
        if (typeof yaxe.min === 'number') {
          gl.minYArr[index] = yaxe.min
        } else if (typeof yaxe.min === 'function') {
          // fixes apexcharts.js/issues/2098
          gl.minYArr[index] = yaxe.min(
            gl.isMultipleYAxis
              ? gl.minYArr[index] === Number.MIN_VALUE
                ? 0
                : gl.minYArr[index]
              : gl.minY
          )
        }
        // gl.minY is for single y-axis chart, it will be ignored in multi-yaxis
        gl.minY = gl.minYArr[index]
      }
    })

    // for horizontal bar charts, we need to check xaxis min/max as user may have specified there
    if (gl.isBarHorizontal) {
      const minmax = ['min', 'max']
      minmax.forEach((m) => {
        if (cnf.xaxis[m] !== undefined && typeof cnf.xaxis[m] === 'number') {
          m === 'min' ? (gl.minY = cnf.xaxis[m]) : (gl.maxY = cnf.xaxis[m])
        }
      })
    }

    // for multi y-axis we need different scales for each
    if (gl.isMultipleYAxis) {
      this.scales.setMultipleYScales()
      gl.minY = lowestYInAllSeries
      gl.yAxisScale.forEach((scale, i) => {
        gl.minYArr[i] = scale.niceMin
        gl.maxYArr[i] = scale.niceMax
      })
    } else {
      this.scales.setYScaleForIndex(0, gl.minY, gl.maxY)
      gl.minY = gl.yAxisScale[0].niceMin
      gl.maxY = gl.yAxisScale[0].niceMax
      gl.minYArr[0] = gl.yAxisScale[0].niceMin
      gl.maxYArr[0] = gl.yAxisScale[0].niceMax
    }

    return {
      minY: gl.minY,
      maxY: gl.maxY,
      minYArr: gl.minYArr,
      maxYArr: gl.maxYArr,
      yAxisScale: gl.yAxisScale,
    }
  }

  setXRange() {
    let gl = this.w.globals
    let cnf = this.w.config

    const isXNumeric =
      cnf.xaxis.type === 'numeric' ||
      cnf.xaxis.type === 'datetime' ||
      (cnf.xaxis.type === 'category' && !gl.noLabelsProvided) ||
      gl.noLabelsProvided ||
      gl.isXNumeric

    const getInitialMinXMaxX = () => {
      for (let i = 0; i < gl.series.length; i++) {
        if (gl.labels[i]) {
          for (let j = 0; j < gl.labels[i].length; j++) {
            if (gl.labels[i][j] !== null && Utils.isNumber(gl.labels[i][j])) {
              gl.maxX = Math.max(gl.maxX, gl.labels[i][j])
              gl.initialMaxX = Math.max(gl.maxX, gl.labels[i][j])
              gl.minX = Math.min(gl.minX, gl.labels[i][j])
              gl.initialMinX = Math.min(gl.minX, gl.labels[i][j])
            }
          }
        }
      }
    }
    // minX maxX starts here
    if (gl.isXNumeric) {
      getInitialMinXMaxX()
    }

    if (gl.noLabelsProvided) {
      if (cnf.xaxis.categories.length === 0) {
        gl.maxX = gl.labels[gl.labels.length - 1]
        gl.initialMaxX = gl.labels[gl.labels.length - 1]
        gl.minX = 1
        gl.initialMinX = 1
      }
    }

    if (gl.isXNumeric || gl.noLabelsProvided || gl.dataFormatXNumeric) {
      let ticks

      if (cnf.xaxis.tickAmount === undefined) {
        ticks = Math.round(gl.svgWidth / 150)

        // no labels provided and total number of dataPoints is less than 30
        if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 30) {
          ticks = gl.dataPoints - 1
        }

        // this check is for when ticks exceeds total datapoints and that would result in duplicate labels
        if (ticks > gl.dataPoints && gl.dataPoints !== 0) {
          ticks = gl.dataPoints - 1
        }
      } else if (cnf.xaxis.tickAmount === 'dataPoints') {
        if (gl.series.length > 1) {
          ticks = gl.series[gl.maxValsInArrayIndex].length - 1
        }
        if (gl.isXNumeric) {
          ticks = gl.maxX - gl.minX - 1
        }
      } else {
        ticks = cnf.xaxis.tickAmount
      }
      gl.xTickAmount = ticks

      // override all min/max values by user defined values (x axis)
      if (cnf.xaxis.max !== undefined && typeof cnf.xaxis.max === 'number') {
        gl.maxX = cnf.xaxis.max
      }
      if (cnf.xaxis.min !== undefined && typeof cnf.xaxis.min === 'number') {
        gl.minX = cnf.xaxis.min
      }

      // if range is provided, adjust the new minX
      if (cnf.xaxis.range !== undefined) {
        gl.minX = gl.maxX - cnf.xaxis.range
      }

      if (gl.minX !== Number.MAX_VALUE && gl.maxX !== -Number.MAX_VALUE) {
        if (cnf.xaxis.convertedCatToNumeric && !gl.dataFormatXNumeric) {
          let catScale = []
          for (let i = gl.minX - 1; i < gl.maxX; i++) {
            catScale.push(i + 1)
          }
          gl.xAxisScale = {
            result: catScale,
            niceMin: catScale[0],
            niceMax: catScale[catScale.length - 1],
          }
        } else {
          gl.xAxisScale = this.scales.setXScale(gl.minX, gl.maxX)
        }
      } else {
        gl.xAxisScale = this.scales.linearScale(
          0,
          ticks,
          ticks,
          0,
          cnf.xaxis.stepSize
        )
        if (gl.noLabelsProvided && gl.labels.length > 0) {
          gl.xAxisScale = this.scales.linearScale(
            1,
            gl.labels.length,
            ticks - 1,
            0,
            cnf.xaxis.stepSize
          )

          // this is the only place seriesX is again mutated
          gl.seriesX = gl.labels.slice()
        }
      }
      // we will still store these labels as the count for this will be different (to draw grid and labels placement)
      if (isXNumeric) {
        gl.labels = gl.xAxisScale.result.slice()
      }
    }

    if (gl.isBarHorizontal && gl.labels.length) {
      gl.xTickAmount = gl.labels.length
    }

    // single dataPoint
    this._handleSingleDataPoint()

    // minimum x difference to calculate bar width in numeric bars
    this._getMinXDiff()

    return {
      minX: gl.minX,
      maxX: gl.maxX,
    }
  }

  setZRange() {
    // minZ, maxZ starts here
    let gl = this.w.globals

    if (!gl.isDataXYZ) return
    for (let i = 0; i < gl.series.length; i++) {
      if (typeof gl.seriesZ[i] !== 'undefined') {
        for (let j = 0; j < gl.seriesZ[i].length; j++) {
          if (gl.seriesZ[i][j] !== null && Utils.isNumber(gl.seriesZ[i][j])) {
            gl.maxZ = Math.max(gl.maxZ, gl.seriesZ[i][j])
            gl.minZ = Math.min(gl.minZ, gl.seriesZ[i][j])
          }
        }
      }
    }
  }

  _handleSingleDataPoint() {
    const gl = this.w.globals
    const cnf = this.w.config

    if (gl.minX === gl.maxX) {
      let datetimeObj = new DateTime(this.ctx)

      if (cnf.xaxis.type === 'datetime') {
        const newMinX = datetimeObj.getDate(gl.minX)
        if (cnf.xaxis.labels.datetimeUTC) {
          newMinX.setUTCDate(newMinX.getUTCDate() - 2)
        } else {
          newMinX.setDate(newMinX.getDate() - 2)
        }

        gl.minX = new Date(newMinX).getTime()

        const newMaxX = datetimeObj.getDate(gl.maxX)
        if (cnf.xaxis.labels.datetimeUTC) {
          newMaxX.setUTCDate(newMaxX.getUTCDate() + 2)
        } else {
          newMaxX.setDate(newMaxX.getDate() + 2)
        }
        gl.maxX = new Date(newMaxX).getTime()
      } else if (
        cnf.xaxis.type === 'numeric' ||
        (cnf.xaxis.type === 'category' && !gl.noLabelsProvided)
      ) {
        gl.minX = gl.minX - 2
        gl.initialMinX = gl.minX
        gl.maxX = gl.maxX + 2
        gl.initialMaxX = gl.maxX
      }
    }
  }

  _getMinXDiff() {
    const gl = this.w.globals

    if (gl.isXNumeric) {
      // get the least x diff if numeric x axis is present
      gl.seriesX.forEach((sX, i) => {
        if (sX.length === 1) {
          // a small hack to prevent overlapping multiple bars when there is just 1 datapoint in bar series.
          // fix #811
          sX.push(
            gl.seriesX[gl.maxValsInArrayIndex][
              gl.seriesX[gl.maxValsInArrayIndex].length - 1
            ]
          )
        }

        // fix #983 (clone the array to avoid side effects)
        const seriesX = sX.slice()
        seriesX.sort((a, b) => a - b)

        seriesX.forEach((s, j) => {
          if (j > 0) {
            let xDiff = s - seriesX[j - 1]
            if (xDiff > 0) {
              gl.minXDiff = Math.min(xDiff, gl.minXDiff)
            }
          }
        })
        if (gl.dataPoints === 1 || gl.minXDiff === Number.MAX_VALUE) {
          // fixes apexcharts.js #1221
          gl.minXDiff = 0.5
        }
      })
    }
  }

  _setStackedMinMax() {
    const gl = this.w.globals
    // for stacked charts, we calculate each series's parallel values. i.e, series[0][j] + series[1][j] .... [series[i.length][j]] and get the max out of it

    if (!gl.series.length) return
    let seriesGroups = gl.seriesGroups

    if (!seriesGroups.length) {
      seriesGroups = [this.w.config.series.map((serie) => serie.name)]
    }
    let stackedPoss = {}
    let stackedNegs = {}

    seriesGroups.forEach((group) => {
      stackedPoss[group] = []
      stackedNegs[group] = []
      const indicesOfSeriesInGroup = this.w.config.series
        .map((serie, si) => (group.indexOf(serie.name) > -1 ? si : null))
        .filter((f) => f !== null)

      indicesOfSeriesInGroup.forEach((i) => {
        for (let j = 0; j < gl.series[gl.maxValsInArrayIndex].length; j++) {
          if (typeof stackedPoss[group][j] === 'undefined') {
            stackedPoss[group][j] = 0
            stackedNegs[group][j] = 0
          }

          let stackSeries =
            (this.w.config.chart.stacked && !gl.comboCharts) ||
            (this.w.config.chart.stacked &&
              gl.comboCharts &&
              (!this.w.config.chart.stackOnlyBar ||
                this.w.config.series?.[i]?.type === 'bar'))

          if (stackSeries) {
            if (gl.series[i][j] !== null && Utils.isNumber(gl.series[i][j])) {
              gl.series[i][j] > 0
                ? (stackedPoss[group][j] +=
                    parseFloat(gl.series[i][j]) + 0.0001)
                : (stackedNegs[group][j] += parseFloat(gl.series[i][j]))
            }
          }
        }
      })
    })

    Object.entries(stackedPoss).forEach(([key]) => {
      stackedPoss[key].forEach((_, stgi) => {
        gl.maxY = Math.max(gl.maxY, stackedPoss[key][stgi])
        gl.minY = Math.min(gl.minY, stackedNegs[key][stgi])
      })
    })
  }
}

export default Range
