import "es6-object-assign/auto";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { SearchUtils } from "@devexpress/utils/lib/utils/search";
import { Metrics } from "@devexpress/utils/lib/geometry/metrics";
import { MathUtils } from "@devexpress/utils/lib/utils/math";
import { Vector } from "@devexpress/utils/lib/geometry/vector";
import { Segment } from "@devexpress/utils/lib/geometry/segment";
import { TextAlignment } from "./Model/Style";
import { Browser } from "@devexpress/utils/lib/browser";
import { EvtUtils } from "@devexpress/utils/lib/utils/evt";
import { ConnectorRenderPoint } from "./Model/Connectors/ConnectorRenderPoint";

export class LineEquation {
    static fromPoints(pointA: Point, pointB: Point, accuracy: number = 0.00001): LineEquation {
        return !GeometryUtils.areDuplicatedPoints(pointA, pointB, accuracy) ?
            new LineEquation(
                pointB.y - pointA.y,
                pointA.x - pointB.x,
                pointB.x * pointA.y - pointA.x * pointB.y) : undefined;
    }
    constructor(private aParam: number, private bParam: number, private cParam: number) { }
    getPointIntersection(other: LineEquation, accuracy: number = 0.00001): Point | null {
        const A1: number = this.aParam;
        const B1: number = this.bParam;
        const C1: number = this.cParam;

        const A2: number = other.aParam;
        const B2: number = other.bParam;
        const C2: number = other.cParam;

        const v: number = A2 * B1 - A1 * B2;
        if(MathUtils.numberCloseTo(v, 0, accuracy))
            return null;
        if(A1 === 0) {
            const x: number = (B2 * C1 - C2 * B1) / (B1 * A2);
            return this.createPoint(x, -C1 / B1);
        }
        const y: number = (C2 * A1 - C1 * A2) / v;
        return this.createPoint((-B1 * y - C1) / A1, y);
    }
    containsPoint(point: Point, accuracy: number = 0.00001) : boolean {
        return MathUtils.numberCloseTo(this.aParam * point.x + this.bParam * point.y + this.cParam, 0, accuracy);
    }
    private createPoint(x: number, y: number, accuracy: number = 0.00001): Point {
        return new Point(
            MathUtils.numberCloseTo(x, 0, accuracy) ? 0 : x,
            MathUtils.numberCloseTo(y, 0, accuracy) ? 0 : y);
    }
}

export class Range {
    public to: number;
    constructor(public from: number, to?: number) {
        this.to = to !== undefined ? to : from;
    }
    get length(): number {
        return Math.abs(this.to - this.from);
    }
    extend(range: Range): void {
        this.from = Math.min(range.from, this.from);
        this.to = Math.max(range.to, this.to);
    }
    includes(value: number): boolean {
        return value >= this.from && value <= this.to;
    }
    static fromLength(from: number, length: number): Range {
        return new Range(from, from + length);
    }
}

export class EventDispatcher<T extends IEventListener> {
    listeners: T[] = [];

    public add(listener: T): void {
        if(!listener)
            throw new Error("Not Implemented");
        if(!this.hasEventListener(listener))
            this.listeners.push(listener);
    }
    public remove(listener: T): void {
        for(let i = 0, currentListener; currentListener = this.listeners[i]; i++)
            if(currentListener === listener) {
                this.listeners.splice(i, 1);
                break;
            }
    }
    public raise(funcName: keyof T, ...args: any[]): void {
        for(let i = 0, listener: IEventListener; listener = this.listeners[i]; i++) {
            const func = listener[<string>funcName];
            func && func.apply(listener, args);
        }
    }
    public raise1(action: (listener: T) => void) {
        for(let i = 0, listener: T; listener = this.listeners[i]; i++)
            action(listener);

    }

    hasEventListener(listener: IEventListener): boolean {
        for(let i = 0, l = this.listeners.length; i < l; i++)
            if(this.listeners[i] === listener)
                return true;
        return false;
    }
}

export interface IEventListener { }

export class Utils {
    static flatten<T>(arr: T[][]): T[] {
        return [].concat(...arr);
    }
}

export class GeometryUtils {
    static arePointsOfOrthogonalLine(point1: ConnectorRenderPoint, point2: ConnectorRenderPoint, isHorizontal: boolean): boolean {
        return isHorizontal ? (point1.y === point2.y) : (point1.x === point2.x);
    }
    static getCommonRectangle(rects: Rectangle[]) {
        if(!rects.length)
            return new Rectangle(0, 0, 0, 0);

        let minX = Number.MAX_VALUE;
        let maxX = -Number.MAX_VALUE;
        let minY = Number.MAX_VALUE;
        let maxY = -Number.MAX_VALUE;
        rects.forEach(rect => {
            minX = Math.min(minX, rect.x);
            maxX = Math.max(maxX, rect.right);
            minY = Math.min(minY, rect.y);
            maxY = Math.max(maxY, rect.bottom);
        });
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }

    static findFreeSpace(rects: Rectangle[], size: Size, exact: boolean, targetRect?: Rectangle): Point {
        let xs: number[] = [targetRect ? targetRect.x : 0];
        let ys: number[] = [targetRect ? targetRect.y : 0];
        rects.forEach(r => {
            xs.push(r.x);
            xs.push(r.right);
            ys.push(r.y);
            ys.push(r.bottom);
        });
        xs = xs.sort((a, b) => a - b).reduce((acc, v, index) => (xs[index - 1] !== v && acc.push(v) && acc) || acc, []); 
        ys = ys.sort((a, b) => a - b).reduce((acc, v, index) => (ys[index - 1] !== v && acc.push(v) && acc) || acc, []);
        const matrix: number[][] = ys.map(y => xs.map((x, i) => xs[i + 1] - x));
        for(let i = 0, rect: Rectangle; rect = rects[i]; i++) {
            const xi0 = SearchUtils.binaryIndexOf(xs, a => a - rect.x);
            const xi1 = SearchUtils.binaryIndexOf(xs, a => a - rect.right);
            const yi0 = SearchUtils.binaryIndexOf(ys, a => a - rect.y);
            const yi1 = SearchUtils.binaryIndexOf(ys, a => a - rect.bottom);
            for(let y = yi0; y < yi1; y++)
                for(let x = xi0; x < xi1; x++)
                    matrix[y][x] *= -1;
        }
        for(let yi = 0; yi < ys.length; yi++)
            for(let xi = 0; xi < xs.length - 1; xi++) {
                const checkResult = this.checkRect(matrix, ys, xs, yi, xi, size, exact);
                if(checkResult > 0)
                    xi = checkResult;
                else if(checkResult === 0)
                    return new Point(xs[xi], ys[yi]);
            }

        return null;
    }
    private static checkRect(matrix: number[][], ys: number[], xs: number[], yimin: number, ximin: number, size: Size, exact: boolean): number {
        let height = 0;
        let width = 0;
        let ximax = xs.length - 2;
        for(let yi = yimin; yi < ys.length; yi++) {
            height = ys[yi + 1] - ys[yimin];
            for(let xi = ximin; xi <= ximax; xi++) {
                if(matrix[yi][xi] < 0)
                    return xi === 0 ? -1 : xi; 
                width = xs[xi + 1] - xs[ximin];
                if(size.width <= width || (!exact && xi === xs.length - 2 && size.width / 2 <= width)) {
                    if(size.height <= height || (!exact && yi === ys.length - 2 && size.height / 2 <= height))
                        return 0;
                    ximax = xi;
                }
            }
        }
    }

    static getArrowPoints(point: Point, directionPoint: Point,
        arrowHeight: number, arrowWidth: number): { point1: Point, point2: Point, point3: Point } {
        if(point.x === directionPoint.x && point.y === directionPoint.y)
            return { point1: point.clone(), point2: point.clone(), point3: point.clone() };

        const catX = directionPoint.x - point.x;
        const catY = directionPoint.y - point.y;
        const hypotenuse = Math.sqrt(Math.pow(catX, 2) + Math.pow(catY, 2));
        const cos = catX / hypotenuse;
        const sin = catY / hypotenuse;

        const x1 = point.x + arrowHeight * cos + arrowWidth * sin;
        const y1 = point.y + arrowHeight * sin - arrowWidth * cos;
        const x2 = point.x + arrowHeight * cos - arrowWidth * sin;
        const y2 = point.y + arrowHeight * sin + arrowWidth * cos;
        const x3 = point.x + arrowHeight * cos;
        const y3 = point.y + arrowHeight * sin;
        return { point1: new Point(x1, y1), point2: new Point(x2, y2), point3: new Point(x3, y3) };
    }
    static createSegments<TPoint extends Point>(points: TPoint[]) : Segment<TPoint>[] {
        const result = [];
        for(let i = 1; i < points.length; i++)
            result.push(new Segment(points[i - 1], points[i]));
        return result;
    }
    static createRectagle<T extends Point>(points: T[]) : Rectangle {
        const xarr = points.map(p => p.x);
        const yarr = points.map(p => p.y);
        const minX = xarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
        const maxX = xarr.reduce((prev, cur) => Math.max(prev, cur), -Number.MAX_VALUE);
        const minY = yarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
        const maxY = yarr.reduce((prev, cur) => Math.max(prev, cur), -Number.MAX_VALUE);
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }
    static createSegmentsFromRectangle(rect: Rectangle): Segment<Point>[] {
        const result : Segment<Point>[] = [];
        const topLeft = new Point(rect.x, rect.y);
        const topRight = new Point(rect.right, rect.y);
        const bottomRight = new Point(rect.right, rect.bottom);
        const bottomLeft = new Point(rect.x, rect.bottom);
        result.push(new Segment(topLeft, topRight));
        result.push(new Segment(topRight, bottomRight));
        result.push(new Segment(bottomRight, bottomLeft));
        result.push(new Segment(bottomLeft, topLeft));
        return result;
    }
    static areSegmentsCutRectangle<T extends Point>(segments: Segment<T>[], rect: Rectangle) : boolean {
        if(!rect)
            return false;
        const rectanlePolygonalChain = GeometryUtils.createSegmentsFromRectangle(rect);
        let hasSegmentIn = false;
        let hasSegmentOut = false;
        for(let i = 0; i < segments.length; i++) {
            if(hasSegmentIn && hasSegmentOut)
                return true;
            const segment = segments[i];
            if(segment.isIntersectedByRect(rect)) {
                const startPoint = segment.startPoint;
                const endPoint = segment.endPoint;
                const currentContainsStart = rect.containsPoint(startPoint);
                const currentContainsEnd = rect.containsPoint(endPoint);
                if(!currentContainsStart && !currentContainsEnd)
                    return true;
                if(currentContainsStart && !currentContainsEnd) {
                    const rectLinesContainsStart = rectanlePolygonalChain.filter(s => s.containsPoint(startPoint));
                    if(rectLinesContainsStart.length > 0) {
                        const otherRectSegments = rectanlePolygonalChain.filter(s => {
                            if(rectLinesContainsStart.length === 1)
                                return !s.containsPoint(rectLinesContainsStart[0].startPoint) && !s.containsPoint(rectLinesContainsStart[0].endPoint);
                            return s !== rectLinesContainsStart[0] && s !== rectLinesContainsStart[1];
                        });
                        if(otherRectSegments.some(s => segment.isIntersected(s)) && !hasSegmentIn)
                            hasSegmentIn = true;
                    }
                    if(!hasSegmentOut)
                        hasSegmentOut = true;
                    continue;
                }
                if(!currentContainsStart && currentContainsEnd) {
                    if(!hasSegmentIn) {
                        hasSegmentIn = true;
                        if(hasSegmentOut)
                            hasSegmentOut = false;
                    }
                    const rectLinesContainsEnd = rectanlePolygonalChain.filter(s => s.containsPoint(endPoint));
                    if(rectLinesContainsEnd.length > 0) {
                        const otherRectSegments = rectanlePolygonalChain.filter(s => {
                            if(rectLinesContainsEnd.length === 1)
                                return !s.containsPoint(rectLinesContainsEnd[0].startPoint) && !s.containsPoint(rectLinesContainsEnd[0].endPoint);
                            return s !== rectLinesContainsEnd[0] && s !== rectLinesContainsEnd[1];
                        });
                        if(otherRectSegments.some(s => segment.isIntersected(s)) && !hasSegmentOut)
                            hasSegmentOut = true;
                    }
                    continue;
                }
                const rectLinesContainsStart = rectanlePolygonalChain.filter(s => s.containsPoint(startPoint));
                const rectLinesContainsEnd = rectanlePolygonalChain.filter(s => s.containsPoint(endPoint));
                if(rectLinesContainsStart.length === 2 && rectLinesContainsEnd.length === 2)
                    return true;
                if(rectLinesContainsStart.length === 1 && rectLinesContainsEnd.length === 1 &&
                    rectLinesContainsStart[0] !== rectLinesContainsEnd[0])
                    return true;
                if(!hasSegmentOut && rectLinesContainsEnd.length === 1 && !rectLinesContainsStart.length)
                    hasSegmentOut = true;
                if(!hasSegmentIn && rectLinesContainsStart.length === 1 && !rectLinesContainsEnd.length) {
                    hasSegmentIn = true;
                    if(hasSegmentOut)
                        hasSegmentOut = false;
                }
            }
        }
        return hasSegmentIn && hasSegmentOut;
    }
    static areIntersectedSegments<T extends Point>(segments: Segment<T>[], otherSegments: Segment<T>[]) : boolean {
        if(!otherSegments)
            return false;
        let segmentIndex = 0;
        let segment;
        while(segment = segments[segmentIndex]) {
            let otherSegmentIndex = 0;
            let otherSegment;
            while(otherSegment = otherSegments[otherSegmentIndex]) {
                if(otherSegment.isIntersected(segment))
                    return true;
                otherSegmentIndex++;
            }
            segmentIndex++;
        }
        return false;
    }

    static isLineIntersected<T extends Point>(beginLinePoint: T, endLinePoint: T, segment : Segment<T>, excludeBeginPoint? : boolean, excludeEndPoint?: boolean) : boolean {
        const line = LineEquation.fromPoints(beginLinePoint, endLinePoint);
        const segmentStartPoint = segment.startPoint;
        const segmentEndPoint = segment.endPoint;
        if(line.containsPoint(segmentStartPoint) && line.containsPoint(segmentEndPoint))
            return !excludeBeginPoint && !excludeEndPoint;
        const segmentLine = LineEquation.fromPoints(segmentStartPoint, segmentEndPoint);
        const intersection = segmentLine.getPointIntersection(line);
        if(!intersection || !segment.containsPoint(intersection))
            return false;
        if(excludeBeginPoint)
            return !GeometryUtils.areDuplicatedPoints(segmentStartPoint, intersection);
        if(excludeEndPoint)
            return !GeometryUtils.areDuplicatedPoints(segmentEndPoint, intersection);
        return true;
    }
    static removeUnnecessaryPoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy);
        this.removeBackwardPoints(points, removeCallback, checkCallback, accuracy);
        this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy);
    }
    static removeUnnecessaryRightAnglePoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy);
        this.removeBackwardPoints(points, removeCallback, checkCallback, accuracy);
        this.removeNotRightAnglePoints(points, removeCallback, checkCallback, accuracy);
        this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy);
    }
    private static removeUnnecessaryPointsCore<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        this.removeDuplicatedPoints(points, removeCallback, checkCallback, accuracy);
        this.removeNotCornersPoints(points, removeCallback, checkCallback, accuracy);
    }
    static removeNotRightAnglePoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        let index = 0;
        let point: T;
        while((point = points[index]) && points.length > 2) {
            const nextPoint = this.getNextPoint(points, index, 1, checkCallback);
            const prevPoint = this.getNextPoint(points, index, -1, checkCallback);
            if(!prevPoint || !nextPoint ||
                GeometryUtils.isRightAngleCorner(prevPoint, point, nextPoint, accuracy) ||
                !removeCallback(point, index))
                index++;
        }
    }
    static removeDuplicatedPoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        let index = 0;
        let point: T;
        while((point = points[index]) && points.length > 2) {
            const nextPoint = this.getNextPoint(points, index, 1, checkCallback);
            if(nextPoint && GeometryUtils.areDuplicatedPoints(point, nextPoint, accuracy)) {
                const actualIndex = index === points.length - 2 ? index : index + 1;
                if(removeCallback(points[actualIndex], actualIndex))
                    continue;
            }
            index++;
        }
    }
    static removeNotCornersPoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        let index = 0;
        let point: T;
        while((point = points[index]) && points.length > 2) {
            const nextPoint = this.getNextPoint(points, index, 1, checkCallback);
            const prevPoint = this.getNextPoint(points, index, -1, checkCallback);
            if(!prevPoint || !nextPoint || GeometryUtils.isCorner(prevPoint, point, nextPoint, accuracy))
                index++;
            else if(!removeCallback(point, index))
                index++;
        }
    }
    static removeBackwardPoints<T extends Point>(points: T[],
        removeCallback: (p: T, index: number) => boolean,
        checkCallback: (p: T) => boolean = p => p !== undefined,
        accuracy: number = 0.00001) {
        let index = 0;
        let point: T;
        while((point = points[index]) && points.length > 2) {
            const nextPoint = this.getNextPoint(points, index, 1, checkCallback);
            const prevPoint = this.getNextPoint(points, index, -1, checkCallback);
            if(!prevPoint || !nextPoint ||
                !GeometryUtils.isBackwardPoint(prevPoint, point, nextPoint, accuracy) ||
                !removeCallback(point, index))
                index++;
        }
    }
    static isRightAngleCorner<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean {
        return MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI / 2.0, accuracy) ||
            MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI, accuracy) ||
            MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), 3.0 * Math.PI / 2.0, accuracy);
    }
    static isCorner<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean {
        return !MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), 0, accuracy);
    }
    static areDuplicatedPoints<T extends Point>(current: T, next: T, accuracy: number = 0.00001) : boolean {
        return (MathUtils.numberCloseTo(current.x, next.x, accuracy) && MathUtils.numberCloseTo(current.y, next.y, accuracy));
    }
    static isBackwardPoint<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean {
        return MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI, accuracy);
    }
    static createAngle<T extends Point>(prev: T, current: T, next: T): number {
        const vector1 = Vector.fromPoints(current, next);
        const vector2 = Vector.fromPoints(prev, current);
        const vector1X = vector1.x;
        const vector1Y = vector1.y;
        const vector2X = vector2.x;
        const vector2Y = vector2.y;
        const atan = Math.atan2(vector1X * vector2Y - vector2X * vector1Y, vector1X * vector2X + vector1Y * vector2Y);
        return atan < 0 ? 2 * Math.PI + atan : atan;
    }

    static getNextPoint<T extends Point>(points: T[], index: number, step: number,
        checkCallback: (pt: T) => boolean): T {
        let result: T;
        let newIndex = index + step;
        while(result = points[newIndex]) {
            if(checkCallback(result))
                return result;
            newIndex = newIndex + step;
        }
    }
    static addSelectedLinesTo(prevPt: Point, pt: Point, nextPt: Point,
        offsetX : number, offsetY : number,
        offsetXNegative: number, offsetYNegative: number,
        nextOffsetX : number, nextOffsetY : number,
        nextOffsetXNegative : number, nextOffsetYNegative : number,
        addSelectedLine : (x: number, y: number) => void,
        addSelectedLineWB : (x: number, y: number) => void,
        accuracy: number = 0.00001) : void {

        const a1 = pt.y - prevPt.y;
        const a2 = nextPt.y - pt.y;

        const b1 = prevPt.x - pt.x;
        const b2 = pt.x - nextPt.x;

        const det = a1 * b2 - a2 * b1;

        if(!MathUtils.numberCloseTo(det, 0, accuracy)) {
            const c1 = a1 * (prevPt.x + offsetX) + b1 * (prevPt.y + offsetY);
            const c2 = a2 * (pt.x + nextOffsetX) + b2 * (pt.y + nextOffsetY);
            addSelectedLine((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det);

            const c1WB = a1 * (prevPt.x + offsetXNegative) + b1 * (prevPt.y + offsetYNegative);
            const c2WB = a2 * (pt.x + nextOffsetXNegative) + b2 * (pt.y + nextOffsetYNegative);
            addSelectedLineWB((b2 * c1WB - b1 * c2WB) / det, (a1 * c2WB - a2 * c1WB) / det);
        }
    }
    static getSelectionOffsetPoint(prev: Point, current: Point, distance: number): Point {
        return new Point((prev.y - current.y) / distance, (current.x - prev.x) / distance);
    }
    static getSelectionTextStartEndPoints(prev: Point, current: Point, distance: number, center: Point, size: Size, align: TextAlignment): [Point, Point] {
        const cos = (current.x - prev.x) / distance;
        const sin = (current.y - prev.y) / distance;
        const width = size.width * cos + size.height * sin;
        switch(align) {
            case TextAlignment.Left:
                return [center, new Point(center.x + cos * width, center.y + sin * width)];
            case TextAlignment.Right:
                return [new Point(center.x - cos * width, center.y - sin * width), center];
            default:
                return [
                    new Point(center.x - 0.5 * cos * width, center.y - 0.5 * sin * width),
                    new Point(center.x + 0.5 * cos * width, center.y + 0.5 * sin * width)
                ];
        }
    }
    static getPathLength(points: Point[]): number {
        let length = 0;
        let prevPt;
        points.forEach(pt => {
            if(prevPt !== undefined)
                length += Metrics.euclideanDistance(pt, prevPt);
            prevPt = pt;
        });
        return length;
    }
    static getPathPointByPosition(points: Point[], relativePosition: number): [Point, number] {
        if(!points.length)
            throw new Error("Invalid points");
        if(0 > relativePosition || relativePosition > 1)
            throw new Error("Invalid relative position");
        const length = this.getPathLength(points);
        if(points.length <= 2 && length === 0 || relativePosition === 0)
            return [points[0], 0];
        const targetLength = length * relativePosition;
        let currentLength = 0;
        for(let i = 1; i < points.length; i++) {
            const lineLength = Metrics.euclideanDistance(points[i], points[i - 1]);
            if(currentLength + lineLength >= targetLength) {
                const delta = targetLength - currentLength;
                const cos = (points[i].x - points[i - 1].x) / lineLength;
                const sin = (points[i].y - points[i - 1].y) / lineLength;
                return [new Point(points[i - 1].x + cos * delta, points[i - 1].y + sin * delta), i];
            }
            currentLength += lineLength;
        }
        return [points[points.length - 1], points.length - 1];
    }
    static getLineAngle(beginPoint: Point, endPoint: Point): number {
        return Math.atan2(endPoint.y - beginPoint.y, endPoint.x - beginPoint.x);
    }
    static getTriangleBeginAngle(beginPoint: Point, endPoint: Point, point: Point) {
        const lineAngle = this.getLineAngle(beginPoint, endPoint);
        const beginPointAngle = this.getLineAngle(beginPoint, point);
        return Math.abs(beginPointAngle - lineAngle);
    }
    static getTriangleEndAngle(beginPoint: Point, endPoint: Point, point: Point) {
        const lineAngle = this.getLineAngle(beginPoint, endPoint);
        const endPointAngle = this.getLineAngle(point, endPoint);
        return Math.abs(lineAngle - endPointAngle);
    }
    static getPathPointByPoint(points: Point[], point: Point): Point {
        if(!points.length)
            throw new Error("Invalid points");
        if(points.length === 1)
            return points[0];
        let distance = Number.MAX_VALUE;
        let result: Point;
        for(let i = 1; i < points.length; i++) {
            const beginPoint = points[i - 1];
            const endPoint = points[i];
            if(point.equals(beginPoint)) {
                result = beginPoint.clone();
                break;
            }
            if(point.equals(endPoint)) {
                result = endPoint.clone();
                break;
            }

            const beginAngle = this.getTriangleBeginAngle(beginPoint, endPoint, point);
            const endAngle = this.getTriangleEndAngle(beginPoint, endPoint, point);
            const beginDistance = Metrics.euclideanDistance(point, beginPoint);
            const endDistance = Metrics.euclideanDistance(point, endPoint);
            const orthOffset = beginDistance * Math.sin(beginAngle);

            let currentDistance;
            if(Math.PI / 2 <= beginAngle && beginAngle <= Math.PI * 3 / 2)
                currentDistance = beginDistance;
            else if(Math.PI / 2 <= endAngle && endAngle <= Math.PI * 3 / 2)
                currentDistance = endDistance;
            else
                currentDistance = Math.abs(orthOffset);

            if(currentDistance < distance) {
                distance = currentDistance;

                if(Math.PI / 2 <= beginAngle && beginAngle <= Math.PI * 3 / 2)
                    result = beginPoint.clone();
                else if(Math.PI / 2 <= endAngle && endAngle <= Math.PI * 3 / 2)
                    result = endPoint.clone();
                else {
                    const round = Math.fround || Math.round;
                    const lineAngle = this.getLineAngle(beginPoint, endPoint);
                    let offsetX = round(Math.abs(orthOffset * Math.sin(lineAngle)));
                    let offsetY = round(Math.abs(orthOffset * Math.cos(lineAngle)));
                    const isAbove = point.y - beginPoint.y < round((point.x - beginPoint.x) * Math.tan(lineAngle));
                    if(0 <= lineAngle && lineAngle <= Math.PI / 2) {
                        offsetX *= isAbove ? -1 : 1;
                        offsetY *= isAbove ? 1 : -1;
                    }
                    else if(Math.PI / 2 <= lineAngle && lineAngle <= Math.PI) {
                        offsetX *= isAbove ? 1 : -1;
                        offsetY *= isAbove ? 1 : -1;
                    }
                    else if(0 >= lineAngle && lineAngle >= -Math.PI / 2) {
                        offsetX *= isAbove ? 1 : -1;
                        offsetY *= isAbove ? 1 : -1;
                    }
                    else if(-Math.PI / 2 >= lineAngle && lineAngle >= -Math.PI) {
                        offsetX *= isAbove ? -1 : 1;
                        offsetY *= isAbove ? 1 : -1;
                    }
                    result = point.clone().offset(offsetX, offsetY);
                }
            }
        }
        return result;
    }
    static getPathPositionByPoint(points: Point[], point: Point, maxPositionCount: number = 100): number {
        point = this.getPathPointByPoint(points, point);
        const length = this.getPathLength(points);
        let currentLength = 0;
        for(let i = 1; i < points.length; i++) {
            const beginPoint = points[i - 1];
            const endPoint = points[i];
            const lineLength = Metrics.euclideanDistance(endPoint, beginPoint);
            const angle = Math.atan((endPoint.y - beginPoint.y) / (endPoint.x - beginPoint.x));

            const round = Math.fround || Math.round;
            if((point.x === endPoint.x && point.x === beginPoint.x) || (point.y === endPoint.y && point.y === beginPoint.y) ||
                round(point.y - beginPoint.y) === round((point.x - beginPoint.x) * Math.tan(angle))) {
                if(Math.sin(angle) !== 0)
                    currentLength += Math.abs((point.y - beginPoint.y) / Math.sin(angle));
                else
                    currentLength += Math.abs(point.x - beginPoint.x);
                return Math.round(currentLength * maxPositionCount / length) / maxPositionCount;
            }
            currentLength += lineLength;
        }
        return 1;
    }
    static arePointsEqual(points1: Point[], points2: Point[]): boolean {
        const count1 = points1.length;
        const count2 = points2.length;
        if(count1 !== count2)
            return false;
        for(let i = 0; i < count1; i++)
            if(!points1[i].equals(points2[i]))
                return false;
        return true;
    }
    static getMaxRectangleEnscribedInEllipse(ellipseSize: Size): Size {
        const dx = ellipseSize.width * Math.sqrt(2) / 2;
        const dy = ellipseSize.height * Math.sqrt(2) / 2;
        return new Size(dx, dy);
    }
    static getEllipseByEnscribedRectangle(rectSize: Size): Size {
        return new Size(2 * rectSize.width / Math.sqrt(2), 2 * rectSize.height / Math.sqrt(2));
    }
}

export class ObjectUtils {
    static cloneObject(source: any): any {
        return source && Object.assign({}, source);
    }
    static compareObjects(obj1: any, obj2: any): boolean {
        if(obj1 === obj2) return true;
        if(typeof obj1 === "object" && typeof obj2 === "object")
            return this.isDeepEqual(obj1, obj2);
        return false;
    }
    private static isDeepEqual(obj1: any, obj2: any): boolean {
        const props1 = obj1 ? Object.getOwnPropertyNames(obj1) : [];
        const props2 = obj2 ? Object.getOwnPropertyNames(obj2) : [];
        if(props1.length !== props2.length) return false;

        for(let i = 0; i < props1.length; i++) {
            const property = props1[i];
            switch(typeof obj1[property]) {
                case "object": {
                    if(!this.isDeepEqual(obj1[property], obj2[property]))
                        return false;
                    break;
                }
                case "number": {
                    if(!isNaN(obj1[property]) || !isNaN(obj2[property]))
                        if(obj1[property] !== obj2[property])
                            return false;
                    break;
                }
                default: {
                    if(obj1[property] !== obj2[property])
                        return false;
                }
            }
        }
        return true;
    }
}

export class HtmlFocusUtils {
    static focusWithPreventScroll(element: HTMLElement) {
        try {
            const isPreventScrollNotSupported = Browser.Safari;
            const savedDocumentScrollPosition = isPreventScrollNotSupported && this.getHtmlScrollPosition();
            if(isPreventScrollNotSupported) {
                const parentPos = element.parentElement && element.parentElement.getBoundingClientRect(); 
                if(parentPos) {
                    let left = parentPos.left < 0 ? -parentPos.left + 1 : 0;
                    let top = parentPos.top < 0 ? -parentPos.top + 1 : 0;
                    const iframePos = window.frameElement && window.frameElement.getBoundingClientRect();
                    if(iframePos) {
                        if(iframePos.top < 0 && (-iframePos.top > parentPos.top))
                            top = -iframePos.top - parentPos.top + 1;
                        if(iframePos.left < 0 && (-iframePos.left > parentPos.left))
                            left = -iframePos.left - parentPos.left + 1;
                    }
                    element.style.setProperty("left", left + "px", "important");
                    element.style.setProperty("top", top + "px", "important");
                }
            }
            element.focus({ preventScroll: true });
            if(isPreventScrollNotSupported) {
                const newDocumentScrollPosition = this.getHtmlScrollPosition();
                if(!ObjectUtils.compareObjects(savedDocumentScrollPosition, newDocumentScrollPosition))
                    this.setHtmlScrollPosition(savedDocumentScrollPosition);

                element.style.setProperty("left", "-1000px", "important");
                element.style.setProperty("top", "-1000px", "important");
            }
        }
        catch(e) {
        }
    }
    private static getHtmlScrollPosition() {
        return {
            pos: this.getDocumentScrollPosition(window, document),
            iframePos: window.top !== window && this.getDocumentScrollPosition(window.top, window.top.document)
        };
    }
    private static getDocumentScrollPosition(win: any, doc: any) {
        return {
            left: win.pageXOffset || doc.documentElement.scrollLeft || doc.body.scrollLeft,
            top: win.pageYOffset || doc.documentElement.scrollTop || doc.body.scrollTop
        };
    }
    private static setHtmlScrollPosition(position: any) {
        this.setDocumentScrollPosition(document, position.pos);

        if(window.top !== window && position.iframePos)
            this.setDocumentScrollPosition(window.top.document, position.iframePos);
    }
    private static setDocumentScrollPosition(doc: any, pos: any) {
        doc.documentElement.scrollTop = pos.top;
        doc.documentElement.scrollLeft = pos.left;

        doc.body.scrollTop = pos.top;
        doc.body.scrollLeft = pos.left;
    }
}

export class EventUtils {
    static isLeftButtonPressed(evt: Event) {
        return EvtUtils.isLeftButtonPressed(evt);
    }
    static isPointerEvents() {
        return window.PointerEvent;
    }
    static isMousePointer(evt: any): boolean {
        return this.isPointerEvents() && ((evt.pointerType && evt.pointerType === "mouse") || (Browser.Firefox && evt.type === "click"));
    }
    static isTouchMode(): boolean {
        return Browser.TouchUI || (window.navigator && window.navigator.maxTouchPoints > 0);
    }
    static isTouchEvent(evt: any): boolean {
        return Browser.TouchUI || !EventUtils.isMousePointer(evt);
    }
}
