/** 
 * 图形相关函数
 * 
 * @module util/graphic 
 */

import * as zrUtil from 'zrender/src/core/util';
import * as pathTool from 'zrender/src/tool/path';
import * as colorTool from 'zrender/src/tool/color';
import * as matrix from 'zrender/src/core/matrix';
import * as vector from 'zrender/src/core/vector';
import Path from 'zrender/src/graphic/Path';
import Transformable from 'zrender/src/mixin/Transformable';
import ZImage from 'zrender/src/graphic/Image';
import Group from 'zrender/src/container/Group';
import Text from 'zrender/src/graphic/Text';
import Circle from 'zrender/src/graphic/shape/Circle';
import Sector from 'zrender/src/graphic/shape/Sector';
import Ring from 'zrender/src/graphic/shape/Ring';
import Polygon from 'zrender/src/graphic/shape/Polygon';
import Polyline from 'zrender/src/graphic/shape/Polyline';
import Rect from 'zrender/src/graphic/shape/Rect';
import Line from 'zrender/src/graphic/shape/Line';
import BezierCurve from 'zrender/src/graphic/shape/BezierCurve';
import Arc from 'zrender/src/graphic/shape/Arc';
// import CompoundPath from 'zrender/src/graphic/CompoundPath';
import LinearGradient from 'zrender/src/graphic/LinearGradient';
import RadialGradient from 'zrender/src/graphic/RadialGradient';
import BoundingRect from 'zrender/src/core/BoundingRect';

var round = Math.round;
var mathMax = Math.max;
var mathMin = Math.min;

/**
 * Extend shape with parameters
 */
export function extendShape(opts) {
    return Path.extend(opts);
};

/**
 * Extend path
 */
export function extendPath(pathData, opts) {
    return pathTool.extendFromString(pathData, opts);
};

export function PathString(pathData, opts) {
    return pathTool.createFromString(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'
 */
export function makePath(pathData, opts, rect, layout) {
    var path = pathTool.createFromString(pathData, opts);
    var boundingRect = path.getBoundingRect();
    if (rect) {
        var aspect = boundingRect.width / boundingRect.height;

        if (layout === 'center') {
            // Set rect to center, keep width / height ratio.
            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;

            rect.x = cx - width / 2;
            rect.y = cy - height / 2;
            rect.width = width;
            rect.height = height;
        }

        resizePath(path, rect);
    }
    return path;
};

export var mergePath = pathTool.mergePath;

/**
 * Resize a path to fit the rect
 * @param {module:zrender/graphic/Path} path
 * @param {Object} rect
 */
export 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
 */
export function subPixelOptimizeLine(param) {
    var shape = param.shape;
    var position = param.position || [0, 0];
    var lineWidth = param.style.lineWidth;

    if (round(shape.x1 * 2) === round(shape.x2 * 2)) {
        shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true, position[0]);
    }
    if (round(shape.y1 * 2) === round(shape.y2 * 2)) {
        shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true, position[1]);
    }
    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
 */
export function subPixelOptimizeRect(param) {
    if (!param.style.stroke) return param;
    var shape = param.shape;
    var lineWidth = param.style.lineWidth;
    var originX = shape.x;
    var originY = shape.y;
    var widthIsNegative = shape.width < 0;
    var heightIsNegative = shape.height < 0;
    var originWidth = Math.abs(shape.width);
    var originHeight = Math.abs(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
    );
    widthIsNegative && (shape.width = -shape.width);
    shape.height = Math.max(
        subPixelOptimize(originY + originHeight, lineWidth, false) - shape.y,
        originHeight === 0 ? 0 : 1
    );
    heightIsNegative && (shape.height = -shape.height);
    return param;
};

/**
 * Sub pixel optimize for canvas
 *
 * @param {number} xOrY Coordinate, such as x, y
 * @param {number} lineWidth Should be nonnegative integer.
 * @param {boolean=} positiveOrNegative Default false (negative).
 * @return {number} Optimized position.
 */
export function subPixelOptimize(xOrY, lineWidth, positiveOrNegative, pos) {
    // Assure that (xOrY + lineWidth / 2) is near integer edge,
    // otherwise line will be fuzzy in canvas.
    var doubledPosition = round(xOrY * 2);
    return (doubledPosition + round(lineWidth) + (pos ? round(pos * 2) : 0)) % 2 === 0
        ? doubledPosition / 2
        : (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2;
};

function hasFillOrStroke(fillOrStroke) {
    return fillOrStroke != null && fillOrStroke != 'none';
}

function liftColor(color) {
    return typeof color === 'string' ? colorTool.lift(color, -0.1) : color;
}

/**
 * @private
 */
function cacheElementStl(el) {
    if (el.__hoverStlDirty) {
        var stroke = el.style.stroke;
        var fill = el.style.fill;

        // Create hoverStyle on mouseover
        var hoverStyle = el.__hoverStl;
        hoverStyle.fill = hoverStyle.fill
            || (hasFillOrStroke(fill) ? liftColor(fill) : null);
        hoverStyle.stroke = hoverStyle.stroke
            || (hasFillOrStroke(stroke) ? liftColor(stroke) : null);

        var normalStyle = {};
        for (var name in hoverStyle) {
            if (hoverStyle.hasOwnProperty(name)) {
                normalStyle[name] = el.style[name];
            }
        }

        el.__normalStl = normalStyle;

        el.__hoverStlDirty = false;
    }
}

/**
 * @private
 */
function doSingleEnterHover(el) {
    if (el.__isHover) {
        return;
    }

    cacheElementStl(el);

    if (el.useHoverLayer) {
        el.__zr && el.__zr.addHover(el, el.__hoverStl);
    }
    else {
        el.setStyle(el.__hoverStl);
        el.z2 += 1;
    }

    el.__isHover = true;
}

/**
 * @inner
 */
function doSingleLeaveHover(el) {
    if (!el.__isHover) {
        return;
    }

    var normalStl = el.__normalStl;
    if (el.useHoverLayer) {
        el.__zr && el.__zr.removeHover(el);
    }
    else {
        normalStl && el.setStyle(normalStl);
        el.z2 -= 1;
    }

    el.__isHover = false;
}

/**
 * @inner
 */
function doEnterHover(el) {
    el.type === 'group'
        ? el.traverse(function (child) {
            if (child.type !== 'group') {
                doSingleEnterHover(child);
            }
        })
        : doSingleEnterHover(el);
}

function doLeaveHover(el) {
    el.type === 'group'
        ? el.traverse(function (child) {
            if (child.type !== 'group') {
                doSingleLeaveHover(child);
            }
        })
        : doSingleLeaveHover(el);
}

/**
 * @inner
 */
function setElementHoverStl(el, hoverStl) {
    // 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
    el.__hoverStl = el.hoverStyle || hoverStl || {};
    el.__hoverStlDirty = true;

    if (el.__isHover) {
        cacheElementStl(el);
    }
}

/**
 * @inner
 */
function onElementMouseOver(e) {
    if (this.__hoverSilentOnTouch && e.zrByTouch) {
        return;
    }

    // Only if element is not in emphasis status
    !this.__isEmphasis && doEnterHover(this);
}

/**
 * @inner
 */
function onElementMouseOut(e) {
    if (this.__hoverSilentOnTouch && e.zrByTouch) {
        return;
    }

    // Only if element is not in emphasis status
    !this.__isEmphasis && doLeaveHover(this);
}

/**
 * @inner
 */
function enterEmphasis() {
    this.__isEmphasis = true;
    doEnterHover(this);
}

/**
 * @inner
 */
function leaveEmphasis() {
    this.__isEmphasis = false;
    doLeaveHover(this);
}

/**
 * Set hover style of element.
 * This method can be called repeatly without side-effects.
 * @param {module:zrender/Element} el
 * @param {Object} [hoverStyle]
 * @param {Object} [opt]
 * @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
 *        conviniently 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.
 */
export function setHoverStyle(el, hoverStyle, opt = { disableMouse: false }) {
    el.__hoverSilentOnTouch = opt && opt.hoverSilentOnTouch;

    el.type === 'group'
        ? el.traverse(function (child) {
            if (child.type !== 'group') {
                setElementHoverStl(child, hoverStyle);
            }
        })
        : setElementHoverStl(el, hoverStyle);

    if (!opt || !opt.disableMouse) {
        // Duplicated function will be auto-ignored, see Eventful.js.
        el.on('mouseover', onElementMouseOver)
            .on('mouseout', onElementMouseOut);
    }

    // Emphasis, normal can be triggered manually
    el.on('emphasis', enterEmphasis)
        .on('normal', leaveEmphasis);
};

const FORMAT_TEXT_STYLE_LIST = [
    'fill', 'stroke', 'shadowBlur', 'shadowColor', 'shadowOffsetX', 'shadowOffsetY',
    'position', 'offset', 'lineWidth'
];
const FORMATTED_TEXT_STYLE_LIST = [
    'textFill', 'textStroke', 'textShadowBlur', 'textShadowColor', 'textShadowOffsetX', 'textShadowOffsetY',
    'textPosition', 'textOffset', 'textLineWidth'
];

export function formatTextStyle(text) {
    if (!text) return;
    
    let style = text.style ? text.style : text;

    if (style.rich) {
        zrUtil.each(style.rich, function (item) {
            formatTextStyle(item)
        })
    }

    zrUtil.each(FORMAT_TEXT_STYLE_LIST, function (item, index) {
        if (typeof style[item] !== 'undefined') {
            style[FORMATTED_TEXT_STYLE_LIST[index]] = style[item];
            delete style[item];
        }
    });

    return text;
}

var isObject = zrUtil.isObject;
var clone = zrUtil.clone;
var map = zrUtil.map;

function reverseStops(style, pos) {
    if (isObject(style)) {
        let stops = style.colorStops;
        // 反向stops
        let concatStops = pos && pos !== 1 ? map(clone(stops).reverse(), ({ offset, color }) => ({
            color,
            offset: 1 - offset * (1 - pos)
        })) : [];

        // 正向stops拼接反向stops
        style.colorStops = map(stops, ({ offset, color }) => ({
            color,
            offset: pos ? pos * offset : (1 - offset)
        })).concat(concatStops);
    }
}

export function reverseStyleStops(style, pos) {
    if (!style) return;

    if ((style.hasOwnProperty('fill') && isObject(style.fill)) || (style.hasOwnProperty('stroke') && isObject(style.stroke))) {
        let cloneStyle = clone(style);

        reverseStops(cloneStyle.fill, pos);
        reverseStops(cloneStyle.stroke, pos);

        return cloneStyle;

    } else {
        return style;
    }
}

export function enableDrag(shape, global, { onDragStart, onDragging, onDragEnd }) {
    let isDragging = false;
    let target;

    function dragStart(e) {
        if (!isDragging) {
            isDragging = true;
            target = e.target;
            onDragStart && onDragStart(e);
        }
    }
    function dragging(e) {
        if (isDragging) {
            e.target = target;
            onDragging && onDragging(e)
        }
    }
    function dragEnd(e) {
        if (isDragging) {
            e.target = target;
            onDragEnd && onDragEnd(e);
            isDragging = false;
            target = undefined;
        }
    }

    shape.on('mousedown', dragStart);

    global.on('mousemove', dragging);

    global.on('mouseup', dragEnd);

    shape.disableDrag = function () {
        shape.off('mousedown', dragStart);
        global.off('mousedown', dragging);
        global.off('mousedown', dragEnd);
    }
}

export {
    Path,
    Group,
    ZImage as Image,
    Text,
    Circle,
    Sector,
    Ring,
    Polygon,
    Polyline,
    Rect,
    Line,
    BezierCurve,
    Arc,
    // CompoundPath,
    LinearGradient,
    RadialGradient,
    BoundingRect,
    colorTool
};