// @ts-check
/** @typedef {import('../../types/apexcharts').ApexYAxis} ApexYAxis */
/*
 ** Util functions which are dependent on ApexCharts instance
 */

class CoreUtils {
  /**
   * @param {import('../types/internal').ChartStateW} w
   */
  constructor(w) {
    this.w = w
  }

  /**
   * @param {any[]} series
   * @param {string} chartType
   */
  static checkComboSeries(series, chartType) {
    let comboCharts = false
    let comboBarCount = 0
    let comboCount = 0

    if (chartType === undefined) {
      chartType = 'line'
    }

    // Check if user specified a type in series that may make us a combo chart.
    // The default type for chart is "line" and the default for series is the
    // chart type, therefore, if the types of all series match the chart type,
    // this should not be considered a combo chart.
    if (series.length && typeof series[0].type !== 'undefined') {
      /**
       * @param {Record<string, any>} s
       */
      series.forEach((s) => {
        if (
          s.type === 'bar' ||
          s.type === 'column' ||
          s.type === 'candlestick' ||
          s.type === 'boxPlot'
        ) {
          comboBarCount++
        }
        if (typeof s.type !== 'undefined' && s.type !== chartType) {
          comboCount++
        }
      })
    }
    if (comboCount > 0) {
      comboCharts = true
    }

    return {
      comboBarCount,
      comboCharts,
    }
  }

  /**
   * @memberof CoreUtils
   * returns the sum of all individual values in a multiple stacked series
   * Eg. w.seriesData.series = [[32,33,43,12], [2,3,5,1]]
   *  @return [34,36,48,13]
   * @param {number[]} excludedSeriesIndices
   **/
  getStackedSeriesTotals(excludedSeriesIndices = []) {
    const w = this.w
    /** @type {any[]} */
    const total = []

    if (w.seriesData.series.length === 0) return total

    for (
      let i = 0;
      i < w.seriesData.series[w.globals.maxValsInArrayIndex].length;
      i++
    ) {
      let t = 0
      for (let j = 0; j < w.seriesData.series.length; j++) {
        if (
          typeof w.seriesData.series[j][i] !== 'undefined' &&
          excludedSeriesIndices.indexOf(j) === -1
        ) {
          t += w.seriesData.series[j][i]
        }
      }
      total.push(t)
    }
    return total
  }

  // get total of the all values inside all series
  /**
   * @param {number | null} [index]
   */
  getSeriesTotalByIndex(index = null) {
    if (index === null) {
      // non-plot chart types - pie / donut / circle
      return /** @type {any[]} */ (this.w.config.series).reduce(
        (/** @type {any} */ acc, /** @type {any} */ cur) => acc + cur,
        0,
      )
    } else {
      // axis charts - supporting multiple series
      return this.w.seriesData.series[index].reduce(
        (/** @type {any} */ acc, /** @type {any} */ cur) => acc + cur,
        0,
      )
    }
  }

  /**
   * @memberof CoreUtils
   * returns the sum of values in a multiple stacked grouped charts
   * Eg. w.seriesData.series = [[32,33,43,12], [2,3,5,1], [43, 23, 34, 22]]
   * series 1 and 2 are in a group, while series 3 is in another group
   *  @return [[34, 36, 48, 12], [43, 23, 34, 22]]
   **/
  getStackedSeriesTotalsByGroups() {
    const w = this.w
    /** @type {any[]} */
    const total = []

    /**
     * @param {string[]} sg
     */
    w.labelData.seriesGroups.forEach((sg) => {
      /** @type {any[]} */
      const includedIndexes = []
      /**
       * @param {number} s
       * @param {number} si
       */
      w.config.series.forEach((s, si) => {
        if (sg.indexOf(w.seriesData.seriesNames[si]) > -1) {
          includedIndexes.push(si)
        }
      })

      const excludedIndices = w.seriesData.series
        /**
         * @param {any} _
         * @param {number} fi
         */
        .map((_, fi) => (includedIndexes.indexOf(fi) === -1 ? fi : -1))
        /**
         * @param {number} f
         */
        .filter((f) => f !== -1)

      total.push(this.getStackedSeriesTotals(excludedIndices))
    })
    return total
  }

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

    // The old config method to map multiple series to a y axis is to
    // include one yaxis config per series but set each yaxis seriesName to the
    // same series name. This relies on indexing equivalence to map series to
    // an axis: series[n] => yaxis[n]. This needs to be retained for compatibility.
    // But we introduce an alternative that explicitly configures yaxis elements
    // with the series that will be referenced to them (seriesName: []). This
    // only requires including the yaxis elements that will be seen on the chart.
    // Old way:
    // ya: s
    // 0: 0
    // 1: 1
    // 2: 1
    // 3: 1
    // 4: 1
    // Axes 0..4 are all scaled and all will be rendered unless the axes are
    // show: false. If the chart is stacked, it's assumed that series 1..4 are
    // the contributing series. This is not particularly intuitive.
    // New way:
    // ya: s
    // 0: [0]
    // 1: [1,2,3,4]
    // If the chart is stacked, it can be assumed that any axis with multiple
    // series is stacked.
    //
    // If this is an old chart and we are being backward compatible, it will be
    // expected that each series is associated with it's corresponding yaxis
    // through their indices, one-to-one.
    // If yaxis.seriesName matches series.name, we have indices yi and si.
    // A name match where yi != si is interpretted as yaxis[yi] and yaxis[si]
    // will both be scaled to fit the combined series[si] and series[yi].
    // Consider series named: S0,S1,S2 and yaxes A0,A1,A2.
    //
    // Example 1: A0 and A1 scaled the same.
    // A0.seriesName: S0
    // A1.seriesName: S0
    // A2.seriesName: S2
    // Then A1 <-> A0
    //
    // Example 2: A0, A1 and A2 all scaled the same.
    // A0.seriesName: S2
    // A1.seriesName: S0
    // A2.seriesName: S1
    // A0 <-> A2, A1 <-> A0, A2 <-> A1 --->>> A0 <-> A1 <-> A2

    /** @type {any[]} */
    let axisSeriesMap = []
    const seriesYAxisReverseMap = []
    /** @type {any[]} */
    const unassignedSeriesIndices = []
    const seriesNameArrayStyle =
      this.w.seriesData.series.length > cnf.yaxis.length ||
      /**
       * @param {ApexYAxis} a
       */
      cnf.yaxis.some((a) => Array.isArray(a.seriesName))

    cnf.series.forEach((_s, /** @type {number} */ i) => {
      unassignedSeriesIndices.push(i)
      seriesYAxisReverseMap.push(null)
    })
    cnf.yaxis.forEach(
      (/** @type {ApexYAxis} */ _yaxe, /** @type {number} */ yi) => {
        axisSeriesMap[yi] = []
      },
    )

    /** @type {any[]} */
    const unassignedYAxisIndices = []

    // here, we loop through the yaxis array and find the item which has "seriesName" property
    /**
     * @param {ApexYAxis} yaxe
     * @param {number} yi
     */
    cnf.yaxis.forEach((yaxe, yi) => {
      let assigned = false
      // Allow seriesName to be either a string (for backward compatibility),
      // in which case, handle multiple yaxes referencing the same series.
      // or an array of strings so that a yaxis can reference multiple series.
      // Feature request #4237
      if (yaxe.seriesName) {
        let seriesNames = []
        if (Array.isArray(yaxe.seriesName)) {
          seriesNames = yaxe.seriesName
        } else {
          seriesNames.push(yaxe.seriesName)
        }
        /**
         * @param {string} name
         */
        seriesNames.forEach((/** @type {any} */ name) => {
          /**
           * @param {Record<string, any>} s
           * @param {number} si
           */
          cnf.series.forEach((s, si) => {
            if (/** @type {any} */ (s).name === name) {
              let remove = si
              if (yi === si || seriesNameArrayStyle) {
                // New style, don't allow series to be double referenced
                if (
                  !seriesNameArrayStyle ||
                  unassignedSeriesIndices.indexOf(si) > -1
                ) {
                  axisSeriesMap[yi].push([yi, si])
                } else {
                  console.warn(
                    "Series '" +
                      /** @type {any} */ (s).name +
                      "' referenced more than once in what looks like the new style." +
                      ' That is, when using either seriesName: [],' +
                      ' or when there are more series than yaxes.',
                  )
                }
              } else {
                // The series index refers to the target yaxis and the current
                // yaxis index refers to the actual referenced series.
                axisSeriesMap[si].push([si, yi])
                remove = yi
              }
              assigned = true
              remove = unassignedSeriesIndices.indexOf(remove)
              if (remove !== -1) {
                unassignedSeriesIndices.splice(remove, 1)
              }
            }
          })
        })
      }
      if (!assigned) {
        unassignedYAxisIndices.push(yi)
      }
    })
    axisSeriesMap = axisSeriesMap.map((yaxe) => {
      /** @type {any[]} */
      const ra = []
      /**
       * @param {any[]} sa
       */
      yaxe.forEach((/** @type {any} */ sa) => {
        seriesYAxisReverseMap[sa[1]] = sa[0]
        ra.push(sa[1])
      })
      return ra
    })

    // All series referenced directly by yaxes have been assigned to those axes.
    // Any series so far unassigned will be assigned to any yaxes that have yet
    // to reference series directly, one-for-one in order of appearance, with
    // all left-over series assigned to either the last unassigned yaxis, or the
    // last yaxis if all have assigned series. This captures the
    // default single and multiaxis config options which simply includes zero,
    // one or as many yaxes as there are series but do not reference them by name.
    let lastUnassignedYAxis = cnf.yaxis.length - 1
    for (let i = 0; i < unassignedYAxisIndices.length; i++) {
      lastUnassignedYAxis = unassignedYAxisIndices[i]
      axisSeriesMap[lastUnassignedYAxis] = []
      if (unassignedSeriesIndices) {
        /** @type {any} */
        const si = unassignedSeriesIndices[0]
        unassignedSeriesIndices.shift()
        axisSeriesMap[lastUnassignedYAxis].push(si)
        seriesYAxisReverseMap[si] = lastUnassignedYAxis
      } else {
        break
      }
    }

    unassignedSeriesIndices.forEach((i) => {
      axisSeriesMap[lastUnassignedYAxis].push(i)
      seriesYAxisReverseMap[i] = lastUnassignedYAxis
    })

    // For the old-style seriesName-as-string-only, leave the zero-length yaxis
    // array elements in for compatibility so that series.length == yaxes.length
    // for multi axis charts.
    gl.seriesYAxisMap = axisSeriesMap.map((x) => x)
    gl.seriesYAxisReverseMap = seriesYAxisReverseMap.map((x) => x)
    // Set default series group names
    /**
     * @param {Record<string, any>} axisSeries
     * @param {number} ai
     */
    gl.seriesYAxisMap.forEach((axisSeries, ai) => {
      /**
       * @param {number} si
       */
      axisSeries.forEach((si) => {
        // series may be bare until loaded in realtime
        if (
          /** @type {any} */ (cnf.series[si]) &&
          /** @type {any} */ (cnf.series[si]).group === undefined
        ) {
          // A series with no group defined will be named after the axis that
          // referenced it and thus form a group automatically.
          const _series = /** @type {any} */ (cnf.series[si])
          _series.group = 'apexcharts-axis-'.concat(ai.toString())
        }
      })
    })
  }

  /**
   * @param {number | null} [index]
   */
  isSeriesNull(index = null) {
    let r = []
    if (index === null) {
      // non-plot chart types - pie / donut / circle
      r = /** @type {any[]} */ (this.w.config.series).filter(
        (/** @type {any} */ d) => d !== null,
      )
    } else {
      // axis charts - supporting multiple series
      r = /** @type {Record<string,any>} */ (
        this.w.config.series[index]
      ).data.filter((/** @type {any} */ d) => d !== null)
    }

    return r.length === 0
  }

  /**
   * @param {number} index
   */
  seriesHaveSameValues(index) {
    /**
     * @param {number} val
     * @param {number} i
     * @param {any[]} arr
     */
    return this.w.seriesData.series[index].every(
      (
        /** @type {any} */ val,
        /** @type {number} */ i,
        /** @type {any} */ arr,
      ) => val === arr[0],
    )
  }

  /**
   * @param {any[]} labels
   */
  getCategoryLabels(labels) {
    const w = this.w
    let catLabels = labels.slice()
    if (w.config.xaxis.convertedCatToNumeric) {
      /**
       * @param {number} i
       */
      catLabels = labels.map((/** @type {number} */ i) => {
        return w.config.xaxis.labels.formatter(i - w.globals.minX + 1)
      })
    }
    return catLabels
  }
  // maxValsInArrayIndex is the index of series[] which has the largest number of items
  getLargestSeries() {
    const w = this.w
    w.globals.maxValsInArrayIndex = w.seriesData.series
      /**
       * @param {number[]} a
       */
      .map((a) => a.length)
      .indexOf(
        Math.max.apply(
          Math,
          /**
           * @param {number[]} a
           */
          w.seriesData.series.map((a) => a.length),
        ),
      )
  }

  getLargestMarkerSize() {
    const w = this.w
    let size = 0

    w.globals.markers.size.forEach((/** @type {number} */ m) => {
      size = Math.max(size, m)
    })

    if (w.config.markers.discrete && w.config.markers.discrete.length) {
      /**
       * @param {Record<string, any>} m
       */
      w.config.markers.discrete.forEach((/** @type {any} */ m) => {
        size = Math.max(size, m.size)
      })
    }

    if (size > 0) {
      if (w.config.markers.hover.size > 0) {
        size = w.config.markers.hover.size
      } else {
        size += w.config.markers.hover.sizeOffset
      }
    }

    w.globals.markers.largestSize = size

    return size
  }

  /**
   * @memberof Core
   * returns the sum of all values in a series
   * Eg. w.seriesData.series = [[32,33,43,12], [2,3,5,1]]
   *  @return [120, 11]
   **/
  getSeriesTotals() {
    const w = this.w

    /**
     * @param {any[]} ser
     */
    w.globals.seriesTotals = w.seriesData.series.map((ser) => {
      let total = 0

      if (Array.isArray(ser)) {
        for (let j = 0; j < ser.length; j++) {
          total += ser[j]
        }
      } else {
        // for pie/donuts/gauges
        total += ser
      }

      return total
    })
  }

  /**
   * @param {number} minX
   * @param {number} maxX
   */
  getSeriesTotalsXRange(minX, maxX) {
    const w = this.w

    /**
     * @param {any[]} ser
     * @param {number} index
     */
    const seriesTotalsXRange = w.seriesData.series.map((ser, index) => {
      let total = 0

      for (let j = 0; j < ser.length; j++) {
        if (
          w.seriesData.seriesX[index][j] > minX &&
          w.seriesData.seriesX[index][j] < maxX
        ) {
          total += ser[j]
        }
      }

      return total
    })

    return seriesTotalsXRange
  }

  /**
   * @memberof CoreUtils
   * returns the percentage value of all individual values which can be used in a 100% stacked series
   * Eg. w.seriesData.series = [[32, 33, 43, 12], [2, 3, 5, 1]]
   *  @return [[94.11, 91.66, 89.58, 92.30], [5.88, 8.33, 10.41, 7.7]]
   **/
  getPercentSeries() {
    const w = this.w

    /**
     * @param {any[]} ser
     */
    w.globals.seriesPercent = w.seriesData.series.map((ser) => {
      const seriesPercent = []
      if (Array.isArray(ser)) {
        for (let j = 0; j < ser.length; j++) {
          const total = w.seriesData.stackedSeriesTotals[j]
          let percent = 0
          if (total) {
            percent = (100 * ser[j]) / total
          }
          seriesPercent.push(percent)
        }
      } else {
        /**
         * @param {number} acc
         * @param {number} val
         */
        const total = w.globals.seriesTotals.reduce((acc, val) => acc + val, 0)
        const percent = (100 * ser) / total
        seriesPercent.push(percent)
      }

      return seriesPercent
    })
  }

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

    /** @type {any[]} */
    const yRatio = []
    let invertedYRatio = 0
    let xRatio = 0
    let invertedXRatio = 0
    let zRatio = 0
    let baseLineY = []
    let baseLineInvertedY = 0.1
    let baseLineX = 0

    gl.yRange = []
    if (gl.isMultipleYAxis) {
      for (let i = 0; i < gl.minYArr.length; i++) {
        gl.yRange.push(Math.abs(gl.minYArr[i] - gl.maxYArr[i]))
        baseLineY.push(0)
      }
    } else {
      gl.yRange.push(Math.abs(gl.minY - gl.maxY))
    }
    gl.xRange = Math.abs(gl.maxX - gl.minX)
    gl.zRange = Math.abs(gl.maxZ - gl.minZ)

    // multiple y axis
    for (let i = 0; i < gl.yRange.length; i++) {
      yRatio.push(gl.yRange[i] / this.w.layout.gridHeight)
    }

    xRatio = gl.xRange / this.w.layout.gridWidth

    invertedYRatio = /** @type {any} */ (gl.yRange) / this.w.layout.gridWidth
    invertedXRatio = gl.xRange / this.w.layout.gridHeight
    zRatio = (gl.zRange / this.w.layout.gridHeight) * 16

    if (!zRatio) {
      zRatio = 1
    }

    if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
      // Negative numbers present in series
      const _hasNegsGl = /** @type {any} */ (gl)
      _hasNegsGl.hasNegs = true
    }

    // Check we have a map as series may still to be added/updated.
    if (w.globals.seriesYAxisReverseMap.length > 0) {
      /**
       * @param {number} y
       * @param {number} i
       */
      const scaleBaseLineYScale = (y, i) => {
        const yAxis = w.config.yaxis[w.globals.seriesYAxisReverseMap[i]]
        const sign = y < 0 ? -1 : 1
        y = Math.abs(y)
        if (yAxis.logarithmic) {
          y = this.getBaseLog(yAxis.logBase, y)
        }
        return (-sign * y) / yRatio[i]
      }
      if (gl.isMultipleYAxis) {
        baseLineY = []
        // baseline variables is the 0 of the yaxis which will be needed when there are negatives
        for (let i = 0; i < yRatio.length; i++) {
          baseLineY.push(scaleBaseLineYScale(gl.minYArr[i], i))
        }
      } else {
        baseLineY = []
        baseLineY.push(scaleBaseLineYScale(gl.minY, 0))

        if (gl.minY !== Number.MIN_VALUE && Math.abs(gl.minY) !== 0) {
          baseLineInvertedY = -gl.minY / invertedYRatio // this is for bar chart
          baseLineX = gl.minX / xRatio
        }
      }
    } else {
      baseLineY = []
      baseLineY.push(0)
      baseLineInvertedY = 0
      baseLineX = 0
    }

    return {
      yRatio,
      invertedYRatio,
      zRatio,
      xRatio,
      invertedXRatio,
      baseLineInvertedY,
      baseLineY,
      baseLineX,
    }
  }

  /**
   * @param {any[]} series
   */
  getLogSeries(series) {
    const w = this.w

    /**
     * @param {number[]} s
     * @param {number} i
     */
    w.globals.seriesLog = series.map((s, i) => {
      const yAxisIndex = w.globals.seriesYAxisReverseMap[i]
      if (
        w.config.yaxis[yAxisIndex] &&
        w.config.yaxis[yAxisIndex].logarithmic
      ) {
        /**
         * @param {number | null} d
         */
        return s.map((/** @type {any} */ d) => {
          if (d === null) return null
          return this.getLogVal(w.config.yaxis[yAxisIndex].logBase, d, i)
        })
      } else {
        return s
      }
    })

    return w.globals.invalidLogScale ? series : w.globals.seriesLog
  }

  /**
   * @param {number} val
   * @param {number} seriesIndex
   * @returns {number}
   */
  getLogValAtSeriesIndex(val, seriesIndex) {
    if (val === null) return /** @type {any} */ (null)
    const w = this.w
    const yAxisIndex = w.globals.seriesYAxisReverseMap[seriesIndex]
    if (w.config.yaxis[yAxisIndex] && w.config.yaxis[yAxisIndex].logarithmic) {
      return this.getLogVal(
        w.config.yaxis[yAxisIndex].logBase,
        val,
        seriesIndex,
      )
    }
    return val
  }

  /**
   * @param {number} base
   * @param {number} value
   */
  getBaseLog(base, value) {
    return Math.log(value) / Math.log(base)
  }
  /**
   * @param {number} b
   * @param {number} d
   * @param {number} seriesIndex
   */
  getLogVal(b, d, seriesIndex) {
    if (d <= 0) {
      return 0 // Should be Number.NEGATIVE_INFINITY
    }
    const w = this.w
    const min_log_val =
      w.globals.minYArr[seriesIndex] === 0
        ? -1 // make sure we dont calculate log of 0
        : this.getBaseLog(b, w.globals.minYArr[seriesIndex])
    const max_log_val =
      w.globals.maxYArr[seriesIndex] === 0
        ? 0 // make sure we dont calculate log of 0
        : this.getBaseLog(b, w.globals.maxYArr[seriesIndex])
    const number_of_height_levels = max_log_val - min_log_val
    if (d < 1) return d / number_of_height_levels
    const log_height_value = this.getBaseLog(b, d) - min_log_val
    return log_height_value / number_of_height_levels
  }

  /**
   * @param {number[]} yRatio
   */
  getLogYRatios(yRatio) {
    const w = this.w
    const gl = this.w.globals
    const _gl = /** @type {any} */ (gl)
    _gl.yLogRatio = yRatio.slice()

    _gl.logYRange = /** @type {any[]} */ (gl.yRange).map(
      (_, /** @type {number} */ i) => {
        const yAxisIndex = w.globals.seriesYAxisReverseMap[i]
        if (
          w.config.yaxis[yAxisIndex] &&
          this.w.config.yaxis[yAxisIndex].logarithmic
        ) {
          let maxY = -Number.MAX_VALUE
          let minY = Number.MIN_VALUE
          let range = 1
          /** @type {any[]} */ gl.seriesLog.forEach(
            (/** @type {any[]} */ s, /** @type {number} */ si) => {
              s.forEach((/** @type {number} */ v) => {
                if (w.config.yaxis[si] && w.config.yaxis[si].logarithmic) {
                  maxY = Math.max(v, maxY)
                  minY = Math.min(v, minY)
                }
              })
            },
          )

          range = Math.pow(gl.yRange[i], Math.abs(minY - maxY) / gl.yRange[i])

          _gl.yLogRatio[i] = range / this.w.layout.gridHeight
          return range
        }
      },
    )

    return _gl.invalidLogScale ? yRatio.slice() : _gl.yLogRatio
  }

  // Some config objects can be array - and we need to extend them correctly
  /**
   * @param {any} configInstance
   * @param {Record<string, any>} options
   * @param {import('../types/internal').ChartStateW} w
   */
  static extendArrayProps(configInstance, options, w) {
    if (options?.yaxis) {
      options = configInstance.extendYAxis(options, w)
    }
    if (options?.annotations) {
      if (options.annotations.yaxis) {
        options = configInstance.extendYAxisAnnotations(options)
      }
      if (options?.annotations?.xaxis) {
        options = configInstance.extendXAxisAnnotations(options)
      }
      if (options?.annotations?.points) {
        options = configInstance.extendPointAnnotations(options)
      }
    }

    return options
  }

  // Series of the same group and type can be stacked together distinct from
  // other series of the same type on the same axis.
  /**
   * @param {Record<string, any>} typeSeries
   * @param {string[]} typeGroups
   * @param {string} type
   * @param {string} chartClass
   */
  drawSeriesByGroup(typeSeries, typeGroups, type, chartClass) {
    const w = this.w
    /** @type {any[]} */
    const graph = []
    if (typeSeries.series.length > 0) {
      // draw each group separately
      /**
       * @param {string} gn
       */
      typeGroups.forEach((/** @type {any} */ gn) => {
        /** @type {any[]} */
        const gs = []
        /** @type {any[]} */
        const gi = []
        /**
         * @param {number} i
         * @param {number} ii
         */
        typeSeries.i.forEach((/** @type {any} */ i, /** @type {any} */ ii) => {
          if (
            /** @type {Record<string,any>} */ (w.config.series[i]).group === gn
          ) {
            gs.push(typeSeries.series[ii])
            gi.push(i)
          }
        })
        gs.length > 0 &&
          graph.push(/** @type {any} */ (chartClass).draw(gs, type, gi))
      })
    }
    return graph
  }
}

export default CoreUtils
