import { action, computed, makeObservable, observable } from 'mobx';
import * as React from 'react';
import { returnFalse } from '../../Utils';
import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
import { DocumentType } from '../documents/DocumentTypes';
import { DocUtils } from '../documents/Documents';
import { DocumentManager } from '../util/DocumentManager';
import { Transform } from '../util/Transform';
import { ObservableReactComponent } from './ObservableReactComponent';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { DocumentView } from './nodes/DocumentView';

/**
 * DocComponent returns a generic React base class used by Doc views (not the 'Box' views that render the contents of Doc views)
 *    (e.g.,CollectionFreeFormDocumentView, DocumentViewInternal)
 *
 * */
export interface DocComponentProps {
    Document: Doc;
    LayoutTemplate?: () => Opt<Doc>;
    LayoutTemplateString?: string;
}
export function DocComponent<P extends DocComponentProps>() {
    class Component extends ObservableReactComponent<React.PropsWithChildren<P>> {
        constructor(props: any) {
            super(props);
            makeObservable(this);
        }

        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
        get Document() {
            return this._props.Document;
        }
        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
        @computed get layoutDoc() {
            return this._props.LayoutTemplateString ? this.Document : Doc.Layout(this.Document, this._props.LayoutTemplate?.());
        }
        // This is the data part of a document -- ie, the data that is constant across all views of the document
        @computed get dataDoc() {
            return this._props.Document[DocData];
        }
    }
    return Component;
}

/**
 *  ViewBoxBaseComponent  -  base class for non-annotatable views that render the interior contents of a DocumentView
 *                           (e.g. InkingStroke, ColorBox)
 */
export interface ViewBoxBaseProps {
    Document: Doc;
    TemplateDataDocument?: Doc;
    DocumentView?: () => DocumentView;
    fieldKey: string;
    isSelected: () => boolean;
    isContentActive: () => boolean | undefined;
    ScreenToLocalTransform: () => Transform;
    renderDepth: number;
}
export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
    class Component extends ObservableReactComponent<React.PropsWithChildren<P>> {
        ScreenToLocalBoxXf = () => this._props.ScreenToLocalTransform();

        get DocumentView() {
            return this._props.DocumentView;
        }
        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
        get Document() {
            return this._props.Document;
        }
        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
        @computed get layoutDoc() {
            return Doc.Layout(this.Document);
        }
        // This is the data part of a document -- ie, the data that is constant across all views of the document
        @computed get dataDoc() {
            return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData];
        }
        // key where data is stored
        get fieldKey() {
            return this._props.fieldKey;
        }
    }
    return Component;
}

/**
 * DocAnnotatableComponent - base class for annotatable views that render the interior contents of a DocumentView
 *                       (e.g., PdfBox, ImageBox)
 * These views should be interactive (respond to pointerEvents) when their conatainer DocumentView is selected
 */
export interface ViewBoxAnnotatableProps {
    Document: Doc;
    TemplateDataDocument?: Doc;
    DocumentView?: () => DocumentView;
    fieldKey: string;
    filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
    isContentActive: () => boolean | undefined;
    select: (isCtrlPressed: boolean) => void;
    whenChildContentsActiveChanged: (isActive: boolean) => void;
    ScreenToLocalTransform: () => Transform;
    isSelected: () => boolean;
    renderDepth: number;
    isAnnotationOverlay?: boolean;
}
export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() {
    class Component extends ObservableReactComponent<React.PropsWithChildren<P>> {
        constructor(props: any) {
            super(props);
            makeObservable(this);
        }

        ScreenToLocalBoxXf = () => this._props.ScreenToLocalTransform();

        @observable _annotationKeySuffix = () => 'annotations';
        @observable _isAnyChildContentActive = false;

        get DocumentView() {
            return this._props.DocumentView;
        }
        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
        @computed get Document() {
            return this._props.Document;
        }
        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
        @computed get layoutDoc() {
            return Doc.Layout(this.Document);
        }
        // This is the data part of a document -- ie, the data that is constant across all views of the document
        @computed get dataDoc() {
            return this.Document.isTemplateForField || this.Document.isTemplateDoc ? this._props.TemplateDataDocument ?? this.Document[DocData] : this.Document[DocData];
        }

        // key where data is stored
        @computed get fieldKey() {
            return this._props.fieldKey;
        }

        isAnyChildContentActive = () => this._isAnyChildContentActive;

        @computed public get annotationKey() {
            return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : '');
        }

        @action.bound
        removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
            const effectiveAcl = GetEffectiveAcl(this.dataDoc);
            const indocs = doc instanceof Doc ? [doc] : doc;
            const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);

            // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
            const targetDataDoc = this.dataDoc;
            const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
            const toRemove = value.filter(v => docs.includes(v));

            if (toRemove.length !== 0) {
                const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined;
                toRemove.forEach(doc => {
                    leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
                    Doc.RemoveDocFromList(Doc.GetProto(doc), 'proto_embeddings', doc);
                    doc.embedContainer = undefined;
                    if (recent) {
                        doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
                    }
                });
                if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) {
                    (DocumentManager.Instance.getFirstDocumentView(targetDataDoc)?.ComponentView as CollectionFreeFormView)?.promoteCollection();
                } else {
                    this.isAnyChildContentActive() && this._props.select(false);
                }
                return true;
            }

            return false;
        }
        // this is called with the document that was dragged and the collection to move it into.
        // if the target collection is the same as this collection, then the move will be allowed.
        // otherwise, the document being moved must be able to be removed from its container before
        // moving it into the target.
        @action.bound
        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
            if (Doc.AreProtosEqual(this._props.Document, targetCollection)) {
                return true;
            }
            const first = doc instanceof Doc ? doc : doc[0];
            if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) {
                return this.removeDocument(doc, annotationKey, false) && addDocument(doc, annotationKey);
            }
            return false;
        };
        @action.bound
        addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => {
            const docs = doc instanceof Doc ? [doc] : doc;
            if (this._props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.Document))) {
                return false;
            }
            const targetDataDoc = this.dataDoc;
            const effectiveAcl = GetEffectiveAcl(targetDataDoc);

            if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
                return false;
            }
            const added = docs;
            if (added.length) {
                if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) {
                    added.forEach(doc => {
                        doc._dragOnlyWithinContainer = undefined;
                        if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.Document;
                        else Doc.GetProto(doc).annotationOn = undefined;
                        Doc.SetContainer(doc, this.Document);
                        inheritParentAcls(targetDataDoc, doc, true);
                    });

                    const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
                    if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
                    targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField();
                }
            }
            return true;
        };

        whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
    }
    return Component;
}
