import Utils from '../utils/Utils'

export default class Range {
  constructor(ctx) {
    this.ctx = ctx
    this.w = ctx.w
  }

  // http://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axiss
  // This routine creates the Y axis values for a graph.
  niceScale(yMin, yMax, ticks = 10, index = 0, NO_MIN_MAX_PROVIDED) {
    const w = this.w
    // Determine Range
    let range = Math.abs(yMax - yMin)

    ticks = this._adjustTicksForSmallRange(ticks, index, range)

    if (ticks === 'dataPoints') {
      ticks = w.globals.dataPoints - 1
    }

    if (
      (yMin === Number.MIN_VALUE && yMax === 0) ||
      (!Utils.isNumber(yMin) && !Utils.isNumber(yMax)) ||
      (yMin === Number.MIN_VALUE && yMax === -Number.MAX_VALUE)
    ) {
      // when all values are 0
      yMin = 0
      yMax = ticks
      let linearScale = this.linearScale(yMin, yMax, ticks)
      return linearScale
    }

    if (yMin > yMax) {
      // if somehow due to some wrong config, user sent max less than min,
      // adjust the min/max again
      console.warn('axis.min cannot be greater than axis.max')
      yMax = yMin + 0.1
    } else if (yMin === yMax) {
      // If yMin and yMax are identical, then
      // adjust the yMin and yMax values to actually
      // make a graph. Also avoids division by zero errors.
      yMin = yMin === 0 ? 0 : yMin - 0.5 // some small value
      yMax = yMax === 0 ? 2 : yMax + 0.5 // some small value
    }

    // Calculate Min amd Max graphical labels and graph
    // increments.  The number of ticks defaults to
    // 10 which is the SUGGESTED value.  Any tick value
    // entered is used as a suggested value which is
    // adjusted to be a 'pretty' value.
    //
    // Output will be an array of the Y axis values that
    // encompass the Y values.
    let result = []

    if (
      range < 1 &&
      NO_MIN_MAX_PROVIDED &&
      (w.config.chart.type === 'candlestick' ||
        w.config.series[index].type === 'candlestick' ||
        w.config.chart.type === 'boxPlot' ||
        w.config.series[index].type === 'boxPlot' ||
        w.globals.isRangeData)
    ) {
      /* fix https://github.com/apexcharts/apexcharts.js/issues/430 */
      yMax = yMax * 1.01
    }

    let tiks = ticks + 1
    // Adjust ticks if needed
    if (tiks < 2) {
      tiks = 2
    } else if (tiks > 2) {
      tiks -= 2
    }

    // Get raw step value
    let tempStep = range / tiks
    // Calculate pretty step value

    let mag = Math.floor(Utils.log10(tempStep))
    let magPow = Math.pow(10, mag)
    let magMsd = Math.round(tempStep / magPow)
    if (magMsd < 1) {
      magMsd = 1
    }
    let stepSize = magMsd * magPow

    // build Y label array.
    // Lower and upper bounds calculations
    let lb = stepSize * Math.floor(yMin / stepSize)
    let ub = stepSize * Math.ceil(yMax / stepSize)
    // Build array
    let val = lb

    if (NO_MIN_MAX_PROVIDED && range > 2) {
      while (1) {
        result.push(val)
        val += stepSize
        if (val > ub) {
          break
        }
      }

      return {
        result,
        niceMin: result[0],
        niceMax: result[result.length - 1]
      }
    } else {
      result = []
      let v = yMin
      result.push(v)
      let valuesDivider = Math.abs(yMax - yMin) / ticks
      for (let i = 0; i <= ticks; i++) {
        v = v + valuesDivider
        result.push(v)
      }

      if (result[result.length - 2] >= yMax) {
        result.pop()
      }

      return {
        result,
        niceMin: result[0],
        niceMax: result[result.length - 1]
      }
    }
  }

  linearScale(yMin, yMax, ticks = 10, index) {
    let range = Math.abs(yMax - yMin)

    ticks = this._adjustTicksForSmallRange(ticks, index, range)

    if (ticks === 'dataPoints') {
      ticks = this.w.globals.dataPoints - 1
    }

    let step = range / ticks
    if (ticks === Number.MAX_VALUE) {
      ticks = 10
      step = 1
    }

    let result = []
    let v = yMin

    while (ticks >= 0) {
      result.push(v)
      v = v + step
      ticks -= 1
    }

    return {
      result,
      niceMin: result[0],
      niceMax: result[result.length - 1]
    }
  }

  logarithmicScaleNice(yMin, yMax, base) {
    // Basic validation to avoid for loop starting at -inf.
    if (yMax <= 0) yMax = Math.max(yMin, base)
    if (yMin <= 0) yMin = Math.min(yMax, base)

    const logs = []

    const logMax = Math.ceil(Math.log(yMax) / Math.log(base) + 1) // Get powers of base for our max and min
    const logMin = Math.floor(Math.log(yMin) / Math.log(base))

    for (let i = logMin; i < logMax; i++) {
      logs.push(Math.pow(base, i))
    }

    return {
      result: logs,
      niceMin: logs[0],
      niceMax: logs[logs.length - 1]
    }
  }

  logarithmicScale(yMin, yMax, base) {
    // Basic validation to avoid for loop starting at -inf.
    if (yMax <= 0) yMax = Math.max(yMin, base)
    if (yMin <= 0) yMin = Math.min(yMax, base)

    const logs = []

    // Get the logarithmic range.
    const logMax = Math.log(yMax) / Math.log(base)
    const logMin = Math.log(yMin) / Math.log(base)

    // Get the exact logarithmic range.
    // (This is the exact number of multiples of the base there are between yMin and yMax).
    const logRange = logMax - logMin

    // Round the logarithmic range to get the number of ticks we will create.
    // If the chosen min/max values are multiples of each other WRT the base, this will be neat.
    // If the chosen min/max aren't, we will at least still provide USEFUL ticks.
    const ticks = Math.round(logRange)

    // Get the logarithmic spacing between ticks.
    const logTickSpacing = logRange / ticks

    // Create as many ticks as there is range in the logs.
    for (
      let i = 0, logTick = logMin;
      i < ticks;
      i++, logTick += logTickSpacing
    ) {
      logs.push(Math.pow(base, logTick))
    }

    // Add a final tick at the yMax.
    logs.push(Math.pow(base, logMax))

    return {
      result: logs,
      niceMin: yMin,
      niceMax: yMax
    }
  }

  _adjustTicksForSmallRange(ticks, index, range) {
    let newTicks = ticks
    if (
      typeof index !== 'undefined' &&
      this.w.config.yaxis[index].labels.formatter &&
      this.w.config.yaxis[index].tickAmount === undefined
    ) {
      const formattedVal = Number(
        this.w.config.yaxis[index].labels.formatter(1)
      )
      if (Utils.isNumber(formattedVal) && this.w.globals.yValueDecimal === 0) {
        newTicks = Math.ceil(range)
      }
    }
    return newTicks < ticks ? newTicks : ticks
  }

  setYScaleForIndex(index, minY, maxY) {
    const gl = this.w.globals
    const cnf = this.w.config

    let y = gl.isBarHorizontal ? cnf.xaxis : cnf.yaxis[index]

    if (typeof gl.yAxisScale[index] === 'undefined') {
      gl.yAxisScale[index] = []
    }

    let diff = Math.abs(maxY - minY)

    if (y.logarithmic && diff <= 5) {
      gl.invalidLogScale = true
    }

    if (y.logarithmic && diff > 5) {
      gl.allSeriesCollapsed = false
      gl.yAxisScale[index] = this.logarithmicScale(minY, maxY, y.logBase)
      gl.yAxisScale[index] = y.forceNiceScale
        ? this.logarithmicScaleNice(minY, maxY, y.logBase)
        : this.logarithmicScale(minY, maxY, y.logBase)
    } else {
      if (maxY === -Number.MAX_VALUE || !Utils.isNumber(maxY)) {
        // no data in the chart. Either all series collapsed or user passed a blank array
        gl.yAxisScale[index] = this.linearScale(0, 5, 5)
      } else {
        // there is some data. Turn off the allSeriesCollapsed flag
        gl.allSeriesCollapsed = false

        if ((y.min !== undefined || y.max !== undefined) && !y.forceNiceScale) {
          // fix https://github.com/apexcharts/apexcharts.js/issues/492
          gl.yAxisScale[index] = this.linearScale(
            minY,
            maxY,
            y.tickAmount,
            index
          )
        } else {
          const noMinMaxProvided =
            (cnf.yaxis[index].max === undefined &&
              cnf.yaxis[index].min === undefined) ||
            cnf.yaxis[index].forceNiceScale
          gl.yAxisScale[index] = this.niceScale(
            minY,
            maxY,
            y.tickAmount ? y.tickAmount : diff < 5 && diff > 1 ? diff + 1 : 5,
            index,
            // fix https://github.com/apexcharts/apexcharts.js/issues/397
            noMinMaxProvided
          )
        }
      }
    }
  }

  setXScale(minX, maxX) {
    const w = this.w
    const gl = w.globals
    const x = w.config.xaxis
    let diff = Math.abs(maxX - minX)
    if (maxX === -Number.MAX_VALUE || !Utils.isNumber(maxX)) {
      // no data in the chart. Either all series collapsed or user passed a blank array
      gl.xAxisScale = this.linearScale(0, 5, 5)
    } else {
      gl.xAxisScale = this.linearScale(
        minX,
        maxX,
        x.tickAmount ? x.tickAmount : diff < 5 && diff > 1 ? diff + 1 : 5,
        0
      )
    }
    return gl.xAxisScale
  }

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

    const minYArr = gl.minYArr.concat([])
    const maxYArr = gl.maxYArr.concat([])

    let scalesIndices = []
    // here, we loop through the yaxis array and find the item which has "seriesName" property
    cnf.yaxis.forEach((yaxe, i) => {
      let index = i
      cnf.series.forEach((s, si) => {
        // if seriesName matches and that series is not collapsed, we use that scale
        // fix issue #1215
        // proceed even if si is in gl.collapsedSeriesIndices
        if (s.name === yaxe.seriesName) {
          index = si

          if (i !== si) {
            scalesIndices.push({
              index: si,
              similarIndex: i,
              alreadyExists: true
            })
          } else {
            scalesIndices.push({
              index: si
            })
          }
        }
      })

      let minY = minYArr[index]
      let maxY = maxYArr[index]

      this.setYScaleForIndex(i, minY, maxY)
    })

    this.sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices)
  }

  sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices) {
    const cnf = this.w.config
    const gl = this.w.globals

    // we got the scalesIndices array in the above code, but we need to filter out the items which doesn't have same scales
    let similarIndices = []
    scalesIndices.forEach((scale) => {
      if (scale.alreadyExists) {
        if (typeof similarIndices[scale.index] === 'undefined') {
          similarIndices[scale.index] = []
        }
        similarIndices[scale.index].push(scale.index)
        similarIndices[scale.index].push(scale.similarIndex)
      }
    })

    function intersect(a, b) {
      return a.filter((value) => b.indexOf(value) !== -1)
    }

    gl.yAxisSameScaleIndices = similarIndices

    similarIndices.forEach((si, i) => {
      similarIndices.forEach((sj, j) => {
        if (i !== j) {
          if (intersect(si, sj).length > 0) {
            similarIndices[i] = similarIndices[i].concat(similarIndices[j])
          }
        }
      })
    })

    // then, we remove duplicates from the similarScale array
    let uniqueSimilarIndices = similarIndices.map((item) => {
      return item.filter((i, pos) => item.indexOf(i) === pos)
    })

    // sort further to remove whole duplicate arrays later
    let sortedIndices = uniqueSimilarIndices.map((s) => s.sort())

    // remove undefined items
    similarIndices = similarIndices.filter((s) => !!s)

    let indices = sortedIndices.slice()
    let stringIndices = indices.map((ind) => JSON.stringify(ind))
    indices = indices.filter(
      (ind, p) => stringIndices.indexOf(JSON.stringify(ind)) === p
    )

    let sameScaleMinYArr = []
    let sameScaleMaxYArr = []
    minYArr.forEach((minYValue, yi) => {
      indices.forEach((scale, i) => {
        // we compare only the yIndex which exists in the indices array
        if (scale.indexOf(yi) > -1) {
          if (typeof sameScaleMinYArr[i] === 'undefined') {
            sameScaleMinYArr[i] = []
            sameScaleMaxYArr[i] = []
          }
          sameScaleMinYArr[i].push({
            key: yi,
            value: minYValue
          })
          sameScaleMaxYArr[i].push({
            key: yi,
            value: maxYArr[yi]
          })
        }
      })
    })

    let sameScaleMin = Array.apply(null, Array(indices.length)).map(
      Number.prototype.valueOf,
      Number.MIN_VALUE
    )
    let sameScaleMax = Array.apply(null, Array(indices.length)).map(
      Number.prototype.valueOf,
      -Number.MAX_VALUE
    )

    sameScaleMinYArr.forEach((s, i) => {
      s.forEach((sc, j) => {
        sameScaleMin[i] = Math.min(sc.value, sameScaleMin[i])
      })
    })

    sameScaleMaxYArr.forEach((s, i) => {
      s.forEach((sc, j) => {
        sameScaleMax[i] = Math.max(sc.value, sameScaleMax[i])
      })
    })

    minYArr.forEach((min, i) => {
      sameScaleMaxYArr.forEach((s, si) => {
        let minY = sameScaleMin[si]
        let maxY = sameScaleMax[si]

        if (cnf.chart.stacked) {
          // for stacked charts, we need to add the values
          maxY = 0

          s.forEach((ind, k) => {
            // fix incorrectly adjust y scale issue #1215
            if (ind.value !== -Number.MAX_VALUE) {
              maxY += ind.value
            }
            if (minY !== Number.MIN_VALUE) {
              minY += sameScaleMinYArr[si][k].value
            }
          })
        }

        s.forEach((ind, k) => {
          if (s[k].key === i) {
            if (cnf.yaxis[i].min !== undefined) {
              if (typeof cnf.yaxis[i].min === 'function') {
                minY = cnf.yaxis[i].min(gl.minY)
              } else {
                minY = cnf.yaxis[i].min
              }
            }
            if (cnf.yaxis[i].max !== undefined) {
              if (typeof cnf.yaxis[i].max === 'function') {
                maxY = cnf.yaxis[i].max(gl.maxY)
              } else {
                maxY = cnf.yaxis[i].max
              }
            }

            this.setYScaleForIndex(i, minY, maxY)
          }
        })
      })
    })
  }

  // experimental feature which scales the y-axis to a min/max based on x-axis range
  autoScaleY(ctx, yaxis, e) {
    if (!ctx) {
      ctx = this
    }

    const w = ctx.w

    if (w.globals.isMultipleYAxis || w.globals.collapsedSeries.length) {
      // The autoScale option for multiple y-axis is turned off as it leads to buggy behavior.
      // Also, when a series is collapsed, it results in incorrect behavior. Hence turned it off for that too - fixes apexcharts.js#795
      console.warn('autoScaleYaxis is not supported in a multi-yaxis chart.')
      return yaxis
    }

    const seriesX = w.globals.seriesX[0]

    let isStacked = w.config.chart.stacked

    yaxis.forEach((yaxe, yi) => {
      let firstXIndex = 0

      for (let xi = 0; xi < seriesX.length; xi++) {
        if (seriesX[xi] >= e.xaxis.min) {
          firstXIndex = xi
          break
        }
      }

      let initialMin = w.globals.minYArr[yi]
      let initialMax = w.globals.maxYArr[yi]
      let min, max

      let stackedSer = w.globals.stackedSeriesTotals

      w.globals.series.forEach((serie, sI) => {
        let firstValue = serie[firstXIndex]

        if (isStacked) {
          firstValue = stackedSer[firstXIndex]
          min = max = firstValue

          stackedSer.forEach((y, yI) => {
            if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
              if (y > max && y !== null) max = y
              if (serie[yI] < min && serie[yI] !== null) min = serie[yI]
            }
          })
        } else {
          min = max = firstValue

          serie.forEach((y, yI) => {
            if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
              let valMin = y
              let valMax = y
              w.globals.series.forEach((wS, wSI) => {
                if (y !== null) {
                  valMin = Math.min(wS[yI], valMin)
                  valMax = Math.max(wS[yI], valMax)
                }
              })
              if (valMax > max && valMax !== null) max = valMax
              if (valMin < min && valMin !== null) min = valMin
            }
          })
        }

        if (min === undefined && max === undefined) {
          min = initialMin
          max = initialMax
        }
        min *= min < 0 ? 1.1 : 0.9
        max *= max < 0 ? 0.9 : 1.1

        if (min === 0 && max === 0) {
          min = -1
          max = 1
        }

        if (max < 0 && max < initialMax) {
          max = initialMax
        }
        if (min < 0 && min > initialMin) {
          min = initialMin
        }

        if (yaxis.length > 1) {
          yaxis[sI].min = yaxe.min === undefined ? min : yaxe.min
          yaxis[sI].max = yaxe.max === undefined ? max : yaxe.max
        } else {
          yaxis[0].min = yaxe.min === undefined ? min : yaxe.min
          yaxis[0].max = yaxe.max === undefined ? max : yaxe.max
        }
      })
    })

    return yaxis
  }
}
