import { Property } from 'csstype';
import * as React from 'react';
import { Utils } from '../../Utils';
import { Gestures } from '../../pen-gestures/GestureTypes';
import './InteractionUtils.scss';

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace InteractionUtils {
    export const MOUSETYPE = 'mouse';
    export const TOUCHTYPE = 'touch';
    export const PENTYPE = 'pen';
    export const ERASERTYPE = 'eraser';

    const ERASER_BUTTON = 5;

    export function makePolygon(shape: Gestures, points: { X: number; Y: number }[]) {
        // if arrow/line/circle, the two end points should be the starting and the ending point
        let left = points[0].X;
        let top = points[0].Y;
        let right = points[1].X;
        let bottom = points[1].Y;
        if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
            // pointer is up (first and last points are the same)
            if (![Gestures.Arrow, Gestures.Line, Gestures.Circle].includes(shape)) {
                // otherwise take max and min
                const xs = points.map(p => p.X);
                const ys = points.map(p => p.Y);
                right = Math.max(...xs);
                left = Math.min(...xs);
                bottom = Math.max(...ys);
                top = Math.min(...ys);
            }
        } else {
            // if in the middle of drawing
            // take first and last points
            right = points[points.length - 1].X;
            left = points[0].X;
            bottom = points[points.length - 1].Y;
            top = points[0].Y;
            if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
                // switch left/right and top/bottom if needed
                if (left > right) {
                    const temp = right;
                    right = left;
                    left = temp;
                }
                if (top > bottom) {
                    const temp = top;
                    top = bottom;
                    bottom = temp;
                }
            }
        }
        const polyPts = [];
        switch (shape) {
            case Gestures.Rectangle:
                polyPts.push({ X: left, Y: top });
                polyPts.push({ X: right, Y: top });
                polyPts.push({ X: right, Y: bottom });
                polyPts.push({ X: left, Y: bottom });
                polyPts.push({ X: left, Y: top });
                break;
            case Gestures.Triangle:
                polyPts.push({ X: left, Y: bottom });
                polyPts.push({ X: right, Y: bottom });
                polyPts.push({ X: (right + left) / 2, Y: top });
                polyPts.push({ X: left, Y: bottom });
                break;
            case Gestures.Circle:
                {
                    const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
                    const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
                    const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
                    for (let x = centerX - radius; x < centerX + radius; x++) {
                        const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY;
                        polyPts.push({ X: x, Y: y });
                    }
                    for (let x = centerX + radius; x > centerX - radius; x--) {
                        const y = Math.sqrt(radius ** 2 - (x - centerX) ** 2) + centerY;
                        const newY = centerY - (y - centerY);
                        polyPts.push({ X: x, Y: newY });
                    }
                    polyPts.push({ X: centerX - radius, Y: Math.sqrt(radius ** 2 - (-radius) ** 2) + centerY });
                }
                break;

            case Gestures.Line:
            default:
                polyPts.push({ X: left, Y: top });
                polyPts.push({ X: right, Y: bottom });
                break;
        }
        return polyPts;
    }

    export function CreatePolyline(
        points: { X: number; Y: number }[],
        left: number,
        top: number,
        color: string,
        width: number,
        strokeWidth: number,
        lineJoin: Property.StrokeLinejoin,
        strokeLineCap: Property.StrokeLinecap,
        bezier: string,
        fill: string,
        arrowStart: string,
        arrowEnd: string,
        markerScale: number,
        dash: string | undefined,
        scalexIn: number,
        scaleyIn: number,
        shape: Gestures,
        pevents: Property.PointerEvents,
        opacity: number,
        nodefs: boolean,
        downHdlr?: (e: React.PointerEvent) => void,
        mask?: boolean
        // dropshadow?: string
    ) {
        const pts = shape ? makePolygon(shape, points) : points;

        const scalex = isNaN(scalexIn) ? 1 : scalexIn;
        const scaley = isNaN(scaleyIn) ? 1 : scaleyIn;

        const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
        const strpts = bezier
            ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '')
            : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, '');

        const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
        const defGuid = Utils.GenerateGuid();

        const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements;
        const markerStrokeWidth = strokeWidth / 2;
        const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5
        const arrowLengthFactor = 5 * (markerScale || 0.5);
        const arrowNotchFactor = 2 * (markerScale || 0.5);
        return (
            <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
                {' '}
                {/* setting the svg fill sets the arrowStart fill */}
                {nodefs ? null : (
                    <defs>
                        {!mask ? null : (
                            <filter id={`mask${defGuid}`} x="-1" y="-1" width="500%" height="500%">
                                <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5" />
                            </filter>
                        )}
                        {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
                            <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
                                <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
                            </marker>
                        )}
                        {arrowStart !== 'arrow' ? null : (
                            <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
                                <polygon
                                    style={{ stroke: color }}
                                    strokeLinejoin={lineJoin as 'inherit' | 'round' | 'bevel' | 'miter'}
                                    strokeWidth={(markerStrokeWidth * 2) / 3}
                                    points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${
                                        markerStrokeWidth * arrowWidthFactor
                                    }, 0 0`}
                                />
                            </marker>
                        )}
                        {arrowEnd !== 'arrow' ? null : (
                            <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
                                <polygon
                                    style={{ stroke: color }}
                                    strokeLinejoin={lineJoin as 'inherit' | 'miter' | 'round' | 'bevel'}
                                    strokeWidth={(markerStrokeWidth * 2) / 3}
                                    points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
                                />
                            </marker>
                        )}
                    </defs>
                )}
                <Tag
                    d={bezier ? strpts + (arrowStart || arrowEnd ? ' ' : '') : undefined}
                    points={bezier ? undefined : strpts}
                    // filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `}
                    style={{
                        // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
                        fill: fill && fill !== 'transparent' ? fill : 'none',
                        filter: mask ? `url(#mask${defGuid})` : undefined,
                        opacity: 1.0,
                        // opacity: strokeWidth !== width ? 0.5 : undefined,
                        pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents,
                        stroke: color ?? 'rgb(0, 0, 0)',
                        strokeWidth,
                        strokeLinecap: strokeLineCap,
                        strokeDasharray: dashArray,
                        transition: 'inherit',
                    }}
                    markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`}
                    markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`}
                />
            </svg>
        );
    }

    /**
     * Returns whether or not the pointer event passed in is of the type passed in
     * @param e - pointer event. this event could be from a mouse, a pen, or a finger
     * @param type - InteractionUtils.(PENTYPE | ERASERTYPE | MOUSETYPE | TOUCHTYPE)
     */
    export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
        // prettier-ignore
        switch (type) {
            // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
            case PENTYPE:    return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
            case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
            case TOUCHTYPE:  return e.pointerType === TOUCHTYPE;
            default: 
        } // prettier-ignore
        return e.pointerType === type;
    }

    /**
     * Returns euclidean distance between two points
     * @param pt1
     * @param pt2
     */
    export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
        return Math.sqrt((pt1.clientX - pt2.clientX) ** 2 + (pt1.clientY - pt2.clientY) ** 2);
    }

    /**
     * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
     * @param pts - n-arbitrary long list of points
     */
    export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } {
        const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
        const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
        return { X: centerX, Y: centerY };
    }

    /**
     * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in
     * @param pt1 - new point that corresponds to oldPoint1
     * @param pt2 - new point that corresponds to oldPoint2
     * @param oldPoint1 - previous point 1
     * @param oldPoint2 - previous point 2
     */
    export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
        const threshold = 4;
        const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
        const newDist = TwoPointEuclidist(pt1, pt2);

        /** if they have the same sign, then we are either pinching in or out.
         * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
         * so that it can still pan without freaking out
         */
        if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
            return Math.sign(oldDist - newDist);
        }
        return 0;
    }

    /**
     * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in
     * @param pt1 - new point that corresponds to oldPoint1
     * @param pt2 - new point that corresponds to oldPoint2
     * @param oldPoint1 - previous point 1
     * @param oldPoint2 - previous point 2
     */
    export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
        const threshold = 4;

        const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
        const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);

        const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);

        if (pinching !== 0) {
            if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
                return pinching;
            }
        }
        return 0;
    }
}
