import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';

enum cardSortings {
    Time = 'time',
    Type = 'type',
    Color = 'color',
    Custom = 'custom',
    Chat = 'chat',
    Tag = 'tag',
    None = '',
}

/**
 * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
 * sort and filter using presets, and customize your experience with chat gpt.
 *
 * This file contains code as to how the docs are to be rendered (there place geographically and also in regards to sorting),
 * and callback functions for the gpt popup
 */
@observer
export class CollectionCardView extends CollectionSubView() {
    private _dropDisposer?: DragManager.DragDropDisposer;
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _textToDoc = new Map<string, Doc>();

    @observable _forceChildXf = false;
    @observable _hoveredNodeIndex = -1;
    @observable _docRefs = new ObservableMap<Doc, DocumentView>();
    @observable _maxRowCount = 10;
    @observable _docDraggedIndex: number = -1;
    @observable overIndex: number = -1;

    static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
        try {
            const response = await fetch(imageUrl);
            const blob = await response.blob();

            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.readAsDataURL(blob);
                reader.onloadend = () => resolve(reader.result as string);
                reader.onerror = error => reject(error);
            });
        } catch (error) {
            console.error('Error:', error);
            throw error;
        }
    };

    constructor(props: SubCollectionViewProps) {
        super(props);
        makeObservable(this);
        this.setRegenerateCallback();
    }
    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
        this._dropDisposer?.();
        if (ele) {
            this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
        }
    };
    /**
     * Callback to ensure gpt's text versions of the child docs are updated
     */
    setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);

    /**
     * update's gpt's doc-text list and initializes callbacks
     */
    @action
    childPairStringListAndUpdateSortDesc = async () => {
        const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
        GPTPopup.Instance.setSortDesc(sortDesc.join());
        GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
        GPTPopup.Instance.onQuizRandom = () => this.quizMode();
    };

    componentDidMount() {
        this.Document.childFilters_boolean = 'OR'; // bcz: really shouldn't be assigning to fields from within didMount -- this should be a default/override beahavior somehow

        // Reaction to cardSort changes
        this._disposers.sort = reaction(
            () => GPTPopup.Instance.visible,
            isVis => {
                if (isVis) {
                    this.openChatPopup();
                } else {
                    this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
                }
            }
        );
    }

    componentWillUnmount() {
        Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
        this._dropDisposer?.();
    }

    @computed get cardSort() {
        return StrCast(this.Document.cardSort) as cardSortings;
    }

    /**
     * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
     * custom group
     */
    @computed get childDocsWithoutLinks() {
        return this.childDocs.filter(l => l.type !== DocumentType.LINK);
    }

    /**
     * how much to scale down the contents of the view so that everything will fit
     */
    @computed get fitContentScale() {
        const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
        return (this.childPanelWidth() * length) / this._props.PanelWidth();
    }

    /**
     * When in quiz mode, randomly selects a document
     */
    quizMode = () => {
        const randomIndex = Math.floor(Math.random() * this.childDocs.length);
        SelectionManager.DeselectAll();
        DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false);
    };

    /**
     * Number of rows of cards to be rendered
     */
    @computed get numRows() {
        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
    }

    @action
    setHoveredNodeIndex = (index: number) => {
        if (!DocumentView.SelectedDocs().includes(this.childDocs[index])) {
            this._hoveredNodeIndex = index;
        }
    };
    /**
     * Translates the hovered node to the center of the screen
     * @param index
     * @returns
     */
    translateHover = (index: number) => (this._hoveredNodeIndex === index && !DocumentView.SelectedDocs().includes(this.childDocs[index]) ? -50 : 0);

    isSelected = (index: number) => DocumentView.SelectedDocs().includes(this.childDocs[index]);

    /**
     * Returns all the documents except the one that's currently selected
     */
    inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d));

    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
    onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
    isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
    isChildContentActive = () => !!this.isContentActive();

    /**
     * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row
     * @param amCards
     * @param index
     * @returns
     */
    rotate = (amCards: number, index: number) => {
        if (amCards == 1) return 0;

        const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
        const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));

        if (amCards % 2 === 0 && possRotate === 0) {
            return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
        }
        if (amCards % 2 === 0 && index > (amCards + 1) / 2) {
            return possRotate + stepMag;
        }

        return possRotate;
    };
    /**
     * Returns the degree to which a card should be translated in the y direction for the arch effect
     */
    translateY = (amCards: number, index: number, realIndex: number) => {
        const evenOdd = amCards % 2;
        const apex = (amCards - evenOdd) / 2;
        const Magnitude = this.childPanelWidth() / 2; // 400
        const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);

        let rowOffset = 0;
        if (realIndex > this._maxRowCount - 1) {
            rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
        }
        if (evenOdd === 1 || index < apex - 1) {
            return Math.abs(stepMag * (apex - index)) - rowOffset;
        }
        if (index === apex || index === apex - 1) {
            return 0 - rowOffset;
        }

        return Math.abs(stepMag * (apex - index - 1)) - rowOffset;
    };

    /**
     * When dragging a card, determines the index the card should be set to if dropped
     * @param mouseX mouse's x location
     * @param mouseY mouses' y location
     * @returns the card's new index
     */
    findCardDropIndex = (mouseX: number, mouseY: number) => {
        const amCardsTotal = this.sortedDocs.length;
        let index = 0;
        const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;

        // Calculate the adjusted X position accounting for the initial offset
        let adjustedX = mouseX;

        const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
        const rowHeight = this._props.PanelHeight() / amRows;
        const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0

        if (adjustedX < 0) {
            return 0; // Before the first column
        }

        if (amCardsTotal < this._maxRowCount) {
            index = Math.floor(adjustedX / cardWidth);
        } else if (currRow != amRows - 1) {
            index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
        } else {
            const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
            const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
            adjustedX = mouseX - offset;

            index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
        }
        return index;
    };

    /**
     * Checks to see if a card is being dragged and calls the appropriate methods if so
     * @param e the current pointer event
     */

    @action
    onPointerMove = (x: number, y: number) => {
        this._docDraggedIndex = -1;
        if (DragManager.docsBeingDragged.length) {
            const newIndex = this.findCardDropIndex(x, y);

            if (newIndex !== this._docDraggedIndex && newIndex != -1) {
                this._docDraggedIndex = newIndex;
            }
        }
    };

    /**
     * Handles external drop of images/PDFs etc from outside Dash.
     */
    onExternalDrop = async (e: React.DragEvent): Promise<void> => {
        super.onExternalDrop(e, {});
    };

    /**
     * Resets all the doc dragging vairables once a card is dropped
     * @param e
     * @param de drop event
     * @returns true if a card has been dropped, falls if not
     */
    onInternalDrop = undoable(
        action((e: Event, de: DragManager.DropEvent) => {
            if (de.complete.docDragData) {
                const dragIndex = this._docDraggedIndex;
                const draggedDoc = DragManager.docsBeingDragged[0];
                if (dragIndex > -1 && draggedDoc) {
                    this._docDraggedIndex = -1;
                    const sorted = this.sortedDocs;
                    const originalIndex = sorted.findIndex(doc => doc === draggedDoc);

                    this.Document.cardSort = '';
                    originalIndex !== -1 && sorted.splice(originalIndex, 1);
                    sorted.splice(dragIndex, 0, draggedDoc);
                    if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
                        this.dataDoc[this.fieldKey] = new List<Doc>(sorted);
                    }
                }
                e.stopPropagation();
                return true;
            }
            return false;
        }),
        ''
    );

    @computed get sortedDocs() {
        return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex);
    }

    /**
     * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are
     * given higher values. Decimals are used to determine placement for cards with multiple tags
     * @param doc the doc whose value is being determined
     * @returns its value based on its tags
     */

    tagValue = (doc: Doc) =>
        Doc.MyFilterHotKeys.map((key, i) => ({ has: TagItem.docHasTag(doc, StrCast(key.toolType)), i }))
            .filter(({ has }) => has)
            .map(({ i }) => i)
            .join('.');

    /**
     * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
     * front, latter cards to the back
     * @param docs
     * @param sortType
     * @param isDesc
     * @returns
     */
    sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
        sortType &&
            docs.sort((docA, docB) => {
                const [typeA, typeB] = (() => {
                    switch (sortType) {
                        case cardSortings.Time:
                            return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
                        case cardSortings.Color: {
                            const d1 = DashColor(StrCast(docA.backgroundColor));
                            const d2 = DashColor(StrCast(docB.backgroundColor));
                            return [d1.hsv().hue(), d2.hsv().hue()];
                        }
                        case cardSortings.Tag:
                            return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999];
                        case cardSortings.Chat:
                            return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999];
                        default:
                            return [StrCast(docA.type), StrCast(docB.type)];
                    }
                })();

                const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
                return isDesc ? out : -out;
            });
        if (dragIndex != -1) {
            const draggedDoc = DragManager.docsBeingDragged[0];
            const originalIndex = docs.findIndex(doc => doc === draggedDoc);

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

        return [...docs]; // need to spread docs into a new object list since sort() modifies the incoming list which confuses mobx caching
    };

    displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
        <DocumentView
            {...this._props}
            ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))}
            Document={doc}
            NativeWidth={returnZero}
            NativeHeight={returnZero}
            fitWidth={returnFalse}
            onDoubleClickScript={this.onChildDoubleClick}
            renderDepth={this._props.renderDepth + 1}
            LayoutTemplate={this._props.childLayoutTemplate}
            LayoutTemplateString={this._props.childLayoutString}
            ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
            isContentActive={emptyFunction}
            isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
            PanelWidth={this.childPanelWidth}
            PanelHeight={() => this._props.PanelHeight() * this.fitContentScale}
            dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
            dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
            showTags={true}
            dontHideOnDrag
        />
    );

    /**
     * Determines how many cards are in the row of a card at a specific index
     * @param index
     * @returns
     */
    overflowAmCardsCalc = (index: number) => {
        if (this.sortedDocs.length < this._maxRowCount) {
            return this.sortedDocs.length;
        }
        // 13 - 3 = 10
        const totalCards = this.sortedDocs.length;
        // if 9 or less
        if (index < totalCards - (totalCards % this._maxRowCount)) {
            return this._maxRowCount;
        }
        // (3)
        return totalCards % this._maxRowCount;
    };
    /**
     * Determines the index a card is in in a row
     * @param realIndex
     * @returns
     */
    overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
    /**
     * Translates the cards in the second rows and beyond over to the right
     * @param realIndex
     * @param calcIndex
     * @param calcRowCards
     * @returns
     */
    translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));

    /**
     * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected
     * @param isHovered
     * @param isSelected
     * @param realIndex
     * @param amCards
     * @param calcRowIndex
     * @returns
     */
    calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
        const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
        const rowIndex = Math.trunc(realIndex / this._maxRowCount);
        const rowToCenterShift = this.numRows / 2 - rowIndex;
        if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2;
        if (amCards == 1) return 50 * this.fitContentScale;
        return this.translateY(amCards, calcRowIndex, realIndex);
    };

    /**
     * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
     * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
     * inputted into the gpt prompt to sort everything together
     * @returns
     */
    childPairStringList = () => {
        const docToText = (doc: Doc) => {
            switch (doc.type) {
                case DocumentType.PDF:  return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
                case DocumentType.IMG:  return this.getImageDesc(doc);
                case DocumentType.RTF:  return StrCast(RTFCast(doc.text).Text);
                default:                return StrCast(doc.title);
            } // prettier-ignore
        };
        const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
            const docText = (await docToText(doc)) ?? '';
            doc.gptInputText = docText;
            this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
            return `======${docText.replace(/\n/g, ' ').trim()}======`;
        });
        return Promise.all<string>(docTextPromises);
    };

    /**
     * Calls the gpt API to generate descriptions for the images in the view
     * @param image
     * @returns
     */
    getImageDesc = async (image: Doc) => {
        if (StrCast(image.description)) return StrCast(image.description); // Return existing description
        const { href } = (image.data as URLField).url;
        const hrefParts = href.split('.');
        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
        try {
            const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
            const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
            image[DocData].description = response.trim();
            return response; // Return the response from gptImageLabel
        } catch (error) {
            console.log(error);
        }
        return '';
    };

    /**
     * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
     * usable code
     * @param gptOutput
     */
    @action
    processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
        // Split the string into individual list items
        const listItems = gptOutput.split('======').filter(item => item.trim() !== '');

        if (questionType === '2' || questionType === '4') {
            this.childDocs.forEach(d => {
                d.chatFilter = false;
            });
        }

        if (questionType === '6') {
            this.Document.cardSort = 'chat';
        }

        listItems.forEach((item, index) => {
            const normalizedItem = item.trim();
            // find the corresponding Doc in the textToDoc map
            const doc = this._textToDoc.get(normalizedItem);

            if (doc) {
                switch (questionType) {
                    case '6':
                        doc.chatIndex = index;
                        break;
                    case '1': {
                        const allHotKeys = Doc.MyFilterHotKeys;

                        let myTag = '';

                        if (tag) {
                            for (let i = 0; i < allHotKeys.length; i++) {
                                // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED
                                const keyTag = StrCast(allHotKeys[i].toolType);
                                if (tag.includes(keyTag)) {
                                    myTag = keyTag;
                                    break;
                                }
                            }

                            if (myTag != '') {
                                doc[myTag] = true;
                            }
                        }
                        break;
                    }
                    case '2':
                    case '4':
                        doc.chatFilter = true;
                        Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
                        break;
                }
            } else {
                console.warn(`No matching document found for item: ${normalizedItem}`);
            }
        });
    }, '');

    /**
     * Opens up the chat popup and starts the process for smart sorting.
     */
    openChatPopup = async () => {
        GPTPopup.Instance.setVisible(true);
        GPTPopup.Instance.setMode(GPTPopupMode.CARD);
        GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
        await this.childPairStringListAndUpdateSortDesc();
    };

    /**
     * Actually renders all the cards
     */
    @computed get renderCards() {
        const sortedDocs = this.sortedDocs;
        const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc));
        const isEmpty = this.childDocsWithoutLinks.length === 0;

        if (isEmpty) {
            return (
                <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}>
                    Sorry ! There are no cards in this group
                </span>
            );
        }

        // Map sorted documents to their rendered components
        return sortedDocs.map((doc, index) => {
            const realIndex = sortedDocs.indexOf(doc);
            const calcRowIndex = this.overflowIndexCalc(realIndex);
            const amCards = this.overflowAmCardsCalc(realIndex);
            const view = DocumentView.getDocumentView(doc, this.DocumentView?.());
            const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false;

            const childScreenToLocal = () => {
                this._forceChildXf;
                const dref = this._docRefs.get(doc);
                const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
                return new Transform(-translateX + (dref?.centeringX || 0) * scale, 
                                     -translateY + (dref?.centeringY || 0) * scale, 1)
                          .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
            };

            const translateIfSelected = () => {
                const indexInRow = index % this._maxRowCount;
                const rowIndex = Math.trunc(index / this._maxRowCount);
                const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2;
                return (rowCenterIndex - indexInRow) * 100 - 50;
            };
            const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
            const vscale = Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), 
                                    (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10))); // prettier-ignore
            const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
            return (
                <div
                    key={doc[Id]}
                    className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
                    onPointerUp={() => {
                        if (DocumentView.SelectedDocs().includes(doc)) return;
                        // this turns off documentDecorations during a transition, then turns them back on afterward.
                        SnappingManager.SetIsResizing(doc[Id]);
                        setTimeout(
                            action(() => {
                                SnappingManager.SetIsResizing(undefined);
                                this._forceChildXf = !this._forceChildXf;
                            }),
                            900
                        );
                    }}
                    style={{
                        width: this.childPanelWidth(),
                        height: 'max-content',
                        transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
                                    translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px))
                                    rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
                                    scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`,
                    }} // prettier-ignore
                    onMouseEnter={() => this.setHoveredNodeIndex(index)}>
                    {this.displayDoc(doc, childScreenToLocal)}
                </div>
            );
        });
    }

    render() {
        const isEmpty = this.childDocsWithoutLinks.length === 0;

        return (
            <div
                className="collectionCardView-outer"
                ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
                onPointerMove={e => this.onPointerMove(e.clientX, e.clientY)}
                onDrop={this.onExternalDrop.bind(this)}
                style={{
                    background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
                    color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
                }}>
                <div
                    className="card-wrapper"
                    style={{
                        ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
                        ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }),
                        gridAutoRows: `${100 / this.numRows}%`,
                    }}
                    onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
                    {this.renderCards}
                </div>
            </div>
        );
    }
}
