import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { IconButton } from 'browndash-components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaUndo } from 'react-icons/fa';
import { Utils, emptyFunction, lightOrDark, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils';
import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
import { InkField } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
import { GetEffectiveAcl } from '../../fields/util';
import { DocumentType } from '../documents/DocumentTypes';
import { Docs } from '../documents/Documents';
import { DocumentManager } from '../util/DocumentManager';
import { DragManager } from '../util/DragManager';
import { SelectionManager } from '../util/SelectionManager';
import { SettingsManager } from '../util/SettingsManager';
import { SnappingManager } from '../util/SnappingManager';
import { UndoManager } from '../util/UndoManager';
import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
import { InkStrokeProperties } from './InkStrokeProperties';
import { InkingStroke } from './InkingStroke';
import { LightboxView } from './LightboxView';
import { ObservableReactComponent } from './ObservableReactComponent';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { Colors } from './global/globalEnums';
import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
import { ImageBox } from './nodes/ImageBox';
import { KeyValueBox } from './nodes/KeyValueBox';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';

interface DocumentDecorationsProps {
    PanelWidth: number;
    PanelHeight: number;
    boundsLeft: number;
    boundsTop: number;
}
@observer
export class DocumentDecorations extends ObservableReactComponent<DocumentDecorationsProps> {
    static Instance: DocumentDecorations;
    private _resizeHdlId = '';
    private _keyinput = React.createRef<HTMLInputElement>();
    private _resizeBorderWidth = 16;
    private _linkBoxHeight = 20 + 3; // link button height + margin
    private _titleHeight = 20;
    private _resizeUndo?: UndoManager.Batch;
    private _offset = { x: 0, y: 0 }; // offset from click pt to inner edge of resize border
    private _snapPt = { x: 0, y: 0 }; // last snapped location of resize border
    private _inkDragDocs: { doc: Doc; x: number; y: number; width: number; height: number }[] = [];
    private _interactionLock?: boolean;

    @observable _showNothing = true;
    @observable private _accumulatedTitle = '';
    @observable private _titleControlString: string = '$title';
    @observable private _editingTitle = false;
    @observable private _hidden = false;
    @observable private _isRotating: boolean = false;
    @observable private _isRounding: boolean = false;
    @observable private _showLayoutAcl: boolean = false;
    @observable private _showRotCenter = false; // whether to show a draggable green dot that represents the center of rotation
    @observable private _rotCenter = [0, 0]; // the center of rotation in object coordinates (0,0) = object center (not top left!)

    constructor(props: React.PropsWithChildren<DocumentDecorationsProps>) {
        super(props);
        makeObservable(this);

        DocumentDecorations.Instance = this;
        document.addEventListener('pointermove', // show decorations whenever pointer moves outside of selection bounds.
            action(e => {
                const center = {x: (this.Bounds.x+this.Bounds.r)/2, y: (this.Bounds.y+this.Bounds.b)/2};
                const {x,y} = Utils.rotPt(e.clientX - center.x, 
                                          e.clientY - center.y, 
                                          NumCast(SelectionManager.Views.lastElement()?.screenToViewTransform().Rotate));
                (this._showNothing = !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
                    (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || 
                     this.Bounds.y > center.y+y || this.Bounds.b < center.y+y )));
            })); // prettier-ignore
    }

    @computed get ClippedBounds() {
        const bounds = { ...this.Bounds };
        const leftBounds = this._props.boundsLeft;
        const topBounds = LightboxView.LightboxDoc ? 0 : this._props.boundsTop;
        bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
        bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
        const borderRadiusDraggerWidth = 15;
        bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
        bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
        return bounds;
    }

    @computed get Bounds() {
        return (SnappingManager.IsLinkFollowing || SnappingManager.ExploreMode) ? 
            { x: 0, y: 0, r: 0, b: 0 }
            : SelectionManager.Views
                .filter(dv => dv._props.renderDepth > 0)
                .map(dv => dv.getBounds)
                .reduce((bounds, rect) => !rect ? bounds
                            : { x: Math.min(rect.left, bounds.x),
                                y: Math.min(rect.top, bounds.y),
                                r: Math.max(rect.right, bounds.r),
                                b: Math.max(rect.bottom, bounds.b)},
                    { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); // prettier-ignore
    }

    @action
    titleBlur = () => {
        if (this._accumulatedTitle.startsWith('$')) {
            this._titleControlString = this._accumulatedTitle;
        } else if (this._titleControlString.startsWith('$')) {
            if (this._accumulatedTitle.startsWith('-->#')) {
                SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`)));
            }
            const titleFieldKey = this._titleControlString.substring(1);
            UndoManager.RunInBatch(
                () =>
                    titleFieldKey &&
                    SelectionManager.Views.forEach(d => {
                        if (titleFieldKey === 'title') {
                            d.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-');
                            if (StrCast(d.Document.title).startsWith('@') && !this._accumulatedTitle.startsWith('@')) {
                                Doc.RemFromMyPublished(d.Document);
                            }
                            if (!StrCast(d.Document.title).startsWith('@') && this._accumulatedTitle.startsWith('@')) {
                                Doc.AddToMyPublished(d.Document);
                            }
                        }
                        KeyValueBox.SetField(d.Document, titleFieldKey, this._accumulatedTitle);
                    }),
                'edit title'
            );
        }
    };

    titleEntered = (e: React.KeyboardEvent) => {
        if (e.key === 'Enter') {
            e.stopPropagation();
            (e.target as any).blur();
        }
    };

    onContainerDown = (e: React.PointerEvent) => {
        const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
        if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
            setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), emptyFunction, emptyFunction);
            e.stopPropagation();
        }
    };

    onTitleDown = (e: React.PointerEvent) => {
        const effectiveLayoutAcl = GetEffectiveAcl(SelectionManager.Views[0].Document);
        if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
            setupMoveUpEvents(
                this,
                e,
                e => this.onBackgroundMove(true, e),
                emptyFunction,
                action(e => {
                    const selected = SelectionManager.Views.length === 1 ? SelectionManager.Docs[0] : undefined;
                    !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString);
                    this._editingTitle = true;
                    this._keyinput.current && setTimeout(this._keyinput.current.focus);
                })
            );
            e.stopPropagation();
        }
    };

    onBackgroundDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction);
        e.stopPropagation();
    };
    @action
    onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
        const dragDocView = SelectionManager.Views[0];
        const effectiveLayoutAcl = GetEffectiveAcl(dragDocView.Document);
        if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) {
            return false;
        }
        const containers = new Set<Doc | undefined>();
        SelectionManager.Views.forEach(v => containers.add(DocCast(v.Document.embedContainer)));
        if (containers.size > 1) return false;
        const { left, top } = dragDocView.getBounds || { left: 0, top: 0 };
        const dragData = new DragManager.DocumentDragData(
            SelectionManager.Views.map(dv => dv.Document),
            dragDocView._props.dropAction
        );
        dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top);
        dragData.moveDocument = dragDocView._props.moveDocument;
        dragData.removeDocument = dragDocView._props.removeDocument;
        dragData.isDocDecorationMove = true;
        dragData.canEmbed = dragTitle;
        this._hidden = true;
        DragManager.StartDocumentDrag(
            SelectionManager.Views.map(dv => dv.ContentDiv!),
            dragData,
            e.x,
            e.y,
            {
                dragComplete: action(e => (this._hidden = false)),
                hideSource: true,
            }
        );
        return true;
    };

    _deleteAfterIconify = false;
    _iconifyBatch: UndoManager.Batch | undefined;
    onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
        const views = SelectionManager.Views.filter(v => v && v._props.renderDepth > 0);
        if (forceDeleteOrIconify === false && this._iconifyBatch) return;
        this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
        var iconifyingCount = views.length;
        const finished = action((force?: boolean) => {
            if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
                if (this._deleteAfterIconify) {
                    views.forEach(iconView => {
                        Doc.setNativeView(iconView.Document);
                        if (iconView.Document.activeFrame) {
                            iconView.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
                        } else {
                            iconView._props.removeDocument?.(iconView.Document);
                        }
                    });
                    views.forEach(SelectionManager.DeselectView);
                }
                this._iconifyBatch?.end();
                this._iconifyBatch = undefined;
            }
        });
        if (!this._iconifyBatch) {
            (document.activeElement as any).blur?.();
            this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying');
        } else {
            forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
        }

        if (forceDeleteOrIconify) finished(forceDeleteOrIconify);
        else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
    };

    onMaximizeDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(this, e, () => DragManager.StartWindowDrag?.(e, [SelectionManager.Views.lastElement().Document]) ?? false, emptyFunction, this.onMaximizeClick, false, false);
        e.stopPropagation();
    };
    onMaximizeClick = (e: any): void => {
        const selectedDocs = SelectionManager.Views;
        if (selectedDocs.length) {
            if (e.ctrlKey) {
                // open an embedding in a new tab with Ctrl Key
                CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].Document), OpenWhereMod.right);
            } else if (e.shiftKey) {
                // open centered in a new workspace with Shift Key
                const embedding = Doc.MakeEmbedding(selectedDocs[0].Document);
                embedding.embedContainer = undefined;
                embedding.x = -NumCast(embedding._width) / 2;
                embedding.y = -NumCast(embedding._height) / 2;
                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
            } else if (e.altKey) {
                // open same document in new tab
                CollectionDockingView.ToggleSplit(selectedDocs[0].Document, OpenWhereMod.right);
            } else {
                var openDoc = selectedDocs[0].Document;
                if (openDoc.layout_fieldKey === 'layout_icon') {
                    openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                    Doc.deiconifyView(openDoc);
                }
                LightboxView.Instance.SetLightboxDoc(
                    openDoc,
                    undefined,
                    selectedDocs.slice(1).map(view => view.Document)
                );
            }
        }
        SelectionManager.DeselectAll();
    };

    onIconifyClick = (): void => {
        SelectionManager.Views.forEach(dv => dv?.iconify());
        SelectionManager.DeselectAll();
    };

    onSelectContainerDocClick = () => SelectionManager.Views?.[0]?.containerViewPath?.().lastElement()?.select(false);
    /**
     * sets up events when user clicks on the border radius editor
     */
    @action
    onRadiusDown = (e: React.PointerEvent): void => {
        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement());
        this._isRounding = true;
        this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
        setupMoveUpEvents(
            this,
            e,
            e => {
                const [x, y] = [this.Bounds.x + 3, this.Bounds.y + 3];
                const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
                const dist = e.clientX < x && e.clientY < y ? 0 : Math.sqrt((e.clientX - x) * (e.clientX - x) + (e.clientY - y) * (e.clientY - y));
                SelectionManager.Docs.map(doc => {
                    const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
                    const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box
                    doc._layout_borderRounding = `${radius}px`;
                });
                return false;
            },
            action(e => {
                SnappingManager.SetIsResizing(undefined);
                this._isRounding = false;
                this._resizeUndo?.end();
            }), // upEvent
            emptyFunction,
            true
        );
        e.stopPropagation();
    };

    @action
    onLockDown = (e: React.PointerEvent): void => {
        setupMoveUpEvents(
            this,
            e,
            returnFalse, // don't care about move or up event,
            emptyFunction, // just care about whether we get a click event
            e => UndoManager.RunInBatch(() => SelectionManager.Docs.forEach(doc => Doc.toggleLockedPosition(doc)), 'toggleBackground')
        );
        e.stopPropagation();
    };

    setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
        const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]);
        const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
        const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
        seldocview.Document.rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
        seldocview.Document.rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
    };

    @action
    onRotateCenterDown = (e: React.PointerEvent): void => {
        this._isRotating = true;
        const seldocview = SelectionManager.Views[0];
        setupMoveUpEvents(
            this,
            e,
            (e: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events
                this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]) as any as boolean, 
            action(e => (this._isRotating = false)), // upEvent
            action(e => (seldocview.Document.rotation_centerX = seldocview.Document.rotation_centerY = 0))
        ); // prettier-ignore
    };

    @action
    onRotateDown = (e: React.PointerEvent): void => {
        this._isRotating = true;
        const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
        const rotateUndo = UndoManager.StartBatch('drag rotation');
        const selectedInk = SelectionManager.Views.filter(i => i.ComponentView instanceof InkingStroke);
        const centerPoint = this.rotCenter.slice();
        const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
        const seldocview = SelectionManager.Views[0];
        SelectionManager.Views.forEach(dv => {
            const accumRot = (NumCast(dv.Document._rotation) / 180) * Math.PI;
            const localRotCtr = dv.screenToViewTransform().transformPoint(rcScreen.X, rcScreen.Y);
            const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document.width) / 2, localRotCtr[1] - NumCast(dv.Document.height) / 2];
            const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot);
            const unrotatedDocPos = { x: NumCast(dv.Document.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.Document.y) + localRotCtrOffset[1] - startRotCtr.y };
            infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot });
        });
        const infoRot = (angle: number, isAbs = false) => {
            SelectionManager.Views.forEach(
                action(dv => {
                    const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.Document)!;
                    const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
                    infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
                    dv.Document.x = infos.get(dv.Document)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
                    dv.Document.y = infos.get(dv.Document)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
                    dv.Document._rotation = ((isAbs ? 0 : NumCast(dv.Document._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
                })
            );
        };
        setupMoveUpEvents(
            this,
            e,
            (e: PointerEvent, down: number[], delta: number[]) => {
                const previousPoint = { X: e.clientX, Y: e.clientY };
                const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
                const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
                if (selectedInk.length) {
                    deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen);
                    this.setRotateCenter(seldocview, centerPoint);
                } else {
                    infoRot(deltaAng);
                }
                return false;
            }, // moveEvent
            action(() => {
                const oldRotation = NumCast(seldocview.Document._rotation);
                const diff = oldRotation - Math.round(oldRotation / 45) * 45;
                if (Math.abs(diff) < 5) {
                    if (selectedInk.length) {
                        InkStrokeProperties.Instance.rotateInk(selectedInk, ((Math.round(oldRotation / 45) * 45 - oldRotation) / 180) * Math.PI, rcScreen);
                    } else {
                        infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
                    }
                }
                if (selectedInk.length) {
                    this.setRotateCenter(seldocview, centerPoint);
                }
                this._isRotating = false;
                rotateUndo?.end();
            }), // upEvent
            action(e => (this._showRotCenter = !this._showRotCenter)) // clickEvent
        );
    };

    @action
    onPointerDown = (e: React.PointerEvent): void => {
        SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
        setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
        e.stopPropagation();
        this._resizeHdlId = e.currentTarget.className;
        const bounds = e.currentTarget.getBoundingClientRect();
        this._offset = { x: this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX, y: this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY };
        this._resizeUndo = UndoManager.StartBatch('drag resizing');
        this._snapPt = { x: e.pageX, y: e.pageY };
        SelectionManager.Views.forEach(docView => docView.CollectionFreeFormView?.dragStarting(false, false));
    };

    projectDragToAspect = (e: PointerEvent, docView: DocumentView, fixedAspect: number) => {
        // need to generalize for bl and tr drag handles
        const project = (p: number[], a: number[], b: number[]) => {
            const atob = [b[0] - a[0], b[1] - a[1]];
            const atop = [p[0] - a[0], p[1] - a[1]];
            const len = atob[0] * atob[0] + atob[1] * atob[1];
            let dot = atop[0] * atob[0] + atop[1] * atob[1];
            const t = dot / len;
            dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
            return [a[0] + atob[0] * t, a[1] + atob[1] * t];
        };
        const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0);
        return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]);
    };
    onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
        const first = SelectionManager.Views[0];
        const effectiveAcl = GetEffectiveAcl(first.Document);
        if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false;
        if (!first) return false;
        var fixedAspect = Doc.NativeAspect(first.layoutDoc);
        const dragHdl = this._resizeHdlId.split(' ')[0].replace('documentDecorations-', '').replace('Resizer', '');

        const thisPt = // do snapping of drag point
            fixedAspect && (dragHdl === 'bottomRight' || dragHdl === 'topLeft')
                ? DragManager.snapDragAspect(this.projectDragToAspect(e, first, fixedAspect), fixedAspect)
                : DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y);

        const { scale, refPt } = this.getResizeVals(thisPt, dragHdl);

        !this._interactionLock && runInAction(async () => {   // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
            this._interactionLock = true;
            this._snapPt = thisPt;
            e.ctrlKey && (SelectionManager.Views.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
            const fixedAspect = SelectionManager.Docs.some(this.hasFixedAspect);
            const scaleAspect = {x:scale.x === 1 && fixedAspect ? scale.y : scale.x, y: scale.x !== 1 && fixedAspect ? scale.x : scale.y};
            SelectionManager.Views.forEach(docView => 
                this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
            await new Promise<any>(res => setTimeout(() => res(this._interactionLock = undefined)));
        }); // prettier-ignore

        return false;
    };

    //
    // determines how much to resize, and determines the resize reference point
    //
    getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => {
        const [w, h] = [this.Bounds.r - this.Bounds.x, this.Bounds.b - this.Bounds.y];
        const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y];
        switch (dragHdl) {
            case 'topLeft':    return { scale: { x: 1 - moveX / w, y: 1 -moveY / h },  refPt: [this.Bounds.r, this.Bounds.b] };
            case 'topRight':   return { scale: { x: 1 + moveX / w, y: 1 -moveY / h },  refPt: [this.Bounds.x, this.Bounds.b] };
            case 'top':        return { scale: { x: 1,             y: 1 -moveY / h },  refPt: [this.Bounds.x, this.Bounds.b] };
            case 'left':       return { scale: { x: 1 - moveX / w, y: 1 },             refPt: [this.Bounds.r, this.Bounds.y] };
            case 'bottomLeft': return { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.Bounds.r, this.Bounds.y] };
            case 'right':      return { scale: { x: 1 + moveX / w, y: 1 },             refPt: [this.Bounds.x, this.Bounds.y] };
            case 'bottomRight':return { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.Bounds.x, this.Bounds.y] };
            case 'bottom':     return { scale: { x: 1,             y: 1 + moveY / h }, refPt: [this.Bounds.x, this.Bounds.y] };
            default:           return { scale: { x: 1,             y: 1 },             refPt: [this.Bounds.x, this.Bounds.y] };
        } // prettier-ignore
    };

    //
    // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly)
    //
    hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));

    //
    // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc
    //
    resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; ctrlKey: boolean }) => {
        const doc = docView.Document;
        if (doc.isGroup) {
            DocListCast(doc.data)
                .map(member => DocumentManager.Instance.getDocumentView(member, docView)!)
                .forEach(member => this.resizeView(member, refPt, scale, opts));
            doc.xPadding = NumCast(doc.xPadding) * scale.x;
            doc.yPadding = NumCast(doc.yPadding) * scale.y;
        } else {
            const refCent = docView.screenToViewTransform().transformPoint(refPt[0], refPt[1]); // fixed reference point for resize (ie, a point that doesn't move)
            const [nwidth, nheight] = [docView.nativeWidth, docView.nativeHeight];
            const [initWidth, initHeight] = [NumCast(doc._width, 1), NumCast(doc._height)];

            const modifyNativeDim =
                (opts.ctrlKey && doc._layout_nativeDimEditable) || // e.g., PDF or web page
                (doc._layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages
                (doc._layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf
            if (nwidth && nheight && !modifyNativeDim) {
                // eg., dragging right resizer on PDF -- enforce native dimensions because not expliclty overridden with ctrl or bottom resize drag
                scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x);
            }

            if (['right', 'left'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeWidth(doc)) {
                const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth;
                doc.nativeWidth = scale.x * Doc.NativeWidth(doc);
                if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth));
                if (doc._layout_reflowVertical && !NumCast(doc.nativeHeight)) {
                    doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF
                }
            }
            if (['bottom', 'top'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeHeight(doc)) {
                const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight;
                doc._nativeHeight = scale.y * Doc.NativeHeight(doc);
                if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight));
            }

            doc._width = Math.max(1, NumCast(doc._width) * scale.x);
            doc._height = Math.max(1, NumCast(doc._height) * scale.y);
            const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth, initHeight);
            doc.x = NumCast(doc.x) + deltaX;
            doc.y = NumCast(doc.y) + deltaY;

            doc._layout_modificationDate = new DateField();
            if (scale.y !== 1) {
                docView.layoutDoc._layout_autoHeight = undefined;
                if (docView.layoutDoc._layout_autoHeight) {
                    // if autoHeight is still on because of a prototype
                    docView.layoutDoc._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
                }
            }
        }
    };

    // This realigns the doc's resize reference point with where it was before resizing it.
    // This is needed, because the transformation for doc's with a rotation is screwy:
    //    the top left of the doc is the 'origin', but the rotation happens about the center of the Doc.
    // So resizing a rotated doc will cause it to shift -- this counteracts that shift by determine how
    // the reference points shifted, and returning a translation to restore the reference point.
    realignRefPt = (doc: Doc, refCent: number[], initWidth: number, initHeight: number) => {
        const refCentPct = [refCent[0] / initWidth, refCent[1] / initHeight];
        const rotRefStart = Utils.rotPt(
            refCent[0] - initWidth / 2, // rotate reference pointe before scaling
            refCent[1] - initHeight / 2,
            (NumCast(doc._rotation) / 180) * Math.PI
        );
        const rotRefEnd = Utils.rotPt(
            refCentPct[0] * NumCast(doc._width) - NumCast(doc._width) / 2, // rotate reference point after scaling
            refCentPct[1] * NumCast(doc._height) - NumCast(doc._height) / 2,
            (NumCast(doc._rotation) / 180) * Math.PI
        );
        return {
            deltaX: rotRefStart.x + initWidth / 2 - (rotRefEnd.x + NumCast(doc._width) / 2), //
            deltaY: rotRefStart.y + initHeight / 2 - (rotRefEnd.y + NumCast(doc._height) / 2),
        };
    };

    @action
    onPointerUp = (e: PointerEvent): void => {
        SnappingManager.SetIsResizing(undefined);
        SnappingManager.clearSnapLines();
        this._resizeHdlId = '';
        this._resizeUndo?.end();

        // detect layout_autoHeight gesture and apply
        SelectionManager.Views.forEach(view => NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true));
        //need to change points for resize, or else rotation/control points will fail.
        this._inkDragDocs
            .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
            .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
                doc[DocData].data = new InkField(inkPts.map(
                    (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth)
                        X: NumCast(doc.x) - x + (NumCast(doc.width) * ipt.X) / width,
                        Y: NumCast(doc.y) - y + (NumCast(doc.height) * ipt.Y) / height,
                    }))); // prettier-ignore
                Doc.SetNativeWidth(doc, undefined);
                Doc.SetNativeHeight(doc, undefined);
            });
    };

    @computed
    get selectionTitle(): string {
        if (SelectionManager.Views.length === 1) {
            const selected = SelectionManager.Views[0];
            if (this._titleControlString.startsWith('$')) {
                return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-';
            }
            return this._accumulatedTitle;
        }
        return SelectionManager.Views.length > 1 ? '-multiple-' : '-unset-';
    }

    @computed get rotCenter() {
        const lastView = SelectionManager.Views.lastElement();
        if (lastView) {
            const invXf = lastView.screenToContentsTransform().inverse();
            const seldoc = lastView.layoutDoc;
            const loccenter = Utils.rotPt(NumCast(seldoc.rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc.rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
            return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2);
        }
        return this._rotCenter;
    }

    render() {
        const { b, r, x, y } = this.Bounds;
        const seldocview = SelectionManager.Views.lastElement();
        if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
            setTimeout(
                action(() => {
                    this._editingTitle = false;
                    this._showNothing = true;
                })
            );
            return null;
        }

        // sharing
        const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document);
        const docShareMode = HierarchyMapping.get(acl)!.name;
        const shareMode = StrCast(docShareMode);
        var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image;

        // hide the decorations if the parent chooses to hide it or if the document itself hides it
        const hideDecorations = SnappingManager.IsResizing || seldocview._props.hideDecorations || seldocview.Document.layout_hideDecorations;
        const hideResizers =
            ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.Document)) || hideDecorations || seldocview._props.hideResizeHandles || seldocview.Document.layout_hideResizeHandles || this._isRounding || this._isRotating;
        const hideTitle = this._showNothing || hideDecorations || seldocview._props.hideDecorationTitle || seldocview.Document.layout_hideDecorationTitle || this._isRounding || this._isRotating;
        const hideDocumentButtonBar = hideDecorations || seldocview._props.hideDocumentButtonBar || seldocview.Document.layout_hideDocumentButtonBar || this._isRounding || this._isRotating;
        // if multiple documents have been opened at the same time, then don't show open button
        const hideOpenButton =
            this._showNothing ||
            hideDecorations ||
            seldocview._props.hideOpenButton ||
            seldocview.Document.layout_hideOpenButton ||
            SelectionManager.Views.some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
            this._isRounding ||
            this._isRotating;
        const hideDeleteButton =
            this._showNothing ||
            hideDecorations ||
            this._isRounding ||
            this._isRotating ||
            seldocview._props.hideDeleteButton ||
            seldocview.Document.hideDeleteButton ||
            SelectionManager.Views.some(docView => {
                const collectionAcl = docView.containerViewPath?.()?.lastElement() ? GetEffectiveAcl(docView.containerViewPath?.().lastElement().dataDoc) : AclEdit;
                return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.Document) !== AclAdmin;
            });
        const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
            <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
                <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}>
                    <FontAwesomeIcon icon={icon as any} />
                </div>
            </Tooltip>
        );

        const bounds = this.ClippedBounds;
        const useLock = bounds.r - bounds.x > 135;
        const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && seldocview.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
        const rotation = SelectionManager.Views.length == 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;

        // Radius constants
        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
        const borderRadius = numberValue(Cast(seldocview.Document.layout_borderRounding, 'string', null));
        const docMax = Math.min(NumCast(seldocview.Document.width) / 2, NumCast(seldocview.Document.height) / 2);
        const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
        const radiusHandle = (borderRadius / docMax) * maxDist;
        const radiusHandleLocation = Math.min(radiusHandle, maxDist);

        const sharingMenu =
            Doc.IsSharingEnabled && docShareMode ? (
                <div className="documentDecorations-share">
                    <div className={`documentDecorations-share${shareMode}`}>
                        &nbsp;
                        {shareSymbolIcon + ' ' + shareMode}
                        &nbsp;
                        {/* {!Doc.noviceMode ? (
                        <div className="checkbox">
                            <div className="checkbox-box">
                                <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} />
                            </div>
                            <div className="checkbox-text"> Layout </div>
                        </div>
                    ) : null}
                    &nbsp; */}
                    </div>
                </div>
            ) : null;

        const titleArea = this._editingTitle ? (
            <>
                {r - x < 150 ? null : <span>{this._titleControlString + ':'}</span>}
                <input
                    ref={this._keyinput}
                    className="documentDecorations-title"
                    type="text"
                    name="dynbox"
                    autoComplete="on"
                    value={hideTitle ? '' : this._accumulatedTitle}
                    onBlur={action((e: React.FocusEvent) => {
                        this._editingTitle = false;
                        !hideTitle && this.titleBlur();
                    })}
                    onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
                    onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
                    onPointerDown={e => e.stopPropagation()}
                />
            </>
        ) : (
            <div className="documentDecorations-title" key="title">
                {hideTitle ? null : (
                    <span className="documentDecorations-titleSpan" onPointerDown={this.onTitleDown}>
                        {this.selectionTitle}
                    </span>
                )}
                {sharingMenu}
                {!useLock ? null : (
                    <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top">
                        <div className="documentDecorations-lock" style={{ color: seldocview.Document._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown}>
                            <FontAwesomeIcon size="sm" icon="lock" />
                        </div>
                    </Tooltip>
                )}
            </div>
        );
        const centery = hideTitle ? 0 : this._titleHeight;
        const transformOrigin = `${50}% calc(50% + ${centery / 2}px)`;
        const freeformDoc = SelectionManager.Views.some(v => v.CollectionFreeFormDocumentView);
        return (
            <div className="documentDecorations" style={{ display: this._showNothing && !freeformDoc ? 'none' : undefined }}>
                <div
                    className="documentDecorations-background"
                    style={{
                        width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                        height: bounds.b - bounds.y + this._resizeBorderWidth + 'px',
                        left: bounds.x - this._resizeBorderWidth / 2,
                        top: bounds.y - this._resizeBorderWidth / 2,
                        transformOrigin,
                        background: SnappingManager.ShiftKey ? undefined : 'yellow',
                        pointerEvents: SnappingManager.ShiftKey || SnappingManager.IsResizing ? 'none' : 'all',
                        display: SelectionManager.Views.length <= 1 || hideDecorations ? 'none' : undefined,
                        transform: `rotate(${rotation}deg)`,
                    }}
                    onPointerDown={this.onBackgroundDown}
                />
                {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? null : (
                    <div>
                        <div
                            className={`documentDecorations-container  ${this._showNothing ? 'showNothing' : ''}`}
                            style={{
                                transform: `translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`,
                                transformOrigin,
                                width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                height: bounds.b - bounds.y + this._resizeBorderWidth + (this._showNothing ? 0 : this._titleHeight) + 'px',
                            }}>
                            <div
                                className="documentDecorations-topbar"
                                style={{
                                    display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined,
                                }}
                                onPointerDown={this.onContainerDown}>
                                {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
                                {titleArea}
                                {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
                            </div>
                            {hideResizers ? null : (
                                <>
                                    <div key="tl" className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} />
                                    <div key="t" className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} />
                                    <div key="tr" className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} />
                                    <div key="l" className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} />
                                    <div key="c" className="documentDecorations-centerCont" />
                                    <div key="r" className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} />
                                    <div key="bl" className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} />
                                    <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} />
                                    <div key="br" className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} />
                                </>
                            )}
                            {seldocview._props.renderDepth <= 1 || !seldocview.containerViewPath?.().lastElement() ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectContainerDocClick, 'tap to select containing document')}
                            {useRounding && (
                                <div
                                    className="documentDecorations-borderRadius"
                                    style={{
                                        background: `${this._isRounding ? Colors.MEDIUM_BLUE : lightOrDark(StrCast(seldocview.layoutDoc._backgroundColor, SettingsManager.userColor))}`,
                                        transform: `translate(${radiusHandleLocation ?? 0}px, ${(radiusHandleLocation ?? 0) + (this._showNothing ? 0 : this._titleHeight)}px)`,
                                    }}
                                    onPointerDown={this.onRadiusDown}
                                />
                            )}

                            {hideDocumentButtonBar || this._showNothing ? null : (
                                <div
                                    className="link-button-container"
                                    style={{
                                        transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
                                    }}>
                                    <DocumentButtonBar views={() => SelectionManager.Views} />
                                </div>
                            )}
                        </div>

                        {useRotation && (
                            <>
                                <div
                                    style={{
                                        position: 'absolute',
                                        transform: `rotate(${rotation}deg)`,
                                        width: this.Bounds.r - this.Bounds.x + 'px',
                                        height: this.Bounds.b - this.Bounds.y + 'px',
                                        left: this.Bounds.x,
                                        top: this.Bounds.y,
                                        pointerEvents: 'none',
                                    }}>
                                    {this._isRotating ? null : (
                                        <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}>
                                            <div className="documentDecorations-rotation" onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
                                                <IconButton icon={<FaUndo />} color={SettingsManager.userColor} />
                                            </div>
                                        </Tooltip>
                                    )}
                                </div>
                                {!this._showRotCenter ? null : (
                                    <div
                                        className="documentDecorations-rotationCenter"
                                        style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
                                        onPointerDown={this.onRotateCenterDown}
                                        onContextMenu={e => e.preventDefault()}
                                    />
                                )}
                            </>
                        )}
                    </div>
                )}
            </div>
        );
    }
}
