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 { numberRange } from '../../../Utils';
import { Doc, NumListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { BoolCast, Cast, DateCast, 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 { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
import { CollectionSubView } from './CollectionSubView';

enum cardSortings {
    Time = 'time',
    Type = 'type',
    Color = 'color',
    Custom = 'custom',
    None = '',
}
@observer
export class CollectionCardView extends CollectionSubView() {
    private _dropDisposer?: DragManager.DragDropDisposer;
    private _childDocumentWidth = 600; // target width of a Doc...
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _textToDoc = new Map<string, Doc>();

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

    static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined {
        return Cast(doc[groupFieldKey], 'number', null);
    }

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

    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
        this._dropDisposer?.();
        if (ele) {
            this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
        }
    };

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

    componentDidMount(): void {
        this._disposers.sort = reaction(
            () => ({ cardSort: this.cardSort, field: this.cardSort_customField }),
            ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false))
        );
    }

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

    @computed get cardSort_customField() {
        return StrCast(this.Document.cardSort_customField) as any as 'chat' | 'star' | 'idea' | 'like';
    }

    @computed get cardSort() {
        return StrCast(this.Document.cardSort) as any as cardSortings;
    }
    /**
     * 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._childDocumentWidth * length) / this._props.PanelWidth();
    }

    @computed get translateWrapperX() {
        let translate = 0;

        if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) {
            translate += this.panelWidth() / 2;
        }
        return translate;
    }

    /**
     * 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() {
        const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK);
        const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups);

        if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
            return regularDocs.filter(doc => {
                // Get the group number for the current index
                const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
                // Check if the group number is in the active groups
                return groupNumber !== undefined && activeGroups.includes(groupNumber);
            });
        }

        // Default return for non-custom cardSort or other cases, filtering out links
        return regularDocs;
    }

    /**
     * Determines the order in which the cards will be rendered depending on the current sort type
     */
    @computed get sortedDocs() {
        return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc));
    }

    @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));

    panelWidth = () => this._childDocumentWidth;
    panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width);
    onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
    isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
    isChildContentActive = () => (this.isContentActive() ? true : false);

    /**
     * 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) => {
        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 stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);

        let rowOffset = 0;
        if (realIndex > this._maxRowCount - 1) {
            rowOffset = 400 * ((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;
    };

    /**
     * Translates the selected node to the middle fo the screen
     * @param index
     * @returns
     */
    translateSelected = (index: number): number => {
        // if (this.isSelected(index)) {
        const middleOfPanel = this._props.PanelWidth() / 2;
        const scaledNodeWidth = this.panelWidth() * 1.25;

        // Calculate the position of the node's left edge before scaling
        const nodeLeftEdge = index * this.panelWidth();
        // Find the center of the node after scaling
        const scaledNodeCenter = nodeLeftEdge + scaledNodeWidth / 2;

        // Calculate the translation needed to align the scaled node's center with the panel's center
        const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4;

        return translation;
    };

    /**
     * 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) => {
        if (sortType === cardSortings.None) return docs;
        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: 
                                  return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string
                                          DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string
                    case cardSortings.Custom:
                                  return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0,
                                          CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0];
                    default:      return [StrCast(docA.type), // If docA.type is undefined, use an empty string
                                          StrCast(docB.type)]; // If docB.type is undefined, use an empty string
                    } // prettier-ignore
            })();

            const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
            return isDesc ? -out : out; // Reverse the sort order if descending is true
        });

        return docs;
    };

    displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => {
        return (
            <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={this.isChildContentActive}
                isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
                PanelWidth={this.panelWidth}
                PanelHeight={this.panelHeight(doc)}
            />
        );
    };

    /**
     * Determines how many cards are in the row of a card at a specific index
     * @param index
     * @returns
     */
    overflowAmCardsCalc = (index: number) => {
        if (this.inactiveDocs().length < this._maxRowCount) {
            return this.inactiveDocs().length;
        }
        // 13 - 3 = 10
        const totalCards = this.inactiveDocs().length;
        // if 9 or less
        if (index < totalCards - (totalCards % 10)) {
            return this._maxRowCount;
        }
        //(3)
        return totalCards % 10;
    };
    /**
     * Determines the index a card is in in a row
     * @param realIndex
     * @returns
     */
    overflowIndexCalc = (realIndex: number) => realIndex % 10;
    /**
     * 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 : (10 - calcRowCards) * (this.panelWidth() / 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) => {
        if (isSelected) return 50 * this.fitContentScale;
        const trans = isHovered ? this.translateHover(realIndex) : 0;
        return trans + this.translateY(amCards, calcRowIndex, realIndex);
    };

    /**
     * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt
     * @param childPairIndex
     * @param buttonID
     * @param doc
     */
    toggleButton = undoable((buttonID: number, doc: Doc) => this.cardSort_customField && (doc[this.cardSort_customField] = buttonID), 'toggle custom button');

    /**
     * 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:  const words = StrCast(doc.text).split(/\s+/);
                                        return words.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)) ?? '';
            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.href;
        const hrefParts = href.split('.');
        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
        try {
            const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
            const response = await gptImageLabel(hrefBase64);
            image[DocData].description = response.trim();
            return response; // Return the response from gptImageLabel
        } catch (error) {
            console.log('bad things have happened');
        }
        return '';
    };

    /**
     * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~
     * @param gptOutput
     */
    processGptOutput = (gptOutput: string) => {
        // Split the string into individual list items
        const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
        listItems.forEach((item, index) => {
            // Split the item by '~~~~~~' to get all descriptors
            const parts = item.split('~~~~~~').map(part => part.trim());

            parts.forEach(part => {
                // Find the corresponding Doc in the textToDoc map
                const doc = this._textToDoc.get(part);
                if (doc) {
                    doc.chat = index;
                }
            });
        });
    };
    /**
     * Opens up the chat popup and starts the process for smart sorting.
     */
    openChatPopup = async () => {
        GPTPopup.Instance.setVisible(true);
        GPTPopup.Instance.setMode(GPTPopupMode.SORT);
        const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
        GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
        GPTPopup.Instance.setSortDesc(sortDesc.join());
        GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult);
    };

    /**
     * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
     * @param childPairIndex
     * @param doc
     * @returns
     */
    renderButtons = (doc: Doc, cardSort: cardSortings) => {
        if (cardSort !== cardSortings.Custom) return '';
        const amButtons = Math.max(4, this.childDocs?.reduce((set, doc) => this.cardSort_customField && set.add(NumCast(doc[this.cardSort_customField])), new Set<number>()).size ?? 0);
        const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
        const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6;
        return (
            <div className="card-button-container" style={{ width: `${totalWidth}px` }}>
                {numberRange(amButtons).map(i => (
                    <button
                        key={i}
                        type="button"
                        style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} //
                        onClick={() => this.toggleButton(i, doc)}
                    />
                ))}
            </div>
        );
    };
    /**
     * Actually renders all the cards
     */
    renderCards = () => {
        const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc));
        // Map sorted documents to their rendered components
        return this.sortedDocs.map((doc, index) => {
            const realIndex = this.sortedDocs.filter(sortDoc => !DocumentView.SelectedDocs().includes(sortDoc)).indexOf(doc);
            const calcRowIndex = this.overflowIndexCalc(realIndex);
            const amCards = this.overflowAmCardsCalc(realIndex);
            const isSelected = DocumentView.SelectedDocs().includes(doc);

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

            return (
                <div
                    key={doc[Id]}
                    className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
                    onPointerUp={() => {
                        // this turns off documentDecorations during a transition, then turns them back on afterward.
                        SnappingManager.SetIsResizing(this.Document[Id]);
                        setTimeout(
                            action(() => {
                                SnappingManager.SetIsResizing(undefined);
                                this._forceChildXf = !this._forceChildXf;
                            }),
                            700
                        );
                    }}
                    style={{
                        width: this.panelWidth(),
                        height: 'max-content', // this.panelHeight(childPair.layout)(),
                        transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
                                    translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px)
                                    rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
                                    scale(${isSelected ? 1.25 : 1})`,
                    }}
                    onMouseEnter={() => this.setHoveredNodeIndex(index)}>
                    {this.displayDoc(doc, childScreenToLocal)}
                    {this.renderButtons(doc, this.cardSort)}
                </div>
            );
        });
    };
    render() {
        return (
            <div
                className="collectionCardView-outer"
                ref={this.createDashEventsTarget}
                style={{
                    background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
                    color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
                }}>
                <div
                    className="card-wrapper"
                    style={{
                        transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`,
                        height: `${100 * this.fitContentScale}%`,
                    }}
                    onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
                    {this.renderCards()}
                </div>
            </div>
        );
    }
}
