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

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

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

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

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

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

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

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

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;
    /**
     * @private
     * @property __dirtyPath
     */

    _this.__dirtyPath = true;
    /**
     * @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;
    return _this;
  }
  /**
   * @method brush
   * @param {Object} ctx 
   * @param {Element} prevEl 
   */


  _createClass(Path, [{
    key: "brush",
    value: function brush(ctx, 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.setTransform(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([]);
      } // Draw rect text


      if (this.style.text != null) {
        // Only restore transform when needs draw text.
        this.restoreTransform(ctx);
        this.drawRectText(ctx, this.getBoundingRect());
      }
    }
    /**
     * @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();
    }
    /**
     * @protected
     * @method getBoundingRect
     */

  }, {
    key: "getBoundingRect",
    value: function getBoundingRect() {
      var rect = this._rect;
      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._rect = rect;

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

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

          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.x -= w / lineScale / 2;
            rectWithStroke.y -= w / lineScale / 2;
          }
        } // Return rect with stroke


        return rectWithStroke;
      }

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

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

      if (rect.contain(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.contain(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;
        this._rect = null;
      }

      this.__dirty = this.__dirtyText = true;
      this.__qr && this.__qr.refresh(); // Used as a clipping path

      if (this.__clipTarget) {
        this.__clipTarget.dirty();
      }
    }
    /**
     * @method animateShape
     * Alias for animate('shape')
     * @param {Boolean} loop
     */

  }, {
    key: "animateShape",
    value: function animateShape(loop) {
      return this.animate('shape', loop);
    }
    /**
     * @method attrKV
     * Overwrite attrKV
     * @param {*} key 
     * @param {Object} value 
     */

  }, {
    key: "attrKV",
    value: function attrKV(key, value) {
      // FIXME
      if (key === 'shape') {
        this.setShape(value);
        this.__dirtyPath = true;
        this._rect = null;
      } 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;