import React from 'react'
import * as d3 from 'd3'
import moment from 'moment-timezone'
import styles from './styles'
import PropTypes from 'prop-types'
import tinycolor from 'tinycolor2'

class D3Chart extends React.Component {
  componentWillMount () {
    let idx = this.constructor._lastIndex ? this.constructor._lastIndex : 0
    this.id = 'id' + idx
    this.constructor._lastIndex++
    this.originalData = []
    this.data = []
    this.selectedFilters = []
    this.pieChartTypes = ['pie', 'donut']
  }
  componentDidMount () {
    this.svg = d3.select('#' + this.id)
    this.container = d3.select('#' + this.id + 'ChartContainer')
    this.didMount = true
    this.setState({})
  }
  shouldComponentUpdate (nextProps, nextState) {
    let update = this.data && nextProps.data && JSON.stringify(this.originalData) !== JSON.stringify(nextProps.data) && this.didMount
    if (update) {
      this.selectedFilters = []
      this.originalData = JSON.parse(JSON.stringify(nextProps.data))
      this.data = JSON.parse(JSON.stringify(nextProps.data))
      let data = !Array.isArray(this.data) ? [this.data] : this.data
      if (nextProps.type === 'line') {
        data.forEach((d) => {
          if (!d.color) {
            d.color = this.getRandomHexColor()
          }
          d.data.forEach((d2) => {
            d2.x = new Date(d2.x)
          })
        })
      } else if (this.isPie(nextProps.type)) {
        let option = nextProps.option
        let colorDomain = option.color.domain
        let colorRange = option.color.range
        if (!colorRange || colorRange.length === 0) {
          colorRange = []
          colorRange.push(this.getRandomHexColor())
          colorRange.push(this.getRandomHexColor())
        }
        if (!colorDomain || colorDomain.length === 0) {
          colorDomain = []
          let totalObject = this.data.length
          let totalRange = colorRange.length
          for (let i = 0; i < totalRange; i++) {
            colorDomain.push(((totalObject - 1) / (totalRange - 1)) * i)
          }
        }
        // Create a color scale
        let colorScale = d3.scaleLinear()
            .domain(colorDomain)
            .range(colorRange)
            .interpolate(d3.interpolateHcl)
        data.forEach((d, i) => {
          if (!d.color) {
            d.color = this.rgbToHex(colorScale(i))
          }
        })
      }
    }
    if (this.filterClicked) { // update by filter
      update = true
      this.filterClicked = false
    }
    return update
  }
  isPie (type) {
    return this.pieChartTypes.indexOf(type) !== -1
  }
  handleFilterClick (index) {
    this.filterClicked = true
    let idx = this.selectedFilters.indexOf(index)
    if (idx === -1) {
      let datas = !Array.isArray(this.data) ? [this.data] : this.data
      if (datas.length === this.selectedFilters.length + 1) return
      this.selectedFilters.push(index)
    } else {
      this.selectedFilters.splice(idx, 1)
    }
    this.lastIndexFiltered = index
    this.setState({})
  }
  invertColor (color) {
    color = color.substring(1)
    color = parseInt(color, 16)
    color = 0xFFFFFF ^ color
    color = color.toString(16)
    color = ('000000' + color).slice(-6)
    color = '#' + color
    return color
  }
  rgbToHex (rgb = 'rgb(255, 255, 255)') {
    rgb = Array.prototype.join.call(arguments).match(/(-?[0-9\.]+)/g)
    let r = parseInt(rgb[0], 10)
    let g = parseInt(rgb[1], 10)
    let b = parseInt(rgb[2], 10)
    return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
  }
  hexToRgb (hex = '#FFF') {
    let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
    hex = hex.replace(shorthandRegex, function (m, r, g, b) {
      return r + r + g + g + b + b
    })
    let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16)
    } : null
  }
  getReadableColorRgb (rgb) {
    rgb = Array.prototype.join.call(arguments).match(/(-?[0-9\.]+)/g)
    let r = parseInt(rgb[0], 10)
    let g = parseInt(rgb[1], 10)
    let b = parseInt(rgb[2], 10)
    return (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000000' : '#FFFFFF'
  }
  getCoordinate (radius, percentage, startDegree) {
    let alfa = 360 * percentage + startDegree
    let rad = alfa * Math.PI / 180
    let x = Math.sin(rad) * radius
    let y = Math.cos(rad) * radius
    y *= -1
    return {
      x, y
    }
  }
  getL (radius, percentage, startDegree) {
    let c = this.getCoordinate(radius, percentage, startDegree)
    let newL = 'L ' + c.x + ' ' + c.y
    return newL
  }
  getRandomHexColor () {
    return '#' + ('000000' + Math.random().toString(16).slice(2, 8).toUpperCase()).slice(-6)
  }
  getCurve (curve) {
    switch (curve) {
      case 'linear':
        return d3.curveLinear
      case 'step':
        return d3.curveStep
      case 'stepBefore':
        return d3.curveStepBefore
      case 'stepAfter':
        return d3.curveStepAfter
      case 'basis':
        return d3.curveBasis
      case 'cardinal':
        return d3.curveCardinal
      case 'monotoneX':
        return d3.curveMonotoneX
      case 'catmullRom':
        return d3.curveCatmullRom
      default:
        return d3.curveMonotoneX
    }
  }
  renderLineChart (datas, option) {
    let svgWidth = option.width
    let svgHeight = option.height
    let min = 0
    let max = 0
    if (option.yAxis.range) {
      min = option.yAxis.range.min
      max = option.yAxis.range.max
    } else {
      datas.forEach((data) => {
        data.data.forEach((d) => {
          min = min > d.y ? d.y : min
          max = max < d.y ? d.y : max
        })
      })
      if (option.yAxis.offset) {
        min -= option.yAxis.offset.min
        max += option.yAxis.offset.max
      }
    }
    datas = datas.filter((data, i) => {
      return this.selectedFilters.indexOf(i) === -1
    })
    let characterSize = (max + '').length
    let leftLength = characterSize * 3 + Math.floor(characterSize / 3) * 2
    let bottomLength = 5
    let xAxisTextSize = 25
    let yAxisTextSize = 25
    let dateFormat = d3.timeFormat(option.timeFormat)

    let margins = option.margins
    this.container.selectAll('*').remove()
    let svg = this.container.append('svg')
        .attr('width', (svgWidth))
        .attr('height', (svgHeight))
    let tickSize = option.showTick ? 5 : 0
    let curve = this.getCurve(option.curve)
    let xScale = d3.scaleTime().range([margins.left + tickSize + leftLength + yAxisTextSize, svgWidth - margins.right]).domain([moment(option.xAxis.range.min).toDate(), moment(option.xAxis.range.max).toDate()])
    let yScale = d3.scaleLinear().range([svgHeight - margins.bottom - tickSize - xAxisTextSize - bottomLength, margins.top + 20]).domain([min, max])
    let xAxis = d3.axisBottom(xScale).tickFormat(dateFormat)
    let yAxis = d3.axisLeft(yScale).ticks(Math.min(max - min, 10))
    if (option.showGrid) {
      xAxis = xAxis.tickSize(-svgHeight + margins.bottom + margins.top + xAxisTextSize + bottomLength)
      yAxis = yAxis.tickSize(-svgWidth + margins.left + yAxisTextSize + leftLength)
    }

    svg.append('g').attr('class', styles.axis).attr('transform', 'translate(0,' + (svgHeight - margins.bottom - xAxisTextSize - bottomLength) + ')').call(xAxis)
        .append('text')
        .attr('x', (svgWidth / 2))
        .attr('y', xAxisTextSize)
        .attr('fill', '#000')
        .attr('class', styles.axisText)
        .text(option.xAxis.text)

    svg.append('g').attr('class', styles.axis).attr('transform', 'translate(' + (margins.left + leftLength + yAxisTextSize) + ', 0)').call(yAxis)
        .append('text')
        .attr('x', -(svgHeight / 2))
        .attr('y', -yAxisTextSize - leftLength)
        .attr('transform', 'rotate(-90)')
        .attr('fill', '#000')
        .attr('class', styles.axisText)
        .text(option.yAxis.text)

    let lineGen = d3.line()
        .x((d) => {
          return xScale(d.x)
        })
        .y((d) => {
          return yScale(d.y)
        })
    if (option.curve) {
      lineGen = lineGen.curve(curve)
    }

    let tooltipWidth = option.tooltipWidth ? option.tooltipWidth : '94px'
    let tooltip = this.container.append('div')
        .attr('class', styles.tooltip)
        .style('width', tooltipWidth)
        .style('opacity', '0')

    let bisect = d3.bisector(function (d) { return d.x }).left

    tooltip.append('div')
        .attr('class', styles.tooltipX)

    let tooltipLikes = tooltip.append('div')
    datas.forEach((data, i) => {
      if (i > 0) {
        tooltipLikes.append('br')
      }
      tooltipLikes.append('span')
          .attr('class', styles.tooltipTitle + ' tooltipTitle' + i)
          .text(data.name + ': ')

      tooltipLikes.append('span')
          .attr('class', styles.tooltipY + ' tooltipY' + i)
    })

    // area
    let area = d3.area()
        .curve(curve)
        .x((d) => { return xScale(d.x) })
        .y0(svgHeight - margins.bottom - xAxisTextSize - bottomLength - tickSize)
        .y1((d) => { return yScale(d.y) })
    if (option.curve) {
      area = area.curve(curve)
    }

    let focuses = []
    datas.reverse().forEach((data, i) => {
      let dataColor = data.color
      let rgb = this.hexToRgb(dataColor)
      let dataFill = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',0.3)'

      if (option.showArea) {
        svg.append('path')
            .datum(data.data)
            .attr('fill', dataFill)
            .attr('d', area)
      }

      svg.append('path')
          .attr('d', lineGen(data.data))
          .attr('stroke', dataColor)
          .attr('stroke-width', 2)
          .attr('fill', 'none')

      let focus = svg.append('g')
          .attr('class', 'focus')
          .style('display', 'none')

      let color = this.invertColor(dataColor)

      focus.append('circle')
          .attr('r', 5)
          .attr('fill', color)

      focuses.push(focus)
    })

    datas.reverse()

    let mousemove = function () {
      let x0 = xScale.invert(d3.mouse(this)[0])
      let y0 = yScale.invert(d3.mouse(this)[1])
      let tooltipShowed = false
      let tooltipData = []
      datas.forEach((data, idx) => {
        let i = bisect(data.data, x0, 1)
        let d0 = data.data[i - 1]
        let d1 = data.data[i]
        if (d1 && d0) {
          let d = x0 - d0.x > d1.x - x0 ? d1 : d0
          focuses[idx].attr('transform', 'translate(' + xScale(d.x) + ',' + yScale(d.y) + ')')
          tooltipData.push({
            idx: idx,
            data: d
          })
          if (!tooltipShowed) {
            tooltipShowed = true
          }
        }
      })
      if (option.showTooltip && tooltipShowed) {
        let d0 = tooltipData[0].data
        tooltip.style('left', (xScale(d0.x) + 10) + 'px')
        tooltip.style('top', yScale(y0) + 'px')
        tooltip.select('.' + styles.tooltipX).text(moment(d0.x).format('D MMMM'))
        tooltip.select('.' + styles.tooltipY).text('')
        let valueFormatter = option.tooltipValueFormatter
        tooltipData.forEach((t) => {
          let d = t.data
          let value = valueFormatter ? valueFormatter(d.y) : d.y
          tooltip.select('.tooltipY' + t.idx).text(value)
        })
      }
    }

    svg.append('rect')
        .attr('class', styles.overlay)
        .attr('width', svgWidth - margins.left - margins.right - yAxisTextSize - leftLength)
        .attr('height', svgHeight - margins.top - margins.bottom - xAxisTextSize - bottomLength)
        .attr('x', (margins.left + yAxisTextSize + leftLength))
        .attr('y', margins.top)
        .on('mouseover', function () {
          focuses.forEach((focus) => {
            focus.style('display', null)
          })
          tooltip.style('opacity', null)
        })
        .on('mouseout', function () {
          focuses.forEach((focus) => {
            focus.style('display', 'none')
          })
          tooltip.style('opacity', 0)
        })
        .on('mousemove', mousemove)
  }
  renderPieChart (datas, option, type) {
    let svgWidth = option.width
    let svgHeight = option.height
    let padAngle = option.padAngle
    let startDegree = option.startDegree
    let margins = option.margins
    let width = svgWidth - margins.left - margins.right
    let height = svgHeight - margins.top - margins.bottom
    let radius = Math.min(width, height) / 2

    datas = JSON.parse(JSON.stringify(datas))
    datas.forEach((data, i) => {
      data.enabled = type === 'donut' || this.selectedFilters.indexOf(i) === -1
    })
    if (type === 'donut') {
      datas = datas.filter((data, i) => { return this.selectedFilters.indexOf(i) === -1 })
    }

    this.container.selectAll('*').remove()
    let svg = this.container.append('svg')
        .attr('width', svgWidth)
        .attr('height', svgHeight)
        .append('g').attr('class', 'wrapper')
        .attr('transform', 'translate(' + (width / 2 + margins.left) + ',' + (height / 2 + margins.top) + ')')

    // Create an arc function
    let arc = d3.arc()
        .innerRadius(type === 'pie' ? 0 : radius / 2)
        .outerRadius(radius)

    let totalData = 0
    let values = []
    datas.forEach((d, i) => {
      if (d.enabled) {
        totalData += d.value
        values.push(d.value)
      }
    })
    let minData = values.length === 0 ? 0 : Math.min(...values)
    minData = minData
    padAngle = minData / totalData
    padAngle = Math.min(padAngle, option.padAngle ? option.padAngle : 0)
    let total = 0

    let pie = d3.pie()
        .startAngle(startDegree * Math.PI / 180)
        .endAngle(startDegree * Math.PI / 180 + 2 * Math.PI)
        .value((d, i) => {
          let enabled = d.enabled
          if (type === 'pie' && this.lastIndexFiltered === i) {
            enabled = !enabled
          }
          return enabled ? d.value : 0
        })
        .sort(null)
    if (type === 'donut') {
      pie.padAngle(padAngle)
    }

    let div = this.container.append('div').attr('style', 'position: absolute; margin-top: -' + (radius + margins.top) + 'px; margin-left: ' + (radius + margins.left) + 'px;')

    if (option.showLabel) {
      let newRadius = type === 'donut' ? radius * 0.75 : radius * 0.6
      div.selectAll('.pieDiv')
          .data(pie(datas))
          .enter().append('div')
          .attr('class', 'pieDiv')
          .attr('style', (d, i) => {
            if (i === 0) total = 0
            if (!d.data.enabled) return 'display: none'
            let style = 'position: absolute; transform: translate(-50%, -50%); pointer-events: none; z-index: 0;'
            let percentage = ((total + d.data.value / 2 + (totalData * (padAngle / 2) * 0.5)) / totalData)
            let c = this.getCoordinate(newRadius, percentage, startDegree)
            total += d.data.value
            style += 'margin-left: ' + c.x + 'px;'
            style += 'margin-top: ' + c.y + 'px;'
            let rgb = this.hexToRgb(d.data.color)
            let color = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'
            style += 'color: ' + this.getReadableColorRgb(color) + ';'
            return style
          })
          .append('span')
          .text((d) => {
            return d.data.name
          })
    }

    let tooltipWidth = option.tooltipWidth ? option.tooltipWidth : '94px'
    let tooltip = this.container.append('div')
        .attr('class', styles.tooltip)
        .style('width', tooltipWidth)
        .style('opacity', '0')

    tooltip.append('div')
        .attr('class', styles.tooltipX)

    let tooltipLikes = tooltip.append('div')
    tooltipLikes.append('span')
        .attr('class', styles.tooltipTitle)
        .text('Jumlah: ')

    tooltipLikes.append('span')
        .attr('class', styles.tooltipY)

    let xScale = d3.scaleLinear().range([0, 500])
    let yScale = d3.scaleLinear().range([0, 500])

    let mousemove = function () {
      if (option.showTooltip) {
        let x0 = xScale.invert(d3.mouse(this)[0])
        let y0 = yScale.invert(d3.mouse(this)[1])
        tooltip.style('left', (xScale(x0) + radius + margins.left) + 'px')
        tooltip.style('top', (yScale(y0) + radius + margins.top) + 'px')
      }
    }

    // Create the donut slices and also the invisible arcs for the text
    let arcs = svg.selectAll('.pieArcs')
        .data(pie(datas))
        .enter().append('path')
        .attr('class', 'pieArcs')
        .attr('d', (d, i) => {
          let dd = arc(d, i)
          if (type === 'pie' || !option.crispEdges || d.value === 0) return dd
          if (i === 0) total = 0
          let percentageTop = ((total + d.value + (totalData * (padAngle / 2) * 0.5)) / totalData)
          let newLTop = this.getL(radius * 0.75, percentageTop, startDegree) + ' L'
          dd = dd.replace('L', newLTop)
          let percentageBottom = ((total + (totalData * (padAngle / 2) * 1)) / totalData)
          let newLBottom = this.getL(radius * 0.75, percentageBottom, startDegree) + ' Z'
          dd = dd.replace('Z', newLBottom)
          total += d.value
          return dd
        })
        .style('fill', (d, i) => {
          return d.data.color
        })
        .each(function (d) { this._current = d })
        .on('mousemove', mousemove)
        .on('mouseover', function (d) {
          let percentage = Number((d.value * 100 / totalData).toFixed(2)).toString()
          let valueFormatter = option.tooltipValueFormatter
          let value = valueFormatter ? valueFormatter(d.value) : d.value
          tooltip.select('.' + styles.tooltipX).text(d.data.name)
          tooltip.select('.' + styles.tooltipY).text(value + ' (' + percentage + '%)')
          tooltip.style('opacity', null)
        })
        .on('mouseout', function () {
          tooltip.style('opacity', '0')
        })
    if (type === 'pie') {
      let paddingColor = option.paddingColor ? option.paddingColor : '#FFF'
      let paddingWidth = option.paddingWidth ? option.paddingWidth : 2
      arcs
          .attr('stroke', paddingColor)
          .attr('stroke-width', paddingWidth)
      pie.value((d) => { return d.enabled ? d.value : 0 })
      arcs
          .data(pie(datas))
          .transition()
          .duration(750)
          .attrTween('d', function (d) {
            var interpolate = d3.interpolate(this._current, d)
            this._current = interpolate(0)
            return function (t) {
              return arc(interpolate(t))
            }
          })
    }
  }
  render () {
    let option = this.props.option
    let type = this.props.type
    let containerWidth = Number(option.width)
    let containerHeight = Number(option.height)
    let svgWidth = option.width
    let svgHeight = option.height
    let legendPosition = 1
    let legends = []
    if (option.showLegend) {
      let legendOption = option.legendOption
      if (legendOption.position === 'top' || legendOption.position === 'bottom') {
        containerHeight += Number(legendOption.height)
      } else {
        containerWidth += Number(legendOption.width)
      }
      if (legendOption.position === 'left' || legendOption.position === 'top') {
        legendPosition = -1
      }
    }
    if (this.container && this.data) {
      let datas = !Array.isArray(this.data) ? [this.data] : this.data
      if (option.showLegend) {
        legends = []
        datas.forEach((data, i) => {
          let background = this.selectedFilters.indexOf(i) === -1 ? data.color : 'transparent'
          let labelStyle = this.selectedFilters.indexOf(i) === -1 ? {} : { opacity: 0.5 }
          let filterStyle = {}
          if (['top', 'bottom'].indexOf(option.legendOption.position) !== -1) {
            filterStyle.float = 'left'
          }
          let labelClassName = option.legendOption.labelStyle || ''
          let clickFn = option.legendOption.filterable ? this.handleFilterClick.bind(this, i) : () => {}
          let darkerColor = tinycolor(data.color).darken(20).toHexString()
          let aggregator = ''
          let value = ''
          let checkBoxStyle = option.legendOption.checkBoxStyle ? option.legendOption.checkBoxStyle + ' ' + styles.filterCheckbox : styles.filterCheckbox
          let valueFormatter = option.legendOption.valueFormatter
          if (option.legendOption.showValue && this.isPie(this.props.type)) {
            aggregator = option.legendOption.aggregator || ' - '
            value = (
                <span style={{ color: data.color }}>{valueFormatter ? valueFormatter(data.value) : data.value}</span>
            )
          }
          legends.push(
              <div key={i} style={filterStyle}>
                <div onClick={clickFn} className={styles.filterLabel}>
                  <div className={checkBoxStyle} style={{ border: '2px solid ' + darkerColor, background: background }}></div>
                  <span className={labelClassName} style={labelStyle}>
                  {data.name}{aggregator}{value}
                </span>
                </div>
              </div>
          )
        })
        // if (this.selectedFilters.length > 0) {
        //   let temp = []
        //   datas.forEach((d, i) => {
        //     if (this.selectedFilters.indexOf(i) === -1) {
        //       temp.push(d)
        //     }
        //   })
        //   datas = temp
        // }
      }
      let renderChart = () => {}
      if (type === 'line') {
        renderChart = this.renderLineChart.bind(this)
      } else if (type === 'pie' || type === 'donut') {
        renderChart = this.renderPieChart.bind(this)
      }
      renderChart(datas, option, type)
    }
    let element = []
    let svgContainerElement = (
        <div key={1} id={this.id + 'ChartContainer'} className={styles.svgContainer} style={{ width: svgWidth, height: svgHeight }}>
        </div>
    )
    let legendContainerElement = (
        <div key={2} className={styles.legend} style={{ width: option.legendOption.width, height: option.legendOption.height }}>
          <div>
            {legends}
          </div>
        </div>
    )
    if (legendPosition === -1) {
      element = [
        legendContainerElement,
        svgContainerElement
      ]
    } else {
      element = [
        svgContainerElement,
        legendContainerElement
      ]
    }
    return (
        <div style={{ display: 'inline-block', width: containerWidth }}>
          {
            element.map((v) => {
              return (
                  v
              )
            })
          }
        </div>
    )
  }
}

D3Chart.propTypes = {
  data: PropTypes.any,
  type: PropTypes.string,
  option: PropTypes.object
}

D3Chart._lastIndex = 0

export default D3Chart
