import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { basename } from 'path';
import * as React from 'react';
import { Doc, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { dropActionType } from '../../util/DragManager';
import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
import { undoBatch } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
import './VideoBox.scss';

/**
 * VideoBox
 * Main component: VideoBox.tsx
 * Supporting Components: CollectionStackedTimeline
 *
 * VideoBox is a node that supports the playback of video files in Dash.
 * When a video file is importeed into Dash, it is immediately rendered as a VideoBox document.
 * CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element
 * User can trim video: nondestructive, just sets new bounds for playback and rendering timeline
 * Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times
 */

@observer
export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(VideoBox, fieldKey);
    }
    static heightPercent = 80; // height of video relative to videoBox when timeline is open
    static numThumbnails = 20;
    private unmounting = false;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _videoRef: HTMLVideoElement | null = null; // <video> ref
    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
    private _audioPlayer: HTMLAudioElement | null = null;
    private _marqueeref = React.createRef<MarqueeAnnotator>();
    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
    private _playRegionTimer: any = null; // timeout for playback
    private _controlsFadeTimer: any = null; // timeout for controls fade

    constructor(props: FieldViewProps) {
        super(props);
        makeObservable(this);
        this._props.setContentViewBox?.(this);
    }

    @observable _stackedTimeline: CollectionStackedTimeline | undefined = undefined; // CollectionStackedTimeline ref
    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
    @observable _screenCapture = false;
    @observable _clicking = false; // used for transition between showing/hiding timeline
    @observable _playTimer?: NodeJS.Timeout = undefined;
    @observable _fullScreen = false;
    @observable _playing = false;
    @observable _finished: boolean = false; // has playback reached end of clip
    @observable _volume: number = 1;
    @observable _muted: boolean = false;
    @observable _controlsTransform?: { X: number; Y: number } = undefined;
    @observable _controlsVisible: boolean = true;
    @observable _scrubbing: boolean = false;

    @computed get links() {
        return LinkManager.Links(this.dataDoc);
    }
    @computed get heightPercent() {
        return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100);
    } // current percent of video relative to VideoBox height
    // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); }
    @observable rawDuration: number = 0;

    // returns the path of the audio file
    @computed get audiopath() {
        const field = Cast(this.Document[this._props.fieldKey + '_audio'], AudioField, null);
        const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
        return field?.url.href ?? vfield?.url.href ?? '';
    }

    // returns the presentation data if it exists, null otherwise
    @computed get presentation() {
        const data = this.dataDoc[this.fieldKey + '_presentation'];
        return data ? JSON.parse(StrCast(data)) : null;
    }

    @computed private get timeline() {
        return this._stackedTimeline;
    }
    private get transition() {
        return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
    } // css transition for hiding/showing timeline
    public get player(): HTMLVideoElement | null {
        return this._videoRef;
    }

    componentDidMount() {
        this.unmounting = false;
        this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
        this.player && this.setPlayheadTime(this.timeline?.clipStart || 0);
        document.addEventListener('keydown', this.keyEvents, true);

        if (this.presentation) {
            ReplayMovements.Instance.setVideoBox(this);
        }
    }

    componentWillUnmount() {
        this.unmounting = true;
        this.removeCurrentlyPlaying();
        this.Pause();
        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
        document.removeEventListener('keydown', this.keyEvents, true);

        if (this.presentation) {
            ReplayMovements.Instance.removeVideoBox();
        }
    }

    // handles key events, when timeline scrubs fade controls
    @action
    keyEvents = (e: KeyboardEvent) => {
        if (
            // need to include range inputs because after dragging time slider it becomes target element
            !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
            this._props.isSelected()
        ) {
            switch (e.key) {
                case 'ArrowLeft':
                case 'ArrowRight':
                    clearTimeout(this._controlsFadeTimer);
                    this._scrubbing = true;
                    this._controlsFadeTimer = setTimeout(
                        action(() => (this._scrubbing = false)),
                        500
                    );
                    e.stopPropagation();
                    break;
            }
        }
    };

    // plays video
    @action public Play = (update: boolean = true) => {
        if (this._playRegionTimer) return;

        this._playing = true;
        const eleTime = this.player?.currentTime || 0;
        if (this.timeline) {
            let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;

            if (this._finished) {
                // restarts video if reached end on previous play
                this._finished = false;
                start = this.timeline.trimStart;
            }
            try {
                this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
                update && this.player && this.playFrom(start, undefined, true);
                update && this._audioPlayer?.play();
            } catch (e) {
                console.log('Video Play Exception:', e);
            }
        }
        this.updateTimecode();
    };

    // goes to time
    @action public Seek(time: number) {
        this.player && (this.player.currentTime = time);
        this._audioPlayer && (this._audioPlayer.currentTime = time);
        // TODO: revisit this and clean it
        if ((this.player?.currentTime || -1) < this.rawDuration) {
            this._finished = false;
        }
    }

    _keepCurrentlyPlaying = false; // flag to prevent document when paused from being removed from global 'currentlyPlaying' list
    IsPlaying = () => this._playing;
    TogglePause = () => {
        if (!this._playing) this.Play();
        else {
            this._keepCurrentlyPlaying = true;
            this.pause();
            setTimeout(() => (this._keepCurrentlyPlaying = false));
        }
    };

    // pauses video
    @action public pause = (update: boolean = true) => {
        this._playing = false;
        try {
            update && this.player?.pause();
            update && this._audioPlayer?.pause();
        } catch (e) {
            console.log('Video Pause Exception:', e);
        }
        this._playTimer = undefined;
        this.updateTimecode();
        if (!this._finished) {
            clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
        }
        this._playRegionTimer = undefined;
    };
    @action Pause = (update: boolean = true) => {
        this.pause(update);
        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
    };

    // toggles video full screen
    @action public FullScreen = () => {
        if (document.fullscreenElement === this._contentRef) {
            this._fullScreen = false;
            this.player && this._contentRef && document.exitFullscreen();
        } else {
            this._fullScreen = true;
            this.player && this._contentRef && this._contentRef.requestFullscreen();
        }
    };

    // fades out controls in fullscreen after mouse stops moving
    @action controlsFade = (e: PointerEvent) => {
        e.stopPropagation();
        if (!this._scrubbing) {
            clearTimeout(this._controlsFadeTimer);
            this._controlsVisible = true;
            this._controlsFadeTimer = setTimeout(
                action(() => (this._controlsVisible = false)),
                3000
            );
        }
    };

    // drag controls around window in fulls screen
    @action controlsDrag = (e: React.PointerEvent) => {
        e.preventDefault();
        e.stopPropagation();
        const eleStyle = getComputedStyle(e.target as Element);
        this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) };

        setupMoveUpEvents(
            e.target,
            e,
            action((e, down, delta) => {
                if (this._controlsTransform) {
                    this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth));
                    this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight));
                }
                return false;
            }),
            emptyFunction,
            emptyFunction
        );
    };

    // creates and links snapshot photo of current video frame
    @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
        const width = NumCast(this.layoutDoc._width);
        const canvas = document.createElement('canvas');
        canvas.width = 640;
        canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
        const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions
        if (ctx) {
            this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
        }

        if (!this._videoRef) {
            const b = Docs.Create.LabelDocument({
                x: NumCast(this.layoutDoc.x) + width,
                y: NumCast(this.layoutDoc.y, 1),
                _width: 150,
                _height: 50,
                title: (this.layoutDoc._layout_currentTimecode || 0).toString(),
                onClick: FollowLinkScript(),
            });
            this._props.addDocument?.(b);
            DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' });
        } else {
            //convert to desired file format
            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
            // if you want to preview the captured image,
            const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
            const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[\.\/\?\=]/g, '_'));
            const filename = basename(encodedFilename);
            Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
        }
    };

    updateIcon = () => {
        const makeIcon = (returnedfilename: string) => {
            this.dataDoc.icon = new ImageField(returnedfilename);
            this.dataDoc.icon_nativeWidth = NumCast(this.layoutDoc._width);
            this.dataDoc.icon_nativeHeight = NumCast(this.layoutDoc._height);
        };
        this.Snapshot(undefined, undefined, makeIcon);
    };

    // creates link for snapshot
    createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
        const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
        const width = NumCast(this.layoutDoc._width) || 1;
        const height = NumCast(this.layoutDoc._height);
        const imageSnapshot = Docs.Create.ImageDocument(url, {
            _nativeWidth: Doc.NativeWidth(this.layoutDoc),
            _nativeHeight: Doc.NativeHeight(this.layoutDoc),
            x: NumCast(this.layoutDoc.x) + width,
            y: NumCast(this.layoutDoc.y),
            onClick: FollowLinkScript(),
            _width: 150,
            _height: (height / width) * 150,
            title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
        });
        Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
        Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
        this._props.addDocument?.(imageSnapshot);
        const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
        // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);  // do we need to set an end time? should default to +0.1
        setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
    };

    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
        const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
        const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
        if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
        const anchor =
            addAsAnnotation && marquee
                ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document
                : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
        return anchor;
    };

    // sets video info on load
    videoLoad = action(() => {
        const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
        if (aspect && !this.isCropped) {
            Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
            Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
            this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
        }
        if (Number.isFinite(this.player!.duration)) {
            this.rawDuration = this.player!.duration;
            this.dataDoc[this.fieldKey + '_duration'] = this.rawDuration;
        } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '_duration']);
    });

    // updates video time
    @action
    updateTimecode = () => {
        !this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
    };

    getView = (doc: Doc, options: FocusViewOptions) => {
        if (this._stackedTimeline?.makeDocUnfiltered(doc)) {
            if (this.heightPercent === 100) {
                // do we want to always open up the timeline when followin a link? kind of clunky visually
                //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
                options.didMove = true;
            }
            return this._stackedTimeline.getView(doc, options);
        }
        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
    };

    // extracts video thumbnails and saves them as field of doc
    getVideoThumbnails = () => {
        if (this.dataDoc[this.fieldKey + '_thumbnails'] !== undefined) return;
        this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>();
        const thumbnailPromises: Promise<any>[] = [];
        const video = document.createElement('video');

        video.onloadedmetadata = () => (video.currentTime = 0);

        video.onseeked = () => {
            const canvas = document.createElement('canvas');
            canvas.height = 100;
            canvas.width = 100;
            canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
            const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
            const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
            thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
            const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
            if (newTime < video.duration) {
                video.currentTime = newTime;
            } else {
                Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails)));
            }
        };

        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
        field && (video.src = field.url.href);
    };

    // sets video element ref
    @action
    setVideoRef = (vref: HTMLVideoElement | null) => {
        this._videoRef = vref;
        if (vref) {
            this._videoRef!.ontimeupdate = this.updateTimecode;
            // @ts-ignore
            // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
            this._disposers.reactionDisposer?.();
            this._disposers.reactionDisposer = reaction(
                () => NumCast(this.layoutDoc._layout_currentTimecode),
                time => !this._playing && (vref.currentTime = time),
                { fireImmediately: true }
            );

            (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails();
        }
    };

    // set ref for div that wraps video and controls for fullscreen
    @action
    setContentRef = (cref: HTMLDivElement | null) => {
        this._contentRef = cref;
        if (cref) {
            cref.onfullscreenchange = action(e => {
                this._fullScreen = document.fullscreenElement === cref;
                this._controlsVisible = true;
                this._scrubbing = false;
                clearTimeout(this._controlsFadeTimer);
                if (this._fullScreen) {
                    document.addEventListener('pointermove', this.controlsFade);
                } else {
                    document.removeEventListener('pointermove', this.controlsFade);
                }
            });
        }
    };

    // context menu
    specificContextMenu = (e: React.MouseEvent): void => {
        const field = Cast(this.dataDoc[this._props.fieldKey], VideoField);
        if (field) {
            const url = field.url.href;
            const subitems: ContextMenuProps[] = [];
            subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
            subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
            this.Document.type === DocumentType.SCREENSHOT &&
                subitems.push({
                    description: 'Screen Capture',
                    event: async () => {
                        runInAction(() => (this._screenCapture = !this._screenCapture));
                        this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
                    },
                    icon: 'expand-arrows-alt',
                });
            subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' });
            subitems.push({
                description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
                event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
                icon: 'expand-arrows-alt',
            });
            subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
            // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
            // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
            // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
            subitems.push({
                description: 'Copy path',
                event: () => {
                    Utils.CopyText(url);
                },
                icon: 'expand-arrows-alt',
            });
            // if the videobox was turned from a recording box
            if (this.dataDoc[this.fieldKey + '_recorded'] === true) {
                subitems.push({
                    description: 'Recreate recording',
                    event: () => {
                        this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
                        // delete assoicated video data
                        this.dataDoc[this._props.fieldKey] = '';
                        this.dataDoc[this.fieldKey + '_duration'] = '';
                        // delete assoicated presentation data
                        this.dataDoc[this.fieldKey + '_presentation'] = '';
                    },
                    icon: 'expand-arrows-alt',
                });
            }
            ContextMenu.Instance.addItem({ description: 'Options...', subitems: subitems, icon: 'video' });
        }
    };

    // ref for updating time
    setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e);

    // renders the video and audio
    @computed get content() {
        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
        const interactive = Doc.ActiveTool !== InkTool.None || !this._props.isSelected() ? '' : '-interactive';
        const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive;
        const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
        return !field ? (
            <div key="loading">Loading</div>
        ) : (
            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}>
                <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
                    {this._fullScreen && (
                        <div
                            className="videoBox-ui"
                            onPointerDown={this.controlsDrag}
                            style={{
                                left: this._controlsTransform && this._controlsTransform.X,
                                top: this._controlsTransform && this._controlsTransform.Y,
                                visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden',
                                opacity: opacity,
                            }}>
                            {this.UIButtons}
                        </div>
                    )}
                    <video
                        key="video"
                        autoPlay={this._screenCapture}
                        ref={this.setVideoRef}
                        style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.layoutDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}}
                        onCanPlay={this.videoLoad}
                        controls={false}
                        onPlay={() => this.Play()}
                        onSeeked={this.updateTimecode}
                        onPause={() => this.Pause()}
                        onClick={this._fullScreen ? () => (this.playing() ? this.Pause() : this.Play()) : e => e.preventDefault()}>
                        <source src={field.url.href} type="video/mp4" />
                        Not supported.
                    </video>
                    {!this.audiopath || this.audiopath === field.url.href ? null : (
                        <audio ref={this.setAudioRef} className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`}>
                            <source src={this.audiopath} type="audio/mpeg" />
                            Not supported.
                        </audio>
                    )}
                </div>
            </div>
        );
    }

    // for play button
    onPlayDown = () => (this._playing ? this.Pause() : this.Play());

    // for fullscreen button
    onFullDown = (e: React.PointerEvent) => {
        this.FullScreen();
        e.stopPropagation();
        e.preventDefault();
    };

    // for snapshot button
    onSnapshotDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            e => {
                this.Snapshot(e.clientX, e.clientY);
                return true;
            },
            emptyFunction,
            () => this.Snapshot()
        );
    };

    // for show/hide timeline button, transitions between show/hide
    @action
    onTimelineHdlDown = (e: React.PointerEvent) => {
        this._clicking = true;
        setupMoveUpEvents(
            this,
            e,
            action(encodeURIComponent => {
                this._clicking = false;
                if (this._props.isContentActive()) {
                    // const local = this.ScreenToLocalTransform().scale(this._props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
                    // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this._props.PanelHeight() * 100));

                    this.layoutDoc._layout_timelineHeightPercent = 80;
                }
                return false;
            }),
            emptyFunction,
            () => {
                this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
                setTimeout(
                    action(() => (this._clicking = false)),
                    500
                );
            },
            this._props.isContentActive(),
            this._props.isContentActive()
        );
    };

    // removes from currently playing display
    @action
    removeCurrentlyPlaying = () => {
        const docView = this.DocumentView?.();
        if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
        }
    };
    // adds doc to currently playing display
    @action
    addCurrentlyPlaying = () => {
        const docView = this.DocumentView?.();
        if (!CollectionStackedTimeline.CurrentlyPlaying) {
            CollectionStackedTimeline.CurrentlyPlaying = [];
        }
        if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
            CollectionStackedTimeline.CurrentlyPlaying.push(docView);
        }
    };

    // for annotating, adds doc with time info
    @action.bound
    addDocWithTimecode(doc: Doc | Doc[]): boolean {
        const docs = doc instanceof Doc ? [doc] : doc;
        const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
        docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
        return this.addDocument(doc);
    }

    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
    @action
    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
        clearTimeout(this._playRegionTimer);
        this._playRegionTimer = undefined;
        if (Number.isNaN(this.player?.duration)) {
            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
        } else if (this.player) {
            // trimBounds override requested playback bounds
            const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
            const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
            const playRegionDuration = end - start;
            // checks if times are within clip range
            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
                this.player.currentTime = start;
                this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
                this.player.play();
                this._audioPlayer?.play();
                this._playing = true;
                this.addCurrentlyPlaying();
                this._playRegionTimer = setTimeout(() => {
                    // need to keep track of if end of clip is reached so on next play, clip restarts
                    if (fullPlay) this._finished = true;
                    // removes from currently playing if playback has reached end of range marker
                    else this.removeCurrentlyPlaying();
                    this.Pause();
                }, playRegionDuration * 1000);
            } else {
                this.Pause();
            }
        }
    };

    // ends trim, hides trim controls and displays new clip
    @undoBatch
    finishTrim = action(() => {
        this.Pause();
        this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
        this.timeline?.StopTrimming();
    });

    // displays trim controls to start trimming clip
    startTrim = (scope: TrimScope) => {
        this.Pause();
        this.timeline?.StartTrimming(scope);
    };

    // for trim button, double click displays full clip, single displays curr trim bounds
    onClipPointerDown = (e: React.PointerEvent) => {
        // if timeline isn't shown, show first then trim
        this.heightPercent >= 100 && this.onTimelineHdlDown(e);
        this.timeline &&
            setupMoveUpEvents(
                this,
                e,
                returnFalse,
                returnFalse,
                action((e: PointerEvent, doubleTap?: boolean) => {
                    if (doubleTap) {
                        this.startTrim(TrimScope.All);
                    } else if (this.timeline) {
                        this.Pause();
                        this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
                    }
                })
            );
    };

    // for volume slider sets volume
    @action
    setVolume = (volume: number) => {
        if (this.player) {
            this._volume = volume;
            this.player.volume = volume;
            if (this._muted) {
                this.toggleMute();
            }
        }
    };

    // toggles video mute
    @action
    toggleMute = () => {
        if (this.player) {
            this._muted = !this._muted;
            this.player.muted = this._muted;
        }
    };

    // stretches vertically or horizontally depending on video orientation so video fits full screen
    fullScreenSize() {
        if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
            //prettier-ignore
            return ({ height: '100%' });
        }
        //prettier-ignore
        return ({ width: '100%' });
    }

    // for zoom slider, sets timeline waveform zoom
    zoom = (zoom: number) => this.timeline?.setZoom(zoom);

    // plays link
    playLink = (doc: Doc, options: FocusViewOptions) => {
        const startTime = Math.max(0, NumCast(doc.config_clipStart, this._stackedTimeline?.anchorStart(doc) || 0));
        const endTime = this.timeline?.anchorEnd(doc);
        if (startTime !== undefined) {
            if (options.playMedia) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
            else this.Seek(startTime);
        }
    };

    // starts marquee selection
    marqueeDown = (e: React.PointerEvent) => {
        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
            setupMoveUpEvents(
                this,
                e,
                action(e => {
                    MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
                    this._marqueeref.current?.onInitiateSelection([e.clientX, e.clientY]);
                    return true;
                }),
                returnFalse,
                () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
                false,
                false
            );
        }
    };

    // ends marquee selection
    @action
    finishMarquee = () => {
        this._marqueeref.current?.onTerminateSelection();
        this._props.select(true);
    };

    timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));

    timelineScreenToLocal = () =>
        this._props
            .ScreenToLocalTransform()
            .scale(this.scaling())
            .translate(0, (-this.heightPercent / 100) * this._props.PanelHeight());

    setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time);

    timelineHeight = () => (this._props.PanelHeight() * (100 - this.heightPercent)) / 100;

    playing = () => this._playing;

    scaling = () => this._props.NativeDimScaling?.() || 1;

    panelWidth = () => (this._props.PanelWidth() * this.heightPercent) / 100;
    panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.dataDoc) || 1) : (this._props.PanelHeight() * this.heightPercent) / 100);

    screenToLocalTransform = () => {
        const offset = (this._props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
        return this._props
            .ScreenToLocalTransform()
            .translate(-offset, 0)
            .scale(100 / this.heightPercent);
    };

    marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];

    timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`];

    // renders video controls
    componentUI = (boundsLeft: number, boundsTop: number) => {
        const xf = this.ScreenToLocalBoxXf().inverse();
        const height = this._props.PanelHeight();
        const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
        const vidWidth = this._props.PanelWidth() / this.scaling();
        const uiHeight = 25;
        const uiMargin = 10;
        const yBot = xf.transformPoint(0, vidHeight)[1];
        // prettier-ignore
        const yMid =  (xf.transformPoint(0, 0)[1] +
                       xf.transformPoint(0, height / this.scaling())[1]) /  2;
        const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
        const xRight = xf.transformPoint(vidWidth, 0)[0];
        const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
        return this._fullScreen || this.isCropped || (xRight - xPos) * 2 < 50 ? null : (
            <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                <div
                    className="videoBox-ui"
                    style={{
                        transform: `rotate(${this.ScreenToLocalBoxXf().inverse().RotateDeg}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
                        left: xPos,
                        top: yMid,
                        height: uiHeight,
                        width: (xRight - xPos) * 2 - 20,
                        transition: this._clicking ? 'top 0.5s' : '',
                        opacity,
                    }}>
                    {this.UIButtons}
                </div>
            </div>
        );
    };

    thumbnails = () => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']);
    // renders CollectionStackedTimeline
    @computed get renderTimeline() {
        return (
            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                <CollectionStackedTimeline
                    ref={action((r: any) => (this._stackedTimeline = r))}
                    {...this._props}
                    dataFieldKey={this.fieldKey}
                    fieldKey={this.annotationKey}
                    dictationKey={this.fieldKey + '_dictation'}
                    mediaPath={this.audiopath}
                    thumbnails={this.thumbnails}
                    renderDepth={this._props.renderDepth + 1}
                    startTag={'_timecodeToShow' /* videoStart */}
                    endTag={'_timecodeToHide' /* videoEnd */}
                    playFrom={this.playFrom}
                    setTime={this.setPlayheadTime}
                    playing={this.playing}
                    isAnyChildContentActive={this.isAnyChildContentActive}
                    whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
                    moveDocument={this.moveDocument}
                    addDocument={this.addDocument}
                    removeDocument={this.removeDocument}
                    ScreenToLocalTransform={this.timelineScreenToLocal}
                    Play={this.Play}
                    Pause={this.Pause}
                    playLink={this.playLink}
                    PanelHeight={this.timelineHeight}
                    rawDuration={this.rawDuration}
                />
            </div>
        );
    }
    @computed get isCropped() {
        return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video
    }

    // renders annotation layer
    @computed get annotationLayer() {
        return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
    }

    crop = (region: Doc | undefined, addCrop?: boolean) => {
        if (!region) return;
        const cropping = Doc.MakeCopy(region, true);
        const regionData = region[DocData];
        regionData.backgroundColor = 'transparent';
        regionData.lockedPosition = true;
        regionData.title = 'region:' + this.Document.title;
        regionData.followLinkToggle = true;
        region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
        this.addDocument(region);
        const anchx = NumCast(cropping.x);
        const anchy = NumCast(cropping.y);
        const anchw = NumCast(cropping._width);
        const anchh = NumCast(cropping._height);
        const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
        cropping.title = 'crop: ' + this.Document.title;
        cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
        cropping.y = NumCast(this.Document.y);
        cropping._width = anchw * (this._props.NativeDimScaling?.() || 1);
        cropping._height = anchh * (this._props.NativeDimScaling?.() || 1);
        cropping.timecodeToHide = undefined;
        cropping.timecodeToShow = undefined;
        cropping.onClick = undefined;
        const croppingProto = cropping[DocData];
        croppingProto.annotationOn = undefined;
        croppingProto.isDataDoc = true;
        croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
        croppingProto.type = DocumentType.VID;
        croppingProto.layout = VideoBox.LayoutString('data');
        croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField);
        croppingProto['data_nativeWidth'] = anchw;
        croppingProto['data_nativeHeight'] = anchh;
        croppingProto.videoCrop = true;
        croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
        croppingProto.freeform_scale = viewScale;
        croppingProto.freeform_scale_min = viewScale;
        croppingProto.freeform_ = anchx / viewScale;
        croppingProto.freeform_panY = anchy / viewScale;
        croppingProto.freeform_panX_min = anchx / viewScale;
        croppingProto.freeform_panX_max = anchw / viewScale;
        croppingProto.freeform_panY_min = anchy / viewScale;
        croppingProto.freeform_panY_max = anchh / viewScale;
        if (addCrop) {
            DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
        }
        this._props.bringToFront?.(cropping);
        return cropping;
    };
    savedAnnotations = () => this._savedAnnotations;
    render() {
        const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding);
        const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad;
        return (
            <div
                className="videoBox"
                onContextMenu={this.specificContextMenu}
                ref={this._mainCont}
                style={{
                    pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
                    borderRadius,
                    overflow: this.DocumentView?.().layout_fitWidth ? 'auto' : undefined,
                }}>
                <div className="videoBox-viewer" onPointerDown={this.marqueeDown}>
                    <div
                        style={{
                            position: 'absolute',
                            transition: this.transition,
                            width: this.panelWidth(),
                            height: this.panelHeight(),
                            top: 0,
                            left: (this._props.PanelWidth() - this.panelWidth()) / 2,
                        }}>
                        <CollectionFreeFormView
                            {...this._props}
                            setContentViewBox={emptyFunction}
                            NativeWidth={returnZero}
                            NativeHeight={returnZero}
                            renderDepth={this._props.renderDepth + 1}
                            fieldKey={this.annotationKey}
                            isAnnotationOverlay={true}
                            annotationLayerHostsContent={true}
                            PanelWidth={this._props.PanelWidth}
                            PanelHeight={this._props.PanelHeight}
                            isAnyChildContentActive={returnFalse}
                            ScreenToLocalTransform={this.screenToLocalTransform}
                            childFilters={this.timelineDocFilter}
                            select={emptyFunction}
                            focus={emptyFunction}
                            NativeDimScaling={returnOne}
                            whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                            removeDocument={this.removeDocument}
                            moveDocument={this.moveDocument}
                            addDocument={this.addDocWithTimecode}>
                            {this.content}
                        </CollectionFreeFormView>
                    </div>
                    {this.annotationLayer}
                    {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
                        <MarqueeAnnotator
                            ref={this._marqueeref}
                            Document={this.Document}
                            scrollTop={0}
                            annotationLayerScrollTop={0}
                            scaling={returnOne}
                            annotationLayerScaling={this._props.NativeDimScaling}
                            docView={this.DocumentView}
                            containerOffset={this.marqueeOffset}
                            addDocument={this.addDocWithTimecode}
                            finishMarquee={this.finishMarquee}
                            savedAnnotations={this.savedAnnotations}
                            selectionText={returnEmptyString}
                            annotationLayer={this._annotationLayer.current}
                            marqueeContainer={this._mainCont.current}
                            anchorMenuCrop={this.crop}
                        />
                    )}
                    {this.renderTimeline}
                </div>
            </div>
        );
    }

    @computed get UIButtons() {
        const bounds = this.DocumentView?.().getBounds;
        const width = (bounds?.right || 0) - (bounds?.left || 0);
        const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
        return (
            <>
                <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
                    <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
                </div>

                {this.timeline && width > 150 && (
                    <div className="timecode-controls">
                        <div className="timecode-current">{formatTime(curTime - (this.timeline?.clipStart || 0))}</div>

                        {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
                            <div className="timeline-slider">
                                <input
                                    type="range"
                                    step="0.1"
                                    min={this.timeline.clipStart}
                                    max={this.timeline.clipEnd}
                                    value={curTime}
                                    className="toolbar-slider time-progress"
                                    onPointerDown={action((e: React.PointerEvent) => {
                                        e.stopPropagation();
                                        this._scrubbing = true;
                                    })}
                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
                                    onPointerUp={action((e: React.PointerEvent) => {
                                        e.stopPropagation();
                                        this._scrubbing = false;
                                    })}
                                />
                            </div>
                        ) : (
                            <div>/</div>
                        )}

                        <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
                    </div>
                )}

                <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
                    <FontAwesomeIcon icon="expand" />
                </div>

                {!this._fullScreen && width > 300 && (
                    <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
                        <FontAwesomeIcon icon="eye" />
                    </div>
                )}

                {!this._fullScreen && width > 300 && (
                    <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
                    </div>
                )}

                <div
                    className="videobox-button"
                    title={this._muted ? 'unmute' : 'mute'}
                    onPointerDown={e => {
                        e.stopPropagation();
                        this.toggleMute();
                    }}>
                    <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
                </div>
                {width > 300 && (
                    <input
                        type="range"
                        style={{ width: `min(25%, 50px)` }}
                        step="0.1"
                        min="0"
                        max="1"
                        value={this._muted ? 0 : this._volume}
                        className="toolbar-slider volume"
                        onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
                    />
                )}

                {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
                    <>
                        <div className="videobox-button" title="zoom">
                            <FontAwesomeIcon icon="search-plus" />
                        </div>
                        <input
                            type="range"
                            step="0.1"
                            min="1"
                            max="5"
                            value={this.timeline?._zoomFactor}
                            className="toolbar-slider zoom"
                            onPointerDown={(e: React.PointerEvent) => {
                                e.stopPropagation();
                            }}
                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                                this.zoom(Number(e.target.value));
                            }}
                        />
                    </>
                )}
            </>
        );
    }
}
