/*
    InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.

    The primary data is:
        data - an InkField which is an array of PointData (X,Y values).  The data is laid out as a sequence of simple bezier segments:
              point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ...  (Note that segment endpoints are duplicated ie Point2 = Point 3)
        brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
        text - a text field that will be centered within a closed ink stroke
        stroke_isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by 
               the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.

    The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
        Thus the mapping can roughly be described by:
        the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
        the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
        NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space

    InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in 
        screen space (to avoid scaling artifacts)
    
    Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
*/
import React = require('react');
import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
import { InkData, InkField } from '../../fields/InkField';
import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
import { Docs } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
import { ContextMenu } from './ContextMenu';
import { ViewBoxBaseComponent } from './DocComponent';
import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
import { Colors } from './global/globalEnums';
import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
import './InkStroke.scss';
import { InkStrokeProperties } from './InkStrokeProperties';
import { InkTangentHandles } from './InkTangentHandles';
import { DocComponentView } from './nodes/DocumentView';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { PinProps, PresBox } from './nodes/trails';
import { StyleProp } from './StyleProvider';
import Color = require('color');

@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
    static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
    public static LayoutString(fieldStr: string) {
        return FieldView.LayoutString(InkingStroke, fieldStr);
    }
    public static IsClosed(inkData: InkData) {
        return inkData?.length && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
    }
    private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
    private _disposers: { [key: string]: IReactionDisposer } = {};

    @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
    @observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
    @observable _nearestScrPt?: { X: number; Y: number }; // nearst screen point on the ink stroke ""

    componentDidMount() {
        this.props.setContentView?.(this);
        this._disposers.selfDisper = reaction(
            () => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
            selected => !selected && (InkStrokeProperties.Instance._controlButton = false)
        );
    }
    componentWillUnmount() {
        Object.keys(this._disposers).forEach(key => this._disposers[key]());
    }

    // transform is the inherited screentolocal xf plus any scaling that was done to make the stroke
    // fit within its panel (e.g., for content fitting views like Lightbox or multicolumn, etc)
    screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1);

    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
        if (this._subContentView) {
            return this._subContentView.getAnchor?.(addAsAnnotation) || this.rootDoc;
        }

        if (!addAsAnnotation && !pinProps) return this.rootDoc;

        const anchor = Docs.Create.InkConfigDocument({
            title: 'Ink anchor:' + this.rootDoc.title,
            // set presentation timing for restoring shape
            presDuration: 1100,
            presTransition: 1000,
            annotationOn: this.rootDoc,
        });
        if (anchor) {
            anchor.backgroundColor = 'transparent';
            // /* addAsAnnotation &&*/ this.addDocument(anchor);
            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), inkable: true } }, this.rootDoc);
            return anchor;
        }
        return this.rootDoc;
    };

    /**
     * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
     * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
     * the center of the ink stroke changes as the stroke is rotated.
     */
    getCenter = (xf: Transform) => {
        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const angle = -NumCast(this.layoutDoc.rotation);
        const newPoints = inkData.map(pt => {
            const newX = Math.cos(angle) * pt.X - (Math.sin(angle) * pt.Y * inkScaleY) / inkScaleX;
            const newY = (Math.sin(angle) * pt.X * inkScaleX) / inkScaleY + Math.cos(angle) * pt.Y;
            return { X: newX, Y: newY };
        });
        const crx = (Math.max(...newPoints.map(np => np.X)) + Math.min(...newPoints.map(np => np.X))) / 2;
        const cry = (Math.max(...newPoints.map(np => np.Y)) + Math.min(...newPoints.map(np => np.Y))) / 2;
        const cx = Math.cos(-angle) * crx - (Math.sin(-angle) * cry * inkScaleY) / inkScaleX;
        const cy = (Math.sin(-angle) * crx * inkScaleX) / inkScaleY + Math.cos(-angle) * cry;
        const tc = xf.transformPoint((cx - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (cy - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2);
        return { X: tc[0], Y: tc[1] };
    };

    /**
     * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
     * and the recognized words to the 'handwriting'
     */
    analyzeStrokes() {
        const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
    }

    /**
     * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
     *    When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
     *  appear to illuminate what it covers up.  At the same time, all pixels that are not under the stroke will be
     *  dimmed by a semi-opaque overlay mask.
     */
    public static toggleMask = action((inkDoc: Doc) => {
        inkDoc.stroke_isInkMask = !inkDoc.stroke_isInkMask;
    });
    @observable controlUndo: UndoManager.Batch | undefined;
    /**
     * Drags the a simple bezier segment of the stroke.
     * Also adds a control point when double clicking on the stroke.
     */
    @action
    onPointerDown = (e: React.PointerEvent) => {
        this._handledClick = false;
        const inkView = this.props.docViewPath().lastElement();
        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const screenPts = inkData
            .map(point =>
                this.screenToLocal()
                    .inverse()
                    .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)
            )
            .map(p => ({ X: p[0], Y: p[1] }));
        const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
        const controlIndex = nearestSeg;
        const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
        const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
        this.controlUndo = undefined;
        setupMoveUpEvents(
            this,
            e,
            !isEditing
                ? returnFalse
                : action((e: PointerEvent, down: number[], delta: number[]) => {
                      if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                      const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
                      const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
                      InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
                      InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
                      return false;
                  }),
            !isEditing
                ? returnFalse
                : action(() => {
                      this.controlUndo?.end();
                      this.controlUndo = undefined;
                      UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                  }),
            action((e: PointerEvent, doubleTap: boolean | undefined) => {
                if (doubleTap) {
                    InkStrokeProperties.Instance._controlButton = true;
                    InkStrokeProperties.Instance._currentPoint = -1;
                    this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
                    if (isEditing) {
                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
                    }
                }
            }),
            isEditing,
            isEditing,
            action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1))
        );
    };

    /**
     * @param scrPt a point in the screen coordinate space
     * @returns the point in the ink data's coordinate space.
     */
    ptFromScreen = (scrPt: { X: number; Y: number }) => {
        const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const docPt = this.screenToLocal().transformPoint(scrPt.X, scrPt.Y);
        const inkPt = {
            X: (docPt[0] - inkStrokeWidth / 2) / inkScaleX + inkStrokeWidth / 2 + inkLeft,
            Y: (docPt[1] - inkStrokeWidth / 2) / inkScaleY + inkStrokeWidth / 2 + inkTop,
        };
        return inkPt;
    };

    /**
     * @param inkPt a point in the ink data's coordinate space
     * @returns the screen point corresponding to the ink point
     */
    ptToScreen = (inkPt: { X: number; Y: number }) => {
        const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const docPt = {
            X: (inkPt.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
            Y: (inkPt.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2,
        };
        const scrPt = this.screenToLocal().inverse().transformPoint(docPt.X, docPt.Y);
        return { X: scrPt[0], Y: scrPt[1] };
    };

    /**
     * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
     * @param scrPt - the point to snap to this stroke
     * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
     *
     * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
     */
    snapPt = (scrPt: { X: number; Y: number }, excludeSegs?: number[]) => {
        const { inkData } = this.inkScaledData();
        const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
        return { nearestPt, distance: distance * this.screenToLocal().inverse().Scale };
    };

    /**
     * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
     * factor for converting between ink and screen space.
     */
    inkScaledData = () => {
        const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
        const inkStrokeWidth = NumCast(this.rootDoc.stroke_width, 1);
        const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2;
        const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2;
        const inkLeft = Math.min(...inkData.map(p => p.X)) - inkStrokeWidth / 2;
        const inkRight = Math.max(...inkData.map(p => p.X)) + inkStrokeWidth / 2;
        const inkWidth = Math.max(1, inkRight - inkLeft);
        const inkHeight = Math.max(1, inkBottom - inkTop);
        return {
            inkData,
            inkStrokeWidth,
            inkTop,
            inkLeft,
            inkWidth,
            inkHeight,
            inkScaleX: (this.props.PanelWidth() - inkStrokeWidth) / (inkWidth - inkStrokeWidth || 1) || 1,
            inkScaleY: (this.props.PanelHeight() - inkStrokeWidth) / (inkHeight - inkStrokeWidth || 1) || 1,
        };
    };

    //
    // this updates the highlight for the nearest point on the curve to the cursor.
    // if the user double clicks, this highlighted point will be added as a control point in the curve.
    //
    @action
    onPointerMove = (e: React.PointerEvent) => {
        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const screenPts = inkData
            .map(point =>
                this.screenToLocal()
                    .inverse()
                    .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)
            )
            .map(p => ({ X: p[0], Y: p[1] }));
        const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });

        if (distance < 40) {
            this._nearestT = nearestT;
            this._nearestSeg = nearestSeg;
            this._nearestScrPt = nearestPt;
        } else {
            this._nearestT = this._nearestSeg = this._nearestScrPt = undefined;
        }
    };

    /**
     * @returns the nearest screen point to the cursor (to render a highlight for the point to be added)
     */
    nearestScreenPt = () => this._nearestScrPt;

    /**
     * @param boundsLeft the screen space left coordinate of the ink stroke
     * @param boundsTop the screen space top coordinate of the ink stroke
     * @returns the JSX controls for displaying an editing UI for the stroke (control point & tangent handles)
     */
    componentUI = (boundsLeft: number, boundsTop: number) => {
        const inkDoc = this.props.Document;
        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
        const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.screenToLocal().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke

        const screenInkWidth = this.screenToLocal().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth);
        const screenPts = inkData
            .map(point =>
                this.screenToLocal()
                    .inverse()
                    .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)
            )
            .map(p => ({ X: p[0], Y: p[1] }));
        const screenHdlPts = screenPts;

        const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
        const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale);
        return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? (
            !this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : (
                <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                    <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
                </div>
            )
        ) : (
            <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                {InteractionUtils.CreatePolyline(
                    screenPts,
                    0,
                    0,
                    Colors.MEDIUM_BLUE,
                    screenInkWidth[0],
                    screenSpaceCenterlineStrokeWidth,
                    StrCast(inkDoc.strokeLineJoin),
                    StrCast(this.layoutDoc.strokeLineCap),
                    StrCast(inkDoc.stroke_bezier),
                    'none',
                    startMarker,
                    endMarker,
                    markerScale * Math.min(screenSpaceCenterlineStrokeWidth, screenInkWidth[0] / screenSpaceCenterlineStrokeWidth),
                    StrCast(inkDoc.stroke_dash),
                    1,
                    1,
                    '',
                    'none',
                    1.0,
                    false
                )}
                <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={screenHdlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
                <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} />
            </div>
        );
    };

    _subContentView: DocComponentView | undefined;
    setSubContentView = (doc: DocComponentView) => (this._subContentView = doc);
    render() {
        TraceMobx();
        const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData();

        const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
        const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
        const closed = InkingStroke.IsClosed(inkData);
        const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
        const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent';
        const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);

        // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
        if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) {
            setTimeout(() => {
                this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[WidthSym]()));
                this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[HeightSym]()));
            });
        }

        // Visually renders the polygonal line made by the user.
        const inkLine = InteractionUtils.CreatePolyline(
            inkData,
            inkLeft,
            inkTop,
            strokeColor,
            inkStrokeWidth,
            inkStrokeWidth,
            StrCast(this.layoutDoc.strokeLineJoin),
            StrCast(this.layoutDoc.strokeLineCap),
            StrCast(this.layoutDoc.stroke_bezier),
            !closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor,
            startMarker,
            endMarker,
            markerScale,
            StrCast(this.layoutDoc.stroke_dash),
            inkScaleX,
            inkScaleY,
            '',
            'none',
            1.0,
            false
        );
        const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting);
        const highlightIndex = highlight?.highlightIndex;
        const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent');
        // Invisible polygonal line that enables the ink to be selected by the user.
        const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false, mask: boolean = false) =>
            InteractionUtils.CreatePolyline(
                inkData,
                inkLeft,
                inkTop,
                highlightColor,
                inkStrokeWidth,
                inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2),
                StrCast(this.layoutDoc.strokeLineJoin),
                StrCast(this.layoutDoc.strokeLineCap),
                StrCast(this.layoutDoc.stroke_bezier),
                !closed ? 'none' : !isInkMask && (fillColor === 'transparent' || suppressFill) ? 'none' : fillColor,
                '',
                '',
                markerScale,
                undefined,
                inkScaleX,
                inkScaleY,
                '',
                this.props.pointerEvents?.() ?? 'visiblepainted',
                0.0,
                false,
                downHdlr,
                mask
            );
        const fsize = +StrCast(this.props.Document.fontSize, '12px').replace('px', '');
        // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size.
        // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429
        // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/
        const lineHeightGuess = +getComputedStyle(document.body).lineHeight.replace('px', '') / +getComputedStyle(document.body).fontSize.replace('px', '');
        const interactions = {
            onPointerLeave: action(() => (this._nearestScrPt = undefined)),
            onPointerMove: this.props.isSelected() ? this.onPointerMove : undefined,
            onClick: (e: React.MouseEvent) => this._handledClick && e.stopPropagation(),
            onContextMenu: () => {
                const cm = ContextMenu.Instance;
                !Doc.noviceMode && cm?.addItem({ description: 'Recognize Writing', event: this.analyzeStrokes, icon: 'paint-brush' });
                cm?.addItem({ description: 'Toggle Mask', event: () => InkingStroke.toggleMask(this.rootDoc), icon: 'paint-brush' });
                cm?.addItem({ description: 'Edit Points', event: action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)), icon: 'paint-brush' });
            },
        };
        return (
            <div className="inkStroke-wrapper">
                <svg
                    className="inkStroke"
                    style={{
                        transform: isInkMask ? `rotate(-${NumCast(this.props.CollectionFreeFormDocumentView?.().Rot)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                        // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                        cursor: this.props.isSelected() ? 'default' : undefined,
                    }}
                    {...interactions}>
                    {clickableLine(this.onPointerDown, undefined, isInkMask)}
                    {isInkMask ? null : inkLine}
                </svg>
                {!closed || (!RTFCast(this.rootDoc.text)?.Text && (!this.props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
                    <div
                        className="inkStroke-text"
                        style={{
                            color: StrCast(this.layoutDoc.textColor, 'black'),
                            pointerEvents: this.props.isDocumentActive?.() ? 'all' : undefined,
                            width: this.layoutDoc[WidthSym](),
                            transform: `scale(${this.props.NativeDimScaling?.() || 1})`,
                            transformOrigin: 'top left',
                            //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
                        }}>
                        <FormattedTextBox
                            {...this.props}
                            setHeight={undefined}
                            setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
                            yPadding={10}
                            xPadding={10}
                            fieldKey="text"
                            fontSize={fsize}
                            dontRegisterView={true}
                            noSidebar={true}
                            dontScale={true}
                            isContentActive={this.props.isContentActive}
                        />
                    </div>
                )}
            </div>
        );
    }
}

export function SetActiveInkWidth(width: string): void {
    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
}
export function SetActiveBezierApprox(bezier: string): void {
    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
}
export function SetActiveInkColor(value: string) {
    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
}
export function SetActiveIsInkMask(value: boolean) {
    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
}
export function SetActiveFillColor(value: string) {
    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
}
export function SetActiveArrowStart(value: string) {
    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
}
export function SetActiveArrowEnd(value: string) {
    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
}
export function SetActiveArrowScale(value: number) {
    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
}
export function SetActiveDash(dash: string): void {
    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
}
export function ActiveInkPen(): Doc {
    return Doc.UserDoc();
}
export function ActiveInkColor(): string {
    return StrCast(ActiveInkPen()?.activeInkColor, 'black');
}
export function ActiveFillColor(): string {
    return StrCast(ActiveInkPen()?.activeFillColor, '');
}
export function ActiveIsInkMask(): boolean {
    return BoolCast(ActiveInkPen()?.activeIsInkMask, false);
}
export function ActiveArrowStart(): string {
    return StrCast(ActiveInkPen()?.activeArrowStart, '');
}
export function ActiveArrowEnd(): string {
    return StrCast(ActiveInkPen()?.activeArrowEnd, '');
}
export function ActiveArrowScale(): number {
    return NumCast(ActiveInkPen()?.activeArrowScale, 1);
}
export function ActiveDash(): string {
    return StrCast(ActiveInkPen()?.activeDash, '0');
}
export function ActiveInkWidth(): number {
    return Number(ActiveInkPen()?.activeInkWidth);
}
export function ActiveInkBezierApprox(): string {
    return StrCast(ActiveInkPen()?.activeInkBezier);
}
