import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Xarrow from 'react-xarrows';
import { FieldResult } from '../../../fields/Doc';
import { DocCss, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { DocCast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils';
import { DocumentManager } from '../../util/DocumentManager';
import { SnappingManager } from '../../util/SnappingManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkBox.scss';

@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string = 'link') {
        return FieldView.LayoutString(LinkBox, fieldKey);
    }
    _disposers: { [name: string]: IReactionDisposer } = {};
    @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
    @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor

    constructor(props: FieldViewProps) {
        super(props);
        makeObservable(this);
    }
    @computed get anchor1() { return this.anchor(1); } // prettier-ignore
    @computed get anchor2() { return this.anchor(2); } // prettier-ignore

    anchor = (which: number) => {
        const anch = DocCast(this.dataDoc['link_anchor_' + which]);
        const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch;
        return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
    };
    _hackToSeeIfDeleted: any;
    componentWillUnmount() {
        this._hackToSeeIfDeleted && clearTimeout(this._hackToSeeIfDeleted);
        Object.keys(this._disposers).forEach(key => this._disposers[key]());
    }
    componentDidMount() {
        this._props.setContentViewBox?.(this);
        this._disposers.deleting = reaction(
            () => !this.anchor1 || !this.anchor2,
            empty => empty && (this._hackToSeeIfDeleted = setTimeout(() => this._props.removeDocument?.(this.Document), 1000))
        );
        this._disposers.dragging = reaction(
            () => ({ drag: SnappingManager.IsDragging }),
            ({ drag }) => {
                !LightboxView.Contains(this.DocumentView?.()) &&
                    setTimeout(
                        // need to wait for drag manager to set 'hidden' flag on dragged DOM elements
                        action(() => {
                            const a = this.anchor1,
                                b = this.anchor2;
                            let a1 = a && document.getElementById(a.Guid);
                            let a2 = b && document.getElementById(b.Guid);
                            // test whether the anchors themselves are hidden,...
                            if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
                            else {
                                // .. or whether and of their DOM parents are hidden
                                for (; a1 && !a1.hidden; a1 = a1.parentElement);
                                for (; a2 && !a2.hidden; a2 = a2.parentElement);
                                this._hide = a1 || a2 ? true : false;
                            }
                        })
                    );
            }
        );
    }

    render() {
        TraceMobx();

        if (this._hide) return null;
        const a = this.anchor1;
        const b = this.anchor2;
        this._forceAnimate;
        const docView = this._props.docViewPath().lastElement();

        if (a && b && !LightboxView.Contains(docView)) {
            // text selection bounds are not directly observable, so we have to
            // force an update when anything that could affect them changes (text edits causing reflow, scrolling)
            a.Document[a.LayoutFieldKey];
            b.Document[b.LayoutFieldKey];
            a.Document.layout_scrollTop;
            b.Document.layout_scrollTop;
            a.Document[DocCss];
            b.Document[DocCss];

            const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove)
            const bxf = b.screenToViewTransform();
            const scale = docView?.screenToViewTransform().Scale ?? 1;
            const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition
            const bt = b.getBounds?.transition; //    inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation)

            var foundParent = false;
            const getAnchor = (field: FieldResult): Element[] => {
                const docField = DocCast(field);
                const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
                const ele = document.getElementById(doc[Id]);
                if (ele?.className === 'linkBox-label') foundParent = true;
                if (ele?.getBoundingClientRect().width) return [ele];
                const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width);
                const annoOn = DocCast(doc.annotationOn);
                if (eles.length || !annoOn) return eles;
                const pareles = getAnchor(annoOn);
                foundParent = pareles.length ? true : false;
                return pareles;
            };
            //   if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>),
            //   then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
            //   otherwise, we just use the computed nearest point on the document boundary to the target Document
            const targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1);
            const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2);

            const container = this.DocumentView?.().containerViewPath?.().lastElement().ContentDiv;
            const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
            const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
            if (!aid || !bid) {
                setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01)));
                return null;
            }
            if (foundParent) {
                setTimeout(
                    action(() => (this._forceAnimate = this._forceAnimate + 0.01)),
                    1
                );
            }

            if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation
            const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting);
            const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
            const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
            const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily);
            const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize);
            const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor));
            const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document;

            const strokeWidth = NumCast(stroke_width, 4);
            const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
            const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
            return (
                <>
                    {!highlightColor ? null : (
                        <Xarrow
                            divContainerStyle={{ transform: `scale(${scale})` }}
                            start={aid}
                            end={bid} //
                            strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)}
                            showHead={stroke_startMarker ? true : false}
                            showTail={stroke_endMarker ? true : false}
                            headSize={NumCast(stroke_markerScale, 3)}
                            tailSize={NumCast(stroke_markerScale, 3)}
                            tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
                            headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
                            color={highlightColor}
                        />
                    )}
                    <Xarrow
                        divContainerStyle={{ transform: `scale(${scale})` }}
                        start={aid}
                        end={bid} //
                        strokeWidth={strokeWidth}
                        dashness={Number(stroke_dash) ? true : false}
                        showHead={stroke_startMarker ? true : false}
                        showTail={stroke_endMarker ? true : false}
                        headSize={NumCast(stroke_markerScale, 3)}
                        tailSize={NumCast(stroke_markerScale, 3)}
                        tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
                        headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
                        color={color}
                        labels={
                            <div
                                id={this.Document[Id]}
                                className={'linkBox-label'}
                                style={{
                                    borderRadius: '8px',
                                    pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
                                    fontSize,
                                    fontFamily /*, fontStyle: 'italic'*/,
                                    color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()),
                                    paddingLeft: 4,
                                    paddingRight: 4,
                                    paddingTop: 3,
                                    paddingBottom: 3,
                                    background: DashColor((!docView?.isSelected() && highlightColor) || color)
                                        .fade(0.5)
                                        .toString(),
                                }}>
                                <EditableView
                                    key="editableView"
                                    oneLine
                                    contents={labelText}
                                    height={fontSize + 4}
                                    fontSize={fontSize}
                                    GetValue={() => linkDesc}
                                    SetValue={action(val => {
                                        this.Document[DocData].link_description = val;
                                        return true;
                                    })}
                                />

                                {/* <EditableText
                                    placeholder={labelText}
                                    background={color}
                                    color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())}
                                    type={Type.PRIM}
                                    val={StrCast(this.Document[DocData].link_description)}
                                    setVal={action(val => (this.Document[DocData].link_description = val))}
                                    fillWidth
                                /> */}
                            </div>
                        }
                        passProps={{}}
                    />
                </>
            );
        }

        setTimeout(
            action(() => (this._forceAnimate = this._forceAnimate + 1)),
            2
        );
        return (
            <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}>
                <ComparisonBox
                    {...this.props} //
                    fieldKey="link_anchor"
                    setHeight={emptyFunction}
                    dontRegisterView={true}
                    renderDepth={this._props.renderDepth + 1}
                    addDocument={returnFalse}
                    removeDocument={returnFalse}
                    moveDocument={returnFalse}
                />
            </div>
        );
    }
}
