/*	Copyright (c) 2019 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/

import ol_style_Style from 'ol/style/Style.js'
import {asString as ol_color_asString} from 'ol/color.js'
import {asArray as ol_color_asArray} from 'ol/color.js'
import {ol_coordinate_dist2d} from '../geom/GeomUtils.js'

/** Flow line style
 * Draw LineString with a variable color / width
 * NB: the FlowLine style doesn't impress the hit-detection.
 * If you want your lines to be sectionable you have to add your own style to handle this.
 * (with transparent line: stroke color opacity to .1 or zero width)
 * @constructor
 * @extends {ol_style_Style}
 * @param {Object} options
 *  @param {boolean} options.visible draw only the visible part of the line, default true
 *  @param {number|function} options.width Stroke width or a function that gets a feature and the position (beetween [0,1]) and returns current width
 *  @param {number} options.width2 Final stroke width (if width is not a function)
 *  @param {number} options.arrow Arrow at start (-1), at end (1), at both (2), none (0), default geta
 *  @param {ol.colorLike|function} options.color Stroke color or a function that gets a feature and the position (beetween [0,1]) and returns current color
 *  @param {ol.colorLike} options.color2 Final sroke color if color is nor a function
 *  @param {ol.colorLike} options.arrowColor Color of arrows, if not defined used color or color2
 *  @param {string} options.lineCap CanvasRenderingContext2D.lineCap 'butt' | 'round' | 'square', default 'butt'
 *  @param {number|ol.size} options.arrowSize height and width of the arrow, default 16
 *  @param {boolean} [options.noOverlap=false] prevent segments overlaping
 *  @param {number} options.offset0 offset at line start
 *  @param {number} options.offset1 offset at line end
 */
var ol_style_FlowLine = class olstyleFlowLine extends ol_style_Style {
  constructor(options) {
    options = options || {}

    super({
      stroke: options.stroke,
      text: options.text,
      zIndex: options.zIndex,
      geometry: options.geometry
    })
    this.setRenderer(this._render.bind(this))

    // Draw only visible
    this._visible = (options.visible !== false)

    // Width
    if (typeof options.width === 'function') {
      this._widthFn = options.width
    } else {
      this.setWidth(options.width)
    }
    this.setWidth2(options.width2)
    // Color
    if (typeof options.color === 'function') {
      this._colorFn = options.color
    } else {
      this.setColor(options.color)
    }
    this.setColor2(options.color2)
    // LineCap
    this.setLineCap(options.lineCap)
    // Arrow
    this.setArrow(options.arrow)
    this.setArrowSize(options.arrowSize)
    this.setArrowColor(options.arrowColor)
    // Offset
    this._offset = [0, 0]
    this.setOffset(options.offset0, 0)
    this.setOffset(options.offset1, 1)
    // Overlap
    this._noOverlap = options.noOverlap
  }
  /** Set the initial width
   * @param {number} width width, default 0
   */
  setWidth(width) {
    this._width = width || 0
  }
  /** Set the final width
   * @param {number} width width, default 0
   */
  setWidth2(width) {
    this._width2 = width
  }
  /** Get offset at start or end
   * @param {number} where 0=start, 1=end
   * @return {number} width
   */
  getOffset(where) {
    return this._offset[where]
  }
  /** Add an offset at start or end
   * @param {number} width
   * @param {number} where 0=start, 1=end
   */
  setOffset(width, where) {
    width = Math.max(0, parseFloat(width))
    switch (where) {
      case 0: {
        this._offset[0] = width
        break
      }
      case 1: {
        this._offset[1] = width
        break
      }
    }
  }
  /** Set the LineCap
   * @param {steing} cap LineCap (round or butt), default butt
   */
  setLineCap(cap) {
    this._lineCap = (cap === 'round' ? 'round' : 'butt')
  }
  /** Get the current width at step
   * @param {ol.feature} feature
   * @param {number} step current drawing step beetween [0,1]
   * @return {number}
   */
  getWidth(feature, step) {
    if (this._widthFn)
      return this._widthFn(feature, step)
    var w2 = (typeof (this._width2) === 'number') ? this._width2 : this._width
    return this._width + (w2 - this._width) * step
  }
  /** Set the initial color
   * @param {ol.colorLike} color
   */
  setColor(color) {
    try {
      this._color = ol_color_asArray(color)
    } catch (e) {
      this._color = [0, 0, 0, 1]
    }
  }
  /** Set the final color
   * @param {ol.colorLike} color
   */
  setColor2(color) {
    try {
      this._color2 = ol_color_asArray(color)
    } catch (e) {
      this._color2 = null
    }
  }
  /** Set the arrow color
   * @param {ol.colorLike} color
   */
  setArrowColor(color) {
    try {
      this._acolor = ol_color_asString(color)
    } catch (e) {
      this._acolor = null
    }
  }
  /** Get the current color at step
   * @param {ol.feature} feature
   * @param {number} step current drawing step beetween [0,1]
   * @return {string}
   */
  getColor(feature, step) {
    if (this._colorFn)
      return ol_color_asString(this._colorFn(feature, step))
    var color = this._color
    var color2 = this._color2 || this._color
    return 'rgba(' +
      +Math.round(color[0] + (color2[0] - color[0]) * step) + ','
      + Math.round(color[1] + (color2[1] - color[1]) * step) + ','
      + Math.round(color[2] + (color2[2] - color[2]) * step) + ','
      + (color[3] + (color2[3] - color[3]) * step)
      + ')'
  }
  /** Get arrow
   */
  getArrow() {
    return this._arrow
  }
  /** Set arrow
   * @param {number} n -1 | 0 | 1 | 2, default: 0
   */
  setArrow(n) {
    this._arrow = parseInt(n)
    if (this._arrow < -1 || this._arrow > 2)
      this._arrow = 0
  }
  /** getArrowSize
   * @return {ol.size}
   */
  getArrowSize() {
    return this._arrowSize || [16, 16]
  }
  /** setArrowSize
   * @param {number|ol.size} size
   */
  setArrowSize(size) {
    if (Array.isArray(size))
      this._arrowSize = size
    else if (typeof (size) === 'number')
      this._arrowSize = [size, size]
  }
  /** drawArrow
   * @param {CanvasRenderingContext2D} ctx
   * @param {ol.coordinate} p0
   * @param ol.coordinate} p1
   * @param {number} width
   * @param {number} ratio pixelratio
   * @private
   */
  drawArrow(ctx, p0, p1, width, ratio) {
    var asize = this.getArrowSize()[0] * ratio
    var l = ol_coordinate_dist2d(p0, p1)
    var dx = (p0[0] - p1[0]) / l
    var dy = (p0[1] - p1[1]) / l
    width = Math.max(this.getArrowSize()[1] / 2, width / 2) * ratio
    ctx.beginPath()
    ctx.moveTo(p0[0], p0[1])
    ctx.lineTo(p0[0] - asize * dx + width * dy, p0[1] - asize * dy - width * dx)
    ctx.lineTo(p0[0] - asize * dx - width * dy, p0[1] - asize * dy + width * dx)
    ctx.lineTo(p0[0], p0[1])
    ctx.fill()
  }
  /** Renderer function
   * @param {Array<ol.coordinate>} geom The pixel coordinates of the geometry in GeoJSON notation
   * @param {ol.render.State} e The olx.render.State of the layer renderer
   */
  _render(geom, e) {
    if (e.geometry.getType() === 'LineString') {
      var i, g, p, ctx = e.context
      // Get geometry used at drawing
      if (!this._visible) {
        var a = e.pixelRatio / e.resolution
        var cos = Math.cos(e.rotation)
        var sin = Math.sin(e.rotation)
        g = e.geometry.getCoordinates()
        var dx = geom[0][0] - g[0][0] * a * cos - g[0][1] * a * sin
        var dy = geom[0][1] - g[0][0] * a * sin + g[0][1] * a * cos
        geom = []
        for (i = 0; p = g[i]; i++) {
          geom[i] = [
            dx + p[0] * a * cos + p[1] * a * sin,
            dy + p[0] * a * sin - p[1] * a * cos,
            p[2]
          ]
        }
      }

      var asize = this.getArrowSize()[0] * e.pixelRatio

      ctx.save()
      // Offsets
      if (this.getOffset(0))
        this._splitAsize(geom, this.getOffset(0) * e.pixelRatio)
      if (this.getOffset(1))
        this._splitAsize(geom, this.getOffset(1) * e.pixelRatio, true)
      // Arrow 1
      if (geom.length > 1 && (this.getArrow() === -1 || this.getArrow() === 2)) {
        p = this._splitAsize(geom, asize)
        if (this._acolor)
          ctx.fillStyle = this._acolor
        else
          ctx.fillStyle = this.getColor(e.feature, 0)
        this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 0), e.pixelRatio)
      }
      // Arrow 2 
      if (geom.length > 1 && this.getArrow() > 0) {
        p = this._splitAsize(geom, asize, true)
        if (this._acolor)
          ctx.fillStyle = this._acolor
        else
          ctx.fillStyle = this.getColor(e.feature, 1)
        this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 1), e.pixelRatio)
      }

      // Split into
      var geoms = this._splitInto(geom, 255, 2)
      var k = 0
      var nb = geoms.length

      // Draw
      ctx.lineJoin = 'round'
      ctx.lineCap = this._lineCap || 'butt'

      if (geoms.length > 1) {
        for (k = 0; k < geoms.length; k++) {
          var step = k / nb
          g = geoms[k]
          ctx.lineWidth = this.getWidth(e.feature, step) * e.pixelRatio
          ctx.strokeStyle = this.getColor(e.feature, step)
          ctx.beginPath()
          ctx.moveTo(g[0][0], g[0][1])
          for (i = 1; p = g[i]; i++) {
            ctx.lineTo(p[0], p[1])
          }
          ctx.stroke()
        }
      }
      ctx.restore()
    }
  }
  /** Split extremity at
   * @param {ol.geom.LineString} geom
   * @param {number} asize
   * @param {boolean} end start=false or end=true, default false (start)
   */
  _splitAsize(geom, asize, end) {
    var p, p1, p0
    var dl, d = 0
    if (end)
      p0 = geom.pop()
    else
      p0 = geom.shift()
    p = p0
    while (geom.length) {
      if (end)
        p1 = geom.pop()
      else
        p1 = geom.shift()
      dl = ol_coordinate_dist2d(p, p1)
      if (d + dl > asize) {
        p = [p[0] + (p1[0] - p[0]) * (asize - d) / dl, p[1] + (p1[1] - p[1]) * (asize - d) / dl]
        dl = ol_coordinate_dist2d(p, p0)
        if (end) {
          geom.push(p1)
          geom.push(p)
          geom.push([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl])
        } else {
          geom.unshift(p1)
          geom.unshift(p)
          geom.unshift([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl])
        }
        break
      }
      d += dl
      p = p1
    }
    return [p0, p]
  }
  /** Split line geometry into equal length geometries
   * @param {Array<ol.coordinate>} geom
   * @param {number} nb number of resulting geometries, default 255
   * @param {number} nim minimum length of the resulting geometries, default 1
   */
  _splitInto(geom, nb, min) {
    var i, p
    var dt = this._noOverlap ? 1 : .9
    // Split geom into equal length geoms
    var geoms = []
    var dl, l = 0
    for (i = 1; p = geom[i]; i++) {
      l += ol_coordinate_dist2d(geom[i - 1], p)
    }
    var length = Math.max(min || 2, l / (nb || 255))
    var p0 = geom[0]
    l = 0
    var g = [p0]
    i = 1
    p = geom[1]
    while (i < geom.length) {
      var dx = p[0] - p0[0]
      var dy = p[1] - p0[1]
      dl = Math.sqrt(dx * dx + dy * dy)
      if (l + dl > length) {
        var d = (length - l) / dl
        g.push([
          p0[0] + dx * d,
          p0[1] + dy * d
        ])
        geoms.push(g)
        p0 = [
          p0[0] + dx * d * dt,
          p0[1] + dy * d * dt
        ]
        g = [p0]
        l = 0
      } else {
        l += dl
        p0 = p
        g.push(p0)
        i++
        p = geom[i]
      }
    }
    geoms.push(g)
    return geoms
  }
}

export default ol_style_FlowLine
