import * as React from 'react';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../fields/Doc';
import { ControlPoint, InkData, PointData } from '../../fields/InkField';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
import { Cast } from '../../fields/Types';
import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
import { SelectionManager } from '../util/SelectionManager';
import { UndoManager } from '../util/UndoManager';
import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
import { InkStrokeProperties } from './InkStrokeProperties';
import { SnappingManager } from '../util/SnappingManager';
import { ObservableReactComponent } from './ObservableReactComponent';

export interface InkControlProps {
    inkDoc: Doc;
    inkView: InkingStroke;
    inkCtrlPoints: InkData;
    screenCtrlPoints: InkData;
    screenSpaceLineWidth: number;
    nearestScreenPt: () => PointData | undefined;
}

@observer
export class InkControlPtHandles extends ObservableReactComponent<InkControlProps> {
    @observable private _overControl = -1;
    get docView() {
        return this._props.inkView.DocumentView?.();
    }

    constructor(props: InkControlProps) {
        super(props);
        makeObservable(this);
    }

    componentDidMount() {
        document.addEventListener('keydown', this.onDelete, true);
    }
    componentWillUnmount() {
        document.removeEventListener('keydown', this.onDelete, true);
    }
    /**
     * Handles the movement of a selected control point when the user clicks and drags.
     * @param controlIndex The index of the currently selected control point.
     */
    @action
    onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
        const ptFromScreen = this._props.inkView.ptFromScreen;
        if (ptFromScreen) {
            const order = controlIndex % 4;
            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this._props.inkCtrlPoints.length) % this._props.inkCtrlPoints.length;
            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this._props.inkCtrlPoints.length;
            const brokenIndices = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'));
            const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
            if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
            const origInk = this._props.inkCtrlPoints.slice();
            setupMoveUpEvents(
                this,
                e,
                action((e: PointerEvent, down: number[], delta: number[]) => {
                    if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                    const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                    const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
                    this.docView && InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                    return false;
                }),
                action(() => {
                    if (this._props.inkView.controlUndo && this.docView) {
                        InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                    }
                    this._props.inkView.controlUndo?.end();
                    this._props.inkView.controlUndo = undefined;
                    UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                }),
                action((e: PointerEvent, doubleTap: boolean | undefined) => {
                    const equivIndex = controlIndex === 0 ? this._props.inkCtrlPoints.length - 1 : controlIndex === this._props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
                    if (doubleTap || e.button === 2) {
                        if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
                            if (brokenIndices) brokenIndices.push(controlIndex);
                            else this._props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
                        } else {
                            if (brokenIndices?.includes(equivIndex)) {
                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                            }
                            if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                            }
                        }
                        this._props.inkView.controlUndo?.end();
                        this._props.inkView.controlUndo = undefined;
                    }
                    this.changeCurrPoint(controlIndex);
                }),
                undefined,
                undefined,
                () => wasSelected && this.changeCurrPoint(-1)
            );
        }
    };
    /**
     * Updates whether a user has hovered over a particular control point or point that could be added
     * on click.
     */
    @action onEnterControl = (i: number) => {
        this._overControl = i;
    };
    @action onLeaveControl = () => {
        this._overControl = -1;
    };

    /**
     * Deletes the currently selected point.
     */
    @action
    onDelete = (e: KeyboardEvent) => {
        if (['-', 'Backspace', 'Delete'].includes(e.key)) {
            this.docView && InkStrokeProperties.Instance.deletePoints(this.docView, e.shiftKey);
            e.stopPropagation();
        }
    };

    /**
     * Changes the current selected control point.
     */
    @action
    changeCurrPoint = (i: number) => (InkStrokeProperties.Instance._currentPoint = i);

    render() {
        // Accessing the current ink's data and extracting all control points.
        const scrData = this._props.screenCtrlPoints;
        const sreenCtrlPoints: ControlPoint[] = [];
        for (let i = 0; i <= scrData.length - 4; i += 4) {
            sreenCtrlPoints.push({ ...scrData[i], I: i });
            sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
        }

        const inkData = this._props.inkCtrlPoints;
        const inkCtrlPts: ControlPoint[] = [];
        for (let i = 0; i <= inkData.length - 4; i += 4) {
            inkCtrlPts.push({ ...inkData[i], I: i });
            inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 });
        }

        const closed = InkingStroke.IsClosed(inkData);
        const nearestScreenPt = this._props.nearestScreenPt();
        const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
        const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
            const broken = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
            const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
            return (
                <Tag
                    key={control.I.toString() + scale}
                    x={control.X - this._props.screenSpaceLineWidth * 2 * scale}
                    y={control.Y - this._props.screenSpaceLineWidth * 2 * scale}
                    cx={control.X}
                    cy={control.Y}
                    r={this._props.screenSpaceLineWidth * 2 * scale}
                    opacity={this._props.inkView.controlUndo ? 0.35 : 1}
                    height={this._props.screenSpaceLineWidth * 4 * scale}
                    width={this._props.screenSpaceLineWidth * 4 * scale}
                    strokeWidth={this._props.screenSpaceLineWidth / 2}
                    stroke={Colors.MEDIUM_BLUE}
                    fill={broken ? Colors.MEDIUM_BLUE : color}
                    onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
                    onMouseEnter={() => this.onEnterControl(control.I)}
                    onMouseLeave={this.onLeaveControl}
                    pointerEvents="all"
                    cursor="default"
                />
            );
        };
        return (
            <svg>
                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
                {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
            </svg>
        );
    }
}

export interface InkEndProps {
    inkDoc: Doc;
    inkView: InkingStroke;
    screenSpaceLineWidth: number;
    startPt: () => PointData;
    endPt: () => PointData;
}
@observer
export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
    @observable _overStart: boolean = false;
    @observable _overEnd: boolean = false;

    constructor(props: InkEndProps) {
        super(props);
        makeObservable(this);
    }

    _throttle = 0; // need to throttle dragging since the position may change when the control points change.  this allows the stroke to settle so that we don't get increasingly bad jitter
    @action
    dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
        SnappingManager.SetIsDragging(true);
        setupMoveUpEvents(
            this,
            e,
            action(e => {
                if (this._throttle++ % 2 !== 0) return false;
                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                // compute stretch factor by finding scaling along axis between start and end points
                const p1 = pt1();
                const p2 = pt2();
                const v1 = { X: p1.X - p2.X, Y: p1.Y - p2.Y };
                const v2 = { X: e.clientX - p2.X, Y: e.clientY - p2.Y };
                const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
                const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
                const scaling = v2len / v1len;
                const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
                const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
                const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
                InkStrokeProperties.Instance.stretchInk(SelectionManager.Views, scaling, p2, v1n, e.shiftKey);
                InkStrokeProperties.Instance.rotateInk(SelectionManager.Views, angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call
                return false;
            }),
            action(() => {
                SnappingManager.SetIsDragging(false);
                this._props.inkView.controlUndo?.end();
                this._props.inkView.controlUndo = undefined;
                UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']);
            }),
            returnFalse
        );
    };

    render() {
        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => (
            <circle
                key={key}
                cx={pt?.X}
                cy={pt?.Y}
                r={this._props.screenSpaceLineWidth * 2}
                fill={this._overStart ? '#aaaaaa' : '#99999977'}
                stroke={'#00007777'}
                strokeWidth={0}
                onPointerLeave={action(() => (this._overStart = false))}
                onPointerEnter={action(() => (this._overStart = true))}
                onPointerDown={dragFunc}
                pointerEvents="all"
            />
        );
        return (
            <svg>
                {hdl('start', this._props.startPt(), e => this.dragRotate(e, this._props.startPt, this._props.endPt))}
                {hdl('end', this._props.endPt(), e => this.dragRotate(e, this._props.endPt, this._props.startPt))}
            </svg>
        );
    }
}
