/*
* 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.
*/

// Poly path support NaN point

import Path, { PathProps } from 'zrender/src/graphic/Path';
import PathProxy from 'zrender/src/core/PathProxy';
import { cubicRootAt, cubicAt } from 'zrender/src/core/curve';
import tokens from '../../visual/tokens';
import { isPointIllegal } from './helper';

const mathMin = Math.min;
const mathMax = Math.max;

/**
 * Draw smoothed line in non-monotone, in may cause undesired curve in extreme
 * situations. This should be used when points are non-monotone neither in x or
 * y dimension.
 */
function drawSegment(
    ctx: PathProxy,
    points: ArrayLike<number>,
    start: number,
    segLen: number,
    allLen: number,
    dir: number,
    smooth: number,
    smoothMonotone: 'x' | 'y' | 'none',
    connectNulls: boolean
) {
    let prevX: number;
    let prevY: number;
    let cpx0: number;
    let cpy0: number;
    let cpx1: number;
    let cpy1: number;
    let idx = start;
    let k = 0;
    for (; k < segLen; k++) {

        let x = points[idx * 2];
        let y = points[idx * 2 + 1];

        if (idx >= allLen || idx < 0) {
            break;
        }
        if (isPointIllegal(x, y)) {
            if (connectNulls) {
                idx += dir;
                continue;
            }
            break;
        }

        if (idx === start) {
            ctx[dir > 0 ? 'moveTo' : 'lineTo'](x, y);
            cpx0 = x;
            cpy0 = y;
        }
        else {
            let dx = x - prevX;
            let dy = y - prevY;

            // Ignore tiny segment.
            if ((dx * dx + dy * dy) < 0.5) {
                idx += dir;
                continue;
            }

            if (smooth > 0) {
                let nextIdx = idx + dir;
                let nextX = points[nextIdx * 2];
                let nextY = points[nextIdx * 2 + 1];
                // Ignore duplicate point
                while (nextX === x && nextY === y && k < segLen) {
                    k++;
                    nextIdx += dir;
                    idx += dir;
                    nextX = points[nextIdx * 2];
                    nextY = points[nextIdx * 2 + 1];
                    x = points[idx * 2];
                    y = points[idx * 2 + 1];
                    dx = x - prevX;
                    dy = y - prevY;
                }

                let tmpK = k + 1;
                if (connectNulls) {
                    // Find next point not null
                    while (isPointIllegal(nextX, nextY) && tmpK < segLen) {
                        tmpK++;
                        nextIdx += dir;
                        nextX = points[nextIdx * 2];
                        nextY = points[nextIdx * 2 + 1];
                    }
                }

                let ratioNextSeg = 0.5;
                let vx: number = 0;
                let vy: number = 0;
                let nextCpx0;
                let nextCpy0;
                // Is last point
                if (tmpK >= segLen || isPointIllegal(nextX, nextY)) {
                    cpx1 = x;
                    cpy1 = y;
                }
                else {
                    vx = nextX - prevX;
                    vy = nextY - prevY;

                    const dx0 = x - prevX;
                    const dx1 = nextX - x;
                    const dy0 = y - prevY;
                    const dy1 = nextY - y;
                    let lenPrevSeg;
                    let lenNextSeg;
                    if (smoothMonotone === 'x') {
                        lenPrevSeg = Math.abs(dx0);
                        lenNextSeg = Math.abs(dx1);
                        const dir = vx > 0 ? 1 : -1;
                        cpx1 = x - dir * lenPrevSeg * smooth;
                        cpy1 = y;
                        nextCpx0 = x + dir * lenNextSeg * smooth;
                        nextCpy0 = y;
                    }
                    else if (smoothMonotone === 'y') {
                        lenPrevSeg = Math.abs(dy0);
                        lenNextSeg = Math.abs(dy1);
                        const dir = vy > 0 ? 1 : -1;
                        cpx1 = x;
                        cpy1 = y - dir * lenPrevSeg * smooth;
                        nextCpx0 = x;
                        nextCpy0 = y + dir * lenNextSeg * smooth;
                    }
                    else {
                        lenPrevSeg = Math.sqrt(dx0 * dx0 + dy0 * dy0);
                        lenNextSeg = Math.sqrt(dx1 * dx1 + dy1 * dy1);

                        // Use ratio of seg length
                        ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg);

                        cpx1 = x - vx * smooth * (1 - ratioNextSeg);
                        cpy1 = y - vy * smooth * (1 - ratioNextSeg);

                        // cp0 of next segment
                        nextCpx0 = x + vx * smooth * ratioNextSeg;
                        nextCpy0 = y + vy * smooth * ratioNextSeg;

                        // Smooth constraint between point and next point.
                        // Avoid exceeding extreme after smoothing.
                        nextCpx0 = mathMin(nextCpx0, mathMax(nextX, x));
                        nextCpy0 = mathMin(nextCpy0, mathMax(nextY, y));
                        nextCpx0 = mathMax(nextCpx0, mathMin(nextX, x));
                        nextCpy0 = mathMax(nextCpy0, mathMin(nextY, y));
                        // Reclaculate cp1 based on the adjusted cp0 of next seg.
                        vx = nextCpx0 - x;
                        vy = nextCpy0 - y;

                        cpx1 = x - vx * lenPrevSeg / lenNextSeg;
                        cpy1 = y - vy * lenPrevSeg / lenNextSeg;

                        // Smooth constraint between point and prev point.
                        // Avoid exceeding extreme after smoothing.
                        cpx1 = mathMin(cpx1, mathMax(prevX, x));
                        cpy1 = mathMin(cpy1, mathMax(prevY, y));
                        cpx1 = mathMax(cpx1, mathMin(prevX, x));
                        cpy1 = mathMax(cpy1, mathMin(prevY, y));

                        // Adjust next cp0 again.
                        vx = x - cpx1;
                        vy = y - cpy1;
                        nextCpx0 = x + vx * lenNextSeg / lenPrevSeg;
                        nextCpy0 = y + vy * lenNextSeg / lenPrevSeg;
                    }
                }

                ctx.bezierCurveTo(cpx0, cpy0, cpx1, cpy1, x, y);

                cpx0 = nextCpx0;
                cpy0 = nextCpy0;
            }
            else {
                ctx.lineTo(x, y);
            }
        }

        prevX = x;
        prevY = y;
        idx += dir;
    }

    return k;
}

class ECPolylineShape {
    points: ArrayLike<number>;
    smooth = 0;
    smoothConstraint = true;
    smoothMonotone: 'x' | 'y' | 'none';
    connectNulls: boolean;
}

interface ECPolylineProps extends PathProps {
    shape?: Partial<ECPolylineShape>
}

export class ECPolyline extends Path<ECPolylineProps> {

    readonly type = 'ec-polyline';

    shape: ECPolylineShape;

    constructor(opts?: ECPolylineProps) {
        super(opts);
    }

    getDefaultStyle() {
        return {
            stroke: tokens.color.neutral99,
            fill: null as string
        };
    }

    getDefaultShape() {
        return new ECPolylineShape();
    }

    buildPath(ctx: PathProxy, shape: ECPolylineShape) {
        const points = shape.points;

        let i = 0;
        let len = points.length / 2;

        // const result = getBoundingBox(points, shape.smoothConstraint);

        if (shape.connectNulls) {
            // Must remove first and last null values avoid draw error in polygon
            for (; len > 0; len--) {
                if (!isPointIllegal(points[len * 2 - 2], points[len * 2 - 1])) {
                    break;
                }
            }
            for (; i < len; i++) {
                if (!isPointIllegal(points[i * 2], points[i * 2 + 1])) {
                    break;
                }
            }
        }
        while (i < len) {
            i += drawSegment(
                ctx, points, i, len, len,
                1,
                shape.smooth,
                shape.smoothMonotone, shape.connectNulls
            ) + 1;
        }
    }

    getPointOn(xOrY: number, dim: 'x' | 'y'): number[] {
        if (!this.path) {
            this.createPathProxy();
            this.buildPath(this.path, this.shape);
        }
        const path = this.path;
        const data = path.data;
        const CMD = PathProxy.CMD;

        let x0;
        let y0;

        const isDimX = dim === 'x';
        const roots: number[] = [];

        for (let i = 0; i < data.length;) {
            const cmd = data[i++];
            let x;
            let y;
            let x2;
            let y2;
            let x3;
            let y3;
            let t;
            switch (cmd) {
                case CMD.M:
                    x0 = data[i++];
                    y0 = data[i++];
                    break;
                case CMD.L:
                    x = data[i++];
                    y = data[i++];
                    t = isDimX ? (xOrY - x0) / (x - x0)
                        : (xOrY - y0) / (y - y0);
                    if (t <= 1 && t >= 0) {
                        const val = isDimX ? (y - y0) * t + y0
                            : (x - x0) * t + x0;
                        return isDimX ? [xOrY, val] : [val, xOrY];
                    }
                    x0 = x;
                    y0 = y;
                    break;
                case CMD.C:
                    x = data[i++];
                    y = data[i++];
                    x2 = data[i++];
                    y2 = data[i++];
                    x3 = data[i++];
                    y3 = data[i++];

                    const nRoot = isDimX ? cubicRootAt(x0, x, x2, x3, xOrY, roots)
                        : cubicRootAt(y0, y, y2, y3, xOrY, roots);
                    if (nRoot > 0) {
                        for (let i = 0; i < nRoot; i++) {
                            const t = roots[i];
                            if (t <= 1 && t >= 0) {
                                const val = isDimX ? cubicAt(y0, y, y2, y3, t)
                                    : cubicAt(x0, x, x2, x3, t);
                                return isDimX ? [xOrY, val] : [val, xOrY];
                            }
                        }
                    }

                    x0 = x3;
                    y0 = y3;
                    break;
            }
        }
    }
}
class ECPolygonShape extends ECPolylineShape {
    // Offset between stacked base points and points
    stackedOnPoints: ArrayLike<number>;
    stackedOnSmooth: number;
}

interface ECPolygonProps extends PathProps {
    shape?: Partial<ECPolygonShape>
}
export class ECPolygon extends Path {

    readonly type = 'ec-polygon';

    shape: ECPolygonShape;

    constructor(opts?: ECPolygonProps) {
        super(opts);
    }

    getDefaultShape() {
        return new ECPolygonShape();
    }

    buildPath(ctx: PathProxy, shape: ECPolygonShape) {
        const points = shape.points;
        const stackedOnPoints = shape.stackedOnPoints;

        let i = 0;
        let len = points.length / 2;
        const smoothMonotone = shape.smoothMonotone;

        if (shape.connectNulls) {
            // Must remove first and last null values avoid draw error in polygon
            for (; len > 0; len--) {
                if (!isPointIllegal(points[len * 2 - 2], points[len * 2 - 1])) {
                    break;
                }
            }
            for (; i < len; i++) {
                if (!isPointIllegal(points[i * 2], points[i * 2 + 1])) {
                    break;
                }
            }
        }
        while (i < len) {
            const k = drawSegment(
                ctx, points, i, len, len,
                1,
                shape.smooth,
                smoothMonotone, shape.connectNulls
            );
            drawSegment(
                ctx, stackedOnPoints, i + k - 1, k, len,
                -1,
                shape.stackedOnSmooth,
                smoothMonotone, shape.connectNulls
            );
            i += k + 1;

            ctx.closePath();
        }
    }
}