var dataUtil = require("../utils/data_structure_util");

var classUtil = require("../utils/class_util");

var pathContain = require("../utils/contain/path");

var _constants = require("../utils/constants");

var mathMax = _constants.mathMax;
var mathAbs = _constants.mathAbs;
var mathSqrt = _constants.mathSqrt;

var Element = require("./Element");

var PathProxy = require("./PathProxy");

var Pattern = require("./Pattern");

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

/**
 * @class qrenderer.graphic.Path 
 * @docauthor 大漠穷秋 <damoqiongqiu@126.com>
 */
var Path =
/*#__PURE__*/
function (_Element) {
  _inherits(Path, _Element);

  /**
   * @method constructor Path
   * @param {Object} options
   */
  function Path(options) {
    var _this;

    _classCallCheck(this, Path);

    _this = _possibleConstructorReturn(this, _getPrototypeOf(Path).call(this, options));
    /**
     * @property {String} type
     */

    _this.type = 'path';
    /**
     * @property {PathProxy}
     * @readonly
     */

    _this.path = null;
    /**
     * @property {Number} strokeContainThreshold
     */

    _this.strokeContainThreshold = 5;
    /**
     * @property {Number} segmentIgnoreThreshold
     * This item default to be false. But in map series in echarts,
     * in order to improve performance, it should be set to true,
     * so the shorty segment won't draw.
     */

    _this.segmentIgnoreThreshold = 0;
    /**
     * @property {Boolean} subPixelOptimize
     * See `subPixelOptimize`.
     */

    _this.subPixelOptimize = false;
    /**
     * @private
     * @property __dirtyPath
     */

    _this.__dirtyPath = true;
    return _this;
  }
  /**
   * @method render
   */


  _createClass(Path, [{
    key: "render",
    value: function render() {
      var ctx = this.ctx;
      var prevEl = this.prevEl;
      var path = this.path || new PathProxy(true);
      var hasStroke = this.style.hasStroke();
      var hasFill = this.style.hasFill();
      var fill = this.style.fill;
      var stroke = this.style.stroke;
      var hasFillGradient = hasFill && !!fill.colorStops;
      var hasStrokeGradient = hasStroke && !!stroke.colorStops;
      var hasFillPattern = hasFill && !!fill.image;
      var hasStrokePattern = hasStroke && !!stroke.image;
      this.style.bind(ctx, this, prevEl);
      this.applyTransform(ctx);

      if (this.__dirty) {
        var rect; // Update gradient because bounding rect may changed

        if (hasFillGradient) {
          rect = rect || this.getBoundingRect();
          this._fillGradient = this.style.getGradient(ctx, fill, rect);
        }

        if (hasStrokeGradient) {
          rect = rect || this.getBoundingRect();
          this._strokeGradient = this.style.getGradient(ctx, stroke, rect);
        }
      } // Use the gradient or pattern


      if (hasFillGradient) {
        // PENDING If may have affect the state
        ctx.fillStyle = this._fillGradient;
      } else if (hasFillPattern) {
        ctx.fillStyle = Pattern.prototype.getCanvasPattern.call(fill, ctx);
      }

      if (hasStrokeGradient) {
        ctx.strokeStyle = this._strokeGradient;
      } else if (hasStrokePattern) {
        ctx.strokeStyle = Pattern.prototype.getCanvasPattern.call(stroke, ctx);
      }

      var lineDash = this.style.lineDash;
      var lineDashOffset = this.style.lineDashOffset;
      var ctxLineDash = !!ctx.setLineDash; // Update path sx, sy

      var scale = this.getGlobalScale();
      path.setScale(scale[0], scale[1], this.segmentIgnoreThreshold); // Proxy context
      // Rebuild path in following 2 cases
      // 1. Path is dirty
      // 2. Path needs javascript implemented lineDash stroking.
      //    In this case, lineDash information will not be saved in PathProxy

      if (this.__dirtyPath || lineDash && !ctxLineDash && hasStroke) {
        path.beginPath(ctx); // Setting line dash before build path

        if (lineDash && !ctxLineDash) {
          path.setLineDash(lineDash);
          path.setLineDashOffset(lineDashOffset);
        }

        this.buildPath(path, this.shape, false); // Clear path dirty flag

        if (this.path) {
          this.__dirtyPath = false;
        }
      } else {
        // Replay path building
        ctx.beginPath();
        this.path.rebuildPath(ctx);
      }

      if (hasFill) {
        if (this.style.fillOpacity != null) {
          var originalGlobalAlpha = ctx.globalAlpha;
          ctx.globalAlpha = this.style.fillOpacity * this.style.opacity;
          path.fill(ctx);
          ctx.globalAlpha = originalGlobalAlpha;
        } else {
          path.fill(ctx);
        }
      }

      if (lineDash && ctxLineDash) {
        ctx.setLineDash(lineDash);
        ctx.lineDashOffset = lineDashOffset;
      }

      if (hasStroke) {
        if (this.style.strokeOpacity != null) {
          var _originalGlobalAlpha = ctx.globalAlpha;
          ctx.globalAlpha = this.style.strokeOpacity * this.style.opacity;
          path.stroke(ctx);
          ctx.globalAlpha = _originalGlobalAlpha;
        } else {
          path.stroke(ctx);
        }
      }

      if (lineDash && ctxLineDash) {
        // PENDING
        // Remove lineDash
        ctx.setLineDash([]);
      }

      Element.prototype.render.call(this, ctx, prevEl);
    }
    /**
     * @method buildPath
     * 
     * Each subclass should provide its own implement for this method.
     * When build path, some shape may decide if use moveTo to begin a new subpath or closePath, like in circle.
     * 
     * 每个子类都需要为此方法提供自己的实现。
     * 在构建路径时,某些形状需要根据情况决定使用 moveTo 来开始一段子路径,或者直接用 closePath 来封闭路径,比如圆形。
     * 
     * @param {*} ctx 
     * @param {*} shapeCfg 
     * @param {*} inBundle 
     */

  }, {
    key: "buildPath",
    value: function buildPath(ctx, shapeCfg, inBundle) {}
    /**
     * @method createPathProxy
     */

  }, {
    key: "createPathProxy",
    value: function createPathProxy() {
      this.path = new PathProxy();
    }
    /**
     * @method getBoundingRect
     * Get bounding rect of this element, NOTE: 
     * this method will return the bounding rect without transforming(translate/scale/rotate/skew). 
     * However, direct modifications to the shape property will be reflected in the bouding-rect.
     * For example,  if we modify this.shape.width directly, then the new width property will be calculated.
     * 
     * 
     * 获取当前元素的边界矩形,注意:
     * 此方法返回的是没有经过 transform(translate/scale/rotate/skew) 处理的边界矩形,但是对 shape 属性直接进行的修改会反映在获取的边界矩形上。
     * 例如,用代码直接对 this.shape.width 进行赋值,那么在计算边界矩形时就会用新的 width 属性进行计算。
     */

  }, {
    key: "getBoundingRect",
    value: function getBoundingRect() {
      var rect = this.__boundingRect;
      var needsUpdateRect = !rect;

      if (needsUpdateRect) {
        var path = this.path;

        if (!path) {
          // Create path on demand.
          path = this.path = new PathProxy();
        }

        if (this.__dirtyPath) {
          path.beginPath();
          this.buildPath(path, this.shape, false);
        }

        rect = path.getBoundingRect();
      }

      this.__boundingRect = rect;

      if (this.style.hasStroke()) {
        // Update rect with stroke lineWidth when
        // 1. Element changes scale or lineWidth
        // 2. Shape is changed
        var rectWithStroke = this._boundingRectWithStroke || (this._boundingRectWithStroke = rect.clone());

        if (this.__dirty || needsUpdateRect) {
          rectWithStroke.copy(rect); // FIXME Must after composeParentTransform

          var w = this.style.lineWidth; // PENDING, Min line width is needed when line is horizontal or vertical

          var lineScale = this.style.strokeNoScale ? this.getLineScale() : 1; // Only add extra hover lineWidth when there are no fill

          if (!this.style.hasFill()) {
            w = mathMax(w, this.strokeContainThreshold || 4);
          } // Consider line width
          // Line scale can't be 0;


          if (lineScale > 1e-10) {
            rectWithStroke.width += w / lineScale;
            rectWithStroke.height += w / lineScale;
            rectWithStroke.x1 -= w / lineScale / 2;
            rectWithStroke.y1 -= w / lineScale / 2;
            rectWithStroke.x2 = rectWithStroke.x1 + rectWithStroke.width;
            rectWithStroke.y2 = rectWithStroke.y1 + rectWithStroke.height;
          }
        } // Return rect with stroke


        return rectWithStroke;
      }

      return rect;
    }
    /**
     * @method containPoint
     * @param {*} x 
     * @param {*} y 
     */

  }, {
    key: "containPoint",
    value: function containPoint(x, y) {
      var localPos = this.globalToLocal(x, y);
      var rect = this.getBoundingRect();
      var style = this.style;
      x = localPos[0];
      y = localPos[1];

      if (rect.containPoint(x, y)) {
        var pathData = this.path.data;

        if (style.hasStroke()) {
          var lineWidth = style.lineWidth;
          var lineScale = style.strokeNoScale ? this.getLineScale() : 1; // Line scale can't be 0;

          if (lineScale > 1e-10) {
            // Only add extra hover lineWidth when there are no fill
            if (!style.hasFill()) {
              lineWidth = mathMax(lineWidth, this.strokeContainThreshold);
            }

            if (pathContain.containStroke(pathData, lineWidth / lineScale, x, y)) {
              return true;
            }
          }
        }

        if (style.hasFill()) {
          return pathContain.containPoint(pathData, x, y);
        }
      }

      return false;
    }
    /**
     * @protected
     * @method dirty
     * @param  {Boolean} dirtyPath
     */

  }, {
    key: "dirty",
    value: function dirty(dirtyPath) {
      if (dirtyPath == null) {
        dirtyPath = true;
      } // Only mark dirty, not mark clean


      if (dirtyPath) {
        this.__dirtyPath = dirtyPath;
      }

      Element.prototype.dirty.call(this); // Used as a clipping path

      if (this.__clipTarget) {
        this.__clipTarget.dirty();
      }
    }
    /**
     * @method _attrKV
     * Overwrite _attrKV
     * @param {*} key 
     * @param {Object} value 
     */

  }, {
    key: "_attrKV",
    value: function _attrKV(key, value) {
      // FIXME
      if (key === 'shape') {
        this.__dirtyPath = true;
        this.__boundingRect = null;
        this.setShape(value);
      } else {
        Element.prototype._attrKV.call(this, key, value);
      }
    }
    /**
     * @method setShape
     * @param {Object|String} key
     * @param {Object} value
     */

  }, {
    key: "setShape",
    value: function setShape(key, value) {
      // Path from string may not have shape
      if (!this.shape) {
        return this;
      }

      if (dataUtil.isObject(key)) {
        classUtil.copyOwnProperties(this.shape, key);
      } else {
        this.shape[key] = value;
      }

      this.dirty(true);
      return this;
    }
    /**
     * @method getLineScale
     */

  }, {
    key: "getLineScale",
    value: function getLineScale() {
      var m = this.transform; // Get the line scale.
      // Determinant of `m` means how much the area is enlarged by the
      // transformation. So its square root can be used as a scale factor
      // for width.

      return m && mathAbs(m[0] - 1) > 1e-10 && mathAbs(m[3] - 1) > 1e-10 ? mathSqrt(mathAbs(m[0] * m[3] - m[2] * m[1])) : 1;
    }
  }]);

  return Path;
}(Element);

var _default = Path;
module.exports = _default;