import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Field } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { Cast, NumCast } from '../../../../fields/Types';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from '../../../util/SelectionManager';
import { SettingsManager } from '../../../util/SettingsManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../../nodes/DocumentView';
import './CollectionFreeFormLinkView.scss';
import React = require('react');

export interface CollectionFreeFormLinkViewProps {
    A: DocumentView;
    B: DocumentView;
    LinkDocs: Doc[];
}

@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
    @observable _opacity: number = 0;
    @observable _start = 0;
    _anchorDisposer: IReactionDisposer | undefined;
    _timeout: NodeJS.Timeout | undefined;
    componentWillUnmount() {
        this._anchorDisposer?.();
    }
    @action timeout = action(() => Date.now() < this._start++ + 1000 && (this._timeout = setTimeout(this.timeout, 25)));
    componentDidMount() {
        this._anchorDisposer = reaction(
            () => [
                this.props.A.props.ScreenToLocalTransform(),
                Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop,
                Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights,
                this.props.B.props.ScreenToLocalTransform(),
                Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop,
                Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights,
            ],
            action(() => {
                this._start = Date.now();
                this._timeout && clearTimeout(this._timeout);
                this._timeout = setTimeout(this.timeout, 25);
                setTimeout(this.placeAnchors);
            }),
            { fireImmediately: true }
        );
    }
    placeAnchors = () => {
        const { A, B, LinkDocs } = this.props;
        const linkDoc = LinkDocs[0];
        if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return;
        setTimeout(
            action(() => (this._opacity = 0.75)),
            0
        ); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
        setTimeout(
            action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)),
            750
        ); // this will unhighlight the link line.
        const a = A.ContentDiv.getBoundingClientRect();
        const b = B.ContentDiv.getBoundingClientRect();
        const { left: aleft, top: atop, width: awidth, height: aheight } = A.ContentDiv.parentElement!.getBoundingClientRect();
        const { left: bleft, top: btop, width: bwidth, height: bheight } = B.ContentDiv.parentElement!.getBoundingClientRect();
        const apt = Utils.closestPtBetweenRectangles(aleft, atop, awidth, aheight, bleft, btop, bwidth, bheight, a.left + a.width / 2, a.top + a.height / 2);
        const bpt = Utils.closestPtBetweenRectangles(bleft, btop, bwidth, bheight, aleft, atop, awidth, aheight, apt.point.x, apt.point.y);

        // really hacky stuff to make the LinkAnchorBox display where we want it to:
        //   if there's an element in the DOM with a classname containing a link anchor's id,
        //   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 targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor1 as Doc)[Id])).lastElement();
        const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor2 as Doc)[Id])).lastElement();
        if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return;
        if (!targetAhyperlink) {
            if (linkDoc.linkAutoMove) {
                linkDoc.anchor1_x = ((apt.point.x - aleft) / awidth) * 100;
                linkDoc.anchor1_y = ((apt.point.y - atop) / aheight) * 100;
            }
        } else {
            const m = targetAhyperlink.getBoundingClientRect();
            const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
            const mpx = mp[0] / A.props.PanelWidth();
            const mpy = mp[1] / A.props.PanelHeight();
            if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100;
            if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100;
            if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0;
            else linkDoc.opacity = 1;
        }
        if (!targetBhyperlink) {
            if (linkDoc.linkAutoMove) {
                linkDoc.anchor2_x = ((bpt.point.x - bleft) / bwidth) * 100;
                linkDoc.anchor2_y = ((bpt.point.y - btop) / bheight) * 100;
            }
        } else {
            const m = targetBhyperlink.getBoundingClientRect();
            const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
            const mpx = mp[0] / B.props.PanelWidth();
            const mpy = mp[1] / B.props.PanelHeight();
            if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100;
            if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100;
            if (getComputedStyle(targetBhyperlink).fontSize === '0px') linkDoc.opacity = 0;
            else linkDoc.opacity = 1;
        }
    };

    pointerDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            (e, down, delta) => {
                this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0];
                this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1];
                return false;
            },
            emptyFunction,
            () => {
                // OverlayView.Instance.addElement(
                //     <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]}
                //         showLinks={action(() => { })}
                //     />, { x: 300, y: 300 });
            }
        );
    };

    visibleY = (el: any) => {
        let rect = el.getBoundingClientRect();
        const top = rect.top,
            height = rect.height;
        var el = el.parentNode;
        while (el && el !== document.body) {
            rect = el.getBoundingClientRect?.();
            if (rect?.width) {
                if (top <= rect.bottom === false && getComputedStyle(el).overflow === 'hidden') return rect.bottom;
                // Check if the element is out of view due to a container scrolling
                if (top + height <= rect.top && getComputedStyle(el).overflow === 'hidden') return rect.top;
            }
            el = el.parentNode;
        }
        // Check its within the document viewport
        return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden";
    };
    visibleX = (el: any) => {
        let rect = el.getBoundingClientRect();
        const left = rect.left,
            width = rect.width;
        var el = el.parentNode;
        while (el && el !== document.body) {
            rect = el?.getBoundingClientRect();
            if (rect?.width) {
                if (left <= rect.right === false && getComputedStyle(el).overflow === 'hidden') return rect.right;
                // Check if the element is out of view due to a container scrolling
                if (left + width <= rect.left && getComputedStyle(el).overflow === 'hidden') return rect.left;
            }
            el = el.parentNode;
        }
        // Check its within the document viewport
        return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden";
    };

    @action
    toggleProperties = () => {
        if (SettingsManager.propertiesWidth > 0) {
            SettingsManager.propertiesWidth = 0;
        } else {
            SettingsManager.propertiesWidth = 250;
        }
    };

    onClickLine = () => {
        SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true);
        this.toggleProperties();
    };

    @computed.struct get renderData() {
        this._start;
        SnappingManager.GetIsDragging();
        const { A, B, LinkDocs } = this.props;
        if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined;
        const acont = A.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
        const bcont = B.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
        const adiv = acont.length ? acont[0] : A.ContentDiv;
        const bdiv = bcont.length ? bcont[0] : B.ContentDiv;
        for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return;
        for (let bpdiv = bdiv; bpdiv; bpdiv = bpdiv.parentElement as any) if ((bpdiv as any).hidden) return;
        const a = adiv.getBoundingClientRect();
        const b = bdiv.getBoundingClientRect();
        const atop = this.visibleY(adiv);
        const btop = this.visibleY(bdiv);
        if (!a.width || !b.width) return undefined;
        const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
        const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
        const aleft = this.visibleX(adiv);
        const bleft = this.visibleX(bdiv);
        const aclipped = aleft !== a.left || atop !== a.top;
        const bclipped = bleft !== b.left || btop !== b.top;
        if (aclipped && bclipped) return undefined;
        const clipped = aclipped || bclipped;
        const pt1 = [aleft + a.width / 2, atop + a.height / 2];
        const pt2 = [bleft + b.width / 2, btop + b.width / 2];
        const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]];
        const pt2vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt2[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt2[1]];
        const pt1len = Math.sqrt(pt1vec[0] * pt1vec[0] + pt1vec[1] * pt1vec[1]);
        const pt2len = Math.sqrt(pt2vec[0] * pt2vec[0] + pt2vec[1] * pt2vec[1]);
        const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
        const pt1norm = clipped ? [0, 0] : [(pt1vec[0] / pt1len) * ptlen, (pt1vec[1] / pt1len) * ptlen];
        const pt2norm = clipped ? [0, 0] : [(pt2vec[0] / pt2len) * ptlen, (pt2vec[1] / pt2len) * ptlen];
        const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
        const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
        const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
        const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen];
        const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
        const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);

        const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
        const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
        return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
    }

    render() {
        if (!this.renderData) return null;

        const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
        LinkManager.currentLink = this.props.LinkDocs[0];
        const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
        const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
        const linkColorList = Doc.UserDoc().linkColorList as List<string>;
        const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>;
        const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);

        const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex];

        //access stroke color using index of the relationship in the color list (default black)
        const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? 'black' : linkColorList[currRelationshipIndex];
        // const hexStroke = this.rgbToHex(stroke)

        //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has)
        //thickness varies linearly from 3px to 12px for increasing link count
        const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px';

        if (this.props.LinkDocs[0].displayArrow === undefined) {
            this.props.LinkDocs[0].displayArrow = false;
        }

        return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || (!this.props.LinkDocs[0].linkDisplay && !aActive && !bActive) ? null : (
            <>
                <defs>
                    <marker id="arrowhead" markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto">
                        <polygon points="0 0, 3 1.5, 0 3" fill={Colors.DARK_GRAY} />
                    </marker>
                </defs>
                <path
                    className="collectionfreeformlinkview-linkLine"
                    style={{ pointerEvents: 'all', opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }}
                    onClick={this.onClickLine}
                    d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
                    markerEnd={this.props.LinkDocs[0].displayArrow ? 'url(#arrowhead)' : ''}
                />
                {textX === undefined ? null : (
                    <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
                        {Field.toString(this.props.LinkDocs[0].description as any as Field)}
                    </text>
                )}
            </>
        );
    }
}
