import React = require("react");
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../fields/Doc";
import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkField";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
import { setupMoveUpEvents, returnFalse } from "../../Utils";
import { Transform } from "../util/Transform";
import { UndoManager } from "../util/UndoManager";
import { Colors } from "./global/globalEnums";
import { InkingStroke } from "./InkingStroke";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { DocumentView } from "./nodes/DocumentView";

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

@observer
export class InkControlPtHandles extends React.Component<InkControlProps> {

    @observable private _overControl = -1;

    @observable controlUndo: UndoManager.Batch | undefined;

    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.ComponentView?.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;
            setupMoveUpEvents(this, e,
                action((e: PointerEvent, down: number[], delta: number[]) => {
                    if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                    const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                    const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
                    return false;
                }),
                action(() => {
                    if (this.controlUndo) {
                        InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
                    }
                    this.controlUndo?.end();
                    this.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.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
                            }
                            if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
                            }
                        }
                        this.controlUndo?.end();
                        this.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)) {
            InkStrokeProperties.Instance.deletePoints(this.props.inkView, 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.controlUndo ? 0.15 : 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: DocumentView;
    screenSpaceLineWidth: number;
    startPt: PointData;
    endPt: PointData;
}
@observer
export class InkEndPtHandles extends React.Component<InkEndProps> {
    @observable controlUndo: UndoManager.Batch | undefined;
    @observable _overStart: boolean = false;
    @observable _overEnd: boolean = false;

    @action
    dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => {
        setupMoveUpEvents(this, e, (e) => {
            if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
            // compute stretch factor by finding scaling along axis between start and end points
            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([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
            return false;
        }, action(() => {
            this.controlUndo?.end();
            this.controlUndo = undefined;
            UndoManager.FilterBatches(["data", "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: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
            {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
        </svg>
        );
    }
}