import { action, computed, makeObservable, observable } from 'mobx';
import * as React from 'react';
import * as rp from 'request-promise';
import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils';
import CursorField from '../../../fields/CursorField';
import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
import { AclPrivate, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { Upload } from '../../../server/SharedMediaTypes';
import { DocServer } from '../../DocServer';
import { Networking } from '../../Network';
import { DocUtils } from '../../documents/DocUtils';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { Docs, DocumentOptions } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { DocumentViewProps } from '../nodes/DocumentContentsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
import { FlashcardPracticeUI } from './FlashcardPracticeUI';

export enum docSortings {
    Time = 'time',
    Type = 'type',
    Color = 'color',
    Chat = 'chat',
    Tag = 'tag',
    None = '',
}

export const ChatSortField = 'chat_sortIndex';

export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
    isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
    isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
    layoutEngine?: () => string;
    setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void;
    ignoreUnrendered?: boolean;

    // property overrides for child documents
    childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
    childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
    childContentsActive?: () => boolean | undefined;
    childLayoutFitWidth?: (child: Doc) => boolean;
    childlayout_showTitle?: () => string;
    childOpacity?: () => number;
    childContextMenuItems?: () => { script: ScriptField; label: string }[];
    childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
    childHideDecorationTitle?: boolean;
    childHideResizeHandles?: boolean;
    childHideDecorations?: boolean;
    childDragAction?: dropActionType;
    childXPadding?: number;
    childYPadding?: number;
    childLayoutString?: string;
    childIgnoreNativeSize?: boolean;
    childClickScript?: ScriptField;
    childDoubleClickScript?: ScriptField;
    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
    hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
}

export interface SubCollectionViewProps extends CollectionViewProps {
    isAnyChildContentActive: () => boolean;
}

export function CollectionSubView<X>() {
    class CollectionSubViewInternal extends ViewBoxBaseComponent<X & SubCollectionViewProps>() {
        private dropDisposer?: DragManager.DragDropDisposer;
        private gestureDisposer?: GestureUtils.GestureEventDisposer;
        protected _mainCont?: HTMLDivElement;

        constructor(props: X & SubCollectionViewProps) {
            super(props);
            makeObservable(this);
        }

        @observable _focusFilters: Opt<string[]> = undefined; // childFilters that are overridden when previewing a link to an anchor which has childFilters set on it
        @observable _focusRangeFilters: Opt<string[]> = undefined; // childFiltersByRanges that are overridden when previewing a link to an anchor which has childFiltersByRanges set on it
        protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
            this.dropDisposer?.();
            this.gestureDisposer?.();
            if (ele) {
                this._mainCont = ele;
                this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this));
                this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
            }
        };
        protected CreateDropTarget(ele: HTMLDivElement) {
            // used in schema view
            this.createDashEventsTarget(ele);
        }

        componentWillUnmount() {
            this.gestureDisposer?.();
        }

        get dataDoc() {
            return this._props.TemplateDataDocument instanceof Doc && this.Document.isTemplateForField //
                ? Doc.GetProto(this._props.TemplateDataDocument)
                : this.Document.rootDocument
                  ? this.Document
                  : this.Document[DocData]; // if the layout document has a rootDocument, then we don't want to get its parent which would be the unexpanded template
        }

        get childContainerViewPath() {
            return this.DocumentView?.().docViewPath;
        }
        // this returns whether either the collection is selected, or the template that it is part of is selected
        rootSelected = () => this._props.isSelected() || BoolCast(this._props.TemplateDataDocument && this._props.rootSelected?.());

        // The data field for rendering this collection will be on the this.Document unless we're rendering a template in which case we try to use props.TemplateDataDocument.
        // When a document has a TemplateDataDoc but it's not a template, then it contains its own rendering data, but needs to pass the TemplateDataDoc through
        // to its children which may be templates.
        // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
        @computed get dataField() {
            return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout.  hopefully nothing depending on the previous code.
        }

        hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
        @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] {
            const { TemplateDataDocument } = this._props;
            const validPairs = this.childDocs
                .map(doc => Doc.GetLayoutDataDocPair(this.Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc))
                .filter(
                    pair =>
                        // filter out any documents that have a proto that we don't have permissions to
                        !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))
                )
                .filter(pair => !this._filterFunc?.(pair.layout!));
            return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
        }
        /**
         * This is the raw, stored list of children on a collection.  If you modify this list, the database will be updated
         */
        @computed get childDocList() {
            return Cast(this.dataField, listSpec(Doc));
        }

        addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
            const doc = toList(docsIn).lastElement();
            const where = location.split(':')[0];
            if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
                if (doc.hidden) doc.hidden = false;
                if (!location.includes(OpenWhereMod.always)) return true;
            }
            return this._props.addDocTab(docsIn, location);
        };

        collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
        collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
        // child filters apply to the descendants of the documents in this collection
        childDocFilters = () => [...(this._props.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
        // unrecursive filters apply to the documents in the collection, but no their children.  See Utils.noRecursionHack
        unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
        childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
        searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);

        @observable docDraggedIndex = -1;
        @computed.struct get childDocs() {
            TraceMobx();
            let rawdocs: (Doc | Promise<Doc>)[] = [];
            if (this.dataField instanceof Doc) {
                // if collection data is just a document, then promote it to a singleton list;
                rawdocs = [this.dataField];
            } else if (Cast(this.dataField, listSpec(Doc), null)) {
                // otherwise, if the collection data is a list, then use it.
                rawdocs = Cast(this.dataField, listSpec(Doc), null);
            } else if (this.dataField) {
                // Finally, if it's not a doc or a list and the document is a template, we try to render the root doc.
                // For example, if an image doc is rendered with a slide template, the template will try to render the data field as a collection.
                // Since the data field is actually an image, we set the list of documents to the singleton of root document's proto which will be an image.
                const templateRoot = this._props.TemplateDataDocument;
                rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : [];
            }
            const childDocs = this.childSortedDocs(
                rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc),
                this.docDraggedIndex
            );

            const childDocFilters = this.childDocFilters();
            const childFiltersByRanges = this.childDocRangeFilters();
            const searchDocs = this.searchFilterDocs();
            if (this.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !childFiltersByRanges.length && !searchDocs.length)) {
                return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
            }

            const docsforFilter: Doc[] = [];
            childDocs.forEach(d => {
                // dragging facets
                const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter));
                if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return;
                let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0;
                if (notFiltered) {
                    notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0;
                    const fieldKey = Doc.LayoutFieldKey(d);
                    const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
                    const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey];
                    const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar'];
                    if (docChildDocs !== undefined || sidebarDocs !== undefined) {
                        let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)];
                        if (subDocs.length > 0) {
                            let newarray: Doc[] = [];
                            notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length);
                            while (subDocs.length > 0 && !notFiltered) {
                                newarray = [];
                                // eslint-disable-next-line no-loop-func
                                subDocs.forEach(t => {
                                    const docFieldKey = Doc.LayoutFieldKey(t);
                                    const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
                                    notFiltered =
                                        notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
                                    DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc));
                                    isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                                });
                                subDocs = newarray;
                            }
                        }
                    }
                }
                notFiltered && docsforFilter.push(d);
            });
            return docsforFilter;
        }

        childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
            const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']);
            const docs = docsIn.slice();
            sortType && docs.sort((docA, docB) => {
                const [typeA, typeB] = (() => {
                    switch (sortType) {
                        default:                
                        case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
                        case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999),                   NumCast(docB[ChatSortField], 9999)];
                        case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
                        case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
                        case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
                    }
                })(); 
                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1);
            }); //prettier-ignore
            if (dragIndex !== -1) {
                const draggedDoc = DragManager.docsBeingDragged[0];
                const originalIndex = docs.findIndex(doc => doc === draggedDoc);

                originalIndex !== -1 && docs.splice(originalIndex, 1);
                draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
            }
            return docs;
        };

        @action
        protected async setCursorPosition(position: [number, number]) {
            let ind;
            const doc = this.Document;
            const id = Doc.UserDoc()[Id];
            const email = ClientUtils.CurrentUserEmail();
            const pos = { x: position[0], y: position[1] };
            if (id && email) {
                const proto = Doc.GetProto(doc);
                if (!proto) {
                    return;
                }
                // The following conditional detects a recurring bug we've seen on the server
                if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
                    alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...');
                    throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
                }
                let cursors = Cast(proto.cursors, listSpec(CursorField));
                if (!cursors) {
                    proto.cursors = cursors = new List<CursorField>();
                }
                if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
                    cursors[ind].setPosition(pos);
                } else {
                    const entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
                    cursors.push(entry);
                }
            }
        }

        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {}

        protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) {
            const dragData = de.complete.docDragData;
            if (dragData) {
                const sourceDragAction = dragData.dropAction;
                const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this.Document);
                dragData.dropAction = !sameCollection // if doc from another tree
                    ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's
                    : sourceDragAction === dropActionType.inPlace // if source drag is inPlace
                      ? sourceDragAction // keep the doc in place
                      : dropActionType.same; // otherwise use same tree semantics to move within tree
                e.stopPropagation();
            }
        }

        addDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.addDocument?.(doc, annotationKey) || false;
        removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.removeDocument?.(doc, annotationKey) || false;
        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false;

        protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
            const { docDragData } = de.complete;
            if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) {
                let added;
                const dropAction = docDragData.dropAction || docDragData.userDropAction;
                const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
                const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
                if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop));
                if ((!dropAction || dropAction === dropActionType.inPlace || dropAction === dropActionType.same || dropAction === dropActionType.move || someMoved) && docDragData.moveDocument) {
                    const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                    const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                    if (movedDocs.length) {
                        const canAdd =
                            (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) &&
                            (dropAction !== dropActionType.inPlace || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document));
                        const moved = docDragData.moveDocument(movedDocs, this.Document, canAdd ? this.addDocument : returnFalse);
                        added = canAdd || moved ? moved : undefined;
                    } else if (addedDocs.length) {
                        added = this.addDocument(addedDocs);
                    }
                    if (!added && ScriptCast(this.Document.dropConverter)) {
                        ScriptCast(this.Document.dropConverter)?.script.run({ dragData: docDragData });
                        added = addedDocs.length ? this.addDocument(addedDocs) : true;
                    }
                } else {
                    ScriptCast(this.Document.dropConverter)?.script.run({ dragData: docDragData });
                    added = this.addDocument(docDragData.droppedDocuments);
                    !added && alert('You cannot perform this move');
                }
                added === false && !this._props.isAnnotationOverlay && e.preventDefault();
                added === true && e.stopPropagation();
                return !!added;
            }
            if (de.complete.annoDragData) {
                const dropCreator = de.complete.annoDragData.dropDocCreator;
                de.complete.annoDragData.dropDocCreator = () => {
                    const dropped = dropCreator(this._props.isAnnotationOverlay ? this.Document : undefined);
                    this.addDocument(dropped);
                    return dropped;
                };
                return true;
            }
            return false;
        }

        protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions = {}, completed?: (docs: Doc[]) => void) {
            if (e.ctrlKey) {
                e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
                return;
            }

            const { dataTransfer } = e;
            const html = dataTransfer.getData('text/html');
            const text = dataTransfer.getData('text/plain');
            const uriList = dataTransfer.getData('text/uri-list');

            if (text && text.startsWith('<div')) {
                return;
            }

            e.stopPropagation();
            e.preventDefault();

            const addDocument = (doc: Doc | Doc[]) => this.addDocument(doc);

            if (html) {
                if (RTFIsFragment(html)) {
                    const href = GetHrefFromHTML(html);
                    if (href) {
                        const docId = GetDocFromUrl(href);
                        if (docId) {
                            // prosemirror text containing link to dash document
                            DocServer.GetRefField(docId).then(f => {
                                if (f instanceof Doc) {
                                    if (options.x || options.y) {
                                        f.x = options.x as number;
                                        f.y = options.y as number;
                                    } // should be in CollectionFreeFormView
                                    f instanceof Doc && addDocument(f);
                                }
                            });
                        } else {
                            addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
                        }
                    } else if (text) {
                        addDocument(Docs.Create.TextDocument(text, { ...options, _layout_showTitle: StrCast(Doc.UserDoc().layout_showTitle), _width: 100, _height: 25 }));
                    }
                    return;
                }
                if (!html.startsWith('<a')) {
                    const tags = html.split('<');
                    if (tags[0] === '') tags.splice(0, 1);
                    let img = tags[0].startsWith('img') ? tags[0] : tags.length > 1 && tags[1].startsWith('img') ? tags[1] : '';
                    const cors = img.includes('corsProxy') ? img.match(/http.*corsProxy\//)![0] : '';
                    img = cors ? img.replace(cors, '') : img;
                    if (img) {
                        const imgSrc = img.split('src="')[1].split('"')[0];
                        const imgOpts = { ...options, _width: 300 };
                        if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) {
                            const result = ((await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })) as Upload.ImageInformation[]).lastElement();
                            const newImgSrc =
                                result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
                                    ? ClientUtils.prepend(result.accessPaths.agnostic.client)
                                    : result.accessPaths.agnostic.client;

                            addDocument(ImageUtils.AssignImgInfo(Docs.Create.ImageDocument(newImgSrc, imgOpts), result));
                        } else if (imgSrc.startsWith('http')) {
                            const doc = Docs.Create.ImageDocument(imgSrc, imgOpts);
                            addDocument(ImageUtils.AssignImgInfo(doc, await ImageUtils.ExtractImgInfo(doc)));
                        }
                        return;
                    }
                    const path = window.location.origin + '/doc/';
                    if (text.startsWith(path)) {
                        const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
                        DocServer.GetRefField(docId).then(f => {
                            const fDoc = f;
                            if (fDoc instanceof Doc) {
                                if (options.x || options.y) {
                                    fDoc.x = options.x as number;
                                    fDoc.y = options.y as number;
                                } // should be in CollectionFreeFormView
                                addDocument(fDoc);
                            }
                        });
                    } else {
                        const srcWeb = DocumentView.Selected().lastElement();
                        const srcUrl = (srcWeb?.Document.data as WebField)?.url?.href?.match(/https?:\/\/[^/]*/)?.[0];
                        const reg = new RegExp(ClientUtils.prepend(''), 'g');
                        const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;
                        const backgroundColor = tags.map(tag => tag.match(/.*(background-color: ?[^;]*)/)?.[1]?.replace(/background-color: ?(.*)/, '$1')).filter(t => t)?.[0];
                        const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: srcUrl ? 'from:' + srcUrl : '-web clip-', _width: 300, _height: 300, backgroundColor });
                        Doc.GetProto(htmlDoc)['data-text'] = Doc.GetProto(htmlDoc).text = text;
                        addDocument(htmlDoc);
                        if (srcWeb) {
                            const iframe = DocumentView.Selected()[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
                            const focusNode = iframe?.contentDocument?.getSelection()?.focusNode;
                            if (focusNode) {
                                const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
                                anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
                            }
                        }
                    }
                    return;
                }
            }

            if (uriList || text) {
                if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed') || text.includes('www.youtube.com/shorts')) {
                    const batch = UndoManager.StartBatch('youtube upload');
                    const generatedDocuments: Doc[] = [];
                    this.slowLoadDocuments((uriList || text).split('v=').lastElement().split('&')[0].split('shorts/').lastElement(), options, generatedDocuments, text, completed, addDocument).then(batch.end);

                    return;
                }
                // let matches: RegExpExecArray | null;
                // if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
                //     const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
                //     const proto = newBox.proto!;
                //     const documentId = matches[2];
                //     proto[GoogleRef] = documentId;
                //     proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
                //     proto.backgroundColor = "#eeeeff";
                //     addDocument(newBox);
                //     return;
                // }
                // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
                //     const albumId = matches[3];
                //     const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
                //     return;
                // }
            }
            if (uriList) {
                addDocument(
                    Docs.Create.WebDocument(uriList.split('#annotations:')[0], {
                        // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig)
                        ...options,
                        title: uriList.split('#annotations:')[0],
                        _width: 400,
                        _height: 512,
                        _nativeWidth: 850,
                        data_useCors: true,
                    })
                );
                return;
            }

            const { items } = e.dataTransfer;
            const { length } = items;
            const files: File[] = [];
            const generatedDocuments: Doc[] = [];
            if (!length) {
                alert('No uploadable content found.');
                return;
            }

            const batch = UndoManager.StartBatch('collection view drop');
            for (let i = 0; i < length; i++) {
                const item = e.dataTransfer.items[i];
                if (item.kind === 'string' && item.type.includes('uri')) {
                    // eslint-disable-next-line no-await-in-loop
                    const stringContents = await new Promise<string>(resolve => {
                        item.getAsString(resolve);
                    });
                    // eslint-disable-next-line no-await-in-loop
                    const type = (await rp.head(ClientUtils.CorsProxy(stringContents)))['content-type'];
                    if (type) {
                        // eslint-disable-next-line no-await-in-loop
                        const doc = await DocUtils.DocumentFromType(type, ClientUtils.CorsProxy(stringContents), options);
                        doc && generatedDocuments.push(doc);
                    }
                }
                if (item.kind === 'file') {
                    const file = item.getAsFile();
                    file?.type && files.push(file);
                }
            }
            this.slowLoadDocuments(files, options, generatedDocuments, text, completed, addDocument).then(batch.end);
        }

        slowLoadDocuments = async (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => {
            // create placeholder docs
            // inside placeholder docs have some func that

            let pileUpDoc;
            if (typeof files === 'string') {
                const loading = Docs.Create.LoadingDocument(files, options);
                generatedDocuments.push(loading);
                Doc.addCurrentlyLoading(loading);
                DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
            } else {
                generatedDocuments.push(
                    ...(await Promise.all(
                        files.map(async file => {
                            if (file.name.endsWith('svg')) {
                                return (await DocUtils.openSVGfile(file, options)) as Doc;
                            }
                            const loading = Docs.Create.LoadingDocument(file, options);
                            Doc.addCurrentlyLoading(loading);
                            DocUtils.uploadFileToDoc(file, {}, loading);
                            return loading;
                        })
                    ))
                );
            }
            if (generatedDocuments.length) {
                // Creating a dash document
                const isFreeformView = this.Document._type_collection === CollectionViewType.Freeform;
                const set = !isFreeformView
                    ? generatedDocuments
                    : generatedDocuments.length > 1
                      ? generatedDocuments.map(d => {
                            DocUtils.iconify(d);
                            return d;
                        })
                      : [];
                if (completed) completed(set);
                else if (isFreeformView && generatedDocuments.length > 1) {
                    pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!;
                    addDocument(pileUpDoc);
                } else {
                    generatedDocuments.forEach(addDocument);
                }
            } else if (text && !text.includes('https://')) {
                addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 }));
            } else {
                alert('Document upload failed - possibly an unsupported file type.');
            }
        };

        protected _sideBtnWidth = 35;
        protected _sideBtnMaxPanelPct = 0.15;
        @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
        /**
         * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
         */
        @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (!this._props.fitWidth?.(this.Document) ? this._props.NativeDimScaling?.()||1: 1); } // prettier-ignore
        /**
         * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
         * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
         * size or a fraction of the collection view.
         */
        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
        /**
         * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
         * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
         */
        @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore

        screenXPadding = (docView?: DocumentView) => {
            if (!docView) return 0;
            const diff = this._props.PanelWidth() - docView.PanelWidth();
            const xpad1 = this.uiBtnScaling * (this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) - diff / 2; // this._sideBtnWidth;
            return xpad1 / (docView.NativeDimScaling?.() || 1);
        };
        filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
        childDocsFunc = () => this.childDocs;
        @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore

        public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => {
            return (
                <FlashcardPracticeUI
                    setFilterFunc={this.setFilterFunc}
                    fieldKey={this.fieldKey}
                    sideBtnWidth={this._sideBtnWidth}
                    allChildDocs={this.childDocsFunc}
                    filteredChildDocs={this.filteredChildDocs}
                    advance={answered}
                    curDoc={curDoc}
                    layoutDoc={this.layoutDoc}
                    uiBtnScaling={this.uiBtnScaling}
                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
                    renderDepth={this._props.renderDepth}
                    docViewProps={docViewProps}
                />
            );
        };
    }

    return CollectionSubViewInternal;
}
