<template>
  <div class="radar-chart">

    <loading v-if="!data" />

    <div class="canvas-container">
      <canvas :ref="ref" />
    </div>

    <label-line :label="labelLine" :colors="drawColors" />
  </div>
</template>

<script>
import colorsMixin from '../../mixins/colorsMixin.js'

import canvasMixin from '../../mixins/canvasMixin.js'

export default {
  name: 'RadarChart',
  mixins: [canvasMixin, colorsMixin],
  props: ['data', 'labelLine', 'colors'],
  data () {
    return {
      ref: `radar-chart-${(new Date()).getTime()}`,

      defaultRadius: 0.8,
      defaultRingNum: 4,
      defaultRingType: 'circle',
      defaultRingLineType: 'dashed',
      defaultRingLineColor: '#666',
      defaultRingFillType: 'none',
      defaultRayLineType: 'line',
      defaultRayLineColor: '#666',

      defaultRayLineOffset: Math.PI * -0.5,
      defaultLabelColor: '#fff',
      defaultLabelFS: 10,

      defaultValueFontSize: 10,
      defaultValueColor: '#999',

      drawColors: '',
      radius: '',
      ringType: '',
      rayLineRadianData: [],
      ringRadiusData: [],
      ringPolylineData: [],
      ringLineDash: [],
      ringlineMultipleColor: false,
      ringLineColor: '',
      ringFillType: '',
      ringFillMultipleColor: false,
      ringFillColor: '',
      rayLineColor: '',
      rayLineDash: '',
      rayLineMultipleColor: false,
      labelPosData: [],
      labelColor: '',
      labelFontSize: '',
      labelMultipleColor: false,

      valuePointData: []
    }
  },
  watch: {
    data (d) {
      const { checkData, reDraw } = this

      checkData && reDraw(d)
    },
    color (d) {
      const { checkData, reDraw } = this

      checkData && reDraw(d)
    }
  },
  methods: {
    async init () {
      const { initCanvas, checkData, draw } = this

      await initCanvas()

      checkData() && draw()
    },
    checkData () {
      const { data } = this

      this.status = false

      if (!data || !data.series) return false

      this.status = true

      return true
    },
    draw () {
      const { ctx, canvasWH } = this

      ctx.clearRect(0, 0, ...canvasWH)

      const { initColors, calcRadarRadius, calcRingType } = this

      initColors()

      calcRadarRadius()

      calcRingType()

      const { calcRayLineRadianData, calcRingRadiusData, calcRingPolylineData } = this

      calcRayLineRadianData()

      calcRingRadiusData()

      calcRingPolylineData()

      const { calcRingDrawConfig, calcRingFillConfig, fillRing } = this

      calcRingDrawConfig()

      calcRingFillConfig()

      fillRing()

      const { drawCircleRing, drawPolylineRing, calcRayLineConfig } = this

      drawCircleRing()

      drawPolylineRing()

      calcRayLineConfig()

      const { drawRayLine, calcLabelPosData, calcLabelConfig } = this

      drawRayLine()

      calcLabelPosData()

      calcLabelConfig()

      const { drawLable, caclValuePointData, fillRadar } = this

      drawLable()

      caclValuePointData()

      fillRadar()

      const { fillValueText } = this

      fillValueText()
    },
    calcRadarRadius () {
      const { canvasWH, data: { radius }, defaultRadius } = this

      this.radius = Math.min(...canvasWH) * (radius || defaultRadius) * 0.5
    },
    calcRingType () {
      const { data: { ringType }, defaultRingType } = this

      this.ringType = ringType || defaultRingType
    },
    calcRayLineRadianData () {
      const { data: { label, rayLineOffset }, defaultRayLineOffset } = this

      const { tags } = label

      const fullRadian = Math.PI * 2

      const radianGap = fullRadian / tags.length

      const radianOffset = rayLineOffset || defaultRayLineOffset

      this.rayLineRadianData = tags.map((t, i) => radianGap * i + radianOffset)
    },
    calcRingRadiusData () {
      const { data: { ringNum }, defaultRingNum, radius } = this

      const num = ringNum || defaultRingNum

      const radiusGap = radius / num

      this.ringRadiusData = new Array(num).fill(0).map((t, i) =>
        radiusGap * (i + 1))
    },
    calcRingPolylineData () {
      const { ringRadiusData, rayLineRadianData, centerPos } = this

      const { canvas: { getCircleRadianPoint } } = this

      this.ringPolylineData = ringRadiusData.map((r, i) =>
        rayLineRadianData.map(radian =>
          getCircleRadianPoint(...centerPos, r, radian)))
    },
    calcRingDrawConfig () {
      const { defaultRingLineType, defaultRingLineColor } = this

      const { data: { ringLineType, ringLineColor }, drawColors } = this

      this.ringLineDash = (ringLineType || defaultRingLineType) === 'dashed' ? [5, 5] : [10, 0]

      const trueRingLineColor = ringLineColor === 'colors' ? drawColors : ringLineColor

      this.ringlineMultipleColor = typeof trueRingLineColor === 'object'

      this.ringLineColor = trueRingLineColor || defaultRingLineColor
    },
    calcRingFillConfig () {
      const { data: { ringFillType, ringFillColor }, defaultRingFillType, drawColors } = this

      this.ringFillType = ringFillType || defaultRingFillType

      const trueRingFillColor = this.ringFillColor = (!ringFillColor || ringFillColor === 'colors') ? drawColors : ringFillColor

      this.ringFillMultipleColor = typeof trueRingFillColor === 'object'
    },
    fillRing () {
      const { ringFillType, fillCoverRing, fillMulCoverRing, fillRingRing } = this

      switch (ringFillType) {
        case 'cover': fillCoverRing()
          break

        case 'mulCover': fillMulCoverRing()
          break

        case 'ring': fillRingRing()
          break
      }
    },
    fillCoverRing () {
      const { ctx, centerPos, ringFillColor, ringType, radius, ringPolylineData } = this

      const { canvas: { getRadialGradientColor, drawPolylinePath } } = this

      const color = getRadialGradientColor(ctx, centerPos, 0, radius, ringFillColor)

      ctx.beginPath()

      ringType === 'circle' && ctx.arc(...centerPos, radius, 0, Math.PI * 2)

      ringType === 'polyline' && drawPolylinePath(ctx, ringPolylineData[ringPolylineData.length - 1])

      ctx.closePath()

      ctx.fillStyle = color

      ctx.fill()
    },
    fillMulCoverRing () {
      const { ctx, ringType, ringFillColor, centerPos } = this

      const { ringFillMultipleColor, ringPolylineData, ringRadiusData, deepClone } = this

      const { canvas: { drawPolylinePath } } = this

      !ringFillMultipleColor && (ctx.fillStyle = ringFillColor)

      const colorNum = ringFillColor.length

      const LastRingIndex = ringRadiusData.length - 1

      ringType === 'circle' &&
        deepClone(ringRadiusData).reverse().forEach((radius, i) => {
          ctx.beginPath()

          ctx.arc(...centerPos, radius, 0, Math.PI * 2)

          ringFillMultipleColor && (ctx.fillStyle = ringFillColor[(LastRingIndex - i) % colorNum])

          ctx.fill()
        })

      ringType === 'polyline' &&
        deepClone(ringPolylineData).reverse().forEach((line, i) => {
          drawPolylinePath(ctx, line, true, true)

          ringFillMultipleColor && (ctx.fillStyle = ringFillColor[(LastRingIndex - i) % colorNum])

          ctx.fill()
        })
    },
    fillRingRing () {
      const { ctx, ringType, ringRadiusData, rayLineRadianData, getPointToLineDistance } = this

      const { ringFillMultipleColor, centerPos, ringFillColor, ringPolylineData } = this

      const { canvas: { drawPolylinePath, getCircleRadianPoint } } = this

      let lineWidth = ctx.lineWidth = ringRadiusData[0]

      const halfLineWidth = lineWidth / 2

      const colorNum = ringFillColor.length

      !ringFillMultipleColor && (ctx.strokeStyle = ringFillColor)

      ringType === 'circle' &&
        ringRadiusData.forEach((r, i) => {
          ctx.beginPath()

          ctx.arc(...centerPos, r - halfLineWidth, 0, Math.PI * 2)

          ringFillMultipleColor && (ctx.strokeStyle = ringFillColor[i % colorNum])

          ctx.stroke()
        })

      ctx.lineCap = 'round'

      ctx.lineWidth = getPointToLineDistance(centerPos, ringPolylineData[0][0], ringPolylineData[0][1])

      ringType === 'polyline' &&
        ringRadiusData.map(r => r - halfLineWidth).map(r =>
          rayLineRadianData.map(radian =>
            getCircleRadianPoint(...centerPos, r, radian))).forEach((line, i) => {
          drawPolylinePath(ctx, line, true, true)

          ringFillMultipleColor && (ctx.strokeStyle = ringFillColor[i % colorNum])

          ctx.stroke()
        })
    },
    drawCircleRing () {
      const { data: { ringType }, defaultRingType } = this

      if ((ringType && ringType !== 'circle') || (!ringType && defaultRingType !== 'circle')) return

      const { ctx, ringRadiusData, centerPos, ringLineDash, ringlineMultipleColor, ringLineColor } = this

      ctx.setLineDash(ringLineDash)

      ctx.lineWidth = 1

      !ringlineMultipleColor && (ctx.strokeStyle = ringLineColor)

      const colorNum = ringLineColor.length

      ringRadiusData.forEach((r, i) => {
        ctx.beginPath()

        ctx.arc(...centerPos, r, 0, Math.PI * 2)

        ringlineMultipleColor && (ctx.strokeStyle = ringLineColor[i % colorNum])

        ctx.stroke()
      })
    },
    drawPolylineRing () {
      const { data: { ringType }, defaultRingType } = this

      if ((ringType && ringType !== 'polyline') || (!ringType && defaultRingType !== 'polyline')) return

      const { ctx, ringPolylineData, ringLineDash, ringlineMultipleColor, ringLineColor } = this

      const { canvas: { drawPolyline } } = this

      const colorNum = ringLineColor.length

      ringPolylineData.forEach((line, i) =>
        drawPolyline(ctx, line, 1,
          (ringlineMultipleColor ? ringLineColor[i % colorNum] : ringLineColor),
          true, ringLineDash, true))
    },
    calcRayLineConfig () {
      const { data: { rayLineType, rayLineColor }, defaultRayLineType, defaultRayLineColor, drawColors } = this

      this.rayLineDash = (rayLineType || defaultRayLineType) === 'line' ? [10, 0] : [5, 5]

      const trueRayLineColor = rayLineColor === 'colors' ? drawColors : (rayLineColor || defaultRayLineColor)

      this.rayLineColor = trueRayLineColor

      this.rayLineMultipleColor = typeof trueRayLineColor === 'object'
    },
    drawRayLine () {
      const { ctx, rayLineColor, rayLineDash, ringPolylineData, centerPos, rayLineMultipleColor } = this

      const lastRingLineIndex = ringPolylineData.length - 1

      ctx.setLineDash(rayLineDash)

      !rayLineMultipleColor && (ctx.strokeStyle = rayLineColor)

      ctx.lineWidth = 1

      const colorNum = rayLineColor.length

      ringPolylineData[lastRingLineIndex].forEach((point, i) => {
        ctx.beginPath()

        ctx.moveTo(...centerPos)

        ctx.lineTo(...point)

        rayLineMultipleColor && (ctx.strokeStyle = rayLineColor[i % colorNum])

        ctx.stroke()
      })
    },
    calcLabelPosData () {
      const { rayLineRadianData, radius, centerPos } = this

      const { canvas: { getCircleRadianPoint } } = this

      const labelRadius = radius + 10

      this.labelPosData = rayLineRadianData.map(radian =>
        getCircleRadianPoint(...centerPos, labelRadius, radian))
    },
    calcLabelConfig () {
      const { defaultLabelColor, defaultLabelFS, drawColors } = this

      const { data: { label: { color, fontSize } } } = this

      const trueLabelColor = color === 'colors' ? drawColors : (color || defaultLabelColor)

      this.labelMultipleColor = typeof trueLabelColor === 'object'

      this.labelFontSize = fontSize || defaultLabelFS

      this.labelColor = trueLabelColor
    },
    drawLable () {
      const { ctx, centerPos: [x], labelPosData, labelColor, labelFontSize, labelMultipleColor } = this

      const { data: { label: { tags } } } = this

      ctx.font = `${labelFontSize}px Arial`

      !labelMultipleColor && (ctx.fillStyle = labelColor)

      ctx.textBaseline = 'middle'

      const colorNum = labelColor.length

      labelPosData.forEach((pos, i) => {
        ctx.textAlign = 'start'

        pos[0] < x && (ctx.textAlign = 'end')

        labelMultipleColor && (ctx.fillStyle = labelColor[i % colorNum])

        ctx.fillText(tags[i], ...pos)
      })
    },
    caclValuePointData () {
      const { data: { series, max }, centerPos, radius, rayLineRadianData } = this

      const { canvas: { getCircleRadianPoint } } = this

      const maxValue = max || Math.max(...series.map(({ value }) => Math.max(...value)))

      const valueRadius = series.map(({ value }) =>
        value.map(v =>
          Number.isFinite(v)
            ? v / maxValue * radius : false))

      this.valuePointData = valueRadius.map(td =>
        td.map((r, i) =>
          (r || r === 0) ? getCircleRadianPoint(...centerPos, r, rayLineRadianData[i]) : false))
    },
    fillRadar () {
      const { ctx, data: { series }, valuePointData, drawColors, filterNull } = this

      const { canvas: { drawPolylinePath } } = this

      const { color: { hexToRgb } } = this

      const colorNum = drawColors.length

      valuePointData.forEach((line, i) => {
        const currentColor = drawColors[i % colorNum]

        const lineColor = series[i].lineColor
        const fillColor = series[i].fillColor

        series[i].dashed ? ctx.setLineDash([5, 5]) : ctx.setLineDash([10, 0])

        drawPolylinePath(ctx, filterNull(line), 1, true, true)

        ctx.strokeStyle = lineColor || currentColor

        ctx.stroke()

        ctx.fillStyle = fillColor || hexToRgb(currentColor, 0.5)

        ctx.fill()
      })
    },
    fillValueText () {
      const { data: { series, showValueText, valueTextFontSize } } = this

      if (!showValueText) return

      const { ctx, defaultValueFontSize } = this

      ctx.font = `${valueTextFontSize || defaultValueFontSize}px Arial`

      const { fillSeriesText } = this

      series.forEach((item, i) => fillSeriesText(item, i))
    },
    fillSeriesText ({ valueTextColor, lineColor, fillColor, value }, i) {
      const { ctx, drawColors, valuePointData, drawTexts } = this

      const { data: { valueTextOffset, valueTextColor: outerValueTC }, defaultValueColor } = this

      const trueOffset = valueTextOffset || [5, -5]

      const drawColorsNum = drawColors.length

      let currentColor = valueTextColor
      currentColor === 'inherit' && (currentColor = lineColor || fillColor || drawColors[i % drawColorsNum])
      currentColor instanceof Array && (currentColor = currentColor[0])

      ctx.fillStyle = currentColor || outerValueTC || defaultValueColor

      drawTexts(ctx, value, valuePointData[i], trueOffset)
    },
    drawTexts (ctx, values, points, [x, y] = [0, 0]) {
      values.forEach((v, i) => {
        if (!v && v !== 0) return

        ctx.fillText(v, points[i][0] + x, points[i][1] + y)
      })
    },
    reDraw (d) {
      const { draw } = this

      d && draw()
    }
  },
  mounted () {
    const { init } = this

    init()
  }
}
</script>

<style lang="less">
.radar-chart {
  position: relative;
  display: flex;
  flex-direction: column;

  .canvas-container {
    flex: 1;
  }

  canvas {
    width: 100%;
    height: 100%;
  }
}
</style>
