import { getRgbaValue, getColorFromRgbValue } from '@jiaminghi/color'

import { deepClone } from '../plugin/util'

/**
 * @description Class Style
 * @param {Object} style  Style configuration
 * @return {Style} Instance of Style
 */
export default class Style {
  constructor (style) {
    this.colorProcessor(style)

    const defaultStyle = {
      /**
       * @description Rgba value of graph fill color
       * @type {Array}
       * @default fill = [0, 0, 0, 1]
       */
      fill: [0, 0, 0, 1],
      /**
       * @description Rgba value of graph stroke color
       * @type {Array}
       * @default stroke = [0, 0, 0, 1]
       */
      stroke: [0, 0, 0, 0],
      /**
       * @description Opacity of graph
       * @type {Number}
       * @default opacity = 1
       */
      opacity: 1,
      /**
       * @description LineCap of Ctx
       * @type {String}
       * @default lineCap = null
       * @example lineCap = 'butt'|'round'|'square'
       */
      lineCap: null,
      /**
       * @description Linejoin of Ctx
       * @type {String}
       * @default lineJoin = null
       * @example lineJoin = 'round'|'bevel'|'miter'
       */
      lineJoin: null,
      /**
       * @description LineDash of Ctx
       * @type {Array}
       * @default lineDash = null
       * @example lineDash = [10, 10]
       */
      lineDash: null,
      /**
       * @description LineDashOffset of Ctx
       * @type {Number}
       * @default lineDashOffset = null
       * @example lineDashOffset = 10
       */
      lineDashOffset: null,
      /**
       * @description ShadowBlur of Ctx
       * @type {Number}
       * @default shadowBlur = 0
       */
      shadowBlur: 0,
      /**
       * @description Rgba value of graph shadow color
       * @type {Array}
       * @default shadowColor = [0, 0, 0, 0]
       */
      shadowColor: [0, 0, 0, 0],
      /**
       * @description ShadowOffsetX of Ctx
       * @type {Number}
       * @default shadowOffsetX = 0
       */
      shadowOffsetX: 0,
      /**
       * @description ShadowOffsetY of Ctx
       * @type {Number}
       * @default shadowOffsetY = 0
       */
      shadowOffsetY: 0,
      /**
       * @description LineWidth of Ctx
       * @type {Number}
       * @default lineWidth = 0
       */
      lineWidth: 0,
      /**
       * @description Center point of the graph
       * @type {Array}
       * @default graphCenter = null
       * @example graphCenter = [10, 10]
       */
      graphCenter: null,
      /**
       * @description Graph scale
       * @type {Array}
       * @default scale = null
       * @example scale = [1.5, 1.5]
       */
      scale: null,
      /**
       * @description Graph rotation degree
       * @type {Number}
       * @default rotate = null
       * @example rotate = 10
       */
      rotate: null,
      /**
       * @description Graph translate distance
       * @type {Array}
       * @default translate = null
       * @example translate = [10, 10]
       */
      translate: null,
      /**
       * @description Cursor status when hover
       * @type {String}
       * @default hoverCursor = 'pointer'
       * @example hoverCursor = 'default'|'pointer'|'auto'|'crosshair'|'move'|'wait'|...
       */
      hoverCursor: 'pointer',
      /**
       * @description Font style of Ctx
       * @type {String}
       * @default fontStyle = 'normal'
       * @example fontStyle = 'normal'|'italic'|'oblique'
       */
      fontStyle: 'normal',
      /**
       * @description Font varient of Ctx
       * @type {String}
       * @default fontVarient = 'normal'
       * @example fontVarient = 'normal'|'small-caps'
       */
      fontVarient: 'normal',
      /**
       * @description Font weight of Ctx
       * @type {String|Number}
       * @default fontWeight = 'normal'
       * @example fontWeight = 'normal'|'bold'|'bolder'|'lighter'|Number
       */
      fontWeight: 'normal',
      /**
       * @description Font size of Ctx
       * @type {Number}
       * @default fontSize = 10
       */
      fontSize: 10,
      /**
       * @description Font family of Ctx
       * @type {String}
       * @default fontFamily = 'Arial'
       */
      fontFamily: 'Arial',
      /**
       * @description TextAlign of Ctx
       * @type {String}
       * @default textAlign = 'center'
       * @example textAlign = 'start'|'end'|'left'|'right'|'center'
       */
      textAlign: 'center',
      /**
       * @description TextBaseline of Ctx
       * @type {String}
       * @default textBaseline = 'middle'
       * @example textBaseline = 'top'|'bottom'|'middle'|'alphabetic'|'hanging'
       */
      textBaseline: 'middle',
      /**
       * @description The color used to create the gradient
       * @type {Array}
       * @default gradientColor = null
       * @example gradientColor = ['#000', '#111', '#222']
       */
      gradientColor: null,
      /**
       * @description Gradient type
       * @type {String}
       * @default gradientType = 'linear'
       * @example gradientType = 'linear' | 'radial'
       */
      gradientType: 'linear',
      /**
       * @description Gradient params
       * @type {Array}
       * @default gradientParams = null
       * @example gradientParams = [x0, y0, x1, y1] (Linear Gradient)
       * @example gradientParams = [x0, y0, r0, x1, y1, r1] (Radial Gradient)
       */
      gradientParams: null,
      /**
       * @description When to use gradients
       * @type {String}
       * @default gradientWith = 'stroke'
       * @example gradientWith = 'stroke' | 'fill'
       */
      gradientWith: 'stroke',
      /**
       * @description Gradient color stops
       * @type {String}
       * @default gradientStops = 'auto'
       * @example gradientStops = 'auto' | [0, .2, .3, 1]
       */
      gradientStops: 'auto',
      /**
       * @description Extended color that supports animation transition
       * @type {Array|Object}
       * @default colors = null
       * @example colors = ['#000', '#111', '#222', 'red' ]
       * @example colors = { a: '#000', b: '#111' }
       */
      colors: null
    }

    Object.assign(this, defaultStyle, style)
  }
}

/**
 * @description Set colors to rgba value
 * @param {Object} style style config
 * @param {Boolean} reverse Whether to perform reverse operation
 * @return {Undefined} Void
 */
Style.prototype.colorProcessor = function (style, reverse = false) {
  const processor = reverse ? getColorFromRgbValue : getRgbaValue

  const colorProcessorKeys = ['fill', 'stroke', 'shadowColor']

  const allKeys = Object.keys(style)

  const colorKeys = allKeys.filter(key => colorProcessorKeys.find(k => k === key))

  colorKeys.forEach(key => (style[key] = processor(style[key])))

  const { gradientColor, colors } = style

  if (gradientColor) style.gradientColor = gradientColor.map(c => processor(c))

  if (colors) {
    const colorsKeys = Object.keys(colors)

    colorsKeys.forEach(key => (colors[key] = processor(colors[key])))
  }
}

/**
 * @description Init graph style
 * @param {Object} ctx Context of canvas
 * @return {Undefined} Void
 */
Style.prototype.initStyle = function (ctx) {
  initTransform(ctx, this)

  initGraphStyle(ctx, this)

  initGradient(ctx, this)
}

/**
 * @description Init canvas transform
 * @param {Object} ctx  Context of canvas
 * @param {Style} style Instance of Style
 * @return {Undefined} Void
 */
function initTransform (ctx, style) {
  ctx.save()

  const { graphCenter, rotate, scale, translate } = style

  if (!(graphCenter instanceof Array)) return

  ctx.translate(...graphCenter)

  if (rotate) ctx.rotate(rotate * Math.PI / 180)

  if (scale instanceof Array) ctx.scale(...scale)

  if (translate) ctx.translate(...translate)

  ctx.translate(-graphCenter[0], -graphCenter[1])
}

const autoSetStyleKeys = [
  'lineCap', 'lineJoin', 'lineDashOffset',
  'shadowOffsetX', 'shadowOffsetY', 'lineWidth',
  'textAlign', 'textBaseline'
]

/**
 * @description Set the style of canvas ctx
 * @param {Object} ctx  Context of canvas
 * @param {Style} style Instance of Style
 * @return {Undefined} Void
 */
function initGraphStyle (ctx, style) {
  let { fill, stroke, shadowColor, opacity } = style

  autoSetStyleKeys.forEach(key => {
    if (key || typeof key === 'number') ctx[key] = style[key]
  })

  fill = [...fill]
  stroke = [...stroke]
  shadowColor = [...shadowColor]

  fill[3] *= opacity
  stroke[3] *= opacity
  shadowColor[3] *= opacity

  ctx.fillStyle = getColorFromRgbValue(fill)
  ctx.strokeStyle = getColorFromRgbValue(stroke)
  ctx.shadowColor = getColorFromRgbValue(shadowColor)

  let { lineDash, shadowBlur } = style

  if (lineDash) {
    lineDash = lineDash.map(v => v >= 0 ? v : 0)

    ctx.setLineDash(lineDash)
  }

  if (typeof shadowBlur === 'number') ctx.shadowBlur = shadowBlur > 0 ? shadowBlur : 0.001

  const { fontStyle, fontVarient, fontWeight, fontSize, fontFamily } = style

  ctx.font = fontStyle + ' ' + fontVarient + ' ' + fontWeight + ' ' + fontSize + 'px' + ' ' + fontFamily
}

/**
 * @description Set the gradient color of canvas ctx
 * @param {Object} ctx  Context of canvas
 * @param {Style} style Instance of Style
 * @return {Undefined} Void
 */
function initGradient (ctx, style) {
  if (!gradientValidator(style)) return

  let { gradientColor, gradientParams, gradientType, gradientWith, gradientStops, opacity } = style

  gradientColor = gradientColor.map(color => {
    let colorOpacity = color[3] * opacity

    let clonedColor = [...color]

    clonedColor[3] = colorOpacity

    return clonedColor
  })

  gradientColor = gradientColor.map(c => getColorFromRgbValue(c))

  if (gradientStops === 'auto') gradientStops = getAutoColorStops(gradientColor)

  const gradient = ctx[`create${gradientType.slice(0, 1).toUpperCase() + gradientType.slice(1)}Gradient`](...gradientParams)

  gradientStops.forEach((stop, i) => gradient.addColorStop(stop, gradientColor[i]))

  ctx[`${gradientWith}Style`] = gradient
}

/**
 * @description Check if the gradient configuration is legal
 * @param {Style} style Instance of Style
 * @return {Boolean} Check Result
 */
function gradientValidator (style) {
  const { gradientColor, gradientParams, gradientType, gradientWith, gradientStops } = style

  if (!gradientColor || !gradientParams) return false

  if (gradientColor.length === 1) {
    console.warn('The gradient needs to provide at least two colors')

    return false
  }

  if (gradientType !== 'linear' && gradientType !== 'radial') {
    console.warn('GradientType only supports linear or radial, current value is ' + gradientType)

    return false
  }

  const gradientParamsLength = gradientParams.length

  if (
    (gradientType === 'linear' && gradientParamsLength !== 4) ||
    (gradientType === 'radial' && gradientParamsLength !== 6)
  ) {
    console.warn('The expected length of gradientParams is ' + (gradientType === 'linear' ? '4' : '6'))

    return false
  }

  if (gradientWith !== 'fill' && gradientWith !== 'stroke') {
    console.warn('GradientWith only supports fill or stroke, current value is ' + gradientWith)

    return false
  }

  if (gradientStops !== 'auto' && !(gradientStops instanceof Array)) {
    console.warn(`gradientStops only supports 'auto' or Number Array ([0, .5, 1]), current value is ` + gradientStops)

    return false
  }

  return true
}

/**
 * @description Get a uniform gradient color stop
 * @param {Array} color Gradient color
 * @return {Array} Gradient color stop
 */
function getAutoColorStops (color) {
  const stopGap = 1 / (color.length - 1)

  return color.map((foo, i) => stopGap * i)
}

/**
 * @description Restore canvas ctx transform
 * @param {Object} ctx  Context of canvas
 * @return {Undefined} Void
 */
Style.prototype.restoreTransform = function (ctx) {
  ctx.restore()
}

/**
 * @description Update style data
 * @param {Object} change Changed data
 * @return {Undefined} Void
 */
Style.prototype.update = function (change) {
  this.colorProcessor(change)

  Object.assign(this, change)
}

/**
 * @description Get the current style configuration
 * @return {Object} Style configuration
 */
Style.prototype.getStyle = function () {
  const clonedStyle = deepClone(this, true)

  this.colorProcessor(clonedStyle, true)

  return clonedStyle
}
