import { action, computed, observable } from 'mobx';
import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
import { Cast, DocCast, StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util';
import { returnFalse } from '../../Utils';
import { DocUtils } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { InteractionUtils } from '../util/InteractionUtils';
import { DocumentView } from './nodes/DocumentView';
import { Touchable } from './Touchable';

///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
export interface DocComponentProps {
    Document: Doc;
    fieldKey?: string;
    LayoutTemplate?: () => Opt<Doc>;
    LayoutTemplateString?: string;
}
export function DocComponent<P extends DocComponentProps>() {
    class Component extends Touchable<P> {
        //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 "The Document" -- it encapsulates, data, layout, and any templates
        @computed get rootDoc() {
            return Cast(this.props.Document.rootDocument, Doc, null) || 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.props.Document : Doc.Layout(this.props.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] as Doc;
        }
        // key where data is stored
        @computed get fieldKey() {
            return this.props.fieldKey;
        }

        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
    }
    return Component;
}

/// FieldViewBoxProps  -  a generic base class for field views that are not annotatable (e.g. InkingStroke, ColorBox)
interface ViewBoxBaseProps {
    Document: Doc;
    DataDoc?: Doc;
    DocumentView?: () => DocumentView;
    fieldKey: string;
    isSelected: (outsideReaction?: boolean) => boolean;
    isContentActive: () => boolean | undefined;
    renderDepth: number;
    rootSelected: (outsideReaction?: boolean) => boolean;
}
export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
    class Component extends Touchable<P> {
        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
        //@computed get Document(): T { return schemaCtor(this.props.Document); }

        // This is the "The Document" -- it encapsulates, data, layout, and any templates
        @computed get rootDoc() {
            return Cast(this.props.Document.rootDocument, Doc, null) || 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.props.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.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData];
        }
        // key where data is stored
        @computed get fieldKey() {
            return this.props.fieldKey;
        }

        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
    }
    return Component;
}

///  DocAnnotatbleComponent -return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
export interface ViewBoxAnnotatableProps {
    Document: Doc;
    DataDoc?: Doc;
    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;
    isSelected: (outsideReaction?: boolean) => boolean;
    rootSelected: (outsideReaction?: boolean) => boolean;
    renderDepth: number;
    isAnnotationOverlay?: boolean;
}
export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() {
    class Component extends Touchable<P> {
        @observable _annotationKeySuffix = () => 'annotations';
        @observable _isAnyChildContentActive = false;
        //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 "The Document" -- it encapsulates, data, layout, and any templates
        @computed get rootDoc() {
            return Cast(this.props.Document.rootDocument, Doc, null) || 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.props.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.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData];
        }

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

        isAnyChildContentActive = () => this._isAnyChildContentActive;

        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;

        @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.props.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.rootDoc !== 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);
                    }
                });
                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.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) {
                return false;
            }
            const targetDataDoc = this.rootDoc[DocData];
            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.rootDoc;
                        Doc.SetContainer(doc, this.rootDoc);
                        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;
}
