/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

var zrUtil = require("zrender/lib/core/util");

var pathTool = require("zrender/lib/tool/path");

var colorTool = require("zrender/lib/tool/color");

var matrix = require("zrender/lib/core/matrix");

var vector = require("zrender/lib/core/vector");

var Path = require("zrender/lib/graphic/Path");

var Transformable = require("zrender/lib/mixin/Transformable");

var ZImage = require("zrender/lib/graphic/Image");

exports.Image = ZImage;

var Group = require("zrender/lib/container/Group");

exports.Group = Group;

var Text = require("zrender/lib/graphic/Text");

exports.Text = Text;

var Circle = require("zrender/lib/graphic/shape/Circle");

exports.Circle = Circle;

var Sector = require("zrender/lib/graphic/shape/Sector");

exports.Sector = Sector;

var Ring = require("zrender/lib/graphic/shape/Ring");

exports.Ring = Ring;

var Polygon = require("zrender/lib/graphic/shape/Polygon");

exports.Polygon = Polygon;

var Polyline = require("zrender/lib/graphic/shape/Polyline");

exports.Polyline = Polyline;

var Rect = require("zrender/lib/graphic/shape/Rect");

exports.Rect = Rect;

var Line = require("zrender/lib/graphic/shape/Line");

exports.Line = Line;

var BezierCurve = require("zrender/lib/graphic/shape/BezierCurve");

exports.BezierCurve = BezierCurve;

var Arc = require("zrender/lib/graphic/shape/Arc");

exports.Arc = Arc;

var CompoundPath = require("zrender/lib/graphic/CompoundPath");

exports.CompoundPath = CompoundPath;

var LinearGradient = require("zrender/lib/graphic/LinearGradient");

exports.LinearGradient = LinearGradient;

var RadialGradient = require("zrender/lib/graphic/RadialGradient");

exports.RadialGradient = RadialGradient;

var BoundingRect = require("zrender/lib/core/BoundingRect");

exports.BoundingRect = BoundingRect;

var IncrementalDisplayable = require("zrender/lib/graphic/IncrementalDisplayable");

exports.IncrementalDisplayable = IncrementalDisplayable;

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var round = Math.round;
var mathMax = Math.max;
var mathMin = Math.min;
var EMPTY_OBJ = {};
/**
 * Extend shape with parameters
 */

function extendShape(opts) {
  return Path.extend(opts);
}
/**
 * Extend path
 */


function extendPath(pathData, opts) {
  return pathTool.extendFromString(pathData, opts);
}
/**
 * Create a path element from path data string
 * @param {string} pathData
 * @param {Object} opts
 * @param {module:zrender/core/BoundingRect} rect
 * @param {string} [layout=cover] 'center' or 'cover'
 */


function makePath(pathData, opts, rect, layout) {
  var path = pathTool.createFromString(pathData, opts);

  if (rect) {
    if (layout === 'center') {
      rect = centerGraphic(rect, path.getBoundingRect());
    }

    resizePath(path, rect);
  }

  return path;
}
/**
 * Create a image element from image url
 * @param {string} imageUrl image url
 * @param {Object} opts options
 * @param {module:zrender/core/BoundingRect} rect constrain rect
 * @param {string} [layout=cover] 'center' or 'cover'
 */


function makeImage(imageUrl, rect, layout) {
  var path = new ZImage({
    style: {
      image: imageUrl,
      x: rect.x,
      y: rect.y,
      width: rect.width,
      height: rect.height
    },
    onload: function (img) {
      if (layout === 'center') {
        var boundingRect = {
          width: img.width,
          height: img.height
        };
        path.setStyle(centerGraphic(rect, boundingRect));
      }
    }
  });
  return path;
}
/**
 * Get position of centered element in bounding box.
 *
 * @param  {Object} rect         element local bounding box
 * @param  {Object} boundingRect constraint bounding box
 * @return {Object} element position containing x, y, width, and height
 */


function centerGraphic(rect, boundingRect) {
  // Set rect to center, keep width / height ratio.
  var aspect = boundingRect.width / boundingRect.height;
  var width = rect.height * aspect;
  var height;

  if (width <= rect.width) {
    height = rect.height;
  } else {
    width = rect.width;
    height = width / aspect;
  }

  var cx = rect.x + rect.width / 2;
  var cy = rect.y + rect.height / 2;
  return {
    x: cx - width / 2,
    y: cy - height / 2,
    width: width,
    height: height
  };
}

var mergePath = pathTool.mergePath;
/**
 * Resize a path to fit the rect
 * @param {module:zrender/graphic/Path} path
 * @param {Object} rect
 */

function resizePath(path, rect) {
  if (!path.applyTransform) {
    return;
  }

  var pathRect = path.getBoundingRect();
  var m = pathRect.calculateTransform(rect);
  path.applyTransform(m);
}
/**
 * Sub pixel optimize line for canvas
 *
 * @param {Object} param
 * @param {Object} [param.shape]
 * @param {number} [param.shape.x1]
 * @param {number} [param.shape.y1]
 * @param {number} [param.shape.x2]
 * @param {number} [param.shape.y2]
 * @param {Object} [param.style]
 * @param {number} [param.style.lineWidth]
 * @return {Object} Modified param
 */


function subPixelOptimizeLine(param) {
  var shape = param.shape;
  var lineWidth = param.style.lineWidth;

  if (round(shape.x1 * 2) === round(shape.x2 * 2)) {
    shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true);
  }

  if (round(shape.y1 * 2) === round(shape.y2 * 2)) {
    shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true);
  }

  return param;
}
/**
 * Sub pixel optimize rect for canvas
 *
 * @param {Object} param
 * @param {Object} [param.shape]
 * @param {number} [param.shape.x]
 * @param {number} [param.shape.y]
 * @param {number} [param.shape.width]
 * @param {number} [param.shape.height]
 * @param {Object} [param.style]
 * @param {number} [param.style.lineWidth]
 * @return {Object} Modified param
 */


function subPixelOptimizeRect(param) {
  var shape = param.shape;
  var lineWidth = param.style.lineWidth;
  var originX = shape.x;
  var originY = shape.y;
  var originWidth = shape.width;
  var originHeight = shape.height;
  shape.x = subPixelOptimize(shape.x, lineWidth, true);
  shape.y = subPixelOptimize(shape.y, lineWidth, true);
  shape.width = Math.max(subPixelOptimize(originX + originWidth, lineWidth, false) - shape.x, originWidth === 0 ? 0 : 1);
  shape.height = Math.max(subPixelOptimize(originY + originHeight, lineWidth, false) - shape.y, originHeight === 0 ? 0 : 1);
  return param;
}
/**
 * Sub pixel optimize for canvas
 *
 * @param {number} position Coordinate, such as x, y
 * @param {number} lineWidth Should be nonnegative integer.
 * @param {boolean=} positiveOrNegative Default false (negative).
 * @return {number} Optimized position.
 */


function subPixelOptimize(position, lineWidth, positiveOrNegative) {
  // Assure that (position + lineWidth / 2) is near integer edge,
  // otherwise line will be fuzzy in canvas.
  var doubledPosition = round(position * 2);
  return (doubledPosition + round(lineWidth)) % 2 === 0 ? doubledPosition / 2 : (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2;
}

function hasFillOrStroke(fillOrStroke) {
  return fillOrStroke != null && fillOrStroke !== 'none';
} // Most lifted color are duplicated.


var liftedColorMap = zrUtil.createHashMap();
var liftedColorCount = 0;

function liftColor(color) {
  if (typeof color !== 'string') {
    return color;
  }

  var liftedColor = liftedColorMap.get(color);

  if (!liftedColor) {
    liftedColor = colorTool.lift(color, -0.1);

    if (liftedColorCount < 10000) {
      liftedColorMap.set(color, liftedColor);
      liftedColorCount++;
    }
  }

  return liftedColor;
}

function cacheElementStl(el) {
  if (!el.__hoverStlDirty) {
    return;
  }

  el.__hoverStlDirty = false;
  var hoverStyle = el.__hoverStl;

  if (!hoverStyle) {
    el.__normalStl = null;
    return;
  }

  var normalStyle = el.__normalStl = {};
  var elStyle = el.style;

  for (var name in hoverStyle) {
    // See comment in `doSingleEnterHover`.
    if (hoverStyle[name] != null) {
      normalStyle[name] = elStyle[name];
    }
  } // Always cache fill and stroke to normalStyle for lifting color.


  normalStyle.fill = elStyle.fill;
  normalStyle.stroke = elStyle.stroke;
}

function doSingleEnterHover(el) {
  var hoverStl = el.__hoverStl;

  if (!hoverStl || el.__highlighted) {
    return;
  }

  var useHoverLayer = el.useHoverLayer;
  el.__highlighted = useHoverLayer ? 'layer' : 'plain';
  var zr = el.__zr;

  if (!zr && useHoverLayer) {
    return;
  }

  var elTarget = el;
  var targetStyle = el.style;

  if (useHoverLayer) {
    elTarget = zr.addHover(el);
    targetStyle = elTarget.style;
  } // Consider case: only `position: 'top'` is set on emphasis, then text
  // color should be returned to `autoColor`, rather than remain '#fff'.
  // So we should rollback then apply again after style merging.


  rollbackDefaultTextStyle(targetStyle);

  if (!useHoverLayer) {
    cacheElementStl(elTarget);
  } // styles can be:
  // {
  //    label: {
  //        show: false,
  //        position: 'outside',
  //        fontSize: 18
  //    },
  //    emphasis: {
  //        label: {
  //            show: true
  //        }
  //    }
  // },
  // where properties of `emphasis` may not appear in `normal`. We previously use
  // module:echarts/util/model#defaultEmphasis to merge `normal` to `emphasis`.
  // But consider rich text and setOption in merge mode, it is impossible to cover
  // all properties in merge. So we use merge mode when setting style here, where
  // only properties that is not `null/undefined` can be set. The disadventage:
  // null/undefined can not be used to remove style any more in `emphasis`.


  targetStyle.extendFrom(hoverStl);
  setDefaultHoverFillStroke(targetStyle, hoverStl, 'fill');
  setDefaultHoverFillStroke(targetStyle, hoverStl, 'stroke');
  applyDefaultTextStyle(targetStyle);

  if (!useHoverLayer) {
    el.dirty(false);
    el.z2 += 1;
  }
}

function setDefaultHoverFillStroke(targetStyle, hoverStyle, prop) {
  if (!hasFillOrStroke(hoverStyle[prop]) && hasFillOrStroke(targetStyle[prop])) {
    targetStyle[prop] = liftColor(targetStyle[prop]);
  }
}

function doSingleLeaveHover(el) {
  if (el.__highlighted) {
    doSingleRestoreHoverStyle(el);
    el.__highlighted = false;
  }
}

function doSingleRestoreHoverStyle(el) {
  var highlighted = el.__highlighted;

  if (highlighted === 'layer') {
    el.__zr && el.__zr.removeHover(el);
  } else if (highlighted) {
    var style = el.style;
    var normalStl = el.__normalStl;

    if (normalStl) {
      rollbackDefaultTextStyle(style); // Consider null/undefined value, should use
      // `setStyle` but not `extendFrom(stl, true)`.

      el.setStyle(normalStl);
      applyDefaultTextStyle(style);
      el.z2 -= 1;
    }
  }
}

function traverseCall(el, method) {
  el.isGroup ? el.traverse(function (child) {
    !child.isGroup && method(child);
  }) : method(el);
}
/**
 * Set hover style of element.
 *
 * @param {module:zrender/Element} el Should not be `zrender/container/Group`.
 * @param {Object|boolean} [hoverStl] The specified hover style.
 *        If set as `false`, disable the hover style.
 *        Similarly, The `el.hoverStyle` can alse be set
 *        as `false` to disable the hover style.
 *        Otherwise, use the default hover style if not provided.
 * @param {Object} [opt]
 * @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`
 */


function setElementHoverStyle(el, hoverStl) {
  hoverStl = el.__hoverStl = hoverStl !== false && (hoverStl || {});
  el.__hoverStlDirty = true;

  if (el.__highlighted) {
    doSingleLeaveHover(el);
    doSingleEnterHover(el);
  }
}
/**
 * Emphasis (called by API) has higher priority than `mouseover`.
 * When element has been called to be entered emphasis, mouse over
 * should not trigger the highlight effect (for example, animation
 * scale) again, and `mouseout` should not downplay the highlight
 * effect. So the listener of `mouseover` and `mouseout` should
 * check `isInEmphasis`.
 *
 * @param {module:zrender/Element} el
 * @return {boolean}
 */


function isInEmphasis(el) {
  return el && el.__isEmphasisEntered;
}

function onElementMouseOver(e) {
  if (this.__hoverSilentOnTouch && e.zrByTouch) {
    return;
  } // Only if element is not in emphasis status


  !this.__isEmphasisEntered && traverseCall(this, doSingleEnterHover);
}

function onElementMouseOut(e) {
  if (this.__hoverSilentOnTouch && e.zrByTouch) {
    return;
  } // Only if element is not in emphasis status


  !this.__isEmphasisEntered && traverseCall(this, doSingleLeaveHover);
}

function enterEmphasis() {
  this.__isEmphasisEntered = true;
  traverseCall(this, doSingleEnterHover);
}

function leaveEmphasis() {
  this.__isEmphasisEntered = false;
  traverseCall(this, doSingleLeaveHover);
}
/**
 * Set hover style of element.
 *
 * [Caveat]:
 * This method can be called repeatly and achieve the same result.
 *
 * [Usage]:
 * Call the method for a "root" element once. Do not call it for each descendants.
 * If the descendants elemenets of a group has itself hover style different from the
 * root group, we can simply mount the style on `el.hoverStyle` for them, but should
 * not call this method for them.
 *
 * @param {module:zrender/Element} el
 * @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`.
 * @param {Object} [opt]
 * @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`.
 */


function setHoverStyle(el, hoverStyle, opt) {
  el.isGroup ? el.traverse(function (child) {
    // If element has sepcified hoverStyle, then use it instead of given hoverStyle
    // Often used when item group has a label element and it's hoverStyle is different
    !child.isGroup && setElementHoverStyle(child, child.hoverStyle || hoverStyle);
  }) : setElementHoverStyle(el, el.hoverStyle || hoverStyle);
  setAsHoverStyleTrigger(el, opt);
}
/**
 * @param {Object|boolean} [opt] If `false`, means disable trigger.
 * @param {boolean} [opt.hoverSilentOnTouch=false]
 *        In touch device, mouseover event will be trigger on touchstart event
 *        (see module:zrender/dom/HandlerProxy). By this mechanism, we can
 *        conveniently use hoverStyle when tap on touch screen without additional
 *        code for compatibility.
 *        But if the chart/component has select feature, which usually also use
 *        hoverStyle, there might be conflict between 'select-highlight' and
 *        'hover-highlight' especially when roam is enabled (see geo for example).
 *        In this case, hoverSilentOnTouch should be used to disable hover-highlight
 *        on touch device.
 */


function setAsHoverStyleTrigger(el, opt) {
  var disable = opt === false;
  el.__hoverSilentOnTouch = opt != null && opt.hoverSilentOnTouch; // Simple optimize, since this method might be
  // called for each elements of a group in some cases.

  if (!disable || el.__hoverStyleTrigger) {
    var method = disable ? 'off' : 'on'; // Duplicated function will be auto-ignored, see Eventful.js.

    el[method]('mouseover', onElementMouseOver)[method]('mouseout', onElementMouseOut); // Emphasis, normal can be triggered manually

    el[method]('emphasis', enterEmphasis)[method]('normal', leaveEmphasis);
    el.__hoverStyleTrigger = !disable;
  }
}
/**
 * @param {Object|module:zrender/graphic/Style} normalStyle
 * @param {Object} emphasisStyle
 * @param {module:echarts/model/Model} normalModel
 * @param {module:echarts/model/Model} emphasisModel
 * @param {Object} opt Check `opt` of `setTextStyleCommon` to find other props.
 * @param {string|Function} [opt.defaultText]
 * @param {module:echarts/model/Model} [opt.labelFetcher] Fetch text by
 *      `opt.labelFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
 * @param {module:echarts/model/Model} [opt.labelDataIndex] Fetch text by
 *      `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
 * @param {module:echarts/model/Model} [opt.labelDimIndex] Fetch text by
 *      `opt.textFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
 * @param {Object} [normalSpecified]
 * @param {Object} [emphasisSpecified]
 */


function setLabelStyle(normalStyle, emphasisStyle, normalModel, emphasisModel, opt, normalSpecified, emphasisSpecified) {
  opt = opt || EMPTY_OBJ;
  var labelFetcher = opt.labelFetcher;
  var labelDataIndex = opt.labelDataIndex;
  var labelDimIndex = opt.labelDimIndex; // This scenario, `label.normal.show = true; label.emphasis.show = false`,
  // is not supported util someone requests.

  var showNormal = normalModel.getShallow('show');
  var showEmphasis = emphasisModel.getShallow('show'); // Consider performance, only fetch label when necessary.
  // If `normal.show` is `false` and `emphasis.show` is `true` and `emphasis.formatter` is not set,
  // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`.

  var baseText;

  if (showNormal || showEmphasis) {
    if (labelFetcher) {
      baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex);
    }

    if (baseText == null) {
      baseText = zrUtil.isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText;
    }
  }

  var normalStyleText = showNormal ? baseText : null;
  var emphasisStyleText = showEmphasis ? zrUtil.retrieve2(labelFetcher ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex) : null, baseText) : null; // Optimize: If style.text is null, text will not be drawn.

  if (normalStyleText != null || emphasisStyleText != null) {
    // Always set `textStyle` even if `normalStyle.text` is null, because default
    // values have to be set on `normalStyle`.
    // If we set default values on `emphasisStyle`, consider case:
    // Firstly, `setOption(... label: {normal: {text: null}, emphasis: {show: true}} ...);`
    // Secondly, `setOption(... label: {noraml: {show: true, text: 'abc', color: 'red'} ...);`
    // Then the 'red' will not work on emphasis.
    setTextStyle(normalStyle, normalModel, normalSpecified, opt);
    setTextStyle(emphasisStyle, emphasisModel, emphasisSpecified, opt, true);
  }

  normalStyle.text = normalStyleText;
  emphasisStyle.text = emphasisStyleText;
}
/**
 * Set basic textStyle properties.
 * @param {Object|module:zrender/graphic/Style} textStyle
 * @param {module:echarts/model/Model} model
 * @param {Object} [specifiedTextStyle] Can be overrided by settings in model.
 * @param {Object} [opt] See `opt` of `setTextStyleCommon`.
 * @param {boolean} [isEmphasis]
 */


function setTextStyle(textStyle, textStyleModel, specifiedTextStyle, opt, isEmphasis) {
  setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis);
  specifiedTextStyle && zrUtil.extend(textStyle, specifiedTextStyle); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);

  return textStyle;
}
/**
 * Set text option in the style.
 * @deprecated
 * @param {Object} textStyle
 * @param {module:echarts/model/Model} labelModel
 * @param {string|boolean} defaultColor Default text color.
 *        If set as false, it will be processed as a emphasis style.
 */


function setText(textStyle, labelModel, defaultColor) {
  var opt = {
    isRectText: true
  };
  var isEmphasis;

  if (defaultColor === false) {
    isEmphasis = true;
  } else {
    // Support setting color as 'auto' to get visual color.
    opt.autoColor = defaultColor;
  }

  setTextStyleCommon(textStyle, labelModel, opt, isEmphasis); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
}
/**
 * {
 *      disableBox: boolean, Whether diable drawing box of block (outer most).
 *      isRectText: boolean,
 *      autoColor: string, specify a color when color is 'auto',
 *              for textFill, textStroke, textBackgroundColor, and textBorderColor.
 *              If autoColor specified, it is used as default textFill.
 *      useInsideStyle:
 *              `true`: Use inside style (textFill, textStroke, textStrokeWidth)
 *                  if `textFill` is not specified.
 *              `false`: Do not use inside style.
 *              `null/undefined`: use inside style if `isRectText` is true and
 *                  `textFill` is not specified and textPosition contains `'inside'`.
 *      forceRich: boolean
 * }
 */


function setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis) {
  // Consider there will be abnormal when merge hover style to normal style if given default value.
  opt = opt || EMPTY_OBJ;

  if (opt.isRectText) {
    var textPosition = textStyleModel.getShallow('position') || (isEmphasis ? null : 'inside'); // 'outside' is not a valid zr textPostion value, but used
    // in bar series, and magric type should be considered.

    textPosition === 'outside' && (textPosition = 'top');
    textStyle.textPosition = textPosition;
    textStyle.textOffset = textStyleModel.getShallow('offset');
    var labelRotate = textStyleModel.getShallow('rotate');
    labelRotate != null && (labelRotate *= Math.PI / 180);
    textStyle.textRotation = labelRotate;
    textStyle.textDistance = zrUtil.retrieve2(textStyleModel.getShallow('distance'), isEmphasis ? null : 5);
  }

  var ecModel = textStyleModel.ecModel;
  var globalTextStyle = ecModel && ecModel.option.textStyle; // Consider case:
  // {
  //     data: [{
  //         value: 12,
  //         label: {
  //             rich: {
  //                 // no 'a' here but using parent 'a'.
  //             }
  //         }
  //     }],
  //     rich: {
  //         a: { ... }
  //     }
  // }

  var richItemNames = getRichItemNames(textStyleModel);
  var richResult;

  if (richItemNames) {
    richResult = {};

    for (var name in richItemNames) {
      if (richItemNames.hasOwnProperty(name)) {
        // Cascade is supported in rich.
        var richTextStyle = textStyleModel.getModel(['rich', name]); // In rich, never `disableBox`.

        setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isEmphasis);
      }
    }
  }

  textStyle.rich = richResult;
  setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, true);

  if (opt.forceRich && !opt.textStyle) {
    opt.textStyle = {};
  }

  return textStyle;
} // Consider case:
// {
//     data: [{
//         value: 12,
//         label: {
//             rich: {
//                 // no 'a' here but using parent 'a'.
//             }
//         }
//     }],
//     rich: {
//         a: { ... }
//     }
// }


function getRichItemNames(textStyleModel) {
  // Use object to remove duplicated names.
  var richItemNameMap;

  while (textStyleModel && textStyleModel !== textStyleModel.ecModel) {
    var rich = (textStyleModel.option || EMPTY_OBJ).rich;

    if (rich) {
      richItemNameMap = richItemNameMap || {};

      for (var name in rich) {
        if (rich.hasOwnProperty(name)) {
          richItemNameMap[name] = 1;
        }
      }
    }

    textStyleModel = textStyleModel.parentModel;
  }

  return richItemNameMap;
}

function setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isBlock) {
  // In merge mode, default value should not be given.
  globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ;
  textStyle.textFill = getAutoColor(textStyleModel.getShallow('color'), opt) || globalTextStyle.color;
  textStyle.textStroke = getAutoColor(textStyleModel.getShallow('textBorderColor'), opt) || globalTextStyle.textBorderColor;
  textStyle.textStrokeWidth = zrUtil.retrieve2(textStyleModel.getShallow('textBorderWidth'), globalTextStyle.textBorderWidth); // Save original textPosition, because style.textPosition will be repalced by
  // real location (like [10, 30]) in zrender.

  textStyle.insideRawTextPosition = textStyle.textPosition;

  if (!isEmphasis) {
    if (isBlock) {
      textStyle.insideRollbackOpt = opt;
      applyDefaultTextStyle(textStyle);
    } // Set default finally.


    if (textStyle.textFill == null) {
      textStyle.textFill = opt.autoColor;
    }
  } // Do not use `getFont` here, because merge should be supported, where
  // part of these properties may be changed in emphasis style, and the
  // others should remain their original value got from normal style.


  textStyle.fontStyle = textStyleModel.getShallow('fontStyle') || globalTextStyle.fontStyle;
  textStyle.fontWeight = textStyleModel.getShallow('fontWeight') || globalTextStyle.fontWeight;
  textStyle.fontSize = textStyleModel.getShallow('fontSize') || globalTextStyle.fontSize;
  textStyle.fontFamily = textStyleModel.getShallow('fontFamily') || globalTextStyle.fontFamily;
  textStyle.textAlign = textStyleModel.getShallow('align');
  textStyle.textVerticalAlign = textStyleModel.getShallow('verticalAlign') || textStyleModel.getShallow('baseline');
  textStyle.textLineHeight = textStyleModel.getShallow('lineHeight');
  textStyle.textWidth = textStyleModel.getShallow('width');
  textStyle.textHeight = textStyleModel.getShallow('height');
  textStyle.textTag = textStyleModel.getShallow('tag');

  if (!isBlock || !opt.disableBox) {
    textStyle.textBackgroundColor = getAutoColor(textStyleModel.getShallow('backgroundColor'), opt);
    textStyle.textPadding = textStyleModel.getShallow('padding');
    textStyle.textBorderColor = getAutoColor(textStyleModel.getShallow('borderColor'), opt);
    textStyle.textBorderWidth = textStyleModel.getShallow('borderWidth');
    textStyle.textBorderRadius = textStyleModel.getShallow('borderRadius');
    textStyle.textBoxShadowColor = textStyleModel.getShallow('shadowColor');
    textStyle.textBoxShadowBlur = textStyleModel.getShallow('shadowBlur');
    textStyle.textBoxShadowOffsetX = textStyleModel.getShallow('shadowOffsetX');
    textStyle.textBoxShadowOffsetY = textStyleModel.getShallow('shadowOffsetY');
  }

  textStyle.textShadowColor = textStyleModel.getShallow('textShadowColor') || globalTextStyle.textShadowColor;
  textStyle.textShadowBlur = textStyleModel.getShallow('textShadowBlur') || globalTextStyle.textShadowBlur;
  textStyle.textShadowOffsetX = textStyleModel.getShallow('textShadowOffsetX') || globalTextStyle.textShadowOffsetX;
  textStyle.textShadowOffsetY = textStyleModel.getShallow('textShadowOffsetY') || globalTextStyle.textShadowOffsetY;
}

function getAutoColor(color, opt) {
  return color !== 'auto' ? color : opt && opt.autoColor ? opt.autoColor : null;
} // When text position is `inside` and `textFill` not specified, we
// provide a mechanism to auto make text border for better view. But
// text position changing when hovering or being emphasis should be
// considered, where the `insideRollback` enables to restore the style.


function applyDefaultTextStyle(textStyle) {
  var opt = textStyle.insideRollbackOpt; // Only insideRollbackOpt create (setTextStyleCommon used),
  // applyDefaultTextStyle works.

  if (!opt || textStyle.textFill != null) {
    return;
  }

  var useInsideStyle = opt.useInsideStyle;
  var textPosition = textStyle.insideRawTextPosition;
  var insideRollback;
  var autoColor = opt.autoColor;

  if (useInsideStyle !== false && (useInsideStyle === true || opt.isRectText && textPosition // textPosition can be [10, 30]
  && typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0)) {
    insideRollback = {
      textFill: null,
      textStroke: textStyle.textStroke,
      textStrokeWidth: textStyle.textStrokeWidth
    };
    textStyle.textFill = '#fff'; // Consider text with #fff overflow its container.

    if (textStyle.textStroke == null) {
      textStyle.textStroke = autoColor;
      textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2);
    }
  } else if (autoColor != null) {
    insideRollback = {
      textFill: null
    };
    textStyle.textFill = autoColor;
  } // Always set `insideRollback`, for clearing previous.


  if (insideRollback) {
    textStyle.insideRollback = insideRollback;
  }
}

function rollbackDefaultTextStyle(style) {
  var insideRollback = style.insideRollback;

  if (insideRollback) {
    style.textFill = insideRollback.textFill;
    style.textStroke = insideRollback.textStroke;
    style.textStrokeWidth = insideRollback.textStrokeWidth;
    style.insideRollback = null;
  }
}

function getFont(opt, ecModel) {
  // ecModel or default text style model.
  var gTextStyleModel = ecModel || ecModel.getModel('textStyle');
  return zrUtil.trim([// FIXME in node-canvas fontWeight is before fontStyle
  opt.fontStyle || gTextStyleModel && gTextStyleModel.getShallow('fontStyle') || '', opt.fontWeight || gTextStyleModel && gTextStyleModel.getShallow('fontWeight') || '', (opt.fontSize || gTextStyleModel && gTextStyleModel.getShallow('fontSize') || 12) + 'px', opt.fontFamily || gTextStyleModel && gTextStyleModel.getShallow('fontFamily') || 'sans-serif'].join(' '));
}

function animateOrSetProps(isUpdate, el, props, animatableModel, dataIndex, cb) {
  if (typeof dataIndex === 'function') {
    cb = dataIndex;
    dataIndex = null;
  } // Do not check 'animation' property directly here. Consider this case:
  // animation model is an `itemModel`, whose does not have `isAnimationEnabled`
  // but its parent model (`seriesModel`) does.


  var animationEnabled = animatableModel && animatableModel.isAnimationEnabled();

  if (animationEnabled) {
    var postfix = isUpdate ? 'Update' : '';
    var duration = animatableModel.getShallow('animationDuration' + postfix);
    var animationEasing = animatableModel.getShallow('animationEasing' + postfix);
    var animationDelay = animatableModel.getShallow('animationDelay' + postfix);

    if (typeof animationDelay === 'function') {
      animationDelay = animationDelay(dataIndex, animatableModel.getAnimationDelayParams ? animatableModel.getAnimationDelayParams(el, dataIndex) : null);
    }

    if (typeof duration === 'function') {
      duration = duration(dataIndex);
    }

    duration > 0 ? el.animateTo(props, duration, animationDelay || 0, animationEasing, cb, !!cb) : (el.stopAnimation(), el.attr(props), cb && cb());
  } else {
    el.stopAnimation();
    el.attr(props);
    cb && cb();
  }
}
/**
 * Update graphic element properties with or without animation according to the
 * configuration in series.
 *
 * Caution: this method will stop previous animation.
 * So if do not use this method to one element twice before
 * animation starts, unless you know what you are doing.
 *
 * @param {module:zrender/Element} el
 * @param {Object} props
 * @param {module:echarts/model/Model} [animatableModel]
 * @param {number} [dataIndex]
 * @param {Function} [cb]
 * @example
 *     graphic.updateProps(el, {
 *         position: [100, 100]
 *     }, seriesModel, dataIndex, function () { console.log('Animation done!'); });
 *     // Or
 *     graphic.updateProps(el, {
 *         position: [100, 100]
 *     }, seriesModel, function () { console.log('Animation done!'); });
 */


function updateProps(el, props, animatableModel, dataIndex, cb) {
  animateOrSetProps(true, el, props, animatableModel, dataIndex, cb);
}
/**
 * Init graphic element properties with or without animation according to the
 * configuration in series.
 *
 * Caution: this method will stop previous animation.
 * So if do not use this method to one element twice before
 * animation starts, unless you know what you are doing.
 *
 * @param {module:zrender/Element} el
 * @param {Object} props
 * @param {module:echarts/model/Model} [animatableModel]
 * @param {number} [dataIndex]
 * @param {Function} cb
 */


function initProps(el, props, animatableModel, dataIndex, cb) {
  animateOrSetProps(false, el, props, animatableModel, dataIndex, cb);
}
/**
 * Get transform matrix of target (param target),
 * in coordinate of its ancestor (param ancestor)
 *
 * @param {module:zrender/mixin/Transformable} target
 * @param {module:zrender/mixin/Transformable} [ancestor]
 */


function getTransform(target, ancestor) {
  var mat = matrix.identity([]);

  while (target && target !== ancestor) {
    matrix.mul(mat, target.getLocalTransform(), mat);
    target = target.parent;
  }

  return mat;
}
/**
 * Apply transform to an vertex.
 * @param {Array.<number>} target [x, y]
 * @param {Array.<number>|TypedArray.<number>|Object} transform Can be:
 *      + Transform matrix: like [1, 0, 0, 1, 0, 0]
 *      + {position, rotation, scale}, the same as `zrender/Transformable`.
 * @param {boolean=} invert Whether use invert matrix.
 * @return {Array.<number>} [x, y]
 */


function applyTransform(target, transform, invert) {
  if (transform && !zrUtil.isArrayLike(transform)) {
    transform = Transformable.getLocalTransform(transform);
  }

  if (invert) {
    transform = matrix.invert([], transform);
  }

  return vector.applyTransform([], target, transform);
}
/**
 * @param {string} direction 'left' 'right' 'top' 'bottom'
 * @param {Array.<number>} transform Transform matrix: like [1, 0, 0, 1, 0, 0]
 * @param {boolean=} invert Whether use invert matrix.
 * @return {string} Transformed direction. 'left' 'right' 'top' 'bottom'
 */


function transformDirection(direction, transform, invert) {
  // Pick a base, ensure that transform result will not be (0, 0).
  var hBase = transform[4] === 0 || transform[5] === 0 || transform[0] === 0 ? 1 : Math.abs(2 * transform[4] / transform[0]);
  var vBase = transform[4] === 0 || transform[5] === 0 || transform[2] === 0 ? 1 : Math.abs(2 * transform[4] / transform[2]);
  var vertex = [direction === 'left' ? -hBase : direction === 'right' ? hBase : 0, direction === 'top' ? -vBase : direction === 'bottom' ? vBase : 0];
  vertex = applyTransform(vertex, transform, invert);
  return Math.abs(vertex[0]) > Math.abs(vertex[1]) ? vertex[0] > 0 ? 'right' : 'left' : vertex[1] > 0 ? 'bottom' : 'top';
}
/**
 * Apply group transition animation from g1 to g2.
 * If no animatableModel, no animation.
 */


function groupTransition(g1, g2, animatableModel, cb) {
  if (!g1 || !g2) {
    return;
  }

  function getElMap(g) {
    var elMap = {};
    g.traverse(function (el) {
      if (!el.isGroup && el.anid) {
        elMap[el.anid] = el;
      }
    });
    return elMap;
  }

  function getAnimatableProps(el) {
    var obj = {
      position: vector.clone(el.position),
      rotation: el.rotation
    };

    if (el.shape) {
      obj.shape = zrUtil.extend({}, el.shape);
    }

    return obj;
  }

  var elMap1 = getElMap(g1);
  g2.traverse(function (el) {
    if (!el.isGroup && el.anid) {
      var oldEl = elMap1[el.anid];

      if (oldEl) {
        var newProp = getAnimatableProps(el);
        el.attr(getAnimatableProps(oldEl));
        updateProps(el, newProp, animatableModel, el.dataIndex);
      } // else {
      //     if (el.previousProps) {
      //         graphic.updateProps
      //     }
      // }

    }
  });
}
/**
 * @param {Array.<Array.<number>>} points Like: [[23, 44], [53, 66], ...]
 * @param {Object} rect {x, y, width, height}
 * @return {Array.<Array.<number>>} A new clipped points.
 */


function clipPointsByRect(points, rect) {
  // FIXME: this way migth be incorrect when grpahic clipped by a corner.
  // and when element have border.
  return zrUtil.map(points, function (point) {
    var x = point[0];
    x = mathMax(x, rect.x);
    x = mathMin(x, rect.x + rect.width);
    var y = point[1];
    y = mathMax(y, rect.y);
    y = mathMin(y, rect.y + rect.height);
    return [x, y];
  });
}
/**
 * @param {Object} targetRect {x, y, width, height}
 * @param {Object} rect {x, y, width, height}
 * @return {Object} A new clipped rect. If rect size are negative, return undefined.
 */


function clipRectByRect(targetRect, rect) {
  var x = mathMax(targetRect.x, rect.x);
  var x2 = mathMin(targetRect.x + targetRect.width, rect.x + rect.width);
  var y = mathMax(targetRect.y, rect.y);
  var y2 = mathMin(targetRect.y + targetRect.height, rect.y + rect.height); // If the total rect is cliped, nothing, including the border,
  // should be painted. So return undefined.

  if (x2 >= x && y2 >= y) {
    return {
      x: x,
      y: y,
      width: x2 - x,
      height: y2 - y
    };
  }
}
/**
 * @param {string} iconStr Support 'image://' or 'path://' or direct svg path.
 * @param {Object} [opt] Properties of `module:zrender/Element`, except `style`.
 * @param {Object} [rect] {x, y, width, height}
 * @return {module:zrender/Element} Icon path or image element.
 */


function createIcon(iconStr, opt, rect) {
  opt = zrUtil.extend({
    rectHover: true
  }, opt);
  var style = opt.style = {
    strokeNoScale: true
  };
  rect = rect || {
    x: -1,
    y: -1,
    width: 2,
    height: 2
  };

  if (iconStr) {
    return iconStr.indexOf('image://') === 0 ? (style.image = iconStr.slice(8), zrUtil.defaults(style, rect), new ZImage(opt)) : makePath(iconStr.replace('path://', ''), opt, rect, 'center');
  }
}

exports.extendShape = extendShape;
exports.extendPath = extendPath;
exports.makePath = makePath;
exports.makeImage = makeImage;
exports.mergePath = mergePath;
exports.resizePath = resizePath;
exports.subPixelOptimizeLine = subPixelOptimizeLine;
exports.subPixelOptimizeRect = subPixelOptimizeRect;
exports.subPixelOptimize = subPixelOptimize;
exports.setElementHoverStyle = setElementHoverStyle;
exports.isInEmphasis = isInEmphasis;
exports.setHoverStyle = setHoverStyle;
exports.setAsHoverStyleTrigger = setAsHoverStyleTrigger;
exports.setLabelStyle = setLabelStyle;
exports.setTextStyle = setTextStyle;
exports.setText = setText;
exports.getFont = getFont;
exports.updateProps = updateProps;
exports.initProps = initProps;
exports.getTransform = getTransform;
exports.applyTransform = applyTransform;
exports.transformDirection = transformDirection;
exports.groupTransition = groupTransition;
exports.clipPointsByRect = clipPointsByRect;
exports.clipRectByRect = clipRectByRect;
exports.createIcon = createIcon;