import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, Opt } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
import { NumCast } from '../../fields/Types';
import { GetEffectiveAcl } from '../../fields/util';
import { unimplementedFunction, Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
import { DocUtils, FollowLinkScript } from '../documents/DocUtils';
import { DragManager } from '../util/DragManager';
import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import './MarqueeAnnotator.scss';
import { DocumentView } from './nodes/DocumentView';
import { ObservableReactComponent } from './ObservableReactComponent';
import { AnchorMenu } from './pdf/AnchorMenu';

export interface MarqueeAnnotatorProps {
    Document: Doc;
    down?: number[];
    scrollTop: number;
    isNativeScaled?: boolean;
    scaling?: () => number;
    annotationLayerScaling?: () => number;
    annotationLayerScrollTop: number;
    containerOffset?: () => number[];
    marqueeContainer: HTMLDivElement;
    docView: () => DocumentView;
    savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
    selectionText: () => string;
    annotationLayer: HTMLDivElement;
    addDocument: (doc: Doc) => boolean;
    getPageFromScroll?: (top: number) => number;
    finishMarquee: (x?: number, y?: number) => void;
    anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
    anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
    highlightDragSrcColor?: string;
}
@observer
export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorProps> {
    private _start: { x: number; y: number } = { x: 0, y: 0 };

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

    @observable private _width: number = 0;
    @observable private _height: number = 0;
    @computed get top() { return Math.min(this._start.y, this._start.y + this._height); } // prettier-ignore
    @computed get left() { return Math.min(this._start.x, this._start.x + this._width);} // prettier-ignore

    static clearAnnotations = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>) => {
        AnchorMenu.Instance.Status = 'marquee';
        AnchorMenu.Instance.fadeOut(true);
        // clear out old marquees and initialize menu for new selection
        Array.from(savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
        savedAnnotations.clear();
    });

    @undoBatch
    makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
        const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
        if (savedAnnoMap.size === 0) return undefined;
        const savedAnnos = Array.from(savedAnnoMap.values())[0];
        const doc = this.props.Document;
        const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
        if (savedAnnos.length && (savedAnnos[0] as any).marqueeing) {
            const anno = savedAnnos[0];
            const containerOffset = this.props.containerOffset?.() || [0, 0];
            const marqueeAnno = Docs.Create.FreeformDocument([], {
                onClick: isLinkButton ? FollowLinkScript() : undefined,
                backgroundColor: color,
                annotationOn: this.props.Document,
                title: 'Annotation on ' + this.props.Document.title,
            });
            marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
            marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
            marqueeAnno._height = parseInt(anno.style.height || '0') / scale;
            marqueeAnno._width = parseInt(anno.style.width || '0') / scale;
            anno.remove();
            savedAnnoMap.clear();
            return marqueeAnno;
        }

        const textRegionAnno = Docs.Create.ConfigDocument({
            annotationOn: this.props.Document,
            text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
            text_html: this.props.selectionText() as any,
            backgroundColor: 'transparent',
            presentation_duration: 2100,
            presentation_transition: 500,
            presentation_zoomText: true,
            title: '>' + this.props.Document.title,
        });
        const textRegionAnnoProto = textRegionAnno[DocData];
        let minX = Number.MAX_VALUE;
        let maxX = -Number.MAX_VALUE;
        let minY = Number.MAX_VALUE;
        let maxY = -Number.MIN_VALUE;
        const annoRects: string[] = [];
        savedAnnoMap.forEach((value: HTMLDivElement[]) =>
            value.forEach(anno => {
                const x = parseInt(anno.style.left ?? '0');
                const y = parseInt(anno.style.top ?? '0');
                const height = parseInt(anno.style.height ?? '0');
                const width = parseInt(anno.style.width ?? '0');
                annoRects.push(`${x}:${y}:${width}:${height}`);
                anno.remove();
                minY = Math.min(NumCast(y), minY);
                minX = Math.min(NumCast(x), minX);
                maxY = Math.max(NumCast(y) + NumCast(height), maxY);
                maxX = Math.max(NumCast(x) + NumCast(width), maxX);
            })
        );

        textRegionAnnoProto.y = Math.max(minY, 0);
        textRegionAnnoProto.x = Math.max(minX, 0);
        textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
        textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
        textRegionAnnoProto.backgroundColor = color;
        // mainAnnoDocProto.text = this._selectionText;
        textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
        textRegionAnnoProto.opacity = 0;
        textRegionAnnoProto.layout_unrendered = true;
        savedAnnoMap.clear();
        return textRegionAnno;
    };
    @action
    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
        // creates annotation documents for current highlights
        const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
        const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
        addAsAnnotation && annotationDoc && this.props.addDocument(annotationDoc);
        return annotationDoc as Doc;
    };

    public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => {
        div.style.backgroundColor = '#ACCEF7';
        div.style.opacity = '0.5';
        annotationLayer.append(div);
        const savedPage = savedAnnotations.get(page);
        if (savedPage) savedPage.push(div);
        savedAnnotations.set(page, savedPage ?? [div]);
    });

    // this transforms a screen point into a local coordinate subject.  It's complicated by documents that are rotated
    // since the DOM's bounding rectangle is not rotated and Dash's ScreenToLocalTransform carries along a rotation value, but doesn't
    // use it when transforming points.
    // So the idea here is to reconstruct a local point by unrotating the screen point about the center of the bounding box. The approach is:
    //    1) Get vector from the screen point to the center of the rotated bounding box in screens space
    //    2) unrotate that vector in screen space
    //    3) localize the unrotated vector by scaling into the marquee container's coordinates
    //    4) reattach the vector to the center of the bounding box
    getTransformedScreenPt = (down: number[]) => {
        const { marqueeContainer } = this.props;
        const containerXf = this.props.isNativeScaled ? this.props.docView().screenToContentsTransform() : this.props.docView().screenToViewTransform();
        const boundingRect = marqueeContainer.getBoundingClientRect();
        const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
        const downVec = Utils.rotPt(down[0] - center.x,  
                                    down[1] - center.y, NumCast(containerXf.Rotate)); // prettier-ignore
        return { x: downVec.x * containerXf.Scale + marqueeContainer.offsetWidth /2,
                 y: downVec.y * containerXf.Scale + marqueeContainer.offsetHeight/2 + this.props.annotationLayerScrollTop }; // prettier-ignore
    };

    @action
    public onInitiateSelection(down: number[]) {
        this._width = this._height = 0;
        this._start = this.getTransformedScreenPt(down);

        document.removeEventListener('pointermove', this.onSelectMove);
        document.removeEventListener('pointerup', this.onSelectEnd);
        document.addEventListener('pointermove', this.onSelectMove);
        document.addEventListener('pointerup', this.onSelectEnd);

        AnchorMenu.Instance.OnCrop = () => {
            if (this.props.anchorMenuCrop) {
                UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
            }
        };
        AnchorMenu.Instance.OnClick = undoable(() => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
        AnchorMenu.Instance.OnAudio = unimplementedFunction;
        AnchorMenu.Instance.Highlight = (color: string) => this.highlight(color, false, undefined, true);
        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]> /* , addAsAnnotation?: boolean */) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
        AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);

        /**
         * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
         * It also initiates a Drag/Drop interaction to place the text annotation.
         */
        AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
            e.preventDefault();
            e.stopPropagation();
            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color

            const targetCreator = (annotationOn: Doc | undefined) => {
                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
                Doc.SetSelectOnLoad(target);
                return target;
            };
            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                dragComplete: dragEv => {
                    if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
                        dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.props.Document;
                        dragEv.annoDragData.linkSourceDoc.followLinkZoom = false;
                    }
                },
            });
        });
        /**
         * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
         * It also initiates a Drag/Drop interaction to place the text annotation.
         */
        AnchorMenu.Instance.StartCropDrag = !this.props.anchorMenuCrop
            ? unimplementedFunction
            : action((e: PointerEvent, ele: HTMLElement) => {
                  e.preventDefault();
                  e.stopPropagation();
                  let cropRegion: Doc | undefined;
                  // eslint-disable-next-line no-return-assign
                  const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
                  const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!;
                  DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                      dragComplete: dragEx => {
                          if (!dragEx.aborted && dragEx.linkDocument) {
                              const linkDocData = dragEx.linkDocument[DocData];
                              linkDocData.link_relationship = 'cropped image';
                              linkDocData.title = 'crop: ' + this.props.Document.title;
                          }
                      },
                  });
              });
    }
    public onTerminateSelection() {
        document.removeEventListener('pointermove', this.onSelectMove);
        document.removeEventListener('pointerup', this.onSelectEnd);
    }

    @action
    onMove = (pt: number[]) => {
        const movLoc = this.getTransformedScreenPt(pt);
        this._width = movLoc.x - this._start.x;
        this._height = movLoc.y - this._start.y;
    };

    @action
    onSelectMove = (e: PointerEvent) => {
        const movLoc = this.getTransformedScreenPt([e.clientX, e.clientY]);
        this._width = movLoc.x - this._start.x;
        this._height = movLoc.y - this._start.y;
        // e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged.  if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor.
    };

    onSelectEnd = (e: PointerEvent) => {
        e.stopPropagation();
        this.onEnd(e.clientX, e.clientY);
    };
    @action
    onEnd = (x: number, y: number) => {
        AnchorMenu.Instance.setSelectedText('');
        const marquees = this.props.marqueeContainer.getElementsByClassName('marqueeAnnotator-dragBox');
        const marqueeStyle = (Array.from(marquees).lastElement() as HTMLDivElement)?.style;
        if (!this.isEmpty && marqueeStyle) {
            // configure and show the annotation/link menu if a the drag region is big enough
            // copy the temporary marquee to allow for multiple selections (not currently available though).
            const copy = document.createElement('div');
            const scale = (this.props.scaling?.() || 1) * NumCast(this.props.Document._freeform_scale, 1);
            ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => {
                copy.style[prop as any] = marqueeStyle[prop as any];
            });
            copy.className = 'marqueeAnnotator-annotationBox';
            copy.style.top = parseInt(marqueeStyle.top.toString().replace('px', '')) / scale + this.props.scrollTop + 'px';
            copy.style.left = parseInt(marqueeStyle.left.toString().replace('px', '')) / scale + 'px';
            copy.style.width = parseInt(marqueeStyle.width.toString().replace('px', '')) / scale + 'px';
            copy.style.height = parseInt(marqueeStyle.height.toString().replace('px', '')) / scale + 'px';
            (copy as any).marqueeing = true;
            MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations(), this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this.top) || 0);
            AnchorMenu.Instance.jumpTo(x, y);
        }
        this.props.finishMarquee(this.isEmpty ? x : undefined, this.isEmpty ? y : undefined);
        this._width = this._height = 0;
    };

    get isEmpty() {
        return Math.abs(this._width) <= 10 && Math.abs(this._height) <= 10;
    }

    render() {
        return (
            <div
                className="marqueeAnnotator-dragBox"
                style={{
                    left: `${this.left}px`,
                    top: `${this.top}px`,
                    width: `${Math.abs(this._width)}px`,
                    height: `${Math.abs(this._height)}px`,
                    border: `${this._width === 0 ? '' : '2px dashed black'}`,
                }}
            />
        );
    }
}
