import Animations from './Animations'
import Filters from './Filters'
import Utils from '../utils/Utils'

/**
 * ApexCharts Graphics Class for all drawing operations.
 *
 * @module Graphics
 **/

class Graphics {
  constructor(ctx) {
    this.ctx = ctx
    this.w = ctx.w
  }

  drawLine(
    x1,
    y1,
    x2,
    y2,
    lineColor = '#a8a8a8',
    dashArray = 0,
    strokeWidth = null,
    strokeLineCap = 'butt'
  ) {
    let w = this.w
    let line = w.globals.dom.Paper.line().attr({
      x1,
      y1,
      x2,
      y2,
      stroke: lineColor,
      'stroke-dasharray': dashArray,
      'stroke-width': strokeWidth,
      'stroke-linecap': strokeLineCap
    })

    return line
  }

  drawRect(
    x1 = 0,
    y1 = 0,
    x2 = 0,
    y2 = 0,
    radius = 0,
    color = '#fefefe',
    opacity = 1,
    strokeWidth = null,
    strokeColor = null,
    strokeDashArray = 0
  ) {
    let w = this.w
    let rect = w.globals.dom.Paper.rect()

    rect.attr({
      x: x1,
      y: y1,
      width: x2 > 0 ? x2 : 0,
      height: y2 > 0 ? y2 : 0,
      rx: radius,
      ry: radius,
      opacity,
      'stroke-width': strokeWidth !== null ? strokeWidth : 0,
      stroke: strokeColor !== null ? strokeColor : 'none',
      'stroke-dasharray': strokeDashArray
    })

    // fix apexcharts.js#1410
    rect.node.setAttribute('fill', color)

    return rect
  }

  drawPolygon(
    polygonString,
    stroke = '#e1e1e1',
    strokeWidth = 1,
    fill = 'none'
  ) {
    const w = this.w
    const polygon = w.globals.dom.Paper.polygon(polygonString).attr({
      fill,
      stroke,
      'stroke-width': strokeWidth
    })

    return polygon
  }

  drawCircle(radius, attrs = null) {
    const w = this.w

    if (radius < 0) radius = 0
    const c = w.globals.dom.Paper.circle(radius * 2)
    if (attrs !== null) {
      c.attr(attrs)
    }
    return c
  }

  drawPath({
    d = '',
    stroke = '#a8a8a8',
    strokeWidth = 1,
    fill,
    fillOpacity = 1,
    strokeOpacity = 1,
    classes,
    strokeLinecap = null,
    strokeDashArray = 0
  }) {
    let w = this.w

    if (strokeLinecap === null) {
      strokeLinecap = w.config.stroke.lineCap
    }

    if (d.indexOf('undefined') > -1 || d.indexOf('NaN') > -1) {
      d = `M 0 ${w.globals.gridHeight}`
    }
    let p = w.globals.dom.Paper.path(d).attr({
      fill,
      'fill-opacity': fillOpacity,
      stroke,
      'stroke-opacity': strokeOpacity,
      'stroke-linecap': strokeLinecap,
      'stroke-width': strokeWidth,
      'stroke-dasharray': strokeDashArray,
      class: classes
    })

    return p
  }

  group(attrs = null) {
    const w = this.w
    const g = w.globals.dom.Paper.group()

    if (attrs !== null) {
      g.attr(attrs)
    }
    return g
  }

  move(x, y) {
    let move = ['M', x, y].join(' ')
    return move
  }

  line(x, y, hORv = null) {
    let line = null
    if (hORv === null) {
      line = ['L', x, y].join(' ')
    } else if (hORv === 'H') {
      line = ['H', x].join(' ')
    } else if (hORv === 'V') {
      line = ['V', y].join(' ')
    }
    return line
  }

  curve(x1, y1, x2, y2, x, y) {
    let curve = ['C', x1, y1, x2, y2, x, y].join(' ')
    return curve
  }

  quadraticCurve(x1, y1, x, y) {
    let curve = ['Q', x1, y1, x, y].join(' ')
    return curve
  }

  arc(rx, ry, axisRotation, largeArcFlag, sweepFlag, x, y, relative = false) {
    let coord = 'A'
    if (relative) coord = 'a'

    let arc = [coord, rx, ry, axisRotation, largeArcFlag, sweepFlag, x, y].join(
      ' '
    )
    return arc
  }

  /**
   * @memberof Graphics
   * @param {object}
   *  i = series's index
   *  realIndex = realIndex is series's actual index when it was drawn time. After several redraws, the iterating "i" may change in loops, but realIndex doesn't
   *  pathFrom = existing pathFrom to animateTo
   *  pathTo = new Path to which d attr will be animated from pathFrom to pathTo
   *  stroke = line Color
   *  strokeWidth = width of path Line
   *  fill = it can be gradient, single color, pattern or image
   *  animationDelay = how much to delay when starting animation (in milliseconds)
   *  dataChangeSpeed = for dynamic animations, when data changes
   *  className = class attribute to add
   * @return {object} svg.js path object
   **/
  renderPaths({
    j,
    realIndex,
    pathFrom,
    pathTo,
    stroke,
    strokeWidth,
    strokeLinecap,
    fill,
    animationDelay,
    initialSpeed,
    dataChangeSpeed,
    className,
    shouldClipToGrid = true,
    bindEventsOnPaths = true,
    drawShadow = true
  }) {
    let w = this.w
    const filters = new Filters(this.ctx)
    const anim = new Animations(this.ctx)

    let initialAnim = this.w.config.chart.animations.enabled
    let dynamicAnim =
      initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled

    let d
    let shouldAnimate = !!(
      (initialAnim && !w.globals.resized) ||
      (dynamicAnim && w.globals.dataChanged && w.globals.shouldAnimate)
    )

    if (shouldAnimate) {
      d = pathFrom
    } else {
      d = pathTo
      w.globals.animationEnded = true
    }

    let strokeDashArrayOpt = w.config.stroke.dashArray
    let strokeDashArray = 0
    if (Array.isArray(strokeDashArrayOpt)) {
      strokeDashArray = strokeDashArrayOpt[realIndex]
    } else {
      strokeDashArray = w.config.stroke.dashArray
    }

    let el = this.drawPath({
      d,
      stroke,
      strokeWidth,
      fill,
      fillOpacity: 1,
      classes: className,
      strokeLinecap,
      strokeDashArray
    })

    el.attr('index', realIndex)

    if (shouldClipToGrid) {
      el.attr({
        'clip-path': `url(#gridRectMask${w.globals.cuid})`
      })
    }

    // const defaultFilter = el.filterer

    if (w.config.states.normal.filter.type !== 'none') {
      filters.getDefaultFilter(el, realIndex)
    } else {
      if (w.config.chart.dropShadow.enabled && drawShadow) {
        if (
          !w.config.chart.dropShadow.enabledOnSeries ||
          (w.config.chart.dropShadow.enabledOnSeries &&
            w.config.chart.dropShadow.enabledOnSeries.indexOf(realIndex) !== -1)
        ) {
          const shadow = w.config.chart.dropShadow
          filters.dropShadow(el, shadow, realIndex)
        }
      }
    }

    if (bindEventsOnPaths) {
      el.node.addEventListener('mouseenter', this.pathMouseEnter.bind(this, el))
      el.node.addEventListener('mouseleave', this.pathMouseLeave.bind(this, el))
      el.node.addEventListener('mousedown', this.pathMouseDown.bind(this, el))
    }

    el.attr({
      pathTo,
      pathFrom
    })

    const defaultAnimateOpts = {
      el,
      j,
      realIndex,
      pathFrom,
      pathTo,
      fill,
      strokeWidth,
      delay: animationDelay
    }

    if (initialAnim && !w.globals.resized && !w.globals.dataChanged) {
      anim.animatePathsGradually({
        ...defaultAnimateOpts,
        speed: initialSpeed
      })
    } else {
      if (w.globals.resized || !w.globals.dataChanged) {
        anim.showDelayedElements()
      }
    }

    if (w.globals.dataChanged && dynamicAnim && shouldAnimate) {
      anim.animatePathsGradually({
        ...defaultAnimateOpts,
        speed: dataChangeSpeed
      })
    }

    return el
  }

  drawPattern(
    style,
    width,
    height,
    stroke = '#a8a8a8',
    strokeWidth = 0,
    opacity = 1
  ) {
    let w = this.w

    let p = w.globals.dom.Paper.pattern(width, height, (add) => {
      if (style === 'horizontalLines') {
        add
          .line(0, 0, height, 0)
          .stroke({ color: stroke, width: strokeWidth + 1 })
      } else if (style === 'verticalLines') {
        add
          .line(0, 0, 0, width)
          .stroke({ color: stroke, width: strokeWidth + 1 })
      } else if (style === 'slantedLines') {
        add
          .line(0, 0, width, height)
          .stroke({ color: stroke, width: strokeWidth })
      } else if (style === 'squares') {
        add
          .rect(width, height)
          .fill('none')
          .stroke({ color: stroke, width: strokeWidth })
      } else if (style === 'circles') {
        add
          .circle(width)
          .fill('none')
          .stroke({ color: stroke, width: strokeWidth })
      }
    })

    return p
  }

  drawGradient(
    style,
    gfrom,
    gto,
    opacityFrom,
    opacityTo,
    size = null,
    stops = null,
    colorStops = null,
    i = 0
  ) {
    let w = this.w
    let g

    if (gfrom.length < 9 && gfrom.indexOf('#') === 0) {
      // if the hex contains alpha and is of 9 digit, skip the opacity
      gfrom = Utils.hexToRgba(gfrom, opacityFrom)
    }
    if (gto.length < 9 && gto.indexOf('#') === 0) {
      gto = Utils.hexToRgba(gto, opacityTo)
    }

    let stop1 = 0
    let stop2 = 1
    let stop3 = 1
    let stop4 = null

    if (stops !== null) {
      stop1 = typeof stops[0] !== 'undefined' ? stops[0] / 100 : 0
      stop2 = typeof stops[1] !== 'undefined' ? stops[1] / 100 : 1
      stop3 = typeof stops[2] !== 'undefined' ? stops[2] / 100 : 1
      stop4 = typeof stops[3] !== 'undefined' ? stops[3] / 100 : null
    }

    let radial = !!(
      w.config.chart.type === 'donut' ||
      w.config.chart.type === 'pie' ||
      w.config.chart.type === 'polarArea' ||
      w.config.chart.type === 'bubble'
    )

    if (colorStops === null || colorStops.length === 0) {
      g = w.globals.dom.Paper.gradient(radial ? 'radial' : 'linear', (stop) => {
        stop.at(stop1, gfrom, opacityFrom)
        stop.at(stop2, gto, opacityTo)
        stop.at(stop3, gto, opacityTo)
        if (stop4 !== null) {
          stop.at(stop4, gfrom, opacityFrom)
        }
      })
    } else {
      g = w.globals.dom.Paper.gradient(radial ? 'radial' : 'linear', (stop) => {
        let gradientStops = Array.isArray(colorStops[i])
          ? colorStops[i]
          : colorStops
        gradientStops.forEach((s) => {
          stop.at(s.offset / 100, s.color, s.opacity)
        })
      })
    }

    if (!radial) {
      if (style === 'vertical') {
        g.from(0, 0).to(0, 1)
      } else if (style === 'diagonal') {
        g.from(0, 0).to(1, 1)
      } else if (style === 'horizontal') {
        g.from(0, 1).to(1, 1)
      } else if (style === 'diagonal2') {
        g.from(1, 0).to(0, 1)
      }
    } else {
      let offx = w.globals.gridWidth / 2
      let offy = w.globals.gridHeight / 2

      if (w.config.chart.type !== 'bubble') {
        g.attr({
          gradientUnits: 'userSpaceOnUse',
          cx: offx,
          cy: offy,
          r: size
        })
      } else {
        g.attr({
          cx: 0.5,
          cy: 0.5,
          r: 0.8,
          fx: 0.2,
          fy: 0.2
        })
      }
    }

    return g
  }

  getTextBasedOnMaxWidth({ text, maxWidth, fontSize, fontFamily }) {
    const tRects = this.getTextRects(text, fontSize, fontFamily)
    const wordWidth = tRects.width / text.length
    const wordsBasedOnWidth = Math.floor(maxWidth / wordWidth)
    if (maxWidth < tRects.width) {
      return text.slice(0, wordsBasedOnWidth - 3) + '...'
    }
    return text
  }

  drawText({
    x,
    y,
    text,
    textAnchor,
    fontSize,
    fontFamily,
    fontWeight,
    foreColor,
    opacity,
    maxWidth,
    cssClass = '',
    isPlainText = true
  }) {
    let w = this.w

    if (typeof text === 'undefined') text = ''

    let truncatedText = text
    if (!textAnchor) {
      textAnchor = 'start'
    }

    if (!foreColor || !foreColor.length) {
      foreColor = w.config.chart.foreColor
    }
    fontFamily = fontFamily || w.config.chart.fontFamily
    fontSize = fontSize || '11px'
    fontWeight = fontWeight || 'regular'

    const commonProps = {
      maxWidth,
      fontSize,
      fontFamily
    }
    let elText
    if (Array.isArray(text)) {
      elText = w.globals.dom.Paper.text((add) => {
        for (let i = 0; i < text.length; i++) {
          truncatedText = text[i]
          if (maxWidth) {
            truncatedText = this.getTextBasedOnMaxWidth({
              text: text[i],
              ...commonProps
            })
          }
          i === 0
            ? add.tspan(truncatedText)
            : add.tspan(truncatedText).newLine()
        }
      })
    } else {
      if (maxWidth) {
        truncatedText = this.getTextBasedOnMaxWidth({
          text,
          ...commonProps
        })
      }
      elText = isPlainText
        ? w.globals.dom.Paper.plain(text)
        : w.globals.dom.Paper.text((add) => add.tspan(truncatedText))
    }

    elText.attr({
      x,
      y,
      'text-anchor': textAnchor,
      'dominant-baseline': 'auto',
      'font-size': fontSize,
      'font-family': fontFamily,
      'font-weight': fontWeight,
      fill: foreColor,
      class: 'apexcharts-text ' + cssClass
    })

    elText.node.style.fontFamily = fontFamily
    elText.node.style.opacity = opacity

    return elText
  }

  drawMarker(x, y, opts) {
    x = x || 0
    let size = opts.pSize || 0

    let elPoint = null

    if (opts.shape === 'square' || opts.shape === 'rect') {
      let radius = opts.pRadius === undefined ? size / 2 : opts.pRadius

      if (y === null || !size) {
        size = 0
        radius = 0
      }

      let nSize = size * 1.2 + radius

      let p = this.drawRect(nSize, nSize, nSize, nSize, radius)

      p.attr({
        x: x - nSize / 2,
        y: y - nSize / 2,
        cx: x,
        cy: y,
        class: opts.class ? opts.class : '',
        fill: opts.pointFillColor,
        'fill-opacity': opts.pointFillOpacity ? opts.pointFillOpacity : 1,
        stroke: opts.pointStrokeColor,
        'stroke-width': opts.pointStrokeWidth ? opts.pointStrokeWidth : 0,
        'stroke-opacity': opts.pointStrokeOpacity ? opts.pointStrokeOpacity : 1
      })

      elPoint = p
    } else if (opts.shape === 'circle' || !opts.shape) {
      if (!Utils.isNumber(y)) {
        size = 0
        y = 0
      }

      // let nSize = size - opts.pRadius / 2 < 0 ? 0 : size - opts.pRadius / 2

      elPoint = this.drawCircle(size, {
        cx: x,
        cy: y,
        class: opts.class ? opts.class : '',
        stroke: opts.pointStrokeColor,
        fill: opts.pointFillColor,
        'fill-opacity': opts.pointFillOpacity ? opts.pointFillOpacity : 1,
        'stroke-width': opts.pointStrokeWidth ? opts.pointStrokeWidth : 0,
        'stroke-opacity': opts.pointStrokeOpacity ? opts.pointStrokeOpacity : 1
      })
    }

    return elPoint
  }

  pathMouseEnter(path, e) {
    let w = this.w
    const filters = new Filters(this.ctx)

    const i = parseInt(path.node.getAttribute('index'), 10)
    const j = parseInt(path.node.getAttribute('j'), 10)

    if (typeof w.config.chart.events.dataPointMouseEnter === 'function') {
      w.config.chart.events.dataPointMouseEnter(e, this.ctx, {
        seriesIndex: i,
        dataPointIndex: j,
        w
      })
    }
    this.ctx.events.fireEvent('dataPointMouseEnter', [
      e,
      this.ctx,
      { seriesIndex: i, dataPointIndex: j, w }
    ])

    if (w.config.states.active.filter.type !== 'none') {
      if (path.node.getAttribute('selected') === 'true') {
        return
      }
    }

    if (w.config.states.hover.filter.type !== 'none') {
      if (!w.globals.isTouchDevice) {
        let hoverFilter = w.config.states.hover.filter
        filters.applyFilter(path, i, hoverFilter.type, hoverFilter.value)
      }
    }
  }

  pathMouseLeave(path, e) {
    let w = this.w
    const filters = new Filters(this.ctx)

    const i = parseInt(path.node.getAttribute('index'), 10)
    const j = parseInt(path.node.getAttribute('j'), 10)

    if (typeof w.config.chart.events.dataPointMouseLeave === 'function') {
      w.config.chart.events.dataPointMouseLeave(e, this.ctx, {
        seriesIndex: i,
        dataPointIndex: j,
        w
      })
    }
    this.ctx.events.fireEvent('dataPointMouseLeave', [
      e,
      this.ctx,
      { seriesIndex: i, dataPointIndex: j, w }
    ])

    if (w.config.states.active.filter.type !== 'none') {
      if (path.node.getAttribute('selected') === 'true') {
        return
      }
    }

    if (w.config.states.hover.filter.type !== 'none') {
      filters.getDefaultFilter(path, i)
    }
  }

  pathMouseDown(path, e) {
    let w = this.w
    const filters = new Filters(this.ctx)

    const i = parseInt(path.node.getAttribute('index'), 10)
    const j = parseInt(path.node.getAttribute('j'), 10)

    let selected = 'false'
    if (path.node.getAttribute('selected') === 'true') {
      path.node.setAttribute('selected', 'false')

      if (w.globals.selectedDataPoints[i].indexOf(j) > -1) {
        let index = w.globals.selectedDataPoints[i].indexOf(j)
        w.globals.selectedDataPoints[i].splice(index, 1)
      }
    } else {
      if (
        !w.config.states.active.allowMultipleDataPointsSelection &&
        w.globals.selectedDataPoints.length > 0
      ) {
        w.globals.selectedDataPoints = []
        const elPaths = w.globals.dom.Paper.select('.apexcharts-series path')
          .members
        const elCircles = w.globals.dom.Paper.select(
          '.apexcharts-series circle, .apexcharts-series rect'
        ).members

        const deSelect = (els) => {
          Array.prototype.forEach.call(els, (el) => {
            el.node.setAttribute('selected', 'false')
            filters.getDefaultFilter(el, i)
          })
        }
        deSelect(elPaths)
        deSelect(elCircles)
      }

      path.node.setAttribute('selected', 'true')
      selected = 'true'

      if (typeof w.globals.selectedDataPoints[i] === 'undefined') {
        w.globals.selectedDataPoints[i] = []
      }
      w.globals.selectedDataPoints[i].push(j)
    }

    if (selected === 'true') {
      let activeFilter = w.config.states.active.filter
      if (activeFilter !== 'none') {
        filters.applyFilter(path, i, activeFilter.type, activeFilter.value)
      } else {
        // Reapply the hover filter in case it was removed by `deselect`when there is no active filter and it is not a touch device
        if (w.config.states.hover.filter !== 'none') {
          if (!w.globals.isTouchDevice) {
            var hoverFilter = w.config.states.hover.filter
            filters.applyFilter(path, i, hoverFilter.type, hoverFilter.value)
          }
        }
      }
    } else {
      // If the item was deselected, apply hover state filter if it is not a touch device
      if (w.config.states.active.filter.type !== 'none') {
        if (
          w.config.states.hover.filter.type !== 'none' &&
          !w.globals.isTouchDevice
        ) {
          var hoverFilter = w.config.states.hover.filter
          filters.applyFilter(path, i, hoverFilter.type, hoverFilter.value)
        } else {
          filters.getDefaultFilter(path, i)
        }
      }
    }

    if (typeof w.config.chart.events.dataPointSelection === 'function') {
      w.config.chart.events.dataPointSelection(e, this.ctx, {
        selectedDataPoints: w.globals.selectedDataPoints,
        seriesIndex: i,
        dataPointIndex: j,
        w
      })
    }

    if (e) {
      this.ctx.events.fireEvent('dataPointSelection', [
        e,
        this.ctx,
        {
          selectedDataPoints: w.globals.selectedDataPoints,
          seriesIndex: i,
          dataPointIndex: j,
          w
        }
      ])
    }
  }

  rotateAroundCenter(el) {
    let coord = {}
    if (el && typeof el.getBBox === 'function') {
      coord = el.getBBox()
    }
    let x = coord.x + coord.width / 2
    let y = coord.y + coord.height / 2

    return {
      x,
      y
    }
  }

  static setAttrs(el, attrs) {
    for (let key in attrs) {
      if (attrs.hasOwnProperty(key)) {
        el.setAttribute(key, attrs[key])
      }
    }
  }

  getTextRects(text, fontSize, fontFamily, transform, useBBox = true) {
    let w = this.w
    let virtualText = this.drawText({
      x: -200,
      y: -200,
      text,
      textAnchor: 'start',
      fontSize,
      fontFamily,
      foreColor: '#fff',
      opacity: 0
    })

    if (transform) {
      virtualText.attr('transform', transform)
    }
    w.globals.dom.Paper.add(virtualText)

    let rect = virtualText.bbox()
    if (!useBBox) {
      rect = virtualText.node.getBoundingClientRect()
    }

    virtualText.remove()

    return {
      width: rect.width,
      height: rect.height
    }
  }

  /**
   * append ... to long text
   * http://stackoverflow.com/questions/9241315/trimming-text-to-a-given-pixel-width-in-svg
   * @memberof Graphics
   **/
  placeTextWithEllipsis(textObj, textString, width) {
    if (typeof textObj.getComputedTextLength !== 'function') return
    textObj.textContent = textString
    if (textString.length > 0) {
      // ellipsis is needed
      if (textObj.getComputedTextLength() >= width / 1.1) {
        for (let x = textString.length - 3; x > 0; x -= 3) {
          if (textObj.getSubStringLength(0, x) <= width / 1.1) {
            textObj.textContent = textString.substring(0, x) + '...'
            return
          }
        }
        textObj.textContent = '.' // can't place at all
      }
    }
  }
}

export default Graphics
