import { Property } from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as textfit from 'textfit';
import { Doc, Field } from '../../../fields/Doc';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoable, UndoManager } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import './LabelBox.scss';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { RichTextMenu } from './formattedText/RichTextMenu';

@observer
export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(LabelBox, fieldKey);
    }
    private dropDisposer?: DragManager.DragDropDisposer;
    private _timeout: NodeJS.Timeout | undefined;
    private _divRef: HTMLDivElement | null = null;
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection

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

    protected createDropTarget = (ele: HTMLDivElement) => {
        this.dropDisposer?.();
        if (ele) {
            this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document);
        }
    };

    componentDidMount() {
        this._props.setContentViewBox?.(this);
        this._disposers.active = reaction(
            () => this.Title,
            () => document.activeElement !== this._divRef && this._forceRerender++
        );
    }
    componentWillUnMount() {
        this._timeout && clearTimeout(this._timeout);
        this.setText(this._divRef?.innerText ?? '');
        Object.values(this._disposers).forEach(disposer => disposer());
    }

    @observable _forceRerender = 0;

    @computed get Title()           { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore
    @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore
    @computed get boxShadow()       { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore

    setText = undoable((text: string) => {
        this.dataDoc[this.fieldKey] = text;
    }, 'set label text');

    drop = (/* e: Event, de: DragManager.DropEvent */) => {
        return false;
    };

    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
        if (!pinProps) return this.Document;
        const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document });

        if (anchor) {
            if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
            //  addAsAnnotation && this.addDocument(anchor);
            PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}) } }, this.Document);
            return anchor;
        }
        return anchor;
    };

    fitTextToBox = (
        r: HTMLElement | null | undefined
    ): {
        minFontSize: number;
        maxFontSize: number;
        multiLine: boolean;
        alignHoriz: boolean;
        alignVert: boolean;
        detectMultiLine: boolean;
    } => {
        this._timeout && clearTimeout(this._timeout);
        const textfitParams = {
            minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1),
            maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100),
            multiLine: r?.textContent?.includes('\n') ? true : false,
            // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline
            alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false,
            alignVert: true,
            detectMultiLine: false,
        };
        if (r) {
            if (!r.offsetHeight || !r.offsetWidth) {
                r.style.opacity = '0';
                this._timeout = setTimeout(() => this.fitTextToBox(r));
                return textfitParams;
            }
            r.style.opacity = '1';
            r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline
            r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want
            r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']));
            textfit(r, textfitParams);
        }
        return textfitParams;
    };
    resetCursor = (cranchor?: number) => {
        if (this._divRef && (cranchor || this._divRef === document.activeElement)) {
            const range = document.createRange();
            const anchor = cranchor ?? this._divRef.childNodes.length;
            const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef);
            range.setStart(container, anchor);
            range.setEnd(container, anchor);
            const sel = window.getSelection();
            sel?.removeAllRanges();
            sel?.addRange(range);
        }
    };

    beforeInput = action((event: InputEvent) => {
        const spanChild = this._divRef?.firstChild?.firstChild;
        if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) {
            event.preventDefault();
            event.stopPropagation();

            const selection = document.getSelection();
            if (selection && document.activeElement === event.target) {
                const text = spanChild.textContent ?? '';
                const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset;
                const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text
                const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor);
                spanChild.textContent = splitText;
                this.resetCursor(cranchor + addReturnHack.length);
            }
            // const span = document.createElement('span');
            // span.innerHTML = '&#8203;';
            // this._divRef!.append(span);
        }
    });
    // .labelBox-mainButton > div > span:nth-child(2) {

    /**
     * When an IconButton is clicked, it will receive focus.  However, we don't want that since we want or need that since we really want
     * to maintain focus in the label's editing div (and cursor position).  so this relies on IconButton's having a tabindex set to -1 so that
     * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox.   If it is, we then
     * restore focus
     * @param e focusout event on the editing div
     */
    keepFocus = (e: FocusEvent) => {
        if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
            for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
                if ((ele as HTMLElement)?.className === 'fonticonbox') {
                    setTimeout(() => this._divRef?.focus());
                    break;
                }
            }
        }
    };

    render() {
        TraceMobx();
        const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
        const [xmargin, ymargin] = [NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._uMargin)];
        return (
            <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
                <div
                    className="labelBox-mainButton"
                    style={{
                        backgroundColor: this.backgroundColor,
                        color: StrCast(this.layoutDoc[`${this.fieldKey}_fontColor`], StrCast(this.layoutDoc._color)),
                        fontFamily: StrCast(this.layoutDoc[`${this.fieldKey}_fontFamily`], StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
                        letterSpacing: StrCast(this.layoutDoc.letterSpacing),
                        textTransform: StrCast(this.layoutDoc[`${this.fieldKey}_transform`]) as Property.TextTransform,
                        paddingLeft: xmargin,
                        paddingRight: xmargin,
                        paddingTop: ymargin,
                        paddingBottom: ymargin,
                        width: this._props.PanelWidth(),
                        height: this._props.PanelHeight(),
                        whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
                    }}>
                    <div
                        key={this._forceRerender}
                        style={{
                            width: this._props.PanelWidth() - 2 * xmargin,
                            height: this._props.PanelHeight() - 2 * ymargin,
                            outline: 'unset !important',
                        }}
                        onKeyDown={e => {
                            e.stopPropagation();
                        }}
                        onKeyUp={action(e => {
                            e.stopPropagation();
                            const text = this._divRef?.firstChild;
                            if (text && (text as HTMLElement)?.nodeType === 3) {
                                this._divRef?.removeChild(text);
                                this._divRef?.firstChild?.appendChild(text);
                                this.resetCursor();
                            }
                            this.fitTextToBox(this._divRef);
                        })}
                        onFocus={() => {
                            RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
                            this._divRef?.removeEventListener('focusout', this.keepFocus);
                            this._divRef?.addEventListener('focusout', this.keepFocus);
                        }}
                        onBlur={e => {
                            this._divRef?.removeEventListener('focusout', this.keepFocus);
                            this.setText(this._divRef?.innerText ?? '');
                            if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
                                RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
                                this._liveTextUndo?.end();
                            }
                        }}
                        dangerouslySetInnerHTML={{
                            __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
                        }}
                        contentEditable={this._props.onClickScript?.() ? undefined : true}
                        ref={r => {
                            this._divRef?.removeEventListener('beforeinput', this.beforeInput);
                            this._divRef = r;
                            if (this._divRef) {
                                this._divRef.addEventListener('beforeinput', this.beforeInput);

                                if (DocumentView.SelectOnLoad === this.Document) {
                                    DocumentView.SetSelectOnLoad(undefined);
                                    this._liveTextUndo = FormattedTextBox.LiveTextUndo;
                                    FormattedTextBox.LiveTextUndo = undefined;
                                    this._divRef.focus();
                                }
                                this.fitTextToBox(this._divRef);
                                if (this.Title) {
                                    this.resetCursor();
                                }
                            } else this._timeout && clearTimeout(this._timeout);
                        }}
                    />
                </div>
            </div>
        );
    }
}

Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
    layout: { view: LabelBox, dataField: 'title' },
    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
    layout: { view: LabelBox, dataField: 'title' },
    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
