/**
 * Path 代理，可以在`buildPath`中用于替代`ctx`, 会保存每个path操作的命令到pathCommands属性中
 * 可以用于 isInsidePath 判断以及获取boundingRect
 */

// TODO getTotalLength, getPointAtLength, arcTo

/* global Float32Array */

import * as vec2 from './vector';
import BoundingRect from './BoundingRect';
import {devicePixelRatio as dpr} from '../config';
import { fromLine, fromCubic, fromQuadratic, fromArc } from './bbox';
import { cubicLength, cubicSubdivide, quadraticLength, quadraticSubdivide } from './curve';

const CMD = {
    M: 1,
    L: 2,
    C: 3,
    Q: 4,
    A: 5,
    Z: 6,
    // Rect
    R: 7
};

// const CMD_MEM_SIZE = {
//     M: 3,
//     L: 3,
//     C: 7,
//     Q: 5,
//     A: 9,
//     R: 5,
//     Z: 1
// };

interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D {
    dpr?: number
}

const tmpOutX: number[] = [];
const tmpOutY: number[] = [];

const min: number[] = [];
const max: number[] = [];
const min2: number[] = [];
const max2: number[] = [];
const mathMin = Math.min;
const mathMax = Math.max;
const mathCos = Math.cos;
const mathSin = Math.sin;
const mathAbs = Math.abs;

const PI = Math.PI;
const PI2 = PI * 2;

const hasTypedArray = typeof Float32Array !== 'undefined';

const tmpAngles: number[] = [];

function modPI2(radian: number) {
    // It's much more stable to mod N instedof PI
    const n = Math.round(radian / PI * 1e8) / 1e8;
    return (n % 2) * PI;
}
/**
 * Normalize start and end angles.
 * startAngle will be normalized to 0 ~ PI*2
 * sweepAngle(endAngle - startAngle) will be normalized to 0 ~ PI*2 if clockwise.
 * -PI*2 ~ 0 if anticlockwise.
 */
export function normalizeArcAngles(angles: number[], anticlockwise: boolean): void {
    let newStartAngle = modPI2(angles[0]);
    if (newStartAngle < 0) {
        // Normlize to 0 - PI2
        newStartAngle += PI2;
    }

    let delta = newStartAngle - angles[0];
    let newEndAngle = angles[1];
    newEndAngle += delta;

    // https://github.com/chromium/chromium/blob/c20d681c9c067c4e15bb1408f17114b9e8cba294/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L184
    // Is circle
    if (!anticlockwise && newEndAngle - newStartAngle >= PI2) {
        newEndAngle = newStartAngle + PI2;
    }
    else if (anticlockwise && newStartAngle - newEndAngle >= PI2) {
        newEndAngle = newStartAngle - PI2;
    }
    // Make startAngle < endAngle when clockwise, otherwise endAngle < startAngle.
    // The sweep angle can never been larger than P2.
    else if (!anticlockwise && newStartAngle > newEndAngle) {
        newEndAngle = newStartAngle + (PI2 - modPI2(newStartAngle - newEndAngle));
    }
    else if (anticlockwise && newStartAngle < newEndAngle) {
        newEndAngle = newStartAngle - (PI2 - modPI2(newEndAngle - newStartAngle));
    }

    angles[0] = newStartAngle;
    angles[1] = newEndAngle;
}


export default class PathProxy {

    dpr = 1

    data: number[] | Float32Array

    /**
     * Version is for tracking if the path has been changed.
     */
    private _version: number

    /**
     * If save path data.
     */
    private _saveData: boolean

    /**
     * If the line segment is too small to draw. It will be added to the pending pt.
     * It will be added if the subpath needs to be finished before stroke, fill, or starting a new subpath.
     */
    private _pendingPtX: number;
    private _pendingPtY: number;
    // Distance of pending pt to previous point.
    // 0 if there is no pending point.
    // Only update the pending pt when distance is larger.
    private _pendingPtDist: number;

    private _ctx: ExtendedCanvasRenderingContext2D

    private _xi = 0
    private _yi = 0

    private _x0 = 0
    private _y0 = 0

    private _len = 0

    // Calculating path len and seg len.
    private _pathSegLen: number[]
    private _pathLen: number
    // Unit x, Unit y. Provide for avoiding drawing that too short line segment
    private _ux: number
    private _uy: number

    static CMD = CMD

    constructor(notSaveData?: boolean) {
        if (notSaveData) {
            this._saveData = false;
        }

        if (this._saveData) {
            this.data = [];
        }
    }

    increaseVersion() {
        this._version++;
    }

    /**
     * Version can be used outside for compare if the path is changed.
     * For example to determine if need to update svg d str in svg renderer.
     */
    getVersion() {
        return this._version;
    }

    /**
     * @readOnly
     */
    setScale(sx: number, sy: number, segmentIgnoreThreshold?: number) {
        // Compat. Previously there is no segmentIgnoreThreshold.
        segmentIgnoreThreshold = segmentIgnoreThreshold || 0;
        if (segmentIgnoreThreshold > 0) {
            this._ux = mathAbs(segmentIgnoreThreshold / dpr / sx) || 0;
            this._uy = mathAbs(segmentIgnoreThreshold / dpr / sy) || 0;
        }
    }

    setDPR(dpr: number) {
        this.dpr = dpr;
    }

    setContext(ctx: ExtendedCanvasRenderingContext2D) {
        this._ctx = ctx;
    }

    getContext(): ExtendedCanvasRenderingContext2D {
        return this._ctx;
    }

    beginPath() {
        this._ctx && this._ctx.beginPath();
        this.reset();
        return this;
    }

    /**
     * Reset path data.
     */
    reset() {
        // Reset
        if (this._saveData) {
            this._len = 0;
        }

        if (this._pathSegLen) {
            this._pathSegLen = null;
            this._pathLen = 0;
        }

        // Update version
        this._version++;
    }

    moveTo(x: number, y: number) {
        // Add pending point for previous path.
        this._drawPendingPt();

        this.addData(CMD.M, x, y);
        this._ctx && this._ctx.moveTo(x, y);

        // x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用
        // xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。
        // 有可能在 beginPath 之后直接调用 lineTo，这时候 x0, y0 需要
        // 在 lineTo 方法中记录，这里先不考虑这种情况，dashed line 也只在 IE10- 中不支持
        this._x0 = x;
        this._y0 = y;

        this._xi = x;
        this._yi = y;

        return this;
    }

    lineTo(x: number, y: number) {
        const dx = mathAbs(x - this._xi);
        const dy = mathAbs(y - this._yi);
        const exceedUnit = dx > this._ux || dy > this._uy;

        this.addData(CMD.L, x, y);

        if (this._ctx && exceedUnit) {
            this._ctx.lineTo(x, y);
        }
        if (exceedUnit) {
            this._xi = x;
            this._yi = y;
            this._pendingPtDist = 0;
        }
        else {
            const d2 = dx * dx + dy * dy;
            // Only use the farthest pending point.
            if (d2 > this._pendingPtDist) {
                this._pendingPtX = x;
                this._pendingPtY = y;
                this._pendingPtDist = d2;
            }
        }

        return this;
    }

    bezierCurveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) {
        this._drawPendingPt();

        this.addData(CMD.C, x1, y1, x2, y2, x3, y3);
        if (this._ctx) {
            this._ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
        }
        this._xi = x3;
        this._yi = y3;
        return this;
    }

    quadraticCurveTo(x1: number, y1: number, x2: number, y2: number) {
        this._drawPendingPt();

        this.addData(CMD.Q, x1, y1, x2, y2);
        if (this._ctx) {
            this._ctx.quadraticCurveTo(x1, y1, x2, y2);
        }
        this._xi = x2;
        this._yi = y2;
        return this;
    }

    arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise?: boolean) {
        this._drawPendingPt();

        tmpAngles[0] = startAngle;
        tmpAngles[1] = endAngle;
        normalizeArcAngles(tmpAngles, anticlockwise);

        startAngle = tmpAngles[0];
        endAngle = tmpAngles[1];

        let delta = endAngle - startAngle;

        this.addData(
            CMD.A, cx, cy, r, r, startAngle, delta, 0, anticlockwise ? 0 : 1
        );

        this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);

        this._xi = mathCos(endAngle) * r + cx;
        this._yi = mathSin(endAngle) * r + cy;
        return this;
    }

    // TODO
    arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) {
        this._drawPendingPt();

        if (this._ctx) {
            this._ctx.arcTo(x1, y1, x2, y2, radius);
        }
        return this;
    }

    // TODO
    rect(x: number, y: number, w: number, h: number) {
        this._drawPendingPt();

        this._ctx && this._ctx.rect(x, y, w, h);
        this.addData(CMD.R, x, y, w, h);
        return this;
    }

    closePath() {
        // Add pending point for previous path.
        this._drawPendingPt();

        this.addData(CMD.Z);

        const ctx = this._ctx;
        const x0 = this._x0;
        const y0 = this._y0;
        if (ctx) {
            ctx.closePath();
        }

        this._xi = x0;
        this._yi = y0;
        return this;
    }

    fill(ctx: CanvasRenderingContext2D) {
        ctx && ctx.fill();
        this.toStatic();
    }

    stroke(ctx: CanvasRenderingContext2D) {
        ctx && ctx.stroke();
        this.toStatic();
    }

    len() {
        return this._len;
    }

    setData(data: Float32Array | number[]) {
        if (!this._saveData) {
            return;
        }

        const len = data.length;

        if (!(this.data && this.data.length === len) && hasTypedArray) {
            this.data = new Float32Array(len);
        }

        for (let i = 0; i < len; i++) {
            this.data[i] = data[i];
        }

        this._len = len;
    }

    appendPath(path: PathProxy | PathProxy[]) {
        if (!this._saveData) {
            return;
        }
        if (!(path instanceof Array)) {
            path = [path];
        }
        const len = path.length;
        let appendSize = 0;
        let offset = this._len;
        for (let i = 0; i < len; i++) {
            appendSize += path[i].len();
        }
        const oldData = this.data;
        if (hasTypedArray && (oldData instanceof Float32Array || !oldData)) {
            this.data = new Float32Array(offset + appendSize);
            if (offset > 0 && oldData) {
                for (let k = 0; k < offset; k++) {
                    this.data[k] = oldData[k];
                }
            }
        }
        for (let i = 0; i < len; i++) {
            const appendPathData = path[i].data;
            for (let k = 0; k < appendPathData.length; k++) {
                this.data[offset++] = appendPathData[k];
            }
        }
        this._len = offset;
    }

    /**
     * 填充 Path 数据。
     * 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。
     */
    addData(
        cmd: number,
        a?: number,
        b?: number,
        c?: number,
        d?: number,
        e?: number,
        f?: number,
        g?: number,
        h?: number
    ) {
        if (!this._saveData) {
            return;
        }

        let data = this.data;
        if (this._len + arguments.length > data.length) {
            // 因为之前的数组已经转换成静态的 Float32Array
            // 所以不够用时需要扩展一个新的动态数组
            this._expandData();
            data = this.data;
        }
        for (let i = 0; i < arguments.length; i++) {
            data[this._len++] = arguments[i];
        }
    }

    private _drawPendingPt() {
        if (this._pendingPtDist > 0) {
            this._ctx && this._ctx.lineTo(this._pendingPtX, this._pendingPtY);
            this._pendingPtDist = 0;
        }
    }

    private _expandData() {
        // Only if data is Float32Array
        if (!(this.data instanceof Array)) {
            const newData = [];
            for (let i = 0; i < this._len; i++) {
                newData[i] = this.data[i];
            }
            this.data = newData;
        }
    }

    /**
     * Convert dynamic array to static Float32Array
     *
     * It will still use a normal array if command buffer length is less than 10
     * Because Float32Array itself may take more memory than a normal array.
     *
     * 10 length will make sure at least one M command and one A(arc) command.
     */
    toStatic() {
        if (!this._saveData) {
            return;
        }

        this._drawPendingPt();

        const data = this.data;
        if (data instanceof Array) {
            data.length = this._len;
            if (hasTypedArray && this._len > 11) {
                this.data = new Float32Array(data);
            }
        }
    }


    getBoundingRect() {
        min[0] = min[1] = min2[0] = min2[1] = Number.MAX_VALUE;
        max[0] = max[1] = max2[0] = max2[1] = -Number.MAX_VALUE;

        const data = this.data;
        let xi = 0;
        let yi = 0;
        let x0 = 0;
        let y0 = 0;

        let i;
        for (i = 0; i < this._len;) {
            const cmd = data[i++] as number;

            const isFirst = i === 1;
            if (isFirst) {
                // 如果第一个命令是 L, C, Q
                // 则 previous point 同绘制命令的第一个 point
                // 第一个命令为 Arc 的情况下会在后面特殊处理
                xi = data[i];
                yi = data[i + 1];

                x0 = xi;
                y0 = yi;
            }

            switch (cmd) {
                case CMD.M:
                    // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
                    // 在 closePath 的时候使用
                    xi = x0 = data[i++];
                    yi = y0 = data[i++];
                    min2[0] = x0;
                    min2[1] = y0;
                    max2[0] = x0;
                    max2[1] = y0;
                    break;
                case CMD.L:
                    fromLine(xi, yi, data[i], data[i + 1], min2, max2);
                    xi = data[i++];
                    yi = data[i++];
                    break;
                case CMD.C:
                    fromCubic(
                        xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
                        min2, max2
                    );
                    xi = data[i++];
                    yi = data[i++];
                    break;
                case CMD.Q:
                    fromQuadratic(
                        xi, yi, data[i++], data[i++], data[i], data[i + 1],
                        min2, max2
                    );
                    xi = data[i++];
                    yi = data[i++];
                    break;
                case CMD.A:
                    const cx = data[i++];
                    const cy = data[i++];
                    const rx = data[i++];
                    const ry = data[i++];
                    const startAngle = data[i++];
                    const endAngle = data[i++] + startAngle;
                    // TODO Arc 旋转
                    i += 1;
                    const anticlockwise = !data[i++];

                    if (isFirst) {
                        // 直接使用 arc 命令
                        // 第一个命令起点还未定义
                        x0 = mathCos(startAngle) * rx + cx;
                        y0 = mathSin(startAngle) * ry + cy;
                    }

                    fromArc(
                        cx, cy, rx, ry, startAngle, endAngle,
                        anticlockwise, min2, max2
                    );

                    xi = mathCos(endAngle) * rx + cx;
                    yi = mathSin(endAngle) * ry + cy;
                    break;
                case CMD.R:
                    x0 = xi = data[i++];
                    y0 = yi = data[i++];
                    const width = data[i++];
                    const height = data[i++];
                    // Use fromLine
                    fromLine(x0, y0, x0 + width, y0 + height, min2, max2);
                    break;
                case CMD.Z:
                    xi = x0;
                    yi = y0;
                    break;
            }

            // Union
            vec2.min(min, min, min2);
            vec2.max(max, max, max2);
        }

        // No data
        if (i === 0) {
            min[0] = min[1] = max[0] = max[1] = 0;
        }

        return new BoundingRect(
            min[0], min[1], max[0] - min[0], max[1] - min[1]
        );
    }

    private _calculateLength(): number {
        const data = this.data;
        const len = this._len;
        const ux = this._ux;
        const uy = this._uy;
        let xi = 0;
        let yi = 0;
        let x0 = 0;
        let y0 = 0;

        if (!this._pathSegLen) {
            this._pathSegLen = [];
        }
        const pathSegLen = this._pathSegLen;
        let pathTotalLen = 0;
        let segCount = 0;

        for (let i = 0; i < len;) {
            const cmd = data[i++] as number;
            const isFirst = i === 1;

            if (isFirst) {
                // 如果第一个命令是 L, C, Q
                // 则 previous point 同绘制命令的第一个 point
                // 第一个命令为 Arc 的情况下会在后面特殊处理
                xi = data[i];
                yi = data[i + 1];

                x0 = xi;
                y0 = yi;
            }

            let l = -1;

            switch (cmd) {
                case CMD.M:
                    // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
                    // 在 closePath 的时候使用
                    xi = x0 = data[i++];
                    yi = y0 = data[i++];
                    break;
                case CMD.L: {
                    const x2 = data[i++];
                    const y2 = data[i++];
                    const dx = x2 - xi;
                    const dy = y2 - yi;
                    if (mathAbs(dx) > ux || mathAbs(dy) > uy || i === len - 1) {
                        l = Math.sqrt(dx * dx + dy * dy);
                        xi = x2;
                        yi = y2;
                    }
                    break;
                }
                case CMD.C: {
                    const x1 = data[i++];
                    const y1 = data[i++];
                    const x2 = data[i++];
                    const y2 = data[i++];
                    const x3 = data[i++];
                    const y3 = data[i++];
                    // TODO adaptive iteration
                    l = cubicLength(xi, yi, x1, y1, x2, y2, x3, y3, 10);
                    xi = x3;
                    yi = y3;
                    break;
                }
                case CMD.Q: {
                    const x1 = data[i++];
                    const y1 = data[i++];
                    const x2 = data[i++];
                    const y2 = data[i++];
                    l = quadraticLength(xi, yi, x1, y1, x2, y2, 10);
                    xi = x2;
                    yi = y2;
                    break;
                }
                case CMD.A:
                    // TODO Arc 判断的开销比较大
                    const cx = data[i++];
                    const cy = data[i++];
                    const rx = data[i++];
                    const ry = data[i++];
                    const startAngle = data[i++];
                    let delta = data[i++];
                    const endAngle = delta + startAngle;
                    // TODO Arc 旋转
                    i += 1;
                    if (isFirst) {
                        // 直接使用 arc 命令
                        // 第一个命令起点还未定义
                        x0 = mathCos(startAngle) * rx + cx;
                        y0 = mathSin(startAngle) * ry + cy;
                    }

                    // TODO Ellipse
                    l = mathMax(rx, ry) * mathMin(PI2, Math.abs(delta));

                    xi = mathCos(endAngle) * rx + cx;
                    yi = mathSin(endAngle) * ry + cy;
                    break;
                case CMD.R: {
                    x0 = xi = data[i++];
                    y0 = yi = data[i++];
                    const width = data[i++];
                    const height = data[i++];
                    l = width * 2 + height * 2;
                    break;
                }
                case CMD.Z: {
                    const dx = x0 - xi;
                    const dy = y0 - yi;
                    l = Math.sqrt(dx * dx + dy * dy);

                    xi = x0;
                    yi = y0;
                    break;
                }
            }

            if (l >= 0) {
                pathSegLen[segCount++] = l;
                pathTotalLen += l;
            }
        }

        // TODO Optimize memory cost.
        this._pathLen = pathTotalLen;

        return pathTotalLen;
    }
    /**
     * Rebuild path from current data
     * Rebuild path will not consider javascript implemented line dash.
     * @param {CanvasRenderingContext2D} ctx
     */
    rebuildPath(ctx: PathRebuilder, percent: number) {
        const d = this.data;
        const ux = this._ux;
        const uy = this._uy;
        const len = this._len;
        let x0;
        let y0;
        let xi;
        let yi;
        let x;
        let y;

        const drawPart = percent < 1;
        let pathSegLen;
        let pathTotalLen;
        let accumLength = 0;
        let segCount = 0;
        let displayedLength;

        let pendingPtDist = 0;
        let pendingPtX: number;
        let pendingPtY: number;


        if (drawPart) {
            if (!this._pathSegLen) {
                this._calculateLength();
            }
            pathSegLen = this._pathSegLen;
            pathTotalLen = this._pathLen;
            displayedLength = percent * pathTotalLen;

            if (!displayedLength) {
                return;
            }
        }

        lo: for (let i = 0; i < len;) {
            const cmd = d[i++];
            const isFirst = i === 1;

            if (isFirst) {
                // 如果第一个命令是 L, C, Q
                // 则 previous point 同绘制命令的第一个 point
                // 第一个命令为 Arc 的情况下会在后面特殊处理
                xi = d[i];
                yi = d[i + 1];

                x0 = xi;
                y0 = yi;
            }
            // Only lineTo support ignoring small segments.
            // Otherwise if the pending point should always been flushed.
            if (cmd !== CMD.L && pendingPtDist > 0) {
                ctx.lineTo(pendingPtX, pendingPtY);
                pendingPtDist = 0;
            }
            switch (cmd) {
                case CMD.M:
                    x0 = xi = d[i++];
                    y0 = yi = d[i++];
                    ctx.moveTo(xi, yi);
                    break;
                case CMD.L: {
                    x = d[i++];
                    y = d[i++];
                    const dx = mathAbs(x - xi);
                    const dy = mathAbs(y - yi);
                    // Not draw too small seg between
                    if (dx > ux || dy > uy) {
                        if (drawPart) {
                            const l = pathSegLen[segCount++];
                            if (accumLength + l > displayedLength) {
                                const t = (displayedLength - accumLength) / l;
                                ctx.lineTo(xi * (1 - t) + x * t, yi * (1 - t) + y * t);
                                break lo;
                            }
                            accumLength += l;
                        }

                        ctx.lineTo(x, y);
                        xi = x;
                        yi = y;
                        pendingPtDist = 0;
                    }
                    else {
                        const d2 = dx * dx + dy * dy;
                        // Only use the farthest pending point.
                        if (d2 > pendingPtDist) {
                            pendingPtX = x;
                            pendingPtY = y;
                            pendingPtDist = d2;
                        }
                    }
                    break;
                }
                case CMD.C: {
                    const x1 = d[i++];
                    const y1 = d[i++];
                    const x2 = d[i++];
                    const y2 = d[i++];
                    const x3 = d[i++];
                    const y3 = d[i++];
                    if (drawPart) {
                        const l = pathSegLen[segCount++];
                        if (accumLength + l > displayedLength) {
                            const t = (displayedLength - accumLength) / l;
                            cubicSubdivide(xi, x1, x2, x3, t, tmpOutX);
                            cubicSubdivide(yi, y1, y2, y3, t, tmpOutY);
                            ctx.bezierCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2], tmpOutX[3], tmpOutY[3]);
                            break lo;
                        }
                        accumLength += l;
                    }

                    ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
                    xi = x3;
                    yi = y3;
                    break;
                }
                case CMD.Q: {
                    const x1 = d[i++];
                    const y1 = d[i++];
                    const x2 = d[i++];
                    const y2 = d[i++];

                    if (drawPart) {
                        const l = pathSegLen[segCount++];
                        if (accumLength + l > displayedLength) {
                            const t = (displayedLength - accumLength) / l;
                            quadraticSubdivide(xi, x1, x2, t, tmpOutX);
                            quadraticSubdivide(yi, y1, y2, t, tmpOutY);
                            ctx.quadraticCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2]);
                            break lo;
                        }
                        accumLength += l;
                    }

                    ctx.quadraticCurveTo(x1, y1, x2, y2);
                    xi = x2;
                    yi = y2;
                    break;
                }
                case CMD.A:
                    const cx = d[i++];
                    const cy = d[i++];
                    const rx = d[i++];
                    const ry = d[i++];
                    let startAngle = d[i++];
                    let delta = d[i++];
                    const psi = d[i++];
                    const anticlockwise = !d[i++];
                    const r = (rx > ry) ? rx : ry;
                    // const scaleX = (rx > ry) ? 1 : rx / ry;
                    // const scaleY = (rx > ry) ? ry / rx : 1;
                    const isEllipse = mathAbs(rx - ry) > 1e-3;
                    let endAngle = startAngle + delta;
                    let breakBuild = false;

                    if (drawPart) {
                        const l = pathSegLen[segCount++];
                        if (accumLength + l > displayedLength) {
                            endAngle = startAngle + delta * (displayedLength - accumLength) / l;
                            breakBuild = true;
                        }
                        accumLength += l;
                    }
                    if (isEllipse && ctx.ellipse) {
                        ctx.ellipse(cx, cy, rx, ry, psi, startAngle, endAngle, anticlockwise);
                    }
                    else {
                        ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
                    }

                    if (breakBuild) {
                        break lo;
                    }

                    if (isFirst) {
                        // 直接使用 arc 命令
                        // 第一个命令起点还未定义
                        x0 = mathCos(startAngle) * rx + cx;
                        y0 = mathSin(startAngle) * ry + cy;
                    }
                    xi = mathCos(endAngle) * rx + cx;
                    yi = mathSin(endAngle) * ry + cy;
                    break;
                case CMD.R:
                    x0 = xi = d[i];
                    y0 = yi = d[i + 1];

                    x = d[i++];
                    y = d[i++];
                    const width = d[i++];
                    const height = d[i++];

                    if (drawPart) {
                        const l = pathSegLen[segCount++];
                        if (accumLength + l > displayedLength) {
                            let d = displayedLength - accumLength;
                            ctx.moveTo(x, y);
                            ctx.lineTo(x + mathMin(d, width), y);
                            d -= width;
                            if (d > 0) {
                                ctx.lineTo(x + width, y + mathMin(d, height));
                            }
                            d -= height;
                            if (d > 0) {
                                ctx.lineTo(x + mathMax(width - d, 0), y + height);
                            }
                            d -= width;
                            if (d > 0) {
                                ctx.lineTo(x, y + mathMax(height - d, 0));
                            }
                            break lo;
                        }
                        accumLength += l;
                    }
                    ctx.rect(x, y, width, height);
                    break;
                case CMD.Z:
                    if (drawPart) {
                        const l = pathSegLen[segCount++];
                        if (accumLength + l > displayedLength) {
                            const t = (displayedLength - accumLength) / l;
                            ctx.lineTo(xi * (1 - t) + x0 * t, yi * (1 - t) + y0 * t);
                            break lo;
                        }
                        accumLength += l;
                    }

                    ctx.closePath();
                    xi = x0;
                    yi = y0;
            }
        }
    }

    clone() {
        const newProxy = new PathProxy();
        const data = this.data;
        newProxy.data = data.slice ? data.slice()
            : Array.prototype.slice.call(data);
        newProxy._len = this._len;
        return newProxy;
    }

    canSave(): boolean {
        return !!this._saveData;
    }

    private static initDefaultProps = (function () {
        const proto = PathProxy.prototype;
        proto._saveData = true;
        proto._ux = 0;
        proto._uy = 0;
        proto._pendingPtDist = 0;
        proto._version = 0;
    })()
}


export interface PathRebuilder {
    moveTo(x: number, y: number): void
    lineTo(x: number, y: number): void
    bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number): void
    quadraticCurveTo(x: number, y: number, x2: number, y2: number): void
    arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
    // eslint-disable-next-line max-len
    ellipse(cx: number, cy: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
    rect(x: number, y: number, width: number, height: number): void
    closePath(): void
}