import { ChartInternal } from './chart-internal'
import { Chart } from './chart'
import { AxisInternal } from './axis-internal'
import Axis from './axis'
import CLASS from './class'

import {
  asHalfPixel,
  getOption,
  getPathBox,
  isFunction,
  isValue,
  notEmpty
} from './util'

var c3 = {
  version: '0.7.20',
  chart: {
    fn: Chart.prototype,
    internal: {
      fn: ChartInternal.prototype,
      axis: {
        fn: Axis.prototype,
        internal: {
          fn: AxisInternal.prototype
        }
      }
    }
  },
  generate: function(config) {
    return new Chart(config)
  }
}

export { c3 }

ChartInternal.prototype.beforeInit = function() {
  // can do something
}
ChartInternal.prototype.afterInit = function() {
  // can do something
}
ChartInternal.prototype.init = function() {
  var $$ = this,
    config = $$.config

  $$.initParams()

  if (config.data_url) {
    $$.convertUrlToData(
      config.data_url,
      config.data_mimeType,
      config.data_headers,
      config.data_keys,
      $$.initWithData
    )
  } else if (config.data_json) {
    $$.initWithData($$.convertJsonToData(config.data_json, config.data_keys))
  } else if (config.data_rows) {
    $$.initWithData($$.convertRowsToData(config.data_rows))
  } else if (config.data_columns) {
    $$.initWithData($$.convertColumnsToData(config.data_columns))
  } else {
    throw Error('url or json or rows or columns is required.')
  }
}

ChartInternal.prototype.initParams = function() {
  var $$ = this,
    d3 = $$.d3,
    config = $$.config

  // MEMO: clipId needs to be unique because it conflicts when multiple charts exist
  $$.clipId = 'c3-' + new Date().valueOf() + '-clip'
  $$.clipIdForXAxis = $$.clipId + '-xaxis'
  $$.clipIdForYAxis = $$.clipId + '-yaxis'
  $$.clipIdForGrid = $$.clipId + '-grid'
  $$.clipIdForSubchart = $$.clipId + '-subchart'
  $$.clipPath = $$.getClipPath($$.clipId)
  $$.clipPathForXAxis = $$.getClipPath($$.clipIdForXAxis)
  $$.clipPathForYAxis = $$.getClipPath($$.clipIdForYAxis)
  $$.clipPathForGrid = $$.getClipPath($$.clipIdForGrid)
  $$.clipPathForSubchart = $$.getClipPath($$.clipIdForSubchart)

  $$.dragStart = null
  $$.dragging = false
  $$.flowing = false
  $$.cancelClick = false
  $$.mouseover = undefined
  $$.transiting = false

  $$.color = $$.generateColor()
  $$.levelColor = $$.generateLevelColor()

  $$.dataTimeParse = (config.data_xLocaltime ? d3.timeParse : d3.utcParse)(
    $$.config.data_xFormat
  )
  $$.axisTimeFormat = config.axis_x_localtime ? d3.timeFormat : d3.utcFormat
  $$.defaultAxisTimeFormat = function(date) {
    if (date.getMilliseconds()) {
      return d3.timeFormat('.%L')(date)
    }
    if (date.getSeconds()) {
      return d3.timeFormat(':%S')(date)
    }
    if (date.getMinutes()) {
      return d3.timeFormat('%I:%M')(date)
    }
    if (date.getHours()) {
      return d3.timeFormat('%I %p')(date)
    }
    if (date.getDay() && date.getDate() !== 1) {
      return d3.timeFormat('%-m/%-d')(date)
    }
    if (date.getDate() !== 1) {
      return d3.timeFormat('%-m/%-d')(date)
    }
    if (date.getMonth()) {
      return d3.timeFormat('%-m/%-d')(date)
    }
    return d3.timeFormat('%Y/%-m/%-d')(date)
  }
  $$.hiddenTargetIds = []
  $$.hiddenLegendIds = []
  $$.focusedTargetIds = []
  $$.defocusedTargetIds = []

  $$.xOrient = config.axis_rotated
    ? config.axis_x_inner
      ? 'right'
      : 'left'
    : config.axis_x_inner
    ? 'top'
    : 'bottom'
  $$.yOrient = config.axis_rotated
    ? config.axis_y_inner
      ? 'top'
      : 'bottom'
    : config.axis_y_inner
    ? 'right'
    : 'left'
  $$.y2Orient = config.axis_rotated
    ? config.axis_y2_inner
      ? 'bottom'
      : 'top'
    : config.axis_y2_inner
    ? 'left'
    : 'right'
  $$.subXOrient = config.axis_rotated ? 'left' : 'bottom'

  $$.isLegendRight = config.legend_position === 'right'
  $$.isLegendInset = config.legend_position === 'inset'
  $$.isLegendTop =
    config.legend_inset_anchor === 'top-left' ||
    config.legend_inset_anchor === 'top-right'
  $$.isLegendLeft =
    config.legend_inset_anchor === 'top-left' ||
    config.legend_inset_anchor === 'bottom-left'
  $$.legendStep = 0
  $$.legendItemWidth = 0
  $$.legendItemHeight = 0

  $$.currentMaxTickWidths = {
    x: 0,
    y: 0,
    y2: 0
  }

  $$.rotated_padding_left = 30
  $$.rotated_padding_right = config.axis_rotated && !config.axis_x_show ? 0 : 30
  $$.rotated_padding_top = 5

  $$.withoutFadeIn = {}

  $$.intervalForObserveInserted = undefined

  $$.axes.subx = d3.selectAll([]) // needs when excluding subchart.js
}

ChartInternal.prototype.initChartElements = function() {
  if (this.initBar) {
    this.initBar()
  }
  if (this.initLine) {
    this.initLine()
  }
  if (this.initArc) {
    this.initArc()
  }
  if (this.initGauge) {
    this.initGauge()
  }
  if (this.initText) {
    this.initText()
  }
}

ChartInternal.prototype.initWithData = function(data) {
  var $$ = this,
    d3 = $$.d3,
    config = $$.config
  var defs,
    main,
    binding = true

  $$.axis = new Axis($$)

  if (!config.bindto) {
    $$.selectChart = d3.selectAll([])
  } else if (typeof config.bindto.node === 'function') {
    $$.selectChart = config.bindto
  } else {
    $$.selectChart = d3.select(config.bindto)
  }
  if ($$.selectChart.empty()) {
    $$.selectChart = d3
      .select(document.createElement('div'))
      .style('opacity', 0)
    $$.observeInserted($$.selectChart)
    binding = false
  }
  $$.selectChart.html('').classed('c3', true)

  // Init data as targets
  $$.data.xs = {}
  $$.data.targets = $$.convertDataToTargets(data)

  if (config.data_filter) {
    $$.data.targets = $$.data.targets.filter(config.data_filter)
  }

  // Set targets to hide if needed
  if (config.data_hide) {
    $$.addHiddenTargetIds(
      config.data_hide === true
        ? $$.mapToIds($$.data.targets)
        : config.data_hide
    )
  }
  if (config.legend_hide) {
    $$.addHiddenLegendIds(
      config.legend_hide === true
        ? $$.mapToIds($$.data.targets)
        : config.legend_hide
    )
  }

  if ($$.isStanfordGraphType()) {
    $$.initStanfordData()
  }

  // Init sizes and scales
  $$.updateSizes()
  $$.updateScales()

  // Set domains for each scale
  $$.x.domain(d3.extent($$.getXDomain($$.data.targets)))
  $$.y.domain($$.getYDomain($$.data.targets, 'y'))
  $$.y2.domain($$.getYDomain($$.data.targets, 'y2'))
  $$.subX.domain($$.x.domain())
  $$.subY.domain($$.y.domain())
  $$.subY2.domain($$.y2.domain())

  // Save original x domain for zoom update
  $$.orgXDomain = $$.x.domain()

  /*-- Basic Elements --*/

  // Define svgs
  $$.svg = $$.selectChart
    .append('svg')
    .style('overflow', 'hidden')
    .on('mouseenter', function() {
      return config.onmouseover.call($$)
    })
    .on('mouseleave', function() {
      return config.onmouseout.call($$)
    })

  if ($$.config.svg_classname) {
    $$.svg.attr('class', $$.config.svg_classname)
  }

  // Define defs
  defs = $$.svg.append('defs')
  $$.clipChart = $$.appendClip(defs, $$.clipId)
  $$.clipXAxis = $$.appendClip(defs, $$.clipIdForXAxis)
  $$.clipYAxis = $$.appendClip(defs, $$.clipIdForYAxis)
  $$.clipGrid = $$.appendClip(defs, $$.clipIdForGrid)
  $$.clipSubchart = $$.appendClip(defs, $$.clipIdForSubchart)
  $$.updateSvgSize()

  // Define regions
  main = $$.main = $$.svg.append('g').attr('transform', $$.getTranslate('main'))

  if ($$.initPie) {
    $$.initPie()
  }
  if ($$.initDragZoom) {
    $$.initDragZoom()
  }
  if (config.subchart_show && $$.initSubchart) {
    $$.initSubchart()
  }
  if ($$.initTooltip) {
    $$.initTooltip()
  }
  if ($$.initLegend) {
    $$.initLegend()
  }
  if ($$.initTitle) {
    $$.initTitle()
  }
  if ($$.initZoom) {
    $$.initZoom()
  }
  if ($$.isStanfordGraphType()) {
    $$.drawColorScale()
  }

  // Update selection based on size and scale
  // TODO: currently this must be called after initLegend because of update of sizes, but it should be done in initSubchart.
  if (config.subchart_show && $$.initSubchartBrush) {
    $$.initSubchartBrush()
  }

  /*-- Main Region --*/

  // text when empty
  main
    .append('text')
    .attr('class', CLASS.text + ' ' + CLASS.empty)
    .attr('text-anchor', 'middle') // horizontal centering of text at x position in all browsers.
    .attr('dominant-baseline', 'middle') // vertical centering of text at y position in all browsers, except IE.

  // Regions
  $$.initRegion()

  // Grids
  $$.initGrid()

  // Define g for chart area
  main
    .append('g')
    .attr('clip-path', $$.clipPath)
    .attr('class', CLASS.chart)

  // Grid lines
  if (config.grid_lines_front) {
    $$.initGridLines()
  }

  $$.initStanfordElements()

  // Cover whole with rects for events
  $$.initEventRect()

  // Define g for chart
  $$.initChartElements()

  // Add Axis
  $$.axis.init()

  // Set targets
  $$.updateTargets($$.data.targets)

  // Set default extent if defined
  if (config.axis_x_selection) {
    $$.brush.selectionAsValue($$.getDefaultSelection())
  }

  // Draw with targets
  if (binding) {
    $$.updateDimension()
    $$.config.oninit.call($$)
    $$.redraw({
      withTransition: false,
      withTransform: true,
      withUpdateXDomain: true,
      withUpdateOrgXDomain: true,
      withTransitionForAxis: false
    })
  }

  // Bind to resize event
  $$.bindResize()

  // Bind to window focus event
  $$.bindWindowFocus()

  // export element of the chart
  $$.api.element = $$.selectChart.node()
}

ChartInternal.prototype.smoothLines = function(el, type) {
  var $$ = this
  if (type === 'grid') {
    el.each(function() {
      var g = $$.d3.select(this),
        x1 = g.attr('x1'),
        x2 = g.attr('x2'),
        y1 = g.attr('y1'),
        y2 = g.attr('y2')
      g.attr({
        x1: Math.ceil(x1),
        x2: Math.ceil(x2),
        y1: Math.ceil(y1),
        y2: Math.ceil(y2)
      })
    })
  }
}

ChartInternal.prototype.updateSizes = function() {
  var $$ = this,
    config = $$.config
  var legendHeight = $$.legend ? $$.getLegendHeight() : 0,
    legendWidth = $$.legend ? $$.getLegendWidth() : 0,
    legendHeightForBottom =
      $$.isLegendRight || $$.isLegendInset ? 0 : legendHeight,
    hasArc = $$.hasArcType(),
    xAxisHeight =
      config.axis_rotated || hasArc ? 0 : $$.getHorizontalAxisHeight('x'),
    subchartXAxisHeight = config.axis_rotated || hasArc ? 0 : $$.getHorizontalAxisHeight('x',true),
    subchartHeight =
      config.subchart_show && !hasArc
        ? config.subchart_size_height + subchartXAxisHeight
        : 0

  $$.currentWidth = $$.getCurrentWidth()
  $$.currentHeight = $$.getCurrentHeight()

  // for main
  $$.margin = config.axis_rotated
    ? {
        top: $$.getHorizontalAxisHeight('y2') + $$.getCurrentPaddingTop(),
        right: hasArc ? 0 : $$.getCurrentPaddingRight(),
        bottom:
          $$.getHorizontalAxisHeight('y') +
          legendHeightForBottom +
          $$.getCurrentPaddingBottom(),
        left: subchartHeight + (hasArc ? 0 : $$.getCurrentPaddingLeft())
      }
    : {
        top: 4 + $$.getCurrentPaddingTop(), // for top tick text
        right: hasArc ? 0 : $$.getCurrentPaddingRight(),
        bottom:
          xAxisHeight +
          subchartHeight +
          legendHeightForBottom +
          $$.getCurrentPaddingBottom(),
        left: hasArc ? 0 : $$.getCurrentPaddingLeft()
      }

  // for subchart
  $$.margin2 = config.axis_rotated
    ? {
        top: $$.margin.top,
        right: NaN,
        bottom: 20 + legendHeightForBottom,
        left: $$.rotated_padding_left
      }
    : {
        top: $$.currentHeight - subchartHeight - legendHeightForBottom,
        right: NaN,
        bottom: subchartXAxisHeight + legendHeightForBottom,
        left: $$.margin.left
      }

  // for legend
  $$.margin3 = {
    top: 0,
    right: NaN,
    bottom: 0,
    left: 0
  }
  if ($$.updateSizeForLegend) {
    $$.updateSizeForLegend(legendHeight, legendWidth)
  }

  $$.width = $$.currentWidth - $$.margin.left - $$.margin.right
  $$.height = $$.currentHeight - $$.margin.top - $$.margin.bottom
  if ($$.width < 0) {
    $$.width = 0
  }
  if ($$.height < 0) {
    $$.height = 0
  }

  $$.width2 = config.axis_rotated
    ? $$.margin.left - $$.rotated_padding_left - $$.rotated_padding_right
    : $$.width
  $$.height2 = config.axis_rotated
    ? $$.height
    : $$.currentHeight - $$.margin2.top - $$.margin2.bottom
  if ($$.width2 < 0) {
    $$.width2 = 0
  }
  if ($$.height2 < 0) {
    $$.height2 = 0
  }

  // for arc
  $$.arcWidth = $$.width - ($$.isLegendRight ? legendWidth + 10 : 0)
  $$.arcHeight = $$.height - ($$.isLegendRight ? 0 : 10)
  if ($$.hasType('gauge') && !config.gauge_fullCircle) {
    $$.arcHeight += $$.height - $$.getGaugeLabelHeight()
  }
  if ($$.updateRadius) {
    $$.updateRadius()
  }

  if ($$.isLegendRight && hasArc) {
    $$.margin3.left = $$.arcWidth / 2 + $$.radiusExpanded * 1.1
  }
}

ChartInternal.prototype.updateTargets = function(targets) {
  var $$ = this,
    config = $$.config

  /*-- Main --*/

  //-- Text --//
  $$.updateTargetsForText(targets)

  //-- Bar --//
  $$.updateTargetsForBar(targets)

  //-- Line --//
  $$.updateTargetsForLine(targets)

  //-- Arc --//
  if ($$.hasArcType() && $$.updateTargetsForArc) {
    $$.updateTargetsForArc(targets)
  }

  /*-- Sub --*/

  if (config.subchart_show && $$.updateTargetsForSubchart) {
    $$.updateTargetsForSubchart(targets)
  }

  // Fade-in each chart
  $$.showTargets()
}
ChartInternal.prototype.showTargets = function() {
  var $$ = this
  $$.svg
    .selectAll('.' + CLASS.target)
    .filter(function(d) {
      return $$.isTargetToShow(d.id)
    })
    .transition()
    .duration($$.config.transition_duration)
    .style('opacity', 1)
}

ChartInternal.prototype.redraw = function(options, transitions) {
  var $$ = this,
    main = $$.main,
    d3 = $$.d3,
    config = $$.config
  var areaIndices = $$.getShapeIndices($$.isAreaType),
    barIndices = $$.getShapeIndices($$.isBarType),
    lineIndices = $$.getShapeIndices($$.isLineType)
  var withY,
    withSubchart,
    withTransition,
    withTransitionForExit,
    withTransitionForAxis,
    withTransform,
    withUpdateXDomain,
    withUpdateOrgXDomain,
    withTrimXDomain,
    withLegend,
    withEventRect,
    withDimension,
    withUpdateXAxis
  var hideAxis = $$.hasArcType()
  var drawArea, drawBar, drawLine, xForText, yForText
  var duration, durationForExit, durationForAxis
  var transitionsToWait, waitForDraw, flow, transition
  var targetsToShow = $$.filterTargetsToShow($$.data.targets),
    tickValues,
    i,
    intervalForCulling,
    xDomainForZoom
  var xv = $$.xv.bind($$),
    cx,
    cy

  options = options || {}
  withY = getOption(options, 'withY', true)
  withSubchart = getOption(options, 'withSubchart', true)
  withTransition = getOption(options, 'withTransition', true)
  withTransform = getOption(options, 'withTransform', false)
  withUpdateXDomain = getOption(options, 'withUpdateXDomain', false)
  withUpdateOrgXDomain = getOption(options, 'withUpdateOrgXDomain', false)
  withTrimXDomain = getOption(options, 'withTrimXDomain', true)
  withUpdateXAxis = getOption(options, 'withUpdateXAxis', withUpdateXDomain)
  withLegend = getOption(options, 'withLegend', false)
  withEventRect = getOption(options, 'withEventRect', true)
  withDimension = getOption(options, 'withDimension', true)
  withTransitionForExit = getOption(
    options,
    'withTransitionForExit',
    withTransition
  )
  withTransitionForAxis = getOption(
    options,
    'withTransitionForAxis',
    withTransition
  )

  duration = withTransition ? config.transition_duration : 0
  durationForExit = withTransitionForExit ? duration : 0
  durationForAxis = withTransitionForAxis ? duration : 0

  transitions = transitions || $$.axis.generateTransitions(durationForAxis)

  // update legend and transform each g
  if (withLegend && config.legend_show) {
    $$.updateLegend($$.mapToIds($$.data.targets), options, transitions)
  } else if (withDimension) {
    // need to update dimension (e.g. axis.y.tick.values) because y tick values should change
    // no need to update axis in it because they will be updated in redraw()
    $$.updateDimension(true)
  }

  // MEMO: needed for grids calculation
  if ($$.isCategorized() && targetsToShow.length === 0) {
    $$.x.domain([0, $$.axes.x.selectAll('.tick').size()])
  }

  if (targetsToShow.length) {
    $$.updateXDomain(
      targetsToShow,
      withUpdateXDomain,
      withUpdateOrgXDomain,
      withTrimXDomain
    )
    if (!config.axis_x_tick_values) {
      tickValues = $$.axis.updateXAxisTickValues(targetsToShow)
    }
  } else {
    $$.xAxis.tickValues([])
    $$.subXAxis.tickValues([])
  }

  if (config.zoom_rescale && !options.flow) {
    xDomainForZoom = $$.x.orgDomain()
  }

  $$.y.domain($$.getYDomain(targetsToShow, 'y', xDomainForZoom))
  $$.y2.domain($$.getYDomain(targetsToShow, 'y2', xDomainForZoom))

  if (!config.axis_y_tick_values && config.axis_y_tick_count) {
    $$.yAxis.tickValues(
      $$.axis.generateTickValues($$.y.domain(), config.axis_y_tick_count)
    )
  }
  if (!config.axis_y2_tick_values && config.axis_y2_tick_count) {
    $$.y2Axis.tickValues(
      $$.axis.generateTickValues($$.y2.domain(), config.axis_y2_tick_count)
    )
  }

  // axes
  $$.axis.redraw(durationForAxis, hideAxis)

  // Update axis label
  $$.axis.updateLabels(withTransition)

  // show/hide if manual culling needed
  if ((withUpdateXDomain || withUpdateXAxis) && targetsToShow.length) {
    if (config.axis_x_tick_culling && tickValues) {
      for (i = 1; i < tickValues.length; i++) {
        if (tickValues.length / i < config.axis_x_tick_culling_max) {
          intervalForCulling = i
          break
        }
      }
      $$.svg.selectAll('.' + CLASS.axisX + ' .tick text').each(function(e) {
        var index = tickValues.indexOf(e)
        if (index >= 0) {
          d3.select(this).style(
            'display',
            index % intervalForCulling ? 'none' : 'block'
          )
        }
      })
    } else {
      $$.svg
        .selectAll('.' + CLASS.axisX + ' .tick text')
        .style('display', 'block')
    }
  }

  // setup drawer - MEMO: these must be called after axis updated
  drawArea = $$.generateDrawArea
    ? $$.generateDrawArea(areaIndices, false)
    : undefined
  drawBar = $$.generateDrawBar ? $$.generateDrawBar(barIndices) : undefined
  drawLine = $$.generateDrawLine
    ? $$.generateDrawLine(lineIndices, false)
    : undefined
  xForText = $$.generateXYForText(areaIndices, barIndices, lineIndices, true)
  yForText = $$.generateXYForText(areaIndices, barIndices, lineIndices, false)

  // update circleY based on updated parameters
  $$.updateCircleY()
  // generate circle x/y functions depending on updated params
  cx = ($$.config.axis_rotated ? $$.circleY : $$.circleX).bind($$)
  cy = ($$.config.axis_rotated ? $$.circleX : $$.circleY).bind($$)

  // Update sub domain
  if (withY) {
    $$.subY.domain($$.getYDomain(targetsToShow, 'y'))
    $$.subY2.domain($$.getYDomain(targetsToShow, 'y2'))
  }

  // xgrid focus
  $$.updateXgridFocus()

  // Data empty label positioning and text.
  main
    .select('text.' + CLASS.text + '.' + CLASS.empty)
    .attr('x', $$.width / 2)
    .attr('y', $$.height / 2)
    .text(config.data_empty_label_text)
    .transition()
    .style('opacity', targetsToShow.length ? 0 : 1)

  // event rect
  if (withEventRect) {
    $$.redrawEventRect()
  }

  // grid
  $$.updateGrid(duration)

  $$.updateStanfordElements(duration)

  // rect for regions
  $$.updateRegion(duration)

  // bars
  $$.updateBar(durationForExit)

  // lines, areas and circles
  $$.updateLine(durationForExit)
  $$.updateArea(durationForExit)
  $$.updateCircle(cx, cy)

  // text
  if ($$.hasDataLabel()) {
    $$.updateText(xForText, yForText, durationForExit)
  }

  // title
  if ($$.redrawTitle) {
    $$.redrawTitle()
  }

  // arc
  if ($$.redrawArc) {
    $$.redrawArc(duration, durationForExit, withTransform)
  }

  // subchart
  if (config.subchart_show && $$.redrawSubchart) {
    $$.redrawSubchart(
      withSubchart,
      transitions,
      duration,
      durationForExit,
      areaIndices,
      barIndices,
      lineIndices
    )
  }

  if ($$.isStanfordGraphType()) {
    $$.drawColorScale()
  }

  // circles for select
  main
    .selectAll('.' + CLASS.selectedCircles)
    .filter($$.isBarType.bind($$))
    .selectAll('circle')
    .remove()

  if (options.flow) {
    flow = $$.generateFlow({
      targets: targetsToShow,
      flow: options.flow,
      duration: options.flow.duration,
      drawBar: drawBar,
      drawLine: drawLine,
      drawArea: drawArea,
      cx: cx,
      cy: cy,
      xv: xv,
      xForText: xForText,
      yForText: yForText
    })
  }

  if (duration && $$.isTabVisible()) {
    // Only use transition if tab visible. See #938.
    // transition should be derived from one transition
    transition = d3.transition().duration(duration)
    transitionsToWait = []
    ;[
      $$.redrawBar(drawBar, true, transition),
      $$.redrawLine(drawLine, true, transition),
      $$.redrawArea(drawArea, true, transition),
      $$.redrawCircle(cx, cy, true, transition),
      $$.redrawText(xForText, yForText, options.flow, true, transition),
      $$.redrawRegion(true, transition),
      $$.redrawGrid(true, transition)
    ].forEach(function(transitions) {
      transitions.forEach(function(transition) {
        transitionsToWait.push(transition)
      })
    })
    // Wait for end of transitions to call flow and onrendered callback
    waitForDraw = $$.generateWait()
    transitionsToWait.forEach(function(t) {
      waitForDraw.add(t)
    })
    waitForDraw(function() {
      if (flow) {
        flow()
      }
      if (config.onrendered) {
        config.onrendered.call($$)
      }
    })
  } else {
    $$.redrawBar(drawBar)
    $$.redrawLine(drawLine)
    $$.redrawArea(drawArea)
    $$.redrawCircle(cx, cy)
    $$.redrawText(xForText, yForText, options.flow)
    $$.redrawRegion()
    $$.redrawGrid()
    if (flow) {
      flow()
    }
    if (config.onrendered) {
      config.onrendered.call($$)
    }
  }

  // update fadein condition
  $$.mapToIds($$.data.targets).forEach(function(id) {
    $$.withoutFadeIn[id] = true
  })
}

ChartInternal.prototype.updateAndRedraw = function(options) {
  var $$ = this,
    config = $$.config,
    transitions
  options = options || {}
  // same with redraw
  options.withTransition = getOption(options, 'withTransition', true)
  options.withTransform = getOption(options, 'withTransform', false)
  options.withLegend = getOption(options, 'withLegend', false)
  // NOT same with redraw
  options.withUpdateXDomain = getOption(options, 'withUpdateXDomain', true)
  options.withUpdateOrgXDomain = getOption(
    options,
    'withUpdateOrgXDomain',
    true
  )
  options.withTransitionForExit = false
  options.withTransitionForTransform = getOption(
    options,
    'withTransitionForTransform',
    options.withTransition
  )
  // MEMO: this needs to be called before updateLegend and it means this ALWAYS needs to be called)
  $$.updateSizes()
  // MEMO: called in updateLegend in redraw if withLegend
  if (!(options.withLegend && config.legend_show)) {
    transitions = $$.axis.generateTransitions(
      options.withTransitionForAxis ? config.transition_duration : 0
    )
    // Update scales
    $$.updateScales()
    $$.updateSvgSize()
    // Update g positions
    $$.transformAll(options.withTransitionForTransform, transitions)
  }
  // Draw with new sizes & scales
  $$.redraw(options, transitions)
}
ChartInternal.prototype.redrawWithoutRescale = function() {
  this.redraw({
    withY: false,
    withSubchart: false,
    withEventRect: false,
    withTransitionForAxis: false
  })
}

ChartInternal.prototype.isTimeSeries = function() {
  return this.config.axis_x_type === 'timeseries'
}
ChartInternal.prototype.isCategorized = function() {
  return this.config.axis_x_type.indexOf('categor') >= 0
}
ChartInternal.prototype.isCustomX = function() {
  var $$ = this,
    config = $$.config
  return !$$.isTimeSeries() && (config.data_x || notEmpty(config.data_xs))
}

ChartInternal.prototype.isTimeSeriesY = function() {
  return this.config.axis_y_type === 'timeseries'
}

ChartInternal.prototype.getTranslate = function(target) {
  var $$ = this,
    config = $$.config,
    x,
    y
  if (target === 'main') {
    x = asHalfPixel($$.margin.left)
    y = asHalfPixel($$.margin.top)
  } else if (target === 'context') {
    x = asHalfPixel($$.margin2.left)
    y = asHalfPixel($$.margin2.top)
  } else if (target === 'legend') {
    x = $$.margin3.left
    y = $$.margin3.top
  } else if (target === 'x') {
    x = 0
    y = config.axis_rotated ? 0 : $$.height
  } else if (target === 'y') {
    x = 0
    y = config.axis_rotated ? $$.height : 0
  } else if (target === 'y2') {
    x = config.axis_rotated ? 0 : $$.width
    y = config.axis_rotated ? 1 : 0
  } else if (target === 'subx') {
    x = 0
    y = config.axis_rotated ? 0 : $$.height2
  } else if (target === 'arc') {
    x = $$.arcWidth / 2
    y = $$.arcHeight / 2 - ($$.hasType('gauge') ? 6 : 0) // to prevent wrong display of min and max label
  }
  return 'translate(' + x + ',' + y + ')'
}
ChartInternal.prototype.initialOpacity = function(d) {
  return d.value !== null && this.withoutFadeIn[d.id] ? 1 : 0
}
ChartInternal.prototype.initialOpacityForCircle = function(d) {
  return d.value !== null && this.withoutFadeIn[d.id]
    ? this.opacityForCircle(d)
    : 0
}
ChartInternal.prototype.opacityForCircle = function(d) {
  var isPointShouldBeShown = isFunction(this.config.point_show)
    ? this.config.point_show(d)
    : this.config.point_show
  var opacity = isPointShouldBeShown || this.isStanfordType(d) ? 1 : 0
  return isValue(d.value) ? (this.isScatterType(d) ? 0.5 : opacity) : 0
}
ChartInternal.prototype.opacityForText = function() {
  return this.hasDataLabel() ? 1 : 0
}
ChartInternal.prototype.xx = function(d) {
  return d ? this.x(d.x) : null
}
ChartInternal.prototype.xvCustom = function(d, xyValue) {
  var $$ = this,
    value = xyValue ? d[xyValue] : d.value
  if ($$.isTimeSeries()) {
    value = $$.parseDate(d.value)
  } else if ($$.isCategorized() && typeof d.value === 'string') {
    value = $$.config.axis_x_categories.indexOf(d.value)
  }
  return Math.ceil($$.x(value))
}
ChartInternal.prototype.yvCustom = function(d, xyValue) {
  var $$ = this,
    yScale = d.axis && d.axis === 'y2' ? $$.y2 : $$.y,
    value = xyValue ? d[xyValue] : d.value
  return Math.ceil(yScale(value))
}
ChartInternal.prototype.xv = function(d) {
  var $$ = this,
    value = d.value
  if ($$.isTimeSeries()) {
    value = $$.parseDate(d.value)
  } else if ($$.isCategorized() && typeof d.value === 'string') {
    value = $$.config.axis_x_categories.indexOf(d.value)
  }
  return Math.ceil($$.x(value))
}
ChartInternal.prototype.yv = function(d) {
  var $$ = this,
    yScale = d.axis && d.axis === 'y2' ? $$.y2 : $$.y
  return Math.ceil(yScale(d.value))
}
ChartInternal.prototype.subxx = function(d) {
  return d ? this.subX(d.x) : null
}

ChartInternal.prototype.transformMain = function(withTransition, transitions) {
  var $$ = this,
    xAxis,
    yAxis,
    y2Axis
  if (transitions && transitions.axisX) {
    xAxis = transitions.axisX
  } else {
    xAxis = $$.main.select('.' + CLASS.axisX)
    if (withTransition) {
      xAxis = xAxis.transition()
    }
  }
  if (transitions && transitions.axisY) {
    yAxis = transitions.axisY
  } else {
    yAxis = $$.main.select('.' + CLASS.axisY)
    if (withTransition) {
      yAxis = yAxis.transition()
    }
  }
  if (transitions && transitions.axisY2) {
    y2Axis = transitions.axisY2
  } else {
    y2Axis = $$.main.select('.' + CLASS.axisY2)
    if (withTransition) {
      y2Axis = y2Axis.transition()
    }
  }
  ;(withTransition ? $$.main.transition() : $$.main).attr(
    'transform',
    $$.getTranslate('main')
  )
  xAxis.attr('transform', $$.getTranslate('x'))
  yAxis.attr('transform', $$.getTranslate('y'))
  y2Axis.attr('transform', $$.getTranslate('y2'))
  $$.main
    .select('.' + CLASS.chartArcs)
    .attr('transform', $$.getTranslate('arc'))
}
ChartInternal.prototype.transformAll = function(withTransition, transitions) {
  var $$ = this
  $$.transformMain(withTransition, transitions)
  if ($$.config.subchart_show) {
    $$.transformContext(withTransition, transitions)
  }
  if ($$.legend) {
    $$.transformLegend(withTransition)
  }
}

ChartInternal.prototype.updateSvgSize = function() {
  var $$ = this,
    brush = $$.svg.select(`.${CLASS.brush} .overlay`)
  $$.svg.attr('width', $$.currentWidth).attr('height', $$.currentHeight)
  $$.svg
    .selectAll(['#' + $$.clipId, '#' + $$.clipIdForGrid])
    .select('rect')
    .attr('width', $$.width)
    .attr('height', $$.height)
  $$.svg
    .select('#' + $$.clipIdForXAxis)
    .select('rect')
    .attr('x', $$.getXAxisClipX.bind($$))
    .attr('y', $$.getXAxisClipY.bind($$))
    .attr('width', $$.getXAxisClipWidth.bind($$))
    .attr('height', $$.getXAxisClipHeight.bind($$))
  $$.svg
    .select('#' + $$.clipIdForYAxis)
    .select('rect')
    .attr('x', $$.getYAxisClipX.bind($$))
    .attr('y', $$.getYAxisClipY.bind($$))
    .attr('width', $$.getYAxisClipWidth.bind($$))
    .attr('height', $$.getYAxisClipHeight.bind($$))
  $$.svg
    .select('#' + $$.clipIdForSubchart)
    .select('rect')
    .attr('width', $$.width)
    .attr('height', (brush.size() && brush.attr('height')) || 0)
  // MEMO: parent div's height will be bigger than svg when <!DOCTYPE html>
  $$.selectChart.style('max-height', $$.currentHeight + 'px')
}

ChartInternal.prototype.updateDimension = function(withoutAxis) {
  var $$ = this
  if (!withoutAxis) {
    if ($$.config.axis_rotated) {
      $$.axes.x.call($$.xAxis)
      $$.axes.subx.call($$.subXAxis)
    } else {
      $$.axes.y.call($$.yAxis)
      $$.axes.y2.call($$.y2Axis)
    }
  }
  $$.updateSizes()
  $$.updateScales()
  $$.updateSvgSize()
  $$.transformAll(false)
}

ChartInternal.prototype.observeInserted = function(selection) {
  var $$ = this,
    observer
  if (typeof MutationObserver === 'undefined') {
    window.console.error('MutationObserver not defined.')
    return
  }
  observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.type === 'childList' && mutation.previousSibling) {
        observer.disconnect()
        // need to wait for completion of load because size calculation requires the actual sizes determined after that completion
        $$.intervalForObserveInserted = window.setInterval(function() {
          // parentNode will NOT be null when completed
          if (selection.node().parentNode) {
            window.clearInterval($$.intervalForObserveInserted)
            $$.updateDimension()
            if ($$.brush) {
              $$.brush.update()
            }
            $$.config.oninit.call($$)
            $$.redraw({
              withTransform: true,
              withUpdateXDomain: true,
              withUpdateOrgXDomain: true,
              withTransition: false,
              withTransitionForTransform: false,
              withLegend: true
            })
            selection.transition().style('opacity', 1)
          }
        }, 10)
      }
    })
  })
  observer.observe(selection.node(), {
    attributes: true,
    childList: true,
    characterData: true
  })
}

/**
 * Binds handlers to the window resize event.
 */
ChartInternal.prototype.bindResize = function() {
  var $$ = this,
    config = $$.config

  $$.resizeFunction = $$.generateResize() // need to call .remove

  $$.resizeFunction.add(function() {
    config.onresize.call($$)
  })
  if (config.resize_auto) {
    $$.resizeFunction.add(function() {
      if ($$.resizeTimeout !== undefined) {
        window.clearTimeout($$.resizeTimeout)
      }
      $$.resizeTimeout = window.setTimeout(function() {
        delete $$.resizeTimeout
        $$.updateAndRedraw({
          withUpdateXDomain: false,
          withUpdateOrgXDomain: false,
          withTransition: false,
          withTransitionForTransform: false,
          withLegend: true
        })
        if ($$.brush) {
          $$.brush.update()
        }
      }, 100)
    })
  }
  $$.resizeFunction.add(function() {
    config.onresized.call($$)
  })

  $$.resizeIfElementDisplayed = function() {
    // if element not displayed skip it
    if ($$.api == null || !$$.api.element.offsetParent) {
      return
    }

    $$.resizeFunction()
  }

  window.addEventListener('resize', $$.resizeIfElementDisplayed, false)
}

/**
 * Binds handlers to the window focus event.
 */
ChartInternal.prototype.bindWindowFocus = function() {
  if (this.windowFocusHandler) {
    // The handler is already set
    return
  }

  this.windowFocusHandler = () => {
    this.redraw()
  }

  window.addEventListener('focus', this.windowFocusHandler)
}

/**
 * Unbinds from the window focus event.
 */
ChartInternal.prototype.unbindWindowFocus = function() {
  window.removeEventListener('focus', this.windowFocusHandler)
  delete this.windowFocusHandler
}

ChartInternal.prototype.generateResize = function() {
  var resizeFunctions = []

  function callResizeFunctions() {
    resizeFunctions.forEach(function(f) {
      f()
    })
  }
  callResizeFunctions.add = function(f) {
    resizeFunctions.push(f)
  }
  callResizeFunctions.remove = function(f) {
    for (var i = 0; i < resizeFunctions.length; i++) {
      if (resizeFunctions[i] === f) {
        resizeFunctions.splice(i, 1)
        break
      }
    }
  }
  return callResizeFunctions
}

ChartInternal.prototype.endall = function(transition, callback) {
  var n = 0
  transition
    .each(function() {
      ++n
    })
    .on('end', function() {
      if (!--n) {
        callback.apply(this, arguments)
      }
    })
}
ChartInternal.prototype.generateWait = function() {
  var $$ = this
  var transitionsToWait = [],
    f = function(callback) {
      var timer = setInterval(function() {
        if (!$$.isTabVisible()) {
          return
        }

        var done = 0
        transitionsToWait.forEach(function(t) {
          if (t.empty()) {
            done += 1
            return
          }
          try {
            t.transition()
          } catch (e) {
            done += 1
          }
        })
        if (done === transitionsToWait.length) {
          clearInterval(timer)
          if (callback) {
            callback()
          }
        }
      }, 50)
    }
  ;(f as any).add = function(transition) {
    transitionsToWait.push(transition)
  }
  return f
}

ChartInternal.prototype.parseDate = function(date) {
  var $$ = this,
    parsedDate
  if (date instanceof Date) {
    parsedDate = date
  } else if (typeof date === 'string') {
    parsedDate = $$.dataTimeParse(date)
  } else if (typeof date === 'object') {
    parsedDate = new Date(+date)
  } else if (typeof date === 'number' && !isNaN(date)) {
    parsedDate = new Date(+date)
  }
  if (!parsedDate || isNaN(+parsedDate)) {
    window.console.error("Failed to parse x '" + date + "' to Date object")
  }
  return parsedDate
}

ChartInternal.prototype.isTabVisible = function() {
  return !document.hidden
}

ChartInternal.prototype.getPathBox = getPathBox
ChartInternal.prototype.CLASS = CLASS

export { Chart }
export { ChartInternal }
