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

export interface InkControlProps {
    inkDoc: Doc;
    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;
    /**
     * 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 => {
        if (InkStrokeProperties.Instance) {
            const screenScale = this.props.ScreenToLocalTransform().Scale;
            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"));
            setupMoveUpEvents(this, e,
                action((e: PointerEvent, down: number[], delta: number[]) => {
                    if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                    InkStrokeProperties.Instance?.moveControl(delta[0] * screenScale, delta[1] * screenScale, controlIndex);
                    return false;
                }),
                action(() => {
                    if (this.controlUndo) {
                        InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, 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(equivIndex, handleIndexA, handleIndexB);
                            }
                            if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
                                InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
                            }
                        }
                        this.controlUndo?.end();
                        this.controlUndo = undefined;
                    }
                }));
        }
    }
    /**
     * 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)) {
            if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation();
        }
    }

    /**
     * Changes the current selected control point.
     */
    @action
    changeCurrPoint = (i: number) => {
        if (InkStrokeProperties.Instance) {
            InkStrokeProperties.Instance._currentPoint = i;
            document.addEventListener("keydown", this.onDelete, true);
        }
    }

    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 screenSpaceLineWidth = this.props.screenSpaceLineWidth;
        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 - screenSpaceLineWidth * 2 * scale}
                y={control.Y - screenSpaceLineWidth * 2 * scale}
                cx={control.X}
                cy={control.Y}
                r={screenSpaceLineWidth * 2 * scale}
                opacity={this.controlUndo ? 0.15 : 1}
                height={screenSpaceLineWidth * 4 * scale}
                width={screenSpaceLineWidth * 4 * scale}
                strokeWidth={screenSpaceLineWidth / 2}
                stroke={Colors.MEDIUM_BLUE}
                fill={broken ? Colors.MEDIUM_BLUE : color}
                onPointerDown={(e: any) => {
                    this.changeCurrPoint(control.I);
                    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={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>
        );
    }
}