import Path, { PathProps } from '../graphic/Path';
import PathProxy from '../core/PathProxy';
import transformPath from './transformPath';
import { VectorArray } from '../core/vector';
import { MatrixArray } from '../core/matrix';
import { extend } from '../core/util';

// command chars
// const cc = [
//     'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
//     'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
// ];

const mathSqrt = Math.sqrt;
const mathSin = Math.sin;
const mathCos = Math.cos;
const PI = Math.PI;

function vMag(v: VectorArray): number {
    return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
};
function vRatio(u: VectorArray, v: VectorArray): number {
    return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
};
function vAngle(u: VectorArray, v: VectorArray): number {
    return (u[0] * v[1] < u[1] * v[0] ? -1 : 1)
            * Math.acos(vRatio(u, v));
};

function processArc(
    x1: number, y1: number, x2: number, y2: number, fa: number, fs: number,
    rx: number, ry: number, psiDeg: number, cmd: number, path: PathProxy
) {
    // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
    const psi = psiDeg * (PI / 180.0);
    const xp = mathCos(psi) * (x1 - x2) / 2.0
                + mathSin(psi) * (y1 - y2) / 2.0;
    const yp = -1 * mathSin(psi) * (x1 - x2) / 2.0
                + mathCos(psi) * (y1 - y2) / 2.0;

    const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);

    if (lambda > 1) {
        rx *= mathSqrt(lambda);
        ry *= mathSqrt(lambda);
    }

    const f = (fa === fs ? -1 : 1)
        * mathSqrt((((rx * rx) * (ry * ry))
                - ((rx * rx) * (yp * yp))
                - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp)
                + (ry * ry) * (xp * xp))
            ) || 0;

    const cxp = f * rx * yp / ry;
    const cyp = f * -ry * xp / rx;

    const cx = (x1 + x2) / 2.0
                + mathCos(psi) * cxp
                - mathSin(psi) * cyp;
    const cy = (y1 + y2) / 2.0
            + mathSin(psi) * cxp
            + mathCos(psi) * cyp;

    const theta = vAngle([ 1, 0 ], [ (xp - cxp) / rx, (yp - cyp) / ry ]);
    const u = [ (xp - cxp) / rx, (yp - cyp) / ry ];
    const v = [ (-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry ];
    let dTheta = vAngle(u, v);

    if (vRatio(u, v) <= -1) {
        dTheta = PI;
    }
    if (vRatio(u, v) >= 1) {
        dTheta = 0;
    }

    if (dTheta < 0) {
        const n = Math.round(dTheta / PI * 1e6) / 1e6;
        // Convert to positive
        dTheta = PI * 2 + (n % 2) * PI;
    }

    path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs);
}


const commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig;
// Consider case:
// (1) delimiter can be comma or space, where continuous commas
// or spaces should be seen as one comma.
// (2) value can be like:
// '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983',
// 'l-.5E1,54', '121-23-44-11' (no delimiter)
const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;
// const valueSplitReg = /[\s,]+/;

function createPathProxyFromString(data: string) {
    const path = new PathProxy();

    if (!data) {
        return path;
    }

    // const data = data.replace(/-/g, ' -')
    //     .replace(/  /g, ' ')
    //     .replace(/ /g, ',')
    //     .replace(/,,/g, ',');

    // const n;
    // create pipes so that we can split the data
    // for (n = 0; n < cc.length; n++) {
    //     cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
    // }

    // data = data.replace(/-/g, ',-');

    // create array
    // const arr = cs.split('|');
    // init context point
    let cpx = 0;
    let cpy = 0;
    let subpathX = cpx;
    let subpathY = cpy;
    let prevCmd;

    const CMD = PathProxy.CMD;

    // commandReg.lastIndex = 0;
    // const cmdResult;
    // while ((cmdResult = commandReg.exec(data)) != null) {
    //     const cmdStr = cmdResult[1];
    //     const cmdContent = cmdResult[2];

    const cmdList = data.match(commandReg);
    if (!cmdList) {
        // Invalid svg path.
        return path;
    }

    for (let l = 0; l < cmdList.length; l++) {
        const cmdText = cmdList[l];
        let cmdStr = cmdText.charAt(0);

        let cmd;

        // String#split is faster a little bit than String#replace or RegExp#exec.
        // const p = cmdContent.split(valueSplitReg);
        // const pLen = 0;
        // for (let i = 0; i < p.length; i++) {
        //     // '' and other invalid str => NaN
        //     const val = parseFloat(p[i]);
        //     !isNaN(val) && (p[pLen++] = val);
        // }


        // Following code will convert string to number. So convert type to number here
        const p = cmdText.match(numberReg) as any[] as number[] || [];
        const pLen = p.length;
        for (let i = 0; i < pLen; i++) {
            p[i] = parseFloat(p[i] as any as string);
        }

        let off = 0;
        while (off < pLen) {
            let ctlPtx;
            let ctlPty;

            let rx;
            let ry;
            let psi;
            let fa;
            let fs;

            let x1 = cpx;
            let y1 = cpy;

            let len: number;
            let pathData: number[] | Float32Array;
            // convert l, H, h, V, and v to L
            switch (cmdStr) {
                case 'l':
                    cpx += p[off++];
                    cpy += p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'L':
                    cpx = p[off++];
                    cpy = p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'm':
                    cpx += p[off++];
                    cpy += p[off++];
                    cmd = CMD.M;
                    path.addData(cmd, cpx, cpy);
                    subpathX = cpx;
                    subpathY = cpy;
                    cmdStr = 'l';
                    break;
                case 'M':
                    cpx = p[off++];
                    cpy = p[off++];
                    cmd = CMD.M;
                    path.addData(cmd, cpx, cpy);
                    subpathX = cpx;
                    subpathY = cpy;
                    cmdStr = 'L';
                    break;
                case 'h':
                    cpx += p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'H':
                    cpx = p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'v':
                    cpy += p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'V':
                    cpy = p[off++];
                    cmd = CMD.L;
                    path.addData(cmd, cpx, cpy);
                    break;
                case 'C':
                    cmd = CMD.C;
                    path.addData(
                        cmd, p[off++], p[off++], p[off++], p[off++], p[off++], p[off++]
                    );
                    cpx = p[off - 2];
                    cpy = p[off - 1];
                    break;
                case 'c':
                    cmd = CMD.C;
                    path.addData(
                        cmd,
                        p[off++] + cpx, p[off++] + cpy,
                        p[off++] + cpx, p[off++] + cpy,
                        p[off++] + cpx, p[off++] + cpy
                    );
                    cpx += p[off - 2];
                    cpy += p[off - 1];
                    break;
                case 'S':
                    ctlPtx = cpx;
                    ctlPty = cpy;
                    len = path.len();
                    pathData = path.data;
                    if (prevCmd === CMD.C) {
                        ctlPtx += cpx - pathData[len - 4];
                        ctlPty += cpy - pathData[len - 3];
                    }
                    cmd = CMD.C;
                    x1 = p[off++];
                    y1 = p[off++];
                    cpx = p[off++];
                    cpy = p[off++];
                    path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
                    break;
                case 's':
                    ctlPtx = cpx;
                    ctlPty = cpy;
                    len = path.len();
                    pathData = path.data;
                    if (prevCmd === CMD.C) {
                        ctlPtx += cpx - pathData[len - 4];
                        ctlPty += cpy - pathData[len - 3];
                    }
                    cmd = CMD.C;
                    x1 = cpx + p[off++];
                    y1 = cpy + p[off++];
                    cpx += p[off++];
                    cpy += p[off++];
                    path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
                    break;
                case 'Q':
                    x1 = p[off++];
                    y1 = p[off++];
                    cpx = p[off++];
                    cpy = p[off++];
                    cmd = CMD.Q;
                    path.addData(cmd, x1, y1, cpx, cpy);
                    break;
                case 'q':
                    x1 = p[off++] + cpx;
                    y1 = p[off++] + cpy;
                    cpx += p[off++];
                    cpy += p[off++];
                    cmd = CMD.Q;
                    path.addData(cmd, x1, y1, cpx, cpy);
                    break;
                case 'T':
                    ctlPtx = cpx;
                    ctlPty = cpy;
                    len = path.len();
                    pathData = path.data;
                    if (prevCmd === CMD.Q) {
                        ctlPtx += cpx - pathData[len - 4];
                        ctlPty += cpy - pathData[len - 3];
                    }
                    cpx = p[off++];
                    cpy = p[off++];
                    cmd = CMD.Q;
                    path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
                    break;
                case 't':
                    ctlPtx = cpx;
                    ctlPty = cpy;
                    len = path.len();
                    pathData = path.data;
                    if (prevCmd === CMD.Q) {
                        ctlPtx += cpx - pathData[len - 4];
                        ctlPty += cpy - pathData[len - 3];
                    }
                    cpx += p[off++];
                    cpy += p[off++];
                    cmd = CMD.Q;
                    path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
                    break;
                case 'A':
                    rx = p[off++];
                    ry = p[off++];
                    psi = p[off++];
                    fa = p[off++];
                    fs = p[off++];

                    x1 = cpx, y1 = cpy;
                    cpx = p[off++];
                    cpy = p[off++];
                    cmd = CMD.A;
                    processArc(
                        x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
                    );
                    break;
                case 'a':
                    rx = p[off++];
                    ry = p[off++];
                    psi = p[off++];
                    fa = p[off++];
                    fs = p[off++];

                    x1 = cpx, y1 = cpy;
                    cpx += p[off++];
                    cpy += p[off++];
                    cmd = CMD.A;
                    processArc(
                        x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
                    );
                    break;
            }
        }

        if (cmdStr === 'z' || cmdStr === 'Z') {
            cmd = CMD.Z;
            path.addData(cmd);
            // z may be in the middle of the path.
            cpx = subpathX;
            cpy = subpathY;
        }

        prevCmd = cmd;
    }

    path.toStatic();

    return path;
}

type SVGPathOption = Omit<PathProps, 'shape' | 'buildPath'>
interface InnerSVGPathOption extends PathProps {
    applyTransform?: (m: MatrixArray) => void
}
class SVGPath extends Path {
    applyTransform(m: MatrixArray) {}
}

function isPathProxy(path: PathProxy | CanvasRenderingContext2D): path is PathProxy {
    return (path as PathProxy).setData != null;
}

// TODO Optimize double memory cost problem
function createPathOptions(str: string, opts: SVGPathOption): InnerSVGPathOption {
    const pathProxy = createPathProxyFromString(str);
    const innerOpts: InnerSVGPathOption = extend({}, opts);
    innerOpts.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
        const beProxy = isPathProxy(path);
        if (beProxy && path.canSave()) {
            // path.setData(pathProxy.data);
            path.appendPath(pathProxy);
            // Svg and vml renderer don't have context
            const ctx = path.getContext();
            if (ctx) {
                path.rebuildPath(ctx, 1);
            }
        }
        else {
            const ctx = beProxy ? path.getContext() : path;
            if (ctx) {
                pathProxy.rebuildPath(ctx, 1);
            }
        }
    };

    innerOpts.applyTransform = function (this: SVGPath, m: MatrixArray) {
        transformPath(pathProxy, m);
        this.dirtyShape();
    };

    return innerOpts;
}

/**
 * Create a Path object from path string data
 * http://www.w3.org/TR/SVG/paths.html#PathData
 * @param  opts Other options
 */
export function createFromString(str: string, opts?: SVGPathOption): SVGPath {
    // PENDING
    return new SVGPath(createPathOptions(str, opts));
}

/**
 * Create a Path class from path string data
 * @param  str
 * @param  opts Other options
 */
export function extendFromString(str: string, defaultOpts?: SVGPathOption): typeof SVGPath {
    const innerOpts = createPathOptions(str, defaultOpts);
    class Sub extends SVGPath {
        constructor(opts: InnerSVGPathOption) {
            super(opts);
            this.applyTransform = innerOpts.applyTransform;
            this.buildPath = innerOpts.buildPath;
        }
    }
    return Sub;
}

/**
 * Merge multiple paths
 */
// TODO Apply transform
// TODO stroke dash
// TODO Optimize double memory cost problem
export function mergePath(pathEls: Path[], opts: PathProps) {
    const pathList: PathProxy[] = [];
    const len = pathEls.length;
    for (let i = 0; i < len; i++) {
        const pathEl = pathEls[i];
        pathList.push(pathEl.getUpdatedPathProxy(true));
    }

    const pathBundle = new Path(opts);
    // Need path proxy.
    pathBundle.createPathProxy();
    pathBundle.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
        if (isPathProxy(path)) {
            path.appendPath(pathList);
            // Svg and vml renderer don't have context
            const ctx = path.getContext();
            if (ctx) {
                // Path bundle not support percent draw.
                path.rebuildPath(ctx, 1);
            }
        }
    };

    return pathBundle;
}

/**
 * Clone a path.
 */
export function clonePath(sourcePath: Path, opts?: {
    /**
     * If bake global transform to path.
     */
    bakeTransform?: boolean
    /**
     * Convert global transform to local.
     */
    toLocal?: boolean
}) {
    opts = opts || {};
    const path = new Path();
    if (sourcePath.shape) {
        path.setShape(sourcePath.shape);
    }
    path.setStyle(sourcePath.style);

    if (opts.bakeTransform) {
        transformPath(path.path, sourcePath.getComputedTransform());
    }
    else {
        // TODO Copy getLocalTransform, updateTransform since they can be changed.
        if (opts.toLocal) {
            path.setLocalTransform(sourcePath.getComputedTransform());
        }
        else {
            path.copyTransform(sourcePath);
        }
    }

    // These methods may be overridden
    path.buildPath = sourcePath.buildPath;
    (path as SVGPath).applyTransform = (path as SVGPath).applyTransform;

    path.z = sourcePath.z;
    path.z2 = sourcePath.z2;
    path.zlevel = sourcePath.zlevel;

    return path;
}
