import React = require("react");
import {
    action,
    computed,
    IReactionDisposer,
    observable,
    reaction,
} from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { Doc, DocListCast } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import {
    emptyFunction,
    formatTime,
    OmitKeys,
    returnFalse,
    returnOne,
    setupMoveUpEvents,
    StopEvent,
    returnTrue,
} from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { LinkManager } from "../../util/LinkManager";
import { Scripting } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { AudioWaveform } from "../AudioWaveform";
import { CollectionSubView } from "../collections/CollectionSubView";
import { LightboxView } from "../LightboxView";
import {
    DocAfterFocusFunc,
    DocFocusFunc,
    DocumentView,
    DocumentViewProps,
} from "../nodes/DocumentView";
import { LabelBox } from "../nodes/LabelBox";
import "./CollectionStackedTimeline.scss";
import { Colors } from "../global/globalEnums";
import { DocumentManager } from "../../util/DocumentManager";
import { SnappingManager } from "../../util/SnappingManager";
import { DragManager } from "../../util/DragManager";

type PanZoomDocument = makeInterface<[]>;
const PanZoomDocument = makeInterface();
export type CollectionStackedTimelineProps = {
    Play: () => void;
    Pause: () => void;
    playLink: (linkDoc: Doc) => void;
    playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
    playing: () => boolean;
    setTime: (time: number) => void;
    startTag: string;
    endTag: string;
    mediaPath: string;
    dictationKey: string;
    rawDuration: number;
    fieldKey: string;
};

export enum TrimScope {
    All = 2,
    Clip = 1,
    None = 0,
}

@observer
export class CollectionStackedTimeline extends CollectionSubView<
    PanZoomDocument,
    CollectionStackedTimelineProps
>(PanZoomDocument) {
    @observable static SelectingRegion: CollectionStackedTimeline | undefined;
    static RangeScript: ScriptField;
    static LabelScript: ScriptField;
    static RangePlayScript: ScriptField;
    static LabelPlayScript: ScriptField;

    private _timeline: HTMLDivElement | null = null;
    private _markerStart: number = 0;
    @observable _markerEnd: number | undefined;
    @observable _trimming: number = TrimScope.None;
    @observable _trimStart: number = 0;
    @observable _trimEnd: number = 0;

    get minTrimLength() { return this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0; }
    @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; }
    @computed get trimDuration() { return this.trimEnd - this.trimStart; }
    @computed get trimEnd() { return this.IsTrimming !== TrimScope.None ? this._trimEnd : this.clipEnd; }

    @computed get clipStart() { return this.IsTrimming === TrimScope.All ? 0 : NumCast(this.layoutDoc.clipStart); }
    @computed get clipDuration() { return this.clipEnd - this.clipStart; }
    @computed get clipEnd() { return this.IsTrimming === TrimScope.All ? this.props.rawDuration : NumCast(this.layoutDoc.clipEnd, this.props.rawDuration); }

    @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }

    constructor(props: any) {
        super(props);
        // onClick play scripts
        CollectionStackedTimeline.RangeScript =
            CollectionStackedTimeline.RangeScript ||
            ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
                self: Doc.name,
                scriptContext: "any",
                clientX: "number",
            })!;
        CollectionStackedTimeline.RangePlayScript =
            CollectionStackedTimeline.RangePlayScript ||
            ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, {
                self: Doc.name,
                scriptContext: "any",
                clientX: "number",
            })!;
    }

    componentDidMount() {
        document.addEventListener("keydown", this.keyEvents, true);
    }

    @action
    componentWillUnmount() {
        document.removeEventListener("keydown", this.keyEvents, true);
        if (CollectionStackedTimeline.SelectingRegion === this) {
            CollectionStackedTimeline.SelectingRegion = undefined;
        }
    }

    public get IsTrimming() { return this._trimming; }

    @action
    public StartTrimming(scope: TrimScope) {
        this._trimStart = this.clipStart;
        this._trimEnd = this.clipEnd;
        this._trimming = scope;
    }
    @action
    public StopTrimming() {
        this.layoutDoc.clipStart = this.trimStart;
        this.layoutDoc.clipEnd = this.trimEnd;
        this._trimming = TrimScope.None;
    }

    anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
    anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val) ?? null);
    toTimeline = (screen_delta: number, width: number) => {
        return Math.max(
            this.clipStart,
            Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
    }

    rangeClickScript = () => CollectionStackedTimeline.RangeScript;
    rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;

    // for creating key anchors with key events
    @action
    keyEvents = (e: KeyboardEvent) => {
        if (
            !(e.target instanceof HTMLInputElement) &&
            this.props.isSelected(true)
        ) {
            switch (e.key) {
                case " ":
                    if (!CollectionStackedTimeline.SelectingRegion) {
                        this._markerStart = this._markerEnd = this.currentTime;
                        CollectionStackedTimeline.SelectingRegion = this;
                    } else {
                        this._markerEnd = this.currentTime;
                        CollectionStackedTimeline.createAnchor(
                            this.rootDoc,
                            this.dataDoc,
                            this.props.fieldKey,
                            this.props.startTag,
                            this.props.endTag,
                            this._markerStart,
                            this._markerEnd
                        );
                        this._markerEnd = undefined;
                        CollectionStackedTimeline.SelectingRegion = undefined;
                    }
            }
        }
    }

    getLinkData(l: Doc) {
        let la1 = l.anchor1 as Doc;
        let la2 = l.anchor2 as Doc;
        const linkTime = NumCast(
            la2[this.props.startTag],
            NumCast(la1[this.props.startTag])
        );
        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
            la1 = l.anchor2 as Doc;
            la2 = l.anchor1 as Doc;
        }
        return { la1, la2, linkTime };
    }

    // starting the drag event for anchor resizing
    @action
    onPointerDownTimeline = (e: React.PointerEvent): void => {
        const rect = this._timeline?.getBoundingClientRect();
        const clientX = e.clientX;
        const shiftKey = e.shiftKey;
        if (rect && this.props.isContentActive()) {
            const wasPlaying = this.props.playing();
            if (wasPlaying) this.props.Pause();
            var wasSelecting = this._markerEnd !== undefined;
            setupMoveUpEvents(
                this,
                e,
                action((e) => {
                    if (!wasSelecting) {
                        this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
                        wasSelecting = true;
                    }
                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
                    return false;
                }),
                action((e, movement, isClick) => {
                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
                    if (this._markerEnd < this._markerStart) {
                        const tmp = this._markerStart;
                        this._markerStart = this._markerEnd;
                        this._markerEnd = tmp;
                    }
                    if (
                        !isClick &&
                        Math.abs(movement[0]) > 15 &&
                        !this.IsTrimming
                    ) {
                        const anchor = CollectionStackedTimeline.createAnchor(
                            this.rootDoc,
                            this.dataDoc,
                            this.props.fieldKey,
                            this.props.startTag,
                            this.props.endTag,
                            this._markerStart,
                            this._markerEnd
                        );
                        setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                    }
                    (!isClick || !wasSelecting) &&
                        (this._markerEnd = undefined);
                }),
                (e, doubleTap) => {
                    if (e.button !== 2) {
                        this.props.select(false);
                        !wasPlaying && doubleTap && this.props.Play();
                    }
                },
                this.props.isSelected(true) || this.props.isContentActive(),
                undefined,
                () => {
                    if (shiftKey) {
                        CollectionStackedTimeline.createAnchor(
                            this.rootDoc,
                            this.dataDoc,
                            this.props.fieldKey,
                            this.props.startTag,
                            this.props.endTag,
                            this.currentTime
                        );
                    } else {
                        !wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
                    }
                }
            );
        }

    }

    @action
    trimLeft = (e: React.PointerEvent): void => {
        const rect = this._timeline?.getBoundingClientRect();
        const clientX = e.movementX;
        setupMoveUpEvents(
            this,
            e,
            action((e, [], []) => {
                if (rect && this.props.isContentActive()) {
                    this._trimStart = Math.min(
                        Math.max(
                            this.trimStart + (e.movementX / rect.width) * this.clipDuration,
                            0
                        ),
                        this.trimEnd - this.minTrimLength
                    );
                }
                return false;
            }),
            emptyFunction,
            action((e, doubleTap) => {
                doubleTap && (this._trimStart = this.clipStart);
            })
        );
    }

    @action
    trimRight = (e: React.PointerEvent): void => {
        const rect = this._timeline?.getBoundingClientRect();
        const clientX = e.movementX;
        setupMoveUpEvents(
            this,
            e,
            action((e, [], []) => {
                if (rect && this.props.isContentActive()) {
                    this._trimEnd = Math.max(
                        Math.min(
                            this.trimEnd + (e.movementX / rect.width) * this.clipDuration,
                            this.clipStart + this.clipDuration
                        ),
                        this.trimStart + this.minTrimLength
                    );
                }
                return false;
            }),
            emptyFunction,
            action((e, doubleTap) => {
                doubleTap && (this._trimEnd = this.clipEnd);
            })
        );
    }

    @action
    internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
        if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
        if (!super.onInternalDrop(e, de)) return false;

        // determine x coordinate of drop and assign it to the documents being dragged  --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
        const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
        const x = localPt[0] - docDragData.offset[0];
        const timelinePt = this.toTimeline(x, this.props.PanelWidth());
        docDragData.droppedDocuments.forEach(drop => {
            const d = Doc.GetProto(drop);
            if (this.anchorEnd(d) !== undefined) {
                d[d._timecodeToHide === undefined ? this.props.endTag : "_timecodeToHide"] = timelinePt + this.anchorEnd(d) - this.anchorStart(d);
            }
            d[d._timecodToShow === undefined ? this.props.startTag : "_timecodToShow"] = timelinePt;
        });

        return true;
    }

    onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
        if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0);
        return false;
    }

    @undoBatch
    @action
    static createAnchor(
        rootDoc: Doc,
        dataDoc: Doc,
        fieldKey: string,
        startTag: string,
        endTag: string,
        anchorStartTime?: number,
        anchorEndTime?: number,
        docAnchor?: Doc
    ) {
        if (anchorStartTime === undefined) return rootDoc;
        const anchor = docAnchor ?? Docs.Create.LabelDocument({
            title: ComputedField.MakeFunction(
                `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
            ) as any,
            _stayInCollection: true,
            useLinkSmallAnchor: true,
            hideLinkButton: true,
            annotationOn: rootDoc,
            _timelineLabel: true,
            borderRounding: anchorEndTime === undefined ? "100%" : undefined
        });
        Doc.GetProto(anchor)[startTag] = anchorStartTime;
        Doc.GetProto(anchor)[endTag] = anchorEndTime;
        if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
            Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
        } else {
            dataDoc[fieldKey] = new List<Doc>([anchor]);
        }
        return anchor;
    }

    @action
    playOnClick = (anchorDoc: Doc, clientX: number) => {
        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
        const endTime = this.anchorEnd(anchorDoc);
        if (this.layoutDoc.autoPlayAnchors) {
            if (this.props.playing()) this.props.Pause();
            else this.props.playFrom(seekTimeInSeconds, endTime);
        } else {
            if (
                seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) &&
                endTime > NumCast(this.layoutDoc._currentTimecode)
            ) {
                if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
                    this.props.Pause();
                } else {
                    this.props.Play();
                }
            } else {
                this.props.playFrom(seekTimeInSeconds, endTime);
            }
        }
        return { select: true };
    }

    @action
    clickAnchor = (anchorDoc: Doc, clientX: number) => {
        if (anchorDoc.isLinkButton) {
            LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
        }
        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
        const endTime = this.anchorEnd(anchorDoc);
        if (
            seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 &&
            endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4
        ) {
            if (this.props.playing()) this.props.Pause();
            else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
            else if (!this.layoutDoc.autoPlayAnchors) {
                const rect = this._timeline?.getBoundingClientRect();
                rect &&
                    this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
            }
        } else {
            if (this.layoutDoc.autoPlayAnchors) {
                this.props.playFrom(seekTimeInSeconds, endTime);
            }
            else {
                this.props.setTime(seekTimeInSeconds);
            }
        }
        return { select: true };
    }

    // makes sure no anchors overlaps each other by setting the correct position and width
    getLevel = (
        m: Doc,
        placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
    ) => {
        const timelineContentWidth = this.props.PanelWidth();
        const x1 = this.anchorStart(m);
        const x2 = this.anchorEnd(
            m,
            x1 + (10 / timelineContentWidth) * this.clipDuration
        );
        let max = 0;
        const overlappedLevels = new Set(
            placed.map((p) => {
                const y1 = p.anchorStartTime;
                const y2 = p.anchorEndTime;
                if (
                    (x1 >= y1 && x1 <= y2) ||
                    (x2 >= y1 && x2 <= y2) ||
                    (y1 >= x1 && y1 <= x2) ||
                    (y2 >= x1 && y2 <= x2)
                ) {
                    max = Math.max(max, p.level);
                    return p.level;
                }
            })
        );
        let level = max + 1;
        for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);

        placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
        return level;
    }

    dictationHeightPercent = 50;
    dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
    timelineContentHeight = () => (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
    dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
    currentTimecode = () => this.currentTime;

    @computed get renderDictation() {
        const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
        return !dictation ? null : (
            <div
                style={{
                    position: "absolute",
                    height: "100%",
                    top: this.timelineContentHeight(),
                    background: Colors.LIGHT_BLUE,
                }}
            >
                <DocumentView
                    {...OmitKeys(this.props, [
                        "NativeWidth",
                        "NativeHeight",
                        "setContentView",
                    ]).omit}
                    Document={dictation}
                    PanelHeight={this.dictationHeight}
                    isAnnotationOverlay={true}
                    isDocumentActive={returnFalse}
                    select={emptyFunction}
                    scaling={returnOne}
                    xMargin={25}
                    yMargin={10}
                    ScreenToLocalTransform={this.dictationScreenToLocalTransform}
                    whenChildContentsActiveChanged={emptyFunction}
                    removeDocument={returnFalse}
                    moveDocument={returnFalse}
                    addDocument={returnFalse}
                    CollectionView={undefined}
                    renderDepth={this.props.renderDepth + 1}
                ></DocumentView>
            </div>
        );
    }
    @computed get renderAudioWaveform() {
        return !this.props.mediaPath ? null : (
            <div className="collectionStackedTimeline-waveform">
                <AudioWaveform
                    rawDuration={this.props.rawDuration}
                    duration={this.clipDuration}
                    mediaPath={this.props.mediaPath}
                    layoutDoc={this.layoutDoc}
                    clipStart={this.clipStart}
                    clipEnd={this.clipEnd}
                    PanelHeight={this.timelineContentHeight}
                />
            </div>
        );
    }
    @computed get selectionContainer() {
        const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
        return markerEnd === undefined ? null : (
            <div
                className="collectionStackedTimeline-selector"
                style={{
                    left: `${((Math.min(this._markerStart, markerEnd) - this.trimStart) / this.trimDuration) * 100}%`,
                    width: `${(Math.abs(this._markerStart - markerEnd) / this.trimDuration) * 100}%`,
                }}
            />
        );
    }

    render() {
        const timelineContentWidth = this.props.PanelWidth();
        const overlaps: {
            anchorStartTime: number;
            anchorEndTime: number;
            level: number;
        }[] = [];
        const drawAnchors = this.childDocs.map((anchor) => ({
            level: this.getLevel(anchor, overlaps),
            anchor,
        }));
        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
        return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}>
            <div
                className="collectionStackedTimeline"
                ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
                onClick={(e) => this.isContentActive() && StopEvent(e)}
                onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)}
            >
                {drawAnchors.map((d) => {
                    const start = this.anchorStart(d.anchor);
                    const end = this.anchorEnd(
                        d.anchor,
                        start + (10 / timelineContentWidth) * this.clipDuration
                    );
                    if (end < this.clipStart || start > this.clipEnd) return (null);
                    const left = Math.max((start - this.clipStart) / this.clipDuration * timelineContentWidth, 0);
                    const top = (d.level / maxLevel) * this.timelineContentHeight();
                    const timespan = Math.max(0, end - this.clipStart) - Math.max(0, start - this.clipStart);
                    const width = (timespan / this.clipDuration) * timelineContentWidth;
                    const height = this.timelineContentHeight() / maxLevel;
                    return this.props.Document.hideAnchors ? null : (
                        <div
                            className={"collectionStackedTimeline-marker-timeline"}
                            key={d.anchor[Id]}
                            style={{
                                left,
                                top,
                                width: `${width}px`,
                                height: `${height}px`,
                            }}
                            onClick={(e) => {
                                this.props.playFrom(start, this.anchorEnd(d.anchor));
                                e.stopPropagation();
                            }}
                        >
                            <StackedTimelineAnchor
                                {...this.props}
                                mark={d.anchor}
                                rangeClickScript={this.rangeClickScript}
                                rangePlayScript={this.rangePlayScript}
                                left={left}
                                top={top}
                                width={width}
                                height={height}
                                toTimeline={this.toTimeline}
                                layoutDoc={this.layoutDoc}
                                isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
                                currentTimecode={this.currentTimecode}
                                _timeline={this._timeline}
                                stackedTimeline={this}
                                trimStart={this.trimStart}
                                trimEnd={this.trimEnd}
                            />
                        </div>
                    );
                })}
                {!this.IsTrimming && this.selectionContainer}
                {this.renderAudioWaveform}
                {this.renderDictation}

                <div
                    className="collectionStackedTimeline-current"
                    style={{
                        left: `${((this.currentTime - this.clipStart) / this.clipDuration) * 100}%`,
                    }}
                />

                {this.IsTrimming !== TrimScope.None && (
                    <>
                        <div
                            className="collectionStackedTimeline-trim-shade"
                            style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }}
                        ></div>

                        <div
                            className="collectionStackedTimeline-trim-controls"
                            style={{
                                left: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%`,
                                width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`,
                            }}
                        >
                            <div
                                className="collectionStackedTimeline-trim-handle"
                                onPointerDown={this.trimLeft}
                            ></div>
                            <div
                                className="collectionStackedTimeline-trim-handle"
                                onPointerDown={this.trimRight}
                            ></div>
                        </div>

                        <div
                            className="collectionStackedTimeline-trim-shade"
                            style={{
                                left: `${((this.trimEnd - this.clipStart) / this.clipDuration) * 100}%`,
                                width: `${((this.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
                            }}
                        ></div>
                    </>
                )}
            </div>
        </div>);
    }
}

interface StackedTimelineAnchorProps {
    mark: Doc;
    rangeClickScript: () => ScriptField;
    rangePlayScript: () => ScriptField;
    left: number;
    top: number;
    width: number;
    height: number;
    toTimeline: (screen_delta: number, width: number) => number;
    playLink: (linkDoc: Doc) => void;
    setTime: (time: number) => void;
    startTag: string;
    endTag: string;
    renderDepth: number;
    layoutDoc: Doc;
    isDocumentActive?: () => boolean;
    ScreenToLocalTransform: () => Transform;
    _timeline: HTMLDivElement | null;
    focus: DocFocusFunc;
    currentTimecode: () => number;
    isSelected: (outsideReaction?: boolean) => boolean;
    stackedTimeline: CollectionStackedTimeline;
    trimStart: number;
    trimEnd: number;
}
@observer
class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
    _lastTimecode: number;
    _disposer: IReactionDisposer | undefined;
    constructor(props: any) {
        super(props);
        this._lastTimecode = this.props.currentTimecode();
    }

    computeTitle = () => {
        const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart;
        const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart;
        return `#${formatTime(start)}-${formatTime(end)}`;
    }
    componentDidMount() {
        this._disposer = reaction(
            () => this.props.currentTimecode(),
            (time) => {
                const dictationDoc = Cast(
                    this.props.layoutDoc["data-dictation"],
                    Doc,
                    null
                );
                const isDictation =
                    dictationDoc &&
                    DocListCast(this.props.mark.links).some(
                        (link) =>
                            Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc
                    );
                if (
                    !LightboxView.LightboxDoc &&
                    // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                    // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
                    /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
                    !this.props.layoutDoc.dontAutoFollowLinks &&
                    DocListCast(this.props.mark.links).length &&
                    time > NumCast(this.props.mark[this.props.startTag]) &&
                    time < NumCast(this.props.mark[this.props.endTag]) &&
                    this._lastTimecode < NumCast(this.props.mark[this.props.startTag])
                ) {
                    LinkManager.FollowLink(
                        undefined,
                        this.props.mark,
                        this.props as any as DocumentViewProps,
                        false,
                        true
                    );
                }
                this._lastTimecode = time;
            }
        );
    }
    componentWillUnmount() {
        this._disposer?.();
    }
    // starting the drag event for anchor resizing
    onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
        this.props._timeline?.setPointerCapture(e.pointerId);
        const newTime = (e: PointerEvent) => {
            const rect = (e.target as any).getBoundingClientRect();
            return this.props.toTimeline(e.clientX - rect.x, rect.width);
        };
        const changeAnchor = (anchor: Doc, left: boolean, time: number | undefined) => {
            const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
            if (timelineOnly) {
                if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined;
                Doc.SetInPlace(
                    anchor,
                    left ? this.props.startTag : this.props.endTag,
                    time,
                    true
                );
                if (!left) Doc.SetInPlace(anchor, "borderRounding", time !== undefined ? undefined : "100%", true);
            }
            else {
                anchor[left ? "_timecodeToShow" : "_timecodeToHide"] = time;
            }
            return false;
        };
        var undo: UndoManager.Batch | undefined;

        setupMoveUpEvents(
            this,
            e,
            (e) => {
                if (!undo) undo = UndoManager.StartBatch("drag anchor");
                this.props.setTime(newTime(e));
                return changeAnchor(anchor, left, newTime(e));
            },
            (e) => {
                this.props.setTime(newTime(e));
                this.props._timeline?.releasePointerCapture(e.pointerId);
                undo?.end();
            },
            emptyFunction
        );
    }

    contextMenuItems = () => {
        const resetTitle = { script: ScriptField.MakeFunction(`self.title = "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"])`)!, icon: "folder-plus", label: "Reset Title" };
        return [resetTitle];
    }

    renderInner = computedFn(function (
        this: StackedTimelineAnchor,
        mark: Doc,
        script: undefined | (() => ScriptField),
        doublescript: undefined | (() => ScriptField),
        screenXf: () => Transform,
        width: () => number,
        height: () => number
    ) {
        const anchor = observable({ view: undefined as any });
        const focusFunc = (
            doc: Doc,
            willZoom?: boolean,
            scale?: number,
            afterFocus?: DocAfterFocusFunc,
            docTransform?: Transform
        ) => {
            this.props.playLink(mark);
            this.props.focus(doc, { willZoom, scale, afterFocus, docTransform });
        };
        return {
            anchor,
            view: (
                <DocumentView
                    key="view"
                    {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
                    ref={action((r: DocumentView | null) => anchor.view = r)}
                    Document={mark}
                    DataDoc={undefined}
                    renderDepth={this.props.renderDepth + 1}
                    LayoutTemplate={undefined}
                    LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())}
                    isDocumentActive={this.props.isDocumentActive}
                    PanelWidth={width}
                    PanelHeight={height}
                    fitWidth={returnTrue}
                    ScreenToLocalTransform={screenXf}
                    focus={focusFunc}
                    rootSelected={returnFalse}
                    onClick={script}
                    onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                    ignoreAutoHeight={false}
                    hideResizeHandles={true}
                    bringToFront={emptyFunction}
                    contextMenuItems={this.contextMenuItems}
                    scriptContext={this.props.stackedTimeline}
                />
            ),
        };
    });

    anchorScreenToLocalXf = () => this.props.ScreenToLocalTransform().translate(-this.props.left, -this.props.top);
    width = () => this.props.width;
    height = () => this.props.height;
    render() {
        const inner = this.renderInner(
            this.props.mark,
            this.props.rangeClickScript,
            this.props.rangePlayScript,
            this.anchorScreenToLocalXf,
            this.width,
            this.height
        );
        return (
            <>
                {inner.view}
                {!inner.anchor.view ||
                    !SelectionManager.IsSelected(inner.anchor.view) ? null : (
                        <>
                            <div
                                key="left"
                                className="collectionStackedTimeline-left-resizer"
                                onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)}
                            />
                            <div
                                key="right"
                                className="collectionStackedTimeline-resizer"
                                onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, false)}
                            />
                        </>
                    )}
            </>
        );
    }
}
Scripting.addGlobal(function formatToTime(time: number): any {
    return formatTime(time);
});
Scripting.addGlobal(function min(num1: number, num2: number): number {
    return Math.min(num1, num2);
});
Scripting.addGlobal(function max(num1: number, num2: number): number {
    return Math.max(num1, num2);
});