import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { Doc, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { Cast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { FollowLinkScript, IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionSubView } from '../collections/CollectionSubView';
import { LightboxView } from '../LightboxView';
import { AudioWaveform } from '../nodes/audio/AudioWaveform';
import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
import { LabelBox } from '../nodes/LabelBox';
import { VideoBox } from '../nodes/VideoBox';
import { ObservableReactComponent } from '../ObservableReactComponent';
import './CollectionStackedTimeline.scss';

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

// trimming state: shows full clip, current trim bounds, or not trimming
export enum TrimScope {
    All = 2,
    Clip = 1,
    None = 0,
}

@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
    @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
    @observable public static CurrentlyPlaying: DocumentView[] = [];
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    static LabelScript: ScriptField;
    static LabelPlayScript: ScriptField;

    private _timeline: HTMLDivElement | null = null; // ref to actual timeline div
    private _timelineWrapper: HTMLDivElement | null = null; // ref to timeline wrapper div for zooming and scrolling
    private _markerStart: number = 0;
    @observable _markerEnd: number | undefined = undefined;
    @observable _trimming: number = TrimScope.None;
    @observable _trimStart: number = 0; // trim controls start pos
    @observable _trimEnd: number = 0; // trim controls end pos

    @observable _zoomFactor: number = 1;
    @observable _scroll: number = 0;

    @observable _hoverTime: number = 0;

    @observable _thumbnail: string | undefined = undefined;

    // ensures that clip doesn't get trimmed so small that controls cannot be adjusted anymore
    get minTrimLength() {
        return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5);
    }
    @computed get thumbnails() {
        return this._props.thumbnails?.();
    }

    @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._layout_currentTimecode);
    }

    @computed get zoomFactor() {
        return this._zoomFactor;
    }

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

    componentWillUnmount() {
        document.removeEventListener('keydown', this.keyEvents, true);
        if (CollectionStackedTimeline.SelectingRegion === this) {
            runInAction(() => (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;
    }
    @action
    public CancelTrimming() {
        this._trimming = TrimScope.None;
    }

    @action
    public setZoom(zoom: number) {
        this._zoomFactor = zoom;
    }

    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);

    // converts screen pixel offset to time
    toTimeline = (screen_delta: number, width: number) => {
        return Math.max(this.clipStart, Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
    };

    @computed get rangeClick() {
        // prettier-ignore
        return ScriptField.MakeFunction('stackedTimeline.clickAnchor(this, clientX)',
            { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: this as any }
        )!;
    }
    @computed get rangePlay() {
        // prettier-ignore
        return ScriptField.MakeFunction('stackedTimeline.playOnClick(this, clientX)', 
            { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: this as any })!;
    }
    rangeClickScript = () => this.rangeClick;
    rangePlayScript = () => this.rangePlay;

    // handles key events for for creating key anchors, scrubbing, exiting trim
    @action
    keyEvents = (e: KeyboardEvent) => {
        if (
            // need to include range inputs because after dragging video time slider it becomes target element
            !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
            this._props.isContentActive()
        ) {
            // if shift pressed scrub 1 second otherwise 1/10th
            const jump = e.shiftKey ? 1 : 0.1;
            switch (e.key) {
                case ' ':
                    this._props.playing() ? this._props.Pause() : this._props.Play();
                    break;
                case '^':
                    if (!CollectionStackedTimeline.SelectingRegion) {
                        this._markerStart = this._markerEnd = this.currentTime;
                        CollectionStackedTimeline.SelectingRegion = this;
                    } else {
                        this._markerEnd = this.currentTime;
                        CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this._props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
                        this._markerEnd = undefined;
                        CollectionStackedTimeline.SelectingRegion = undefined;
                    }
                    e.stopPropagation();
                    break;
                case 'Escape':
                    // abandons current trim
                    this._trimStart = this.clipStart;
                    this._trimStart = this.clipEnd;
                    this._trimming = TrimScope.None;
                    break;
                case 'ArrowLeft':
                    this._props.setTime(Math.min(Math.max(this.clipStart, this.currentTime - jump), this.clipEnd));
                    e.stopPropagation();
                    break;
                case 'ArrowRight':
                    this._props.setTime(Math.min(Math.max(this.clipStart, this.currentTime + jump), this.clipEnd));
                    e.stopPropagation();
                    break;
            }
        }
    };

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

    // handles dragging selection to create markers
    @action
    onPointerDownTimeline = (e: React.PointerEvent): void => {
        const rect = this._timeline?.getBoundingClientRect();
        const clientX = e.clientX;
        const diff = rect ? clientX - rect?.x : null;
        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._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize');
                    }
                    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.Document, this.dataDoc, this._props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
                        setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                    }
                    (!isClick || !wasSelecting) && (this._markerEnd = undefined);
                    this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
                }),
                (e, doubleTap) => {
                    if (e.button !== 2) {
                        this._props.select(false);
                        !wasPlaying && doubleTap && this._props.Play();
                    }
                },
                this._props.isSelected() || this._props.isContentActive(),
                undefined,
                () => {
                    if (shiftKey) {
                        CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this._props.fieldKey, this.currentTime, undefined, undefined, true);
                    } else {
                        !wasPlaying && this._props.setTime(this.toTimeline(clientX - rect.x, rect.width));
                    }
                }
            );
        }
    };

    @action
    onHover = (e: React.MouseEvent): void => {
        e.stopPropagation();
        const rect = this._timeline?.getBoundingClientRect();
        const clientX = e.clientX;
        if (rect) {
            this._hoverTime = this.toTimeline(clientX - rect.x, rect.width);
            if (this.thumbnails) {
                const nearest = Math.floor((this._hoverTime / this._props.rawDuration) * VideoBox.numThumbnails);
                const imgField = this.thumbnails.length > 0 ? new ImageField(this.thumbnails[nearest]) : undefined;
                this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined;
            }
        }
    };

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

    // for dragging trim end handle
    @action
    trimRight = (e: React.PointerEvent): void => {
        const rect = this._timeline?.getBoundingClientRect();
        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.clipEnd), this.trimStart + this.minTrimLength);
                }
                return false;
            }),
            emptyFunction,
            action((e, doubleTap) => {
                doubleTap && (this._trimEnd = this.clipEnd);
            })
        );
    };

    // for rendering scrolling when timeline zoomed
    @action
    setScroll = (e: React.UIEvent) => {
        e.stopPropagation();
        this._scroll = this._timelineWrapper!.scrollLeft;
    };

    // smooth scrolls to time like when following links overflowed due to zoom
    @action
    scrollToTime = (time: number) => {
        if (this._timelineWrapper) {
            if (time > this.toTimeline(this._scroll + this._props.PanelWidth(), this.timelineContentWidth)) {
                this._scroll = Math.min(this._scroll + this._props.PanelWidth(), this.timelineContentWidth - this._props.PanelWidth());
                smoothScrollHorizontal(200, this._timelineWrapper, this._scroll);
            } else if (time < this.toTimeline(this._scroll, this.timelineContentWidth)) {
                this._scroll = (time / this.timelineContentWidth) * this.clipDuration;
                smoothScrollHorizontal(200, this._timelineWrapper, this._scroll);
            }
        }
    };

    // handles dragging and dropping markers in timeline
    @action
    internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
        if (super.onInternalDrop(e, de)) {
            // 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._scroll, this.timelineContentWidth);
            docDragData.droppedDocuments.forEach(drop => {
                const anchorEnd = this.anchorEnd(drop);
                if (anchorEnd !== undefined) {
                    Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this._props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false);
                }
                Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this._props.startTag : 'timecodeToShow', timelinePt, false);
            });

            return true;
        }
        return false;
    }

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

    // creates marker on timeline
    @undoBatch
    static createAnchor(doc: Doc, dataDoc: Doc, fieldKey: string, anchorStartTime: Opt<number>, anchorEndTime: Opt<number>, docAnchor: Opt<Doc>, addAsAnnotation: boolean) {
        if (anchorStartTime === undefined) return doc;
        const startTag = '_timecodeToShow';
        const endTag = '_timecodeToHide';
        const anchor =
            docAnchor ??
            Docs.Create.LabelDocument({
                title: ComputedField.MakeFunction(`this["${endTag}"] ? "#" + formatToTime(this["${startTag}"]) + "-" + formatToTime(this["${endTag}"]) : "#" + formatToTime(this["${startTag}"])`) as any,
                _label_minFontSize: 12,
                _label_maxFontSize: 24,
                _dragOnlyWithinContainer: true,
                backgroundColor: 'rgba(128, 128, 128, 0.5)',
                layout_hideLinkButton: true,
                onClick: FollowLinkScript(),
                annotationOn: doc,
                _isTimelineLabel: true,
                layout_borderRounding: anchorEndTime === undefined ? '100%' : undefined,
            });
        Doc.GetProto(anchor)[startTag] = anchorStartTime;
        Doc.GetProto(anchor)[endTag] = anchorEndTime;
        if (addAsAnnotation) {
            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.05;
        const endTime = this.anchorEnd(anchorDoc);
        if (this.layoutDoc.autoPlayAnchors) {
            if (this._props.playing()) this._props.Pause();
            else {
                this._props.playFrom(seekTimeInSeconds, endTime);
                this.scrollToTime(seekTimeInSeconds);
            }
        } else {
            if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) {
                if (!this.layoutDoc.autoPlayAnchors && this._props.playing()) {
                    this._props.Pause();
                } else {
                    this._props.Play();
                }
            } else {
                this._props.playFrom(seekTimeInSeconds, endTime);
                this.scrollToTime(seekTimeInSeconds);
            }
        }
        return { select: true };
    };

    @action
    clickAnchor = (anchorDoc: Doc, clientX: number) => {
        if (IsFollowLinkScript(anchorDoc.onClick)) {
            LinkFollower.FollowLink(undefined, anchorDoc, false);
        }
        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
        const endTime = this.anchorEnd(anchorDoc);
        if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._layout_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.timelineContentWidth;
        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;

    @computed get timelineContentHeight() {
        return (this._props.PanelHeight() * this.dictationHeightPercent) / 100;
    }
    @computed get timelineContentWidth() {
        return this._props.PanelWidth() * this.zoomFactor;
    } // subtract size of container border

    dictationScreenToLocalTransform = () => this._props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight);

    isContentActive = () => this._props.isSelected() || this._props.isContentActive();

    currentTimecode = () => this.currentTime;

    // renders selection region on timeline
    @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}%`,
                }}
            />
        );
    }

    @computed get timelineEvents() {
        return this._props.isContentActive() ? 'all' : this._props.isContentActive() === false ? 'none' : undefined;
    }
    render() {
        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 this.clipDuration === 0 ? null : (
            <div ref={this.createDashEventsTarget} style={{ pointerEvents: this.timelineEvents }}>
                <div
                    className="collectionStackedTimeline-timelineContainer"
                    style={{ width: this._props.PanelWidth(), cursor: SnappingManager.IsDragging ? 'grab' : '' }}
                    onWheel={e => this.isContentActive() && e.stopPropagation()}
                    onScroll={this.setScroll}
                    onMouseMove={e => this.isContentActive() && this.onHover(e)}
                    ref={wrapper => (this._timelineWrapper = wrapper)}>
                    <div
                        className="collectionStackedTimeline"
                        ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
                        onClick={e => this.isContentActive() && StopEvent(e)}
                        onPointerDown={e => this.isContentActive() && this.onPointerDownTimeline(e)}
                        style={{ width: this.timelineContentWidth }}>
                        {drawAnchors.map(d => {
                            const start = this.anchorStart(d.anchor);
                            const end = this.anchorEnd(d.anchor, start + (10 / this.timelineContentWidth) * this.clipDuration);
                            if (end < this.clipStart || start > this.clipEnd) return null;
                            const left = Math.max(((start - this.clipStart) / this.clipDuration) * this.timelineContentWidth, 0);
                            const top = (d.level / maxLevel) * this._props.PanelHeight();
                            const timespan = Math.max(0, Math.min(end - this.clipStart, this.clipEnd)) - Math.max(0, start - this.clipStart);
                            const width = (timespan / this.clipDuration) * this.timelineContentWidth;
                            const height = this._props.PanelHeight() / 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`,
                                        pointerEvents: 'none',
                                    }}>
                                    <StackedTimelineAnchor
                                        {...this._props}
                                        mark={d.anchor}
                                        rangeClickScript={this.rangeClickScript}
                                        rangePlayScript={this.rangePlayScript}
                                        left={left - this._scroll}
                                        top={top}
                                        width={width}
                                        height={height}
                                        toTimeline={this.toTimeline}
                                        layoutDoc={this.layoutDoc}
                                        isDocumentActive={this.isContentActive}
                                        currentTimecode={this.currentTimecode}
                                        _timeline={this._timeline}
                                        stackedTimeline={this}
                                        trimStart={this.trimStart}
                                        trimEnd={this.trimEnd}
                                    />
                                </div>
                            );
                        })}
                        {!this.IsTrimming && this.selectionContainer}
                        {!this._props.PanelHeight() ? null : (
                            <AudioWaveform
                                rawDuration={this._props.rawDuration}
                                fieldKey={this._props.dataFieldKey}
                                duration={this.clipDuration}
                                mediaPath={this._props.mediaPath}
                                layoutDoc={this.layoutDoc}
                                clipStart={this.clipStart}
                                clipEnd={this.clipEnd}
                                zoomFactor={this.zoomFactor}
                                PanelHeight={this.timelineContentHeight}
                                PanelWidth={this.timelineContentWidth}
                                progress={(this.currentTime - this.clipStart) / this.clipDuration}
                            />
                        )}

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

                        <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>
                <div className="timeline-hoverUI" style={{ left: ((this._hoverTime - this.clipStart) / this.clipDuration) * this.timelineContentWidth - this._scroll }}>
                    <div className="hoverTime">{formatTime(this._hoverTime - this.clipStart)}</div>
                    {this._thumbnail && <img className="videoBox-thumbnail" src={this._thumbnail} />}
                </div>
            </div>
        );
    }
}

/**
 * StackedTimelineAnchor
 * creates the anchors to display markers, links, and embedded documents on timeline
 */

interface StackedTimelineAnchorProps {
    mark: Doc;
    whenChildContentsActiveChanged: (isActive: boolean) => void;
    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
    rangeClickScript: () => ScriptField;
    rangePlayScript: () => ScriptField;
    left: number;
    top: number;
    width: number;
    height: number;
    toTimeline: (screen_delta: number, width: number) => number;
    styleProvider?: (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
    playLink: (linkDoc: Doc, options: DocFocusOptions) => void;
    setTime: (time: number) => void;
    startTag: string;
    endTag: string;
    renderDepth: number;
    layoutDoc: Doc;
    isDocumentActive?: () => boolean | undefined;
    ScreenToLocalTransform: () => Transform;
    _timeline: HTMLDivElement | null;
    focus: DocFocusFunc;
    currentTimecode: () => number;
    isSelected: () => boolean;
    stackedTimeline: CollectionStackedTimeline;
    trimStart: number;
    trimEnd: number;
}

@observer
class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnchorProps> {
    _lastTimecode: number;
    _disposer: IReactionDisposer | undefined;

    constructor(props: any) {
        super(props);
        makeObservable(this);
        this._lastTimecode = this._props.currentTimecode();
    }

    // updates marker document title to reflect correct timecodes
    computeTitle = () => {
        if (this._props.mark.type !== DocumentType.LABEL) return undefined;
        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 && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, 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 &&
                    LinkManager.Links(this._props.mark).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]) - 1e-5
                ) {
                    LinkFollower.FollowLink(undefined, this._props.mark, false);
                }
                this._lastTimecode = time;
            }
        );
    }

    componentWillUnmount() {
        this._disposer?.();
    }

    @observable noEvents = false;
    // starting the drag event for anchor resizing
    @action
    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, 'layout_borderRounding', time !== undefined ? undefined : '100%', true);
            } else {
                anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time;
            }
            return false;
        };
        this.noEvents = true;
        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));
            },
            action(e => {
                this._props.setTime(newTime(e));
                undo?.end();
                this.noEvents = false;
            }),
            emptyFunction
        );
    };

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

    // renders anchor LabelBox
    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 Opt<DocumentView> | null });
        const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => {
            this._props.playLink(mark, options);
            return undefined;
        };
        return {
            anchor,
            view: (
                <DocumentView
                    key="view"
                    {...this._props}
                    NativeWidth={returnZero}
                    NativeHeight={returnZero}
                    ref={action((r: DocumentView | null) => (anchor.view = r))}
                    Document={mark}
                    TemplateDataDocument={undefined}
                    docViewPath={returnEmptyDoclist}
                    pointerEvents={this.noEvents ? returnNone : undefined}
                    styleProvider={this._props.styleProvider}
                    renderDepth={this._props.renderDepth + 1}
                    LayoutTemplate={undefined}
                    LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
                    isDocumentActive={this._props.isDocumentActive}
                    PanelWidth={width}
                    PanelHeight={height}
                    layout_fitWidth={returnTrue}
                    ScreenToLocalTransform={screenXf}
                    pinToPres={emptyFunction}
                    focus={focusFunc}
                    isContentActive={returnFalse}
                    searchFilterDocs={returnEmptyDoclist}
                    childFilters={returnEmptyFilter}
                    childFiltersByRanges={returnEmptyFilter}
                    onClick={script}
                    onDoubleClick={this._props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                    ignoreAutoHeight={false}
                    hideResizeHandles={true}
                    bringToFront={emptyFunction}
                    contextMenuItems={this.contextMenuItems}
                />
            ),
        };
    });

    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 (
            <div style={{ pointerEvents: this.noEvents ? 'none' : undefined }}>
                {inner.view}
                {!inner.anchor.view || !inner.anchor.view.SELECTED ? null : (
                    <>
                        <div key="left" className="collectionStackedTimeline-left-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this._props.mark, true)} />
                        <div key="right" className="collectionStackedTimeline-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this._props.mark, false)} />
                    </>
                )}
            </div>
        );
    }
}
ScriptingGlobals.add(function formatToTime(time: number): any {
    return formatTime(time);
});
ScriptingGlobals.add(function min(num1: number, num2: number): number {
    return Math.min(num1, num2);
});
ScriptingGlobals.add(function max(num1: number, num2: number): number {
    return Math.max(num1, num2);
});
