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 { copyProps, unimplementedFunction, Utils } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import { DragManager } from '../util/DragManager';
import { FollowLinkScript } from '../util/LinkFollower';
import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import './MarqueeAnnotator.scss';
import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { AnchorMenu } from './pdf/AnchorMenu';
const _global = (window /* browser */ || global) /* node */ as any;

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

    _prevProps: React.PropsWithChildren<MarqueeAnnotatorProps>;
    @observable _props: React.PropsWithChildren<MarqueeAnnotatorProps>;
    constructor(props: React.PropsWithChildren<MarqueeAnnotatorProps>) {
        super(props);
        this._props = this._prevProps = props;
        makeObservable(this);
    }
    componentDidMount() {
        copyProps(this);
    }

    @action
    static clearAnnotations(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.docView().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.HTMLMarkerDocument([], {
            annotationOn: this.props.Document,
            text: this.props.selectionText(),
            backgroundColor: 'transparent',
            presentation_duration: 2100,
            presentation_transition: 500,
            presentation_zoomText: true,
            title: 'Selection on ' + this.props.Document.title,
        });
        let minX = Number.MAX_VALUE;
        let maxX = -Number.MAX_VALUE;
        let minY = Number.MAX_VALUE;
        let maxY = -Number.MIN_VALUE;
        const annoDocs: Doc[] = [];
        savedAnnoMap.forEach((value: HTMLDivElement[], key: number) =>
            value.map(anno => {
                const textRegion = new Doc();
                textRegion.x = parseInt(anno.style.left ?? '0');
                textRegion.y = parseInt(anno.style.top ?? '0');
                textRegion._height = parseInt(anno.style.height ?? '0');
                textRegion._width = parseInt(anno.style.width ?? '0');
                textRegion.annoTextRegion = textRegionAnno;
                textRegion.backgroundColor = color;
                annoDocs.push(textRegion);
                anno.remove();
                minY = Math.min(NumCast(textRegion.y), minY);
                minX = Math.min(NumCast(textRegion.x), minX);
                maxY = Math.max(NumCast(textRegion.y) + NumCast(textRegion._height), maxY);
                maxX = Math.max(NumCast(textRegion.x) + NumCast(textRegion._width), maxX);
            })
        );

        const textRegionAnnoProto = Doc.GetProto(textRegionAnno);
        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);
        // mainAnnoDocProto.text = this._selectionText;
        textRegionAnnoProto.text_inlineAnnotations = new List<Doc>(annoDocs);
        savedAnnoMap.clear();
        return textRegionAnno;
    };
    @action
    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: 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]);
    });

    getTransformedScreenPt = (down: number[]) => {
        const boundingRect = this.props.mainCont.getBoundingClientRect();
        const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
        const downPt = Utils.rotPt(down[0] - center.x, down[1] - center.y, NumCast(this.props.docView().screenToLocalTransform().Rotate));
        const scale = this.props.docView().props.ScreenToLocalTransform().Scale;
        const scalex = this.props.mainCont.offsetWidth / NumCast(this.props.Document.width);
        const scaley = this.props.mainCont.offsetHeight / NumCast(this.props.Document.height);
        // set marquee x and y positions to the spatially transformed position
        return { x: scalex * (downPt.x + NumCast(this.props.Document.width) / scale / 2) * scale, 
                 y: scaley * (downPt.y + NumCast(this.props.Document.height) / scale / 2) * scale + 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 = (e: PointerEvent) => {
            if (this.props.anchorMenuCrop) {
                UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
            }
        };
        AnchorMenu.Instance.OnClick = undoable((e: PointerEvent) => 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 = this.highlight;
        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, undefined, annotationOn, undefined, 'yellow');
                FormattedTextBox.SetSelectOnLoad(target);
                return target;
            };
            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                dragComplete: e => {
                    if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
                        e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document;
                        e.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();
                  var cropRegion: Doc | undefined;
                  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: e => {
                          if (!e.aborted && e.linkDocument) {
                              Doc.GetProto(e.linkDocument).link_relationship = 'cropped image';
                              Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView().Document.title;
                              Doc.GetProto(e.linkDocument).link_displayLine = false;
                          }
                      },
                  });
              });
    }
    public onTerminateSelection() {
        document.removeEventListener('pointermove', this.onSelectMove);
        document.removeEventListener('pointerup', this.onSelectEnd);
    }

    @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.
    };

    @action
    onSelectEnd = (e: PointerEvent) => {
        e.stopPropagation();
        const marquees = this.props.mainCont.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.docView().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(e.clientX, e.clientY);
        }
        this.props.finishMarquee(this.isEmpty ? e.clientX : undefined, this.isEmpty ? e.clientY : undefined, e);
        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'}`,
                }}
            />
        );
    }
}
