import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton, Size } from 'browndash-components';
import * as faceapi from 'face-api.js';
import { FaceMatcher } from 'face-api.js';
import 'ldrs/ring';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
import { Utils } from '../../../../Utils';
import { Doc, DocListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { Cast, ImageCast, StrCast } from '../../../../fields/Types';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { undoable } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { DocumentView } from '../../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../../nodes/FieldView';
import './FaceCollectionBox.scss';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler';

interface FaceDocumentProps {
    faceDoc: Doc;
}

/**
 * A componenent to visually represent a Face Document.
 */
@observer
export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps> {
    private _dropDisposer?: DragManager.DragDropDisposer;

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

    @observable _displayImages: boolean = true;

    protected createDropTarget = (ele: HTMLDivElement) => {
        this._dropDisposer?.();
        ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc));
    };

    protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
        const { docDragData } = de.complete;
        if (docDragData) {
            const filteredDocs = docDragData.droppedDocuments.filter(doc => doc.type === DocumentType.IMG);
            filteredDocs.forEach(doc => {
                // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first.
                if ((this._props.faceDoc[DocData].face_descriptors as List<List<number>>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).length > 1) {
                    alert('Cannot add a document with multiple faces as the first item!');
                } else {
                    // Loop through the documents' face descriptors.
                    // Choose the face with the smallest distance to add.
                    const float32Array = (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor)));
                    const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].face_label), float32Array);
                    const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor];

                    const faceMatcher = new FaceMatcher(faceDescriptors, 1);
                    let cur_lowest_distance = 1;
                    let cur_matching_face = new List<number>();

                    (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).forEach(face => {
                        // If the face has the current lowest distance, mark it as such
                        // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc
                        const convered_32_array: Float32Array = new Float32Array(Array.from(face));
                        const match = faceMatcher.matchDescriptor(convered_32_array);

                        if (match.distance < cur_lowest_distance) {
                            cur_lowest_distance = match.distance;
                            cur_matching_face = face;
                        }
                    });

                    const faceFieldKey = FaceRecognitionHandler.FaceField(doc, this._props.faceDoc);
                    if (doc[DocData][faceFieldKey]) {
                        Cast(doc[DocData][faceFieldKey], listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that
                    } else {
                        doc[DocData][faceFieldKey] = new List<List<number>>([cur_matching_face]);
                    }

                    Doc.AddDocToList(this._props.faceDoc[DocData], 'face_docList', doc);
                    Cast(this._props.faceDoc[DocData].face_descriptors, listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that
                }
            });
            return false;
        }
        return false;
    }

    /**
     * Toggles whether a Face Document displays its associated docs.
     */
    @action
    onDisplayClick() {
        this._displayImages = !this._displayImages;
    }

    /**
     * Deletes a Face Document.
     */
    deleteFaceDocument = undoable(() => {
        if (Doc.ActiveDashboard) {
            Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc);
        }
    }, 'remove face');

    /**
     * Deletes a document from a Face Document's associated docs list.
     * @param doc
     */
    @action
    deleteAssociatedDoc = (doc: Doc) => {
        this._props.faceDoc[DocData].face_descriptors = new List<List<number>>(
            (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).filter(fd => !(doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] as List<List<number>>).includes(fd))
        );
        doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List<List<number>>();
        Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc);
    };

    render() {
        return (
            <div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
                <div className="face-collection-buttons">
                    <IconButton tooltip="Delete Face From Collection" onPointerDown={this.deleteFaceDocument} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
                </div>
                <div className="face-document-top">
                    <h1>{StrCast(this._props.faceDoc[DocData].face_label)}</h1>
                </div>
                <IconButton
                    tooltip="See image information"
                    onPointerDown={() => this.onDisplayClick()}
                    icon={<FontAwesomeIcon icon={this._displayImages ? 'caret-up' : 'caret-down'} />}
                    color={MarqueeOptionsMenu.Instance.userColor}
                    style={{ width: '19px' }}
                />
                {this._displayImages ? (
                    <div className="face-document-image-container">
                        {DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => {
                            const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
                            return (
                                <div className="image-wrapper" key={Utils.GenerateGuid()}>
                                    <img onClick={() => DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} />
                                    <div className="remove-item">
                                        <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
                                    </div>
                                </div>
                            );
                        })}
                    </div>
                ) : null}
            </div>
        );
    }
}

@observer
export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(FaceCollectionBox, fieldKey);
    }

    public static Instance: FaceCollectionBox;

    @computed get currentDocs() {
        if (Doc.ActiveDashboard) {
            return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments);
        }
        return [];
    }

    constructor(props: FieldViewProps) {
        super(props);
        makeObservable(this);
        FaceCollectionBox.Instance = this;
    }

    render() {
        return (
            <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
                {this.currentDocs.map(doc => (
                    <FaceDocumentItem key={doc[Id]} faceDoc={doc} />
                ))}
            </div>
        );
    }
}

Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, {
    layout: { view: FaceCollectionBox, dataField: 'data' },
    options: { acl: '', _width: 400 },
});
