/*	Copyright (c) 2016 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_interaction_Interaction from 'ol/interaction/Interaction.js'
import ol_style_Style from 'ol/style/Style.js'
import ol_style_Stroke from 'ol/style/Stroke.js'
import {buffer as ol_extent_buffer, containsCoordinate as ol_extent_containsCoordinate} from 'ol/extent.js'
import ol_source_Vector from 'ol/source/Vector.js'
import ol_layer_Vector from 'ol/layer/Vector.js'
import ol_Collection from 'ol/Collection.js'
import ol_Feature from 'ol/Feature.js'
import ol_geom_LineString from 'ol/geom/LineString.js'
import './Modify.js'

/** Interaction to snap to guidelines
 * @constructor
 * @extends {ol_interaction_Interaction}
 * @param {*} options
 *  @param {number | undefined} options.pixelTolerance distance (in px) to snap to a guideline, default 10 px
 *  @param {bool | undefined} options.enableInitialGuides whether to draw initial guidelines based on the maps orientation, default false.
 *  @param {ol_style_Style | Array<ol_style_Style> | undefined} options.style Style for the sektch features.
 *  @param {*} options.vectorClass a vector layer class to create the guides with ol6, use ol/layer/VectorImage using ol6
 */
var ol_interaction_SnapGuides = class olinteractionSnapGuides extends ol_interaction_Interaction {
  constructor(options) {
    options = options || {}

    // Intersect 2 guides
    function getIntersectionPoint(d1, d2) {
      var d1x = d1[1][0] - d1[0][0]
      var d1y = d1[1][1] - d1[0][1]
      var d2x = d2[1][0] - d2[0][0]
      var d2y = d2[1][1] - d2[0][1]
      var det = d1x * d2y - d1y * d2x

      if (det != 0) {
        var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det
        return [d2[0][0] + k * d2x, d2[0][1] + k * d2y]
      }
      else
        return false
    }
    function dist2D(p1, p2) {
      var dx = p1[0] - p2[0]
      var dy = p1[1] - p2[1]
      return Math.sqrt(dx * dx + dy * dy)
    }

    // Use snap interaction
    super({
      handleEvent: function (e) {
        if (this.getActive()) {
          var features = this.overlaySource_.getFeatures()
          var prev = null
          var p = null
          var res = e.frameState.viewState.resolution
          for (var i = 0, f; f = features[i]; i++) {
            var c = f.getGeometry().getClosestPoint(e.coordinate)
            if (dist2D(c, e.coordinate) / res < this.snapDistance_) {
              // Intersection on 2 lines
              if (prev) {
                var c2 = getIntersectionPoint(prev.getGeometry().getCoordinates(), f.getGeometry().getCoordinates())
                if (c2) {
                  if (dist2D(c2, e.coordinate) / res < this.snapDistance_) {
                    p = c2
                  }
                }
              } else {
                p = c
              }
              prev = f
            }
          }
          if (p)
            e.coordinate = p
        }
        return true
      }
    })

    // Snap distance (in px)
    this.snapDistance_ = options.pixelTolerance || 10
    this.enableInitialGuides_ = options.enableInitialGuides || false

    // Default style
    var sketchStyle = [
      new ol_style_Style({
        stroke: new ol_style_Stroke({
          color: '#ffcc33',
          lineDash: [8, 5],
          width: 1.25
        })
      })
    ]

    // Custom style
    if (options.style) {
      sketchStyle = options.style instanceof Array ? options.style : [options.style]
    }

    // Create a new overlay for the sketch
    this.overlaySource_ = new ol_source_Vector({
      features: new ol_Collection(),
      useSpatialIndex: false
    })

    // Use ol/layer/VectorImage to render the snap guides as an image to improve performance on rerenderers
    const vectorClass = options.vectorClass || ol_layer_Vector
    this.overlayLayer_ = new vectorClass({
      // render the snap guides as an image to improve performance on rerenderers
      renderMode: 'image',
      source: this.overlaySource_,
      style: function () {
        return sketchStyle
      },
      name: 'Snap overlay',
      displayInLayerSwitcher: false
    })
    this.overlayLayer_.setVisible(this.getActive());
  }
  /**
   * Remove the interaction from its current map, if any,  and attach it to a new
   * map, if any. Pass `null` to just remove the interaction from the current map.
   * @param {ol.Map} map Map.
   * @api stable
   */
  setMap(map) {
    if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_)
    super.setMap(map)
    this.overlayLayer_.setMap(map)
    if (map) this.projExtent_ = map.getView().getProjection().getExtent()
  }
  /** Activate or deactivate the interaction.
   * @param {boolean} active
   */
  setActive(active) {
    if (this.overlayLayer_) this.overlayLayer_.setVisible(active)
    super.setActive(active)
  }
  /** Clear previous added guidelines
   * @param {Array<ol.Feature> | undefined} features a list of feature to remove, default remove all feature
   */
  clearGuides(features) {
    if (!features) {
      this.overlaySource_.clear()
    } else {
      for (var i = 0, f; f = features[i]; i++) {
        try {
          this.overlaySource_.removeFeature(f)
        } catch (e) { /* nothing to to */ }
      }
    }
  }
  /** Get guidelines
   * @return {ol.Collection} guidelines features
   */
  getGuides() {
    return this.overlaySource_.getFeaturesCollection()
  }
  /** Add a new guide to snap to
   * @param {Array<ol.coordinate>} v the direction vector
   * @return {ol.Feature} feature guide
   */
  addGuide(v, ortho) {
    if (v) {
      var map = this.getMap()
      // Limit extent
      var extent = map.getView().calculateExtent(map.getSize())

      var guideLength = Math.max(
        this.projExtent_[2] - this.projExtent_[0],
        this.projExtent_[3] - this.projExtent_[1]
      )

      extent = ol_extent_buffer(extent, guideLength * 1.5)
      //extent = ol_extent_boundingExtent(extent, this.projExtent_);
      if (extent[0] < this.projExtent_[0])
        extent[0] = this.projExtent_[0]
      if (extent[1] < this.projExtent_[1])
        extent[1] = this.projExtent_[1]
      if (extent[2] > this.projExtent_[2])
        extent[2] = this.projExtent_[2]
      if (extent[3] > this.projExtent_[3])
        extent[3] = this.projExtent_[3]

      var dx = v[0][0] - v[1][0]
      var dy = v[0][1] - v[1][1]
      var d = 1 / Math.sqrt(dx * dx + dy * dy)

      var generateLine = function (loopDir) {
        var p, g = []
        var loopCond = guideLength * loopDir * 2
        for (var i = 0; loopDir > 0 ? i < loopCond : i > loopCond; i += (guideLength * loopDir) / 100) {
          if (ortho)
            p = [v[0][0] + dy * d * i, v[0][1] - dx * d * i]
          else
            p = [v[0][0] + dx * d * i, v[0][1] + dy * d * i]
          if (ol_extent_containsCoordinate(extent, p))
            g.push(p)
          else
            break
        }
        return new ol_Feature(new ol_geom_LineString([g[0], g[g.length - 1]]))
      }

      var f0 = generateLine(1)
      var f1 = generateLine(-1)
      this.overlaySource_.addFeature(f0)
      this.overlaySource_.addFeature(f1)
      return [f0, f1]
    }
  }
  /** Add a new orthogonal guide to snap to
   * @param {Array<ol.coordinate>} v the direction vector
   * @return {ol.Feature} feature guide
   */
  addOrthoGuide(v) {
    return this.addGuide(v, true)
  }
  /** Listen to draw event to add orthogonal guidelines on the first and last point.
   * @param {_ol_interaction_Draw_} drawi a draw interaction to listen to
   * @api
   */
  setDrawInteraction(drawi) {
    var self = this
    // Number of points currently drawing
    var nb = 0
    // Current guidelines
    var features = []
    function setGuides(e) {
      var coord = e.target.getCoordinates()
      var s = 2
      switch (e.target.getType()) {
        case 'Point':
          return
        case 'Polygon':
          coord = coord[0].slice(0, -1)
          break
        default: break
      }

      var l = coord.length
      if (l === s && self.enableInitialGuides_) {
        var x = coord[0][0]
        var y = coord[0][1]
        coord = [[x, y], [x, y - 1]]
      }
      if (l != nb && (self.enableInitialGuides_ ? l >= s : l > s)) {
        self.clearGuides(features)
        // use try catch to remove a bug on freehand draw...
        try {
          var p1 = coord[l - s], p2 = coord[l - s - 1]
          if (l > s && !(p1[0] === p2[0] && p1[1] === p2[1])) {
            features = self.addOrthoGuide([coord[l - s], coord[l - s - 1]])
          }
          features = features.concat(self.addGuide([coord[0], coord[1]]))
          features = features.concat(self.addOrthoGuide([coord[0], coord[1]]))
          nb = l
        } catch (e) { /* ok*/ }
      }
    }
    // New drawing
    drawi.on("drawstart", function (e) {
      // When geom is changing add a new orthogonal direction 
      e.feature.getGeometry().on("change", setGuides)
    })
    // end drawing / deactivate => clear directions
    drawi.on(["drawend", "change:active"], function (e) {
      self.clearGuides(features)
      if (e.feature)
        e.feature.getGeometry().un("change", setGuides)
      nb = 0
      features = []
    })
  }
  /** Listen to modify event to add orthogonal guidelines relative to the currently dragged point
   * @param {_ol_interaction_Modify_} modifyi a modify interaction to listen to
   * @api
   */
  setModifyInteraction(modifyi) {
    function mod(d, n) {
      return ((d % n) + n) % n
    }

    var self = this
    // Current guidelines
    var features = []

    function computeGuides(e) {
      var modifyVertex = e.coordinate
      if (!modifyVertex) {
        var selectedVertex = e.target.vertexFeature_
        if (!selectedVertex) return
        modifyVertex = selectedVertex.getGeometry().getCoordinates()
      }
      var f = e.target.getModifiedFeatures()[0]
      var geom = f.getGeometry()

      var coord = geom.getCoordinates()
      switch (geom.getType()) {
        case 'Point':
          return
        case 'Polygon':
          coord = coord[0].slice(0, -1)
          break
        default: break
      }

      var idx = coord.findIndex(function (c) {
        return c[0] === modifyVertex[0] && c[1] === modifyVertex[1]
      })

      var l = coord.length

      self.clearGuides(features)
      features = self.addOrthoGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]])
      features = features.concat(self.addGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]]))
      features = features.concat(self.addGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]))
      features = features.concat(self.addOrthoGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]))
    }

    function setGuides(e) {
      // This callback is called before ol adds the vertex to the feature, so
      // defer a moment for openlayers to add the new vertex
      setTimeout(computeGuides, 0, e)
    }


    function drawEnd() {
      self.clearGuides(features)
      features = []
    }

    // New drawing
    modifyi.on('modifystart', setGuides)
    // end drawing, clear directions
    modifyi.on('modifyend', drawEnd)
  }
}

export default ol_interaction_SnapGuides
