/*
 ** Generic functions which are not dependent on ApexCharts
 */

class Utils {
  static bind(fn, me) {
    return function () {
      return fn.apply(me, arguments)
    }
  }

  static isObject(item) {
    return (
      item && typeof item === 'object' && !Array.isArray(item) && item != null
    )
  }

  // Type checking that works across different window objects
  static is(type, val) {
    return Object.prototype.toString.call(val) === '[object ' + type + ']'
  }

  static listToArray(list) {
    let i,
      array = []
    for (i = 0; i < list.length; i++) {
      array[i] = list[i]
    }
    return array
  }

  // to extend defaults with user options
  // credit: http://stackoverflow.com/questions/27936772/deep-object-merging-in-es6-es7#answer-34749873
  static extend(target, source) {
    if (typeof Object.assign !== 'function') {
      ;(function () {
        Object.assign = function (target) {
          'use strict'
          // We must check against these specific cases.
          if (target === undefined || target === null) {
            throw new TypeError('Cannot convert undefined or null to object')
          }

          let output = Object(target)
          for (let index = 1; index < arguments.length; index++) {
            let source = arguments[index]
            if (source !== undefined && source !== null) {
              for (let nextKey in source) {
                if (source.hasOwnProperty(nextKey)) {
                  output[nextKey] = source[nextKey]
                }
              }
            }
          }
          return output
        }
      })()
    }

    let output = Object.assign({}, target)
    if (this.isObject(target) && this.isObject(source)) {
      Object.keys(source).forEach((key) => {
        if (this.isObject(source[key])) {
          if (!(key in target)) {
            Object.assign(output, {
              [key]: source[key],
            })
          } else {
            output[key] = this.extend(target[key], source[key])
          }
        } else {
          Object.assign(output, {
            [key]: source[key],
          })
        }
      })
    }
    return output
  }

  static extendArray(arrToExtend, resultArr) {
    let extendedArr = []
    arrToExtend.map((item) => {
      extendedArr.push(Utils.extend(resultArr, item))
    })
    arrToExtend = extendedArr
    return arrToExtend
  }

  // If month counter exceeds 12, it starts again from 1
  static monthMod(month) {
    return month % 12
  }

  static clone(source, visited = new WeakMap()) {
    if (source === null || typeof source !== 'object') {
      return source
    }

    if (visited.has(source)) {
      return visited.get(source)
    }

    let cloneResult

    if (Array.isArray(source)) {
      cloneResult = []
      visited.set(source, cloneResult)
      for (let i = 0; i < source.length; i++) {
        cloneResult[i] = this.clone(source[i], visited)
      }
    } else if (source instanceof Date) {
      cloneResult = new Date(source.getTime())
    } else {
      cloneResult = {}
      visited.set(source, cloneResult)
      for (let prop in source) {
        if (source.hasOwnProperty(prop)) {
          cloneResult[prop] = this.clone(source[prop], visited)
        }
      }
    }
    return cloneResult
  }

  static log10(x) {
    return Math.log(x) / Math.LN10
  }

  static roundToBase10(x) {
    return Math.pow(10, Math.floor(Math.log10(x)))
  }

  static roundToBase(x, base) {
    return Math.pow(base, Math.floor(Math.log(x) / Math.log(base)))
  }

  static parseNumber(val) {
    if (val === null) return val
    return parseFloat(val)
  }

  static stripNumber(num, precision = 2) {
    return Number.isInteger(num) ? num : parseFloat(num.toPrecision(precision))
  }

  static randomId() {
    return (Math.random() + 1).toString(36).substring(4)
  }

  static noExponents(num) {
    // Check if the number contains 'e' (exponential notation)
    if (num.toString().includes('e')) {
      return Math.round(num) // Round the number
    }
    return num // Return as-is if no exponential notation
  }

  static elementExists(element) {
    if (!element || !element.isConnected) {
      return false
    }
    return true
  }

  static getDimensions(el) {
    const computedStyle = getComputedStyle(el, null)

    let elementHeight = el.clientHeight
    let elementWidth = el.clientWidth
    elementHeight -=
      parseFloat(computedStyle.paddingTop) +
      parseFloat(computedStyle.paddingBottom)
    elementWidth -=
      parseFloat(computedStyle.paddingLeft) +
      parseFloat(computedStyle.paddingRight)

    return [elementWidth, elementHeight]
  }

  static getBoundingClientRect(element) {
    const rect = element.getBoundingClientRect()
    return {
      top: rect.top,
      right: rect.right,
      bottom: rect.bottom,
      left: rect.left,
      width: element.clientWidth,
      height: element.clientHeight,
      x: rect.left,
      y: rect.top,
    }
  }

  static getLargestStringFromArr(arr) {
    return arr.reduce((a, b) => {
      if (Array.isArray(b)) {
        b = b.reduce((aa, bb) => (aa.length > bb.length ? aa : bb))
      }
      return a.length > b.length ? a : b
    }, 0)
  }

  // http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-12342275
  static hexToRgba(hex = '#999999', opacity = 0.6) {
    if (hex.substring(0, 1) !== '#') {
      hex = '#999999'
    }

    let h = hex.replace('#', '')
    h = h.match(new RegExp('(.{' + h.length / 3 + '})', 'g'))

    for (let i = 0; i < h.length; i++) {
      h[i] = parseInt(h[i].length === 1 ? h[i] + h[i] : h[i], 16)
    }

    if (typeof opacity !== 'undefined') h.push(opacity)

    return 'rgba(' + h.join(',') + ')'
  }

  static getOpacityFromRGBA(rgba) {
    return parseFloat(rgba.replace(/^.*,(.+)\)/, '$1'))
  }

  static rgb2hex(rgb) {
    rgb = rgb.match(
      /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i
    )
    return rgb && rgb.length === 4
      ? '#' +
          ('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) +
          ('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) +
          ('0' + parseInt(rgb[3], 10).toString(16)).slice(-2)
      : ''
  }

  shadeRGBColor(percent, color) {
    let f = color.split(','),
      t = percent < 0 ? 0 : 255,
      p = percent < 0 ? percent * -1 : percent,
      R = parseInt(f[0].slice(4), 10),
      G = parseInt(f[1], 10),
      B = parseInt(f[2], 10)
    return (
      'rgb(' +
      (Math.round((t - R) * p) + R) +
      ',' +
      (Math.round((t - G) * p) + G) +
      ',' +
      (Math.round((t - B) * p) + B) +
      ')'
    )
  }

  shadeHexColor(percent, color) {
    let f = parseInt(color.slice(1), 16),
      t = percent < 0 ? 0 : 255,
      p = percent < 0 ? percent * -1 : percent,
      R = f >> 16,
      G = (f >> 8) & 0x00ff,
      B = f & 0x0000ff
    return (
      '#' +
      (
        0x1000000 +
        (Math.round((t - R) * p) + R) * 0x10000 +
        (Math.round((t - G) * p) + G) * 0x100 +
        (Math.round((t - B) * p) + B)
      )
        .toString(16)
        .slice(1)
    )
  }

  // beautiful color shading blending code
  // http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
  shadeColor(p, color) {
    if (Utils.isColorHex(color)) {
      return this.shadeHexColor(p, color)
    } else {
      return this.shadeRGBColor(p, color)
    }
  }

  static isColorHex(color) {
    return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)/i.test(color)
  }

  static getPolygonPos(size, dataPointsLen) {
    let dotsArray = []
    let angle = (Math.PI * 2) / dataPointsLen
    for (let i = 0; i < dataPointsLen; i++) {
      let curPos = {}
      curPos.x = size * Math.sin(i * angle)
      curPos.y = -size * Math.cos(i * angle)
      dotsArray.push(curPos)
    }
    return dotsArray
  }

  static polarToCartesian(centerX, centerY, radius, angleInDegrees) {
    let angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0

    return {
      x: centerX + radius * Math.cos(angleInRadians),
      y: centerY + radius * Math.sin(angleInRadians),
    }
  }

  static escapeString(str, escapeWith = 'x') {
    let newStr = str.toString().slice()
    newStr = newStr.replace(
      /[` ~!@#$%^&*()|+\=?;:'",.<>{}[\]\\/]/gi,
      escapeWith
    )
    return newStr
  }

  static negToZero(val) {
    return val < 0 ? 0 : val
  }

  static moveIndexInArray(arr, old_index, new_index) {
    if (new_index >= arr.length) {
      let k = new_index - arr.length + 1
      while (k--) {
        arr.push(undefined)
      }
    }
    arr.splice(new_index, 0, arr.splice(old_index, 1)[0])
    return arr
  }

  static extractNumber(s) {
    return parseFloat(s.replace(/[^\d.]*/g, ''))
  }

  static findAncestor(el, cls) {
    while ((el = el.parentElement) && !el.classList.contains(cls));
    return el
  }

  static setELstyles(el, styles) {
    for (let key in styles) {
      if (styles.hasOwnProperty(key)) {
        el.style.key = styles[key]
      }
    }
  }
  // prevents JS prevision errors when adding
  static preciseAddition(a, b) {
    let aDecimals = (String(a).split('.')[1] || '').length
    let bDecimals = (String(b).split('.')[1] || '').length

    let factor = Math.pow(10, Math.max(aDecimals, bDecimals))

    return (Math.round(a * factor) + Math.round(b * factor)) / factor
  }

  static isNumber(value) {
    return (
      !isNaN(value) &&
      parseFloat(Number(value)) === value &&
      !isNaN(parseInt(value, 10))
    )
  }

  static isFloat(n) {
    return Number(n) === n && n % 1 !== 0
  }

  static isMsEdge() {
    let ua = window.navigator.userAgent

    let edge = ua.indexOf('Edge/')
    if (edge > 0) {
      // Edge (IE 12+) => return version number
      return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10)
    }

    // other browser
    return false
  }
  //
  // Find the Greatest Common Divisor of two numbers
  //
  static getGCD(a, b, p = 7) {
    let big = Math.pow(10, p - Math.floor(Math.log10(Math.max(a, b))))
    a = Math.round(Math.abs(a) * big)
    b = Math.round(Math.abs(b) * big)

    while (b) {
      let t = b
      b = a % b
      a = t
    }
    return a / big
  }

  static getPrimeFactors(n) {
    const factors = []
    let divisor = 2

    while (n >= 2) {
      if (n % divisor == 0) {
        factors.push(divisor)
        n = n / divisor
      } else {
        divisor++
      }
    }
    return factors
  }

  static mod(a, b, p = 7) {
    let big = Math.pow(10, p - Math.floor(Math.log10(Math.max(a, b))))
    a = Math.round(Math.abs(a) * big)
    b = Math.round(Math.abs(b) * big)

    return (a % b) / big
  }
}

export default Utils
