import CLASS from './class'
import { ChartInternal } from './core'
import {
  isValue,
  isFunction,
  isNumber,
  isArray,
  notEmpty,
  hasValue,
  flattenArray,
  getBBox
} from './util'

ChartInternal.prototype.isEpochs = function(key) {
  var $$ = this,
    config = $$.config
  return config.data_epochs && key === config.data_epochs
}
ChartInternal.prototype.isX = function(key) {
  var $$ = this,
    config = $$.config
  return (
    (config.data_x && key === config.data_x) ||
    (notEmpty(config.data_xs) && hasValue(config.data_xs, key))
  )
}
ChartInternal.prototype.isNotX = function(key) {
  return !this.isX(key)
}
ChartInternal.prototype.isNotXAndNotEpochs = function(key) {
  return !this.isX(key) && !this.isEpochs(key)
}

/**
 * Returns whether the normalized stack option is enabled or not.
 *
 * To be enabled it must also have data.groups defined.
 *
 * @return {boolean}
 */
ChartInternal.prototype.isStackNormalized = function() {
  return this.config.data_stack_normalize && this.config.data_groups.length > 0
}

/**
 * Returns whether the axis is normalized or not.
 *
 * An axis is normalized as long as one of its associated target
 * is normalized.
 *
 * @param axisId Axis ID (y or y2)
 * @return {Boolean}
 */
ChartInternal.prototype.isAxisNormalized = function(axisId) {
  const $$ = this

  if (!$$.isStackNormalized()) {
    // shortcut
    return false
  }

  return $$.data.targets
    .filter(target => $$.axis.getId(target.id) === axisId)
    .some(target => $$.isTargetNormalized(target.id))
}

/**
 * Returns whether the values for this target ID is normalized or not.
 *
 * To be normalized the option needs to be enabled and target needs
 * to be defined in `data.groups`.
 *
 * @param targetId ID of the target
 * @return {Boolean} True if the target is normalized, false otherwise.
 */
ChartInternal.prototype.isTargetNormalized = function(targetId) {
  const $$ = this

  return (
    $$.isStackNormalized() &&
    $$.config.data_groups.some(group => group.includes(targetId))
  )
}

ChartInternal.prototype.getXKey = function(id) {
  var $$ = this,
    config = $$.config
  return config.data_x
    ? config.data_x
    : notEmpty(config.data_xs)
    ? config.data_xs[id]
    : null
}

/**
 * Get sum of visible data per index for given axis.
 *
 * Expect axisId to be either 'y' or 'y2'.
 *
 * @private
 * @param axisId Compute sum for data associated to given axis.
 * @return {Array}
 */
ChartInternal.prototype.getTotalPerIndex = function(axisId) {
  const $$ = this

  if (!$$.isStackNormalized()) {
    return null
  }

  const cached = $$.getFromCache('getTotalPerIndex')
  if (cached !== undefined) {
    return cached[axisId]
  }

  const sum = { y: [], y2: [] }

  $$.data.targets
    // keep only target that are normalized
    .filter(target => $$.isTargetNormalized(target.id))

    // keep only target that are visible
    .filter(target => $$.isTargetToShow(target.id))

    // compute sum per axis
    .forEach(target => {
      const sumByAxis = sum[$$.axis.getId(target.id)]

      target.values.forEach((v, i) => {
        if (!sumByAxis[i]) {
          sumByAxis[i] = 0
        }
        sumByAxis[i] += isNumber(v.value) ? v.value : 0
      })
    })

  $$.addToCache('getTotalPerIndex', sum)

  return sum[axisId]
}

/**
 * Get sum of visible data.
 *
 * Should be used for normalised data only since all values
 * are expected to be positive.
 *
 * @private
 * @return {Number}
 */
ChartInternal.prototype.getTotalDataSum = function() {
  const $$ = this

  const cached = $$.getFromCache('getTotalDataSum')
  if (cached !== undefined) {
    return cached
  }

  const totalDataSum = flattenArray(
    $$.data.targets
      .filter(target => $$.isTargetToShow(target.id))
      .map(target => target.values)
  )
    .map(d => d.value)
    .reduce((p, c) => p + c, 0)

  $$.addToCache('getTotalDataSum', totalDataSum)

  return totalDataSum
}

ChartInternal.prototype.getXValuesOfXKey = function(key, targets) {
  var $$ = this,
    xValues,
    ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : []
  ids.forEach(function(id) {
    if ($$.getXKey(id) === key) {
      xValues = $$.data.xs[id]
    }
  })
  return xValues
}
ChartInternal.prototype.getXValue = function(id, i) {
  var $$ = this
  return id in $$.data.xs && $$.data.xs[id] && isValue($$.data.xs[id][i])
    ? $$.data.xs[id][i]
    : i
}
ChartInternal.prototype.getOtherTargetXs = function() {
  var $$ = this,
    idsForX = Object.keys($$.data.xs)
  return idsForX.length ? $$.data.xs[idsForX[0]] : null
}
ChartInternal.prototype.getOtherTargetX = function(index) {
  var xs = this.getOtherTargetXs()
  return xs && index < xs.length ? xs[index] : null
}
ChartInternal.prototype.addXs = function(xs) {
  var $$ = this
  Object.keys(xs).forEach(function(id) {
    $$.config.data_xs[id] = xs[id]
  })
}
ChartInternal.prototype.addName = function(data) {
  var $$ = this,
    name
  if (data) {
    name = $$.config.data_names[data.id]
    data.name = name !== undefined ? name : data.id
  }
  return data
}
ChartInternal.prototype.getValueOnIndex = function(values, index) {
  var valueOnIndex = values.filter(function(v) {
    return v.index === index
  })
  return valueOnIndex.length ? valueOnIndex[0] : null
}
ChartInternal.prototype.updateTargetX = function(targets, x) {
  var $$ = this
  targets.forEach(function(t) {
    t.values.forEach(function(v, i) {
      v.x = $$.generateTargetX(x[i], t.id, i)
    })
    $$.data.xs[t.id] = x
  })
}
ChartInternal.prototype.updateTargetXs = function(targets, xs) {
  var $$ = this
  targets.forEach(function(t) {
    if (xs[t.id]) {
      $$.updateTargetX([t], xs[t.id])
    }
  })
}
ChartInternal.prototype.generateTargetX = function(rawX, id, index) {
  var $$ = this,
    x
  if ($$.isTimeSeries()) {
    x = rawX ? $$.parseDate(rawX) : $$.parseDate($$.getXValue(id, index))
  } else if ($$.isCustomX() && !$$.isCategorized()) {
    x = isValue(rawX) ? +rawX : $$.getXValue(id, index)
  } else {
    x = index
  }
  return x
}
ChartInternal.prototype.cloneTarget = function(target) {
  return {
    id: target.id,
    id_org: target.id_org,
    values: target.values.map(function(d) {
      return {
        x: d.x,
        value: d.value,
        id: d.id
      }
    })
  }
}
ChartInternal.prototype.getMaxDataCount = function() {
  var $$ = this
  return $$.d3.max($$.data.targets, function(t) {
    return t.values.length
  })
}
ChartInternal.prototype.mapToIds = function(targets) {
  return targets.map(function(d) {
    return d.id
  })
}
ChartInternal.prototype.mapToTargetIds = function(ids) {
  var $$ = this
  return ids ? [].concat(ids) : $$.mapToIds($$.data.targets)
}
ChartInternal.prototype.hasTarget = function(targets, id) {
  var ids = this.mapToIds(targets),
    i
  for (i = 0; i < ids.length; i++) {
    if (ids[i] === id) {
      return true
    }
  }
  return false
}
ChartInternal.prototype.isTargetToShow = function(targetId) {
  return this.hiddenTargetIds.indexOf(targetId) < 0
}
ChartInternal.prototype.isLegendToShow = function(targetId) {
  return this.hiddenLegendIds.indexOf(targetId) < 0
}

/**
 * Returns only visible targets.
 *
 * This is the same as calling {@link filterTargetsToShow} on $$.data.targets.
 *
 * @return {Array}
 */
ChartInternal.prototype.getTargetsToShow = function() {
  const $$ = this
  return $$.filterTargetsToShow($$.data.targets)
}

ChartInternal.prototype.filterTargetsToShow = function(targets) {
  var $$ = this
  return targets.filter(function(t) {
    return $$.isTargetToShow(t.id)
  })
}

/**
 * @return {Array} Returns all the targets attached to the chart, visible or not
 */
ChartInternal.prototype.getTargets = function() {
  const $$ = this
  return $$.data.targets
}

ChartInternal.prototype.mapTargetsToUniqueXs = function(targets) {
  var $$ = this
  var xs = $$.d3
    .set(
      $$.d3.merge(
        targets.map(function(t) {
          return t.values.map(function(v) {
            return +v.x
          })
        })
      )
    )
    .values()
  xs = $$.isTimeSeries()
    ? xs.map(function(x) {
        return new Date(+x)
      })
    : xs.map(function(x) {
        return +x
      })
  return xs.sort(function(a, b) {
    return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN
  })
}
ChartInternal.prototype.addHiddenTargetIds = function(targetIds) {
  targetIds = targetIds instanceof Array ? targetIds : new Array(targetIds)
  for (var i = 0; i < targetIds.length; i++) {
    if (this.hiddenTargetIds.indexOf(targetIds[i]) < 0) {
      this.hiddenTargetIds = this.hiddenTargetIds.concat(targetIds[i])
    }
  }
  this.resetCache()
}
ChartInternal.prototype.removeHiddenTargetIds = function(targetIds) {
  this.hiddenTargetIds = this.hiddenTargetIds.filter(function(id) {
    return targetIds.indexOf(id) < 0
  })
  this.resetCache()
}
ChartInternal.prototype.addHiddenLegendIds = function(targetIds) {
  targetIds = targetIds instanceof Array ? targetIds : new Array(targetIds)
  for (var i = 0; i < targetIds.length; i++) {
    if (this.hiddenLegendIds.indexOf(targetIds[i]) < 0) {
      this.hiddenLegendIds = this.hiddenLegendIds.concat(targetIds[i])
    }
  }
}
ChartInternal.prototype.removeHiddenLegendIds = function(targetIds) {
  this.hiddenLegendIds = this.hiddenLegendIds.filter(function(id) {
    return targetIds.indexOf(id) < 0
  })
}
ChartInternal.prototype.getValuesAsIdKeyed = function(targets) {
  var ys = {}
  targets.forEach(function(t) {
    ys[t.id] = []
    t.values.forEach(function(v) {
      ys[t.id].push(v.value)
    })
  })
  return ys
}
ChartInternal.prototype.checkValueInTargets = function(targets, checker) {
  var ids = Object.keys(targets),
    i,
    j,
    values
  for (i = 0; i < ids.length; i++) {
    values = targets[ids[i]].values
    for (j = 0; j < values.length; j++) {
      if (checker(values[j].value)) {
        return true
      }
    }
  }
  return false
}
ChartInternal.prototype.hasNegativeValueInTargets = function(targets) {
  return this.checkValueInTargets(targets, function(v) {
    return v < 0
  })
}
ChartInternal.prototype.hasPositiveValueInTargets = function(targets) {
  return this.checkValueInTargets(targets, function(v) {
    return v > 0
  })
}
ChartInternal.prototype.isOrderDesc = function() {
  var config = this.config
  return (
    typeof config.data_order === 'string' &&
    config.data_order.toLowerCase() === 'desc'
  )
}
ChartInternal.prototype.isOrderAsc = function() {
  var config = this.config
  return (
    typeof config.data_order === 'string' &&
    config.data_order.toLowerCase() === 'asc'
  )
}
ChartInternal.prototype.getOrderFunction = function() {
  var $$ = this,
    config = $$.config,
    orderAsc = $$.isOrderAsc(),
    orderDesc = $$.isOrderDesc()
  if (orderAsc || orderDesc) {
    var reducer = function(p, c) {
      return p + Math.abs(c.value)
    }
    return function(t1, t2) {
      var t1Sum = t1.values.reduce(reducer, 0),
        t2Sum = t2.values.reduce(reducer, 0)
      return orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum
    }
  } else if (isFunction(config.data_order)) {
    return config.data_order
  } else if (isArray(config.data_order)) {
    var order = config.data_order
    return function(t1, t2) {
      return order.indexOf(t1.id) - order.indexOf(t2.id)
    }
  }
}
ChartInternal.prototype.orderTargets = function(targets) {
  var fct = this.getOrderFunction()
  if (fct) {
    targets.sort(fct)
  }
  return targets
}

/**
 * Returns all the values from the given targets at the given index.
 *
 * @param {Array} targets
 * @param {Number} index
 * @return {Array}
 */
ChartInternal.prototype.filterByIndex = function(targets, index) {
  return this.d3.merge(
    targets.map(t => t.values.filter(v => v.index === index))
  )
}

ChartInternal.prototype.filterByX = function(targets, x) {
  return this.d3
    .merge(
      targets.map(function(t) {
        return t.values
      })
    )
    .filter(function(v) {
      return v.x - x === 0
    })
}
ChartInternal.prototype.filterRemoveNull = function(data) {
  return data.filter(function(d) {
    return isValue(d.value)
  })
}
ChartInternal.prototype.filterByXDomain = function(targets, xDomain) {
  return targets.map(function(t) {
    return {
      id: t.id,
      id_org: t.id_org,
      values: t.values.filter(function(v) {
        return xDomain[0] <= v.x && v.x <= xDomain[1]
      })
    }
  })
}
ChartInternal.prototype.hasDataLabel = function() {
  var config = this.config
  if (typeof config.data_labels === 'boolean' && config.data_labels) {
    return true
  } else if (
    typeof config.data_labels === 'object' &&
    notEmpty(config.data_labels)
  ) {
    return true
  }
  return false
}
ChartInternal.prototype.getDataLabelLength = function(min, max, key) {
  var $$ = this,
    lengths = [0, 0],
    paddingCoef = 1.3
  $$.selectChart
    .select('svg')
    .selectAll('.dummy')
    .data([min, max])
    .enter()
    .append('text')
    .text(function(d) {
      return $$.dataLabelFormat(d.id)(d)
    })
    .each(function(d, i) {
      lengths[i] = getBBox(this)[key] * paddingCoef
    })
    .remove()
  return lengths
}
/**
 * Returns true if the given data point is not arc type, otherwise false.
 * @param {Object} d The data point
 * @return {boolean}
 */
ChartInternal.prototype.isNoneArc = function(d) {
  return this.hasTarget(this.data.targets, d.id)
}

/**
 * Returns true if the given data point is arc type, otherwise false.
 * @param {Object} d The data point
 * @return {boolean}
 */
ChartInternal.prototype.isArc = function(d) {
  return 'data' in d && this.hasTarget(this.data.targets, d.data.id)
}

/**
 * Find the closest point from the given pos among the given targets or
 * undefined if none satisfies conditions.
 *
 * @param {Array} targets
 * @param {Array} pos An [x,y] coordinate
 * @return {Object|undefined}
 */
ChartInternal.prototype.findClosestFromTargets = function(targets, pos) {
  const $$ = this

  // for each target, find the closest point
  const candidates = targets
    .map(t =>
      $$.findClosest(
        t.values,
        pos,
        $$.config.tooltip_horizontal
          ? $$.horizontalDistance.bind($$)
          : $$.dist.bind($$),
        $$.config.point_sensitivity
      )
    )
    .filter(v => v)

  // returns the closest of candidates
  if (candidates.length === 0) {
    return undefined
  } else if (candidates.length === 1) {
    return candidates[0]
  } else {
    return $$.findClosest(candidates, pos, $$.dist.bind($$))
  }
}

/**
 * Find the closest point from the x value or undefined if none satisfies conditions.
 *
 * @param {Array} targets
 * @param {Array} x A value on X axis
 * @return {Object|undefined}
 */
ChartInternal.prototype.findClosestFromTargetsByX = function(targets, x) {
  let closest
  let diff

  targets.forEach(t => {
    t.values.forEach(d => {
      let newDiff = Math.abs(x - d.x)

      if (diff === undefined || newDiff < diff) {
        closest = d
        diff = newDiff
      }
    })
  })

  return closest
}

/**
 * Using given compute distance method, returns the closest data point from the
 * given position.
 *
 * Giving optionally a minimum distance to satisfy.
 *
 * @param {Array} dataPoints List of DataPoints
 * @param {Array} pos An [x,y] coordinate
 * @param {Function} computeDist Function to compute distance between 2 points
 * @param {Number} minDist Minimal distance to satisfy
 * @return {Object|undefined} Closest data point
 */
ChartInternal.prototype.findClosest = function(
  dataPoints,
  pos,
  computeDist,
  minDist = Infinity
) {
  const $$ = this

  let closest

  // find closest bar
  dataPoints
    .filter(v => v && $$.isBarType(v.id))
    .forEach(function(v) {
      if (!closest) {
        const shape = $$.main
          .select(
            '.' +
              CLASS.bars +
              $$.getTargetSelectorSuffix(v.id) +
              ' .' +
              CLASS.bar +
              '-' +
              v.index
          )
          .node()
        if ($$.isWithinBar(pos, shape)) {
          closest = v
        }
      }
    })

  // find closest point from non-bar
  dataPoints
    .filter(v => v && !$$.isBarType(v.id))
    .forEach(v => {
      let d = computeDist(v, pos)
      if (d < minDist) {
        minDist = d
        closest = v
      }
    })

  return closest
}
ChartInternal.prototype.dist = function(data, pos) {
  var $$ = this,
    config = $$.config,
    xIndex = config.axis_rotated ? 1 : 0,
    yIndex = config.axis_rotated ? 0 : 1,
    y = $$.circleY(data, data.index),
    x = $$.x(data.x)
  return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2))
}
ChartInternal.prototype.horizontalDistance = function(data, pos) {
  var $$ = this,
    config = $$.config,
    xIndex = config.axis_rotated ? 1 : 0,
    x = $$.x(data.x)

  return Math.abs(x - pos[xIndex])
}
ChartInternal.prototype.convertValuesToStep = function(values) {
  var converted = [].concat(values),
    i

  if (!this.isCategorized()) {
    return values
  }

  for (i = values.length + 1; 0 < i; i--) {
    converted[i] = converted[i - 1]
  }

  converted[0] = {
    x: converted[0].x - 1,
    value: converted[0].value,
    id: converted[0].id
  }
  converted[values.length + 1] = {
    x: converted[values.length].x + 1,
    value: converted[values.length].value,
    id: converted[values.length].id
  }

  return converted
}

/**
 * Get ratio value
 *
 * @param {String} type Ratio for given type
 * @param {Object} d Data value object
 * @param {Boolean} asPercent Convert the return as percent or not
 * @return {Number} Ratio value
 * @private
 */
ChartInternal.prototype.getRatio = function(type, d, asPercent = false) {
  const $$ = this
  const api = $$.api
  let ratio = 0

  if (d && api.data.shown.call(api).length) {
    ratio = d.ratio || d.value

    if (type === 'arc') {
      if ($$.hasType('gauge')) {
        ratio =
          (d.endAngle - d.startAngle) /
          (Math.PI * ($$.config.gauge_fullCircle ? 2 : 1))
      } else {
        const total = $$.getTotalDataSum()

        ratio = d.value / total
      }
    } else if (type === 'index') {
      const total = $$.getTotalPerIndex($$.axis.getId(d.id))

      d.ratio =
        isNumber(d.value) && total && total[d.index] > 0
          ? d.value / total[d.index]
          : 0

      ratio = d.ratio
    }
  }

  return asPercent && ratio ? ratio * 100 : ratio
}

ChartInternal.prototype.updateDataAttributes = function(name, attrs) {
  var $$ = this,
    config = $$.config,
    current = config['data_' + name]
  if (typeof attrs === 'undefined') {
    return current
  }
  Object.keys(attrs).forEach(function(id) {
    current[id] = attrs[id]
  })
  $$.redraw({
    withLegend: true
  })
  return current
}
