import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Colors, IconButton, Type } from '@dash/components';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
import { DocCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import { ObservableReactComponent } from './ObservableReactComponent';
import './TagsView.scss';
import { DocumentView } from './nodes/DocumentView';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { IconTagBox } from './nodes/IconTagBox';
import { Id } from '../../fields/FieldSymbols';
import { StyleProp } from './StyleProp';
import { Docs } from '../documents/Documents';

/**
 * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection.
 *
 * This panel allow sthe user to add metadata tags to a Doc, and to display those tags, or any metadata field
 * in a panel of 'buttons' (TagItems) just below the DocumentView.  TagItems are interactive -
 * the user can drag them off in order to display a collection of all documents that share the tag value.
 *
 * The tags that are added using the panel are the same as the #tags that can entered in a text Doc.
 * Note that tags starting with @ display a metadata key/value pair instead of the tag itself.
 * e.g., '@author' shows the document author
 *
 */

interface TagItemProps {
    docs: Doc[];
    tag: string;
    tagDoc: Opt<Doc>;
    showRemoveUI: boolean;
    setToEditing: () => void;
}

/**
 * Interactive component that display a single metadata tag or value.
 *
 * These items can be dragged and dropped to create a collection of Docs that
 * share the same metadata tag / value.
 */
@observer
export class TagItem extends ObservableReactComponent<TagItemProps> {
    /**
     * return list of all tag Docs (ie, Doc that are collections of Docs sharing a specific tag / value)
     */
    public static get AllTagCollectionDocs() {
        return DocListCast(Doc.ActiveDashboard?.myTagCollections);
    }
    /**
     * Find tag Doc that collects all Docs with given tag / value
     * @param tag tag string
     * @returns tag collection Doc or undefined
     */
    public static findTagCollectionDoc = (tag: string) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag);

    /**
     * Creates a Doc that collects Docs with the specified tag / value
     * @param tag tag string
     * @returns tag collection Doc
     */
    public static createTagCollectionDoc = (tag: string) => {
        const newTagCol = new Doc();
        newTagCol.title = tag;
        newTagCol.collections = new List<Doc>();
        newTagCol.$docs = new List<Doc>();
        Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myTagCollections', newTagCol);

        return newTagCol;
    };
    /**
     * Gets all Docs that have the specified tag / value
     * @param tag tag string
     * @returns An array of documents that contain the tag.
     */
    public static allDocsWithTag = (tag: string) => DocListCast(TagItem.findTagCollectionDoc(tag)?.$docs);

    public static docHasTag = (doc: Doc, tag: string) => StrListCast(doc?.tags).includes(tag);
    /**
     * Adds a tag to the metadata of this document and adds the Doc to the corresponding tag collection Doc (or creates it)
     * @param tag tag string
     */
    public static addTagToDoc = (doc: Doc, tag: string) => {
        // If the tag collection is not in active Dashboard, add it as a new doc, with the tag as its title.
        const tagCollection = TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag);

        // If the document is of type COLLECTION, make it a smart collection, otherwise, add the tag to the document.
        if (doc.type === DocumentType.COL && !doc.annotationOn) {
            Doc.AddDocToList(tagCollection, 'collections', doc);

            // Iterate through the tag Doc collections and add a copy of the document to each collection
            for (const cdoc of DocListCast(tagCollection.$docs)) {
                if (!DocListCast(doc.$data).find(d => Doc.AreProtosEqual(d, cdoc))) {
                    const newEmbedding = Doc.MakeEmbedding(cdoc);
                    Doc.AddDocToList(doc[DocData], 'data', newEmbedding);
                    Doc.SetContainer(newEmbedding, doc);
                }
            }
        } else {
            // Add this document to the tag's collection of associated documents.
            Doc.AddDocToList(tagCollection[DocData], 'docs', doc);

            // Iterate through the tag document's collections and add a copy of the document to each collection
            for (const collection of DocListCast(tagCollection.collections)) {
                if (!DocListCast(collection.$data).find(d => Doc.AreProtosEqual(d, doc))) {
                    const newEmbedding = Doc.MakeEmbedding(doc);
                    Doc.AddDocToList(collection[DocData], 'data', newEmbedding);
                    Doc.SetContainer(newEmbedding, collection);
                }
            }
        }

        if (!doc.$tags) doc.$tags = new List<string>();
        const tagList = doc.$tags as List<string>;
        if (!tagList.includes(tag)) tagList.push(tag);
    };

    /**
     * Removes a tag from a Doc and removes the Doc from the corresponding tag collection Doc
     * @param doc Doc to add tag
     * @param tag tag string
     * @param tagDoc doc that collections the Docs with the tag
     */
    public static removeTagFromDoc = (doc: Doc, tag: string, tagDoc?: Doc) => {
        if (doc.$tags) {
            if (doc.type === DocumentType.COL) {
                tagDoc && Doc.RemoveDocFromList(tagDoc[DocData], 'collections', doc);

                for (const cur_doc of TagItem.allDocsWithTag(tag)) {
                    doc.$data = new List<Doc>(DocListCast(doc.$data).filter(d => !Doc.AreProtosEqual(cur_doc, d)));
                }
            } else {
                tagDoc && Doc.RemoveDocFromList(tagDoc[DocData], 'docs', doc);

                for (const collection of DocListCast(tagDoc?.collections)) {
                    collection.$data = new List<Doc>(DocListCast(collection.$data).filter(d => !Doc.AreProtosEqual(doc, d)));
                }
            }
        }
        doc.$tags = new List<string>(StrListCast(doc.$tags).filter(label => label !== tag));
    };

    private _ref: React.RefObject<HTMLDivElement>;

    constructor(props: TagItemProps) {
        super(props);
        makeObservable(this);
        this._ref = React.createRef();
    }

    /**
     * Creates a smart collection.
     * @returns
     */
    createTagCollection = () => {
        if (!this._props.tagDoc) {
            const face = FaceRecognitionHandler.FindUniqueFaceByName(this._props.tag);
            return face ? Doc.MakeEmbedding(face) : undefined;
        }
        // Get the documents that contain the tag.
        const newEmbeddings = TagItem.allDocsWithTag(this._props.tag).map(doc => Doc.MakeEmbedding(doc));

        // Create a new collection and set up configurations.
        const emptyCol = DocCast(Doc.UserDoc().emptyCollection);
        const newCollection = ((doc: Doc) => {
            doc.$data = new List<Doc>(newEmbeddings);
            doc.$title = this._props.tag;
            doc.$tags = new List<string>([this._props.tag]);
            doc.$freeform_fitContentsToBox = true;
            doc._freeform_panX = doc._freeform_panY = 0;
            doc._width = 900;
            doc._height = 900;
            doc.layout_fitWidth = true;
            doc._layout_showTags = true;
            return doc;
        })(emptyCol ? Doc.MakeCopy(emptyCol, true) : Docs.Create.FreeformDocument([], {}));
        newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection));

        // Add the collection to the tag document's list of associated smart collections.
        this._props.tagDoc && Doc.AddDocToList(this._props.tagDoc, 'collections', newCollection);
        return newCollection;
    };

    @action
    handleDragStart = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            () => {
                const dragCollection = this.createTagCollection();
                if (dragCollection) {
                    const dragData = new DragManager.DocumentDragData([dragCollection]);
                    DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {});
                    return true;
                }
                return false;
            },
            returnFalse,
            clickEv => {
                clickEv.stopPropagation();
                this._props.setToEditing();
            }
        );
        e.preventDefault();
    };

    @computed get doc() {
        return this._props.docs.lastElement();
    }

    render() {
        this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
        const metadata = this._props.tag.startsWith('@') ? this._props.tag.replace(/^@/, '') : '';
        return (
            <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onPointerDown={this.handleDragStart} ref={this._ref}>
                {metadata ? (
                    <span>
                        <b style={{ fontSize: 'smaller' }}>{'@' + metadata}&nbsp;</b>
                        {typeof this.doc[metadata] === 'boolean' ? (
                            <input
                                type="checkbox"
                                onClick={e => e.stopPropagation()}
                                onPointerDown={e => e.stopPropagation()}
                                onChange={undoable(() => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
                                checked={this.doc[metadata] as boolean}
                            />
                        ) : (
                            Field.toString(this.doc[metadata])
                        )}
                    </span>
                ) : (
                    this._props.tag
                )}
                {this.props.showRemoveUI && this._props.tagDoc && (
                    <IconButton
                        tooltip="Remove tag"
                        onPointerDown={undoable(() => this._props.docs.forEach(doc => TagItem.removeTagFromDoc(doc, this._props.tag, this._props.tagDoc)), `remove tag ${this._props.tag}`)}
                        icon={<FontAwesomeIcon icon="times" size="sm" />}
                        style={{ width: '8px', height: '8px', marginLeft: '10px' }}
                    />
                )}
            </div>
        );
    }
}

interface TagViewProps {
    Views: DocumentView[];
    background: string;
}

/**
 * Displays a panel of tags that have been added to a Doc.  Also allows for editing the applied tags through a dropdown UI.
 */
@observer
export class TagsView extends ObservableReactComponent<TagViewProps> {
    constructor(props: TagViewProps) {
        super(props);
        makeObservable(this);
    }

    @observable _panelHeightDirty = 0;
    @observable _currentInput = '';
    @observable _isEditing: boolean | undefined = undefined;
    _heightDisposer: IReactionDisposer | undefined;
    _lastXf = this.View.screenToContentsTransform();

    componentDidMount() {
        this._heightDisposer = reaction(
            () => this.View.screenToContentsTransform(),
            xf => {
                if (xf.Scale === 0) return;
                if (this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1)) return;
                if (xf.TranslateX !== this._lastXf.TranslateX || xf.TranslateY !== this._lastXf.TranslateY || xf.Scale !== this._lastXf.Scale) {
                    this._panelHeightDirty = this._panelHeightDirty + 1;
                }
                this._lastXf = xf;
            }
        );
    }
    componentWillUnmount() {
        this._heightDisposer?.();
    }

    @computed get View() {
        return this._props.Views.lastElement();
    }

    @computed get isEditing() {
        const selected = DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View);
        if (this._isEditing === undefined) return selected && this.View.TagPanelEditing; // && !StrListCast(this.View.dataDoc.tags).length && !StrListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']).length;
        return this._isEditing && (this._props.Views.length > 1 || (selected && this.View.TagPanelEditing));
    }

    /**
     * Shows or hides the editing UI for adding/removing Doc tags
     * @param editing
     */
    @action
    setToEditing = (editing = true) => {
        this._isEditing = editing;
        if (this._props.Views.length === 1) {
            this.View.TagPanelEditing = editing;
            editing && this.View.select(false);
        }
    };

    /**
     * Adds the specified tag or metadata to the Doc.  If the tag is not prefixed with '#', then a '#' prefix is added.
     * When the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of
     * just the tag.  In addition, a suffix of :<value> can be added to set a metadata value
     * @param tag tag string to add (format: #<tag> | #@field(:(=)?value)? )
     */
    submitTag = undoable(
        action((tag: string) => {
            const submittedLabel = tag.trim().replace(/^#/, '').split(':');
            if (submittedLabel[0]) {
                this._props.Views.forEach(view => {
                    TagItem.addTagToDoc(view.Document, (submittedLabel[0].startsWith('@') ? '' : '#') + submittedLabel[0]);
                    if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
                });
            }
            this._currentInput = ''; // Clear the input box
        }),
        'added doc label'
    );

    /**
     * When 'layout_showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc).
     * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed.
     */
    render() {
        const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags));
        const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat));
        const facesList = new Set<string>(
            DocListCast(this.View.dataDoc[Doc.LayoutDataKey(this.View.Document) + '_annotations'])
                .concat(this.View.Document)
                .filter(d => d.face)
                .map(doc => StrCast(DocCast(doc.face)?.title))
        );
        this._panelHeightDirty;

        return this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1) ? null : (
            <div
                className="tagsView-container"
                ref={r =>
                    r &&
                    new ResizeObserver(
                        action(() => {
                            if (this._props.Views.length === 1) {
                                this.View.TagPanelHeight = Math.floor(r?.children[0].children[0].getBoundingClientRect().height ?? 0) - Math.floor(r?.children[0].children[0].children[0].getBoundingClientRect().height ?? 0);
                            }
                        })
                    ).observe(r?.children[0])
                }
                style={{
                    display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
                    backgroundColor: this.isEditing ? this._props.background : Colors.TRANSPARENT,
                    borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
                    height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined,
                }}>
                <div className="tagsView-content">
                    <div className="tagsView-list">
                        {this._props.Views.length === 1 && !this.View.showTags ? null : ( //
                            <IconButton
                                style={{ width: '8px', height: this._props.Views.lastElement().TagBtnHeight }}
                                tooltip="Close Menu"
                                onPointerDown={e =>
                                    setupMoveUpEvents(this, e, returnFalse, emptyFunction, upEv => {
                                        this.setToEditing(!this.isEditing);
                                        upEv.stopPropagation();
                                    })
                                }
                                type={Type.TERT}
                                background="transparent"
                                color={this.View._props.styleProvider?.(this.View.Document, this.View.ComponentView?._props, StyleProp.FontColor) as string}
                                icon={<FontAwesomeIcon icon={this.isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />}
                            />
                        )}
                        <IconTagBox Views={this._props.Views} IsEditing={this.isEditing} />
                        {Array.from(tagsList)
                            .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
                            .map(tag => (
                                <TagItem
                                    key={tag}
                                    docs={this._props.Views.map(view => view.Document)}
                                    tag={tag}
                                    tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
                                    setToEditing={this.setToEditing}
                                    showRemoveUI={this.isEditing}
                                />
                            ))}
                        {Array.from(facesList).map(tag => (
                            <TagItem key={tag} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
                        ))}
                    </div>
                    {this.isEditing ? (
                        <div className="tagsView-editing-box">
                            <div className="tagsView-input-box">
                                <input
                                    value={this._currentInput}
                                    autoComplete="off"
                                    onChange={action(e => (this._currentInput = e.target.value))}
                                    onKeyDown={e => {
                                        e.key === 'Enter' ? this.submitTag(this._currentInput) : null;
                                        e.stopPropagation();
                                    }}
                                    type="text"
                                    placeholder="Enter #tags or @metadata"
                                    className="tagsView-input"
                                    style={{ width: '100%', borderRadius: '5px' }}
                                />
                            </div>
                            <div className="tagsView-suggestions-box">
                                {TagItem.AllTagCollectionDocs.map(doc => StrCast(doc.title))
                                    .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
                                    .map(tag => (
                                        <Button
                                            style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }}
                                            text={tag}
                                            color={SnappingManager.userVariantColor}
                                            tooltip="Add existing tag"
                                            onClick={() => this.submitTag(tag)}
                                            key={tag}
                                        />
                                    ))}
                                {Array.from(chatTagsList).map(tag => (
                                    <Button
                                        style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightpink', color: 'black' }}
                                        text={tag}
                                        color={SnappingManager.userVariantColor}
                                        tooltip="Add existing tag"
                                        onClick={() => this.submitTag(tag)}
                                        key={tag}
                                    />
                                ))}
                            </div>
                        </div>
                    ) : null}
                </div>
            </div>
        );
    }
}
