import Utils from '../utils/Utils'
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 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 (this.w.config.chart.type === 'candlestick') {
      seriesMin = gl.seriesCandleL
      seriesMax = gl.seriesCandleH
    } 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)

      for (let j = 0; j < gl.series[i].length; j++) {
        let val = series[i][j]
        if (val !== null && Utils.isNumber(val)) {
          maxY = Math.max(maxY, seriesMax[i][j])
          lowestY = Math.min(lowestY, seriesMin[i][j])
          highestY = Math.max(highestY, seriesMin[i][j])

          if (this.w.config.chart.type === 'candlestick') {
            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])
            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
        }
      }
    }

    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) {
      // 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
      let stackedPoss = []
      let stackedNegs = []

      for (let j = 0; j < gl.series[gl.maxValsInArrayIndex].length; j++) {
        let poss = 0
        let negs = 0
        for (let i = 0; i < gl.series.length; i++) {
          if (gl.series[i][j] !== null && Utils.isNumber(gl.series[i][j])) {
            if (gl.series[i][j] > 0) {
              // 0.0001 fixes #185 when values are very small
              poss = poss + parseFloat(gl.series[i][j]) + 0.0001
            } else {
              negs = negs + parseFloat(gl.series[i][j])
            }
          }

          if (i === gl.series.length - 1) {
            // push all the totals to the array for future use
            stackedPoss.push(poss)
            stackedNegs.push(negs)
          }
        }
      }

      // get the max/min out of the added parallel values
      for (let z = 0; z < stackedPoss.length; z++) {
        gl.maxY = Math.max(gl.maxY, stackedPoss[z])
        gl.minY = Math.min(gl.minY, stackedNegs[z])
      }
    }

    // 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
    if (
      cnf.chart.type === 'line' ||
      cnf.chart.type === 'area' ||
      cnf.chart.type === 'candlestick'
    ) {
      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) {
          // 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
        // if (lowestYInAllSeries > 0 && gl.minY < 0) {
        /* fix https://github.com/apexcharts/apexcharts.js/issues/614 */
        //  gl.minY = 0
        // }
        /* fix https://github.com/apexcharts/apexcharts.js/issues/426 */
        gl.maxY = gl.maxY + (diff * 5) / 100
      }
    }

    cnf.yaxis.map((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') {
          gl.maxYArr[index] = yaxe.max(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') {
          gl.minYArr[index] = yaxe.min(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) {
      if (cnf.xaxis.min !== undefined && typeof cnf.xaxis.min === 'number') {
        gl.minY = cnf.xaxis.min
      }

      if (cnf.xaxis.max !== undefined && typeof cnf.xaxis.max === 'number') {
        gl.maxY = cnf.xaxis.max
      }
    }

    // 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
    }
  }

  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

    // minX maxX starts here
    if (gl.isXNumeric) {
      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])
            }
          }
        }
      }
    }

    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
      }
    }

    // for numeric xaxis, we need to adjust some padding left and right for bar charts
    if (
      gl.comboChartsHasBars ||
      cnf.chart.type === 'candlestick' ||
      (cnf.chart.type === 'bar' && gl.isXNumeric)
    ) {
      if (cnf.xaxis.type !== 'category' || gl.isXNumeric) {
        const t =
          (gl.svgWidth / gl.dataPoints) *
          (Math.abs(gl.maxX - gl.minX) / gl.svgWidth)

        // some padding to the left to prevent cropping of the bars
        const minX = gl.minX - t / 2
        gl.minX = minX
        gl.initialminX = minX

        // some padding to the right to prevent cropping of the bars
        const maxX = gl.maxX + t / ((gl.series.length + 1) / gl.series.length)
        gl.maxX = maxX
        gl.initialmaxX = maxX
      }
    }

    if (
      (gl.isXNumeric || gl.noLabelsProvided) &&
      (!cnf.xaxis.convertedCatToNumeric || 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 20
        if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 20) {
          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') {
        ticks = gl.series[gl.maxValsInArrayIndex].length - 1
      } else {
        ticks = cnf.xaxis.tickAmount
      }

      // 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) {
        gl.xAxisScale = this.scales.linearScale(gl.minX, gl.maxX, ticks)
      } else {
        gl.xAxisScale = this.scales.linearScale(1, ticks, ticks)
        if (gl.noLabelsProvided && gl.labels.length > 0) {
          gl.xAxisScale = this.scales.linearScale(
            1,
            gl.labels.length,
            ticks - 1
          )

          // 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.minX === gl.maxX) {
      // single dataPoint
      if (cnf.xaxis.type === 'datetime') {
        const newMinX = new Date(gl.minX)
        newMinX.setDate(newMinX.getDate() - 2)
        gl.minX = new Date(newMinX).getTime()

        const newMaxX = new Date(gl.maxX)
        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.maxX = gl.maxX + 2
      }
    }

    if (gl.isXNumeric) {
      // get the least x diff if numeric x axis is present
      gl.seriesX.forEach((sX, i) => {
        sX.forEach((s, j) => {
          if (j > 0) {
            let xDiff = s - gl.seriesX[i][j - 1]
            gl.minXDiff = Math.min(xDiff, gl.minXDiff)
          }
        })
      })

      this.calcMinXDiffForTinySeries()
    }

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

  calcMinXDiffForTinySeries() {
    const w = this.w

    let len = w.globals.labels.length

    if (w.globals.labels.length === 1) {
      w.globals.minXDiff = (w.globals.maxX - w.globals.minX) / len / 3
    } else {
      if (w.globals.minXDiff === Number.MAX_VALUE) {
        // possibly a single dataPoint (fixes react-apexcharts/issue#34)
        if (w.globals.timelineLabels.length > 0) {
          len = w.globals.timelineLabels.length
        }
        if (len < 3) {
          len = 3
        }
        w.globals.minXDiff = (w.globals.maxX - w.globals.minX) / len
      }
    }

    return w.globals.minXDiff
  }

  setZRange() {
    let gl = this.w.globals

    // minZ, maxZ starts here
    if (gl.isDataXYZ) {
      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])
            }
          }
        }
      }
    }
  }
}

export default Range
