import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { Id } from '../../../fields/FieldSymbols';
import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField, PdfField } from "../../../fields/URLField";
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { KeyCodes } from '../../util/KeyCodes';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
import { Colors } from '../global/globalEnums';
import { CreateImage } from "../nodes/WebBoxRenderer";
import { AnchorMenu } from '../pdf/AnchorMenu';
import { PDFViewer } from "../pdf/PDFViewer";
import { SidebarAnnos } from '../SidebarAnnos';
import { FieldView, FieldViewProps } from './FieldView';
import { ImageBox } from './ImageBox';
import "./PDFBox.scss";
import { VideoBox } from './VideoBox';
import React = require("react");

@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
    public static openSidebarWidth = 250;
    public static sidebarResizerWidth = 5;
    private _searchString: string = "";
    private _initialScrollTarget: Opt<Doc>;
    private _pdfViewer: PDFViewer | undefined;
    private _searchRef = React.createRef<HTMLInputElement>();
    private _selectReactionDisposer: IReactionDisposer | undefined;
    private _sidebarRef = React.createRef<SidebarAnnos>();

    @observable private _searching: boolean = false;
    @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
    @observable private _pageControls = false;

    @computed get pdfUrl() { return Cast(this.dataDoc[this.props.fieldKey], PdfField); }
    @computed get pdfThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; }

    constructor(props: any) {
        super(props);
        const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927;
        const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
        !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
        if (this.pdfUrl) {
            if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href));
            else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => this._pdf = pdf));
        }
    }

    replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
        if (oldDiv.childNodes) {
            for (let i = 0; i < oldDiv.childNodes.length; i++) {
                this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement);
            }
        }

        if (oldDiv.className === "pdfBox-ui" ||
            oldDiv.className === "pdfViewerDash-overlay-inking") {
            newDiv.style.display = "none";
        }
        if (newDiv && newDiv.style) newDiv.style.overflow = "hidden";
        if (oldDiv instanceof HTMLCanvasElement) {
            const canvas = oldDiv;
            const img = document.createElement('img'); // create a Image Element
            img.src = canvas.toDataURL();   //image sourcez
            img.style.width = canvas.style.width;
            img.style.height = canvas.style.height;
            const newCan = newDiv as HTMLCanvasElement;
            const parEle = newCan.parentElement as HTMLElement;
            parEle.removeChild(newCan);
            parEle.appendChild(img);
        }
    }

    crop = (region: Doc | undefined, addCrop?: boolean) => {
        if (!region) return;
        const cropping = Doc.MakeCopy(region, true);
        Doc.GetProto(region).lockedPosition = true;
        Doc.GetProto(region).title = "region:" + this.rootDoc.title;
        Doc.GetProto(region).isPushpin = true;
        this.addDocument(region);

        const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
        const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
        newDiv.style.width = (this.layoutDoc[WidthSym]()).toString();
        newDiv.style.height = (this.layoutDoc[HeightSym]()).toString();
        this.replaceCanvases(docViewContent, newDiv);
        const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);

        const anchx = NumCast(cropping.x);
        const anchy = NumCast(cropping.y);
        const anchw = cropping[WidthSym]() * (this.props.scaling?.() || 1);
        const anchh = cropping[HeightSym]() * (this.props.scaling?.() || 1);
        const viewScale = 1;
        cropping.title = "crop: " + this.rootDoc.title;
        cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
        cropping.y = NumCast(this.rootDoc.y);
        cropping._width = anchw;
        cropping._height = anchh;
        cropping.isLinkButton = undefined;
        const croppingProto = Doc.GetProto(cropping);
        croppingProto.annotationOn = undefined;
        croppingProto.isPrototype = true;
        croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
        croppingProto.type = DocumentType.IMG;
        croppingProto.layout = ImageBox.LayoutString("data");
        croppingProto.data = new ImageField(Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png"));
        croppingProto["data-nativeWidth"] = anchw;
        croppingProto["data-nativeHeight"] = anchh;
        if (addCrop) {
            DocUtils.MakeLink({ doc: region }, { doc: cropping }, "cropped image", "");
        }
        this.props.bringToFront(cropping);

        CreateImage(
            "",
            document.styleSheets,
            htmlString,
            anchw,
            anchh,
            NumCast(region.y) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]),
            NumCast(region.x) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]),
            4
        ).then
            ((data_url: any) => {
                VideoBox.convertDataUri(data_url, region[Id]).then(
                    returnedfilename => setTimeout(action(() => {
                        croppingProto.data = new ImageField(returnedfilename);
                    }), 500));
            })
            .catch(function (error: any) {
                console.error('oops, something went wrong!', error);
            });


        return cropping;
    }

    updateIcon = () => {
        return; // currently we render pdf icons as text labels
        const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
        const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
        newDiv.style.width = (this.layoutDoc[WidthSym]()).toString();
        newDiv.style.height = (this.layoutDoc[HeightSym]()).toString();
        this.replaceCanvases(docViewContent, newDiv);
        const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
        const nativeWidth = this.layoutDoc[WidthSym]();
        const nativeHeight = this.layoutDoc[HeightSym]();

        CreateImage(
            "",
            document.styleSheets,
            htmlString,
            nativeWidth,
            nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
            NumCast(this.layoutDoc._scrollTop) * this.props.PanelHeight() / NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"])
        ).then
            ((data_url: any) => {
                VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then(
                    returnedfilename => setTimeout(action(() => {
                        this.dataDoc.icon = new ImageField(returnedfilename);
                        this.dataDoc["icon-nativeWidth"] = nativeWidth;
                        this.dataDoc["icon-nativeHeight"] = nativeHeight;
                    }), 500));
            })
            .catch(function (error: any) {
                console.error('oops, something went wrong!', error);
            });
    }

    componentWillUnmount() { this._selectReactionDisposer?.(); }
    componentDidMount() {
        this.props.setContentView?.(this);
        this._selectReactionDisposer = reaction(() => this.props.isSelected(),
            () => {
                document.removeEventListener("keydown", this.onKeyDown);
                this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown);
            }, { fireImmediately: true });
    }

    scrollFocus = (doc: Doc, smooth: boolean) => {
        if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
            this.toggleSidebar(!smooth);
        }
        if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
        this._initialScrollTarget = doc;
        return this._pdfViewer?.scrollFocus(doc, smooth);
    }
    getAnchor = () => {
        const anchor =
            this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ??
            Docs.Create.TextanchorDocument({
                title: StrCast(this.rootDoc.title + "@" + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
                y: NumCast(this.layoutDoc._scrollTop),
                unrendered: true
            });
        this.addDocument(anchor);
        return anchor;
    }

    @action
    loaded = (nw: number, nh: number, np: number) => {
        this.dataDoc[this.props.fieldKey + "-numPages"] = np;
        Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), nw * 96 / 72));
        Doc.SetNativeHeight(this.dataDoc, nh * 96 / 72);
        this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1);
        !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
    }

    public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
        if (!this._searching && !clear) {
            this._searching = true;
            setTimeout(() => {
                this._searchRef.current?.focus();
                this._searchRef.current?.select();
                this._searchRef.current?.setRangeText(searchString);
            });
        }
        return this._pdfViewer?.search(searchString, bwd, clear) || false;
    });
    public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
    public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
    public backPage = () => {
        this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1);
        return true;
    }
    public forwardPage = () => {
        this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + "-numPages"]), (NumCast(this.Document._curPage) || 1) + 1);
        return true;
    }
    public gotoPage = (p: number) => this.Document._curPage = p;

    @undoBatch
    onKeyDown = action((e: KeyboardEvent) => {
        let processed = false;
        switch (e.key) {
            case "PageDown": processed = this.forwardPage(); break;
            case "PageUp": processed = this.backPage(); break;
        }
        if (processed) {
            e.stopImmediatePropagation();
            e.preventDefault();
        }
    });

    setPdfViewer = (pdfViewer: PDFViewer) => {
        this._pdfViewer = pdfViewer;
        if (this._initialScrollTarget) {
            this.scrollFocus(this._initialScrollTarget, false);
            this._initialScrollTarget = undefined;
        }
    }
    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;

    // adding external documents; to sidebar key
    // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
        if (!this.layoutDoc._showSidebar) this.toggleSidebar();
        return this.addDocument(doc, sidebarKey);
    }
    sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf
        const batch = UndoManager.StartBatch("sidebar");
        setupMoveUpEvents(this, e, (e, down, delta) => {
            const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]);
            const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
            const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
            const ratio = (curNativeWidth + (onButton ? 1 : -1) * localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth;
            if (ratio >= 1) {
                this.layoutDoc.nativeWidth = nativeWidth * ratio;
                onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
                this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
            }
            return false;
        }, () => batch.end(), () => this.toggleSidebar());
    }
    @observable _previewNativeWidth: Opt<number> = undefined;
    @observable _previewWidth: Opt<number> = undefined;
    toggleSidebar = action((preview: boolean = false) => {
        const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
        const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
        const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth;
        const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
        if (preview) {
            this._previewNativeWidth = nativeWidth * sideratio;
            this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * sideratio / curNativeWidth;
            this._showSidebar = true;
        }
        else {
            this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
            this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth;
            this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
        }
    });
    settingsPanel() {
        const pageBtns = <>
            <button className="pdfBox-backBtn" key="back" title="Page Back"
                onPointerDown={e => e.stopPropagation()} onClick={this.backPage} >
                <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
            </button>
            <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward"
                onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} >
                <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
            </button>
        </>;
        const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`;
        const curPage = NumCast(this.Document._curPage) || 1;
        return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? (null) :
            <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true}
                onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}>
                <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
                    <button className="pdfBox-overlayButton" title={searchTitle} />
                    <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged}
                        onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} />
                    <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
                        <FontAwesomeIcon icon="search" size="sm" />
                    </button>
                    <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} >
                        <FontAwesomeIcon icon={"arrow-up"} size="lg" />
                    </button>
                    <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} >
                        <FontAwesomeIcon icon={"arrow-down"} size="lg" />
                    </button>
                </div>
                <button className="pdfBox-overlayButton" title={searchTitle}
                    onClick={action(() => { this._searching = !this._searching; this.search("", true, true); })} >
                    <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} />
                    <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
                        <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" />
                    </div>
                </button>

                <div className="pdfBox-pageNums">
                    <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }}
                        onChange={e => this.Document._curPage = Number(e.currentTarget.value)}
                        onClick={action(() => this._pageControls = !this._pageControls)} />
                    {this._pageControls ? pageBtns : (null)}
                </div>
                <div className="pdfBox-sidebarBtn" key="sidebar" title="Toggle Sidebar"
                    style={{
                        display: !this.props.isContentActive() ? "none" : undefined,
                        top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5,
                        backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK
                    }}
                    onPointerDown={e => this.sidebarBtnDown(e, true)} >
                    <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
                </div>
            </div>;
    }
    sidebarWidth = () => !this.SidebarShown ? 0 :
        PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth :
            (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth))

    specificContextMenu = (e: React.MouseEvent): void => {
        const funcs: ContextMenuProps[] = [];
        funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(Utils.prepend("") + this.pdfUrl.url.pathname), icon: "expand-arrows-alt" });
        funcs.push({ description: "update icon", event: () => this.pdfUrl && this.updateIcon(), icon: "expand-arrows-alt" });
        //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
        ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
    }

    @computed get renderTitleBox() {
        const classname = "pdfBox" + (this.props.isContentActive() ? "-interactive" : "");
        return <div className={classname} >
            <div className="pdfBox-title-outer">
                <strong className="pdfBox-title" >{this.props.Document.title}</strong>
            </div>
        </div>;
    }

    anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
    @observable _showSidebar = false;
    @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }

    contentScaling = () => 1;
    isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected();
    @computed get renderPdfView() {
        TraceMobx();
        const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
        const scale = previewScale * (this.props.scaling?.() || 1);
        return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}
            style={{
                display: this.props.thumbShown?.() ? "none" : undefined,
                height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ?
                    NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined
            }}>
            <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
            <div style={{
                width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
                height: `${100 / scale}%`,
                transform: `scale(${scale})`,
                position: "absolute",
                transformOrigin: "top left",
                top: 0,
            }}>
                <PDFViewer {...this.props}
                    rootDoc={this.rootDoc}
                    layoutDoc={this.layoutDoc}
                    dataDoc={this.dataDoc}
                    pdf={this._pdf!}
                    url={this.pdfUrl!.url.pathname}
                    isContentActive={this.isPdfContentActive}
                    anchorMenuClick={this.anchorMenuClick}
                    loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
                    setPdfViewer={this.setPdfViewer}
                    addDocument={this.addDocument}
                    moveDocument={this.moveDocument}
                    removeDocument={this.removeDocument}
                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                    crop={this.crop}
                    ContentScaling={returnOne}
                />
            </div>
            <SidebarAnnos ref={this._sidebarRef}
                {...this.props}
                rootDoc={this.rootDoc}
                layoutDoc={this.layoutDoc}
                dataDoc={this.dataDoc}
                setHeight={emptyFunction}
                nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
                showSidebar={this.SidebarShown}
                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                sidebarAddDocument={this.sidebarAddDocument}
                moveDocument={this.moveDocument}
                removeDocument={this.removeDocument}
            />
            {this.settingsPanel()}
        </div>;
    }

    static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
    static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
    render() {
        TraceMobx();
        if (this._pdf) {
            if (!this.props.thumbShown?.()) {
                return this.renderPdfView;
            }
            return null;
        }

        const href = this.pdfUrl?.url.href;
        if (href) {
            if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href)));
            else {
                if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
                PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, this._pdf = pdf)));
            }
        }
        return this.renderTitleBox;
    }
}

