import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import { basename } from "path";
import * as rp from 'request-promise';
import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
import { InkTool } from "../../../fields/InkField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField";
import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DocumentManager } from "../../util/DocumentManager";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
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, ViewBoxAnnotatableProps } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
import { MarqueeAnnotator } from "../MarqueeAnnotator";
import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
import { Presentation } from "../../util/TrackMovements";
import { RecordingBox } from "./RecordingBox";
import { ReplayMovements } from "../../util/ReplayMovements";
const path = require('path');


/**
 * 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 or YouTube video 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<ViewBoxAnnotatableProps & FieldViewProps>() {
    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
    /**
     * Uploads an image buffer to the server and stores with specified filename.  by default the image
     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
     * @param imageUri the bytes of the image
     * @param returnedFilename the base filename to store the image on the server
     * @param nosuffix optionally suppress creating multiple resolution images
     */
    public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
        try {
            const posting = Utils.prepend("/uploadURI");
            const returnedUri = await rp.post(posting, {
                body: {
                    uri: imageUri,
                    name: returnedFilename,
                    nosuffix,
                    replaceRootFilename
                },
                json: true,
            });
            return returnedUri;

        } catch (e) {
            console.log("VideoBox :" + e);
        }
    }

    static _youtubeIframeCounter: number = 0;
    static heightPercent = 80; // height of video relative to videoBox when timeline is open
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _youtubePlayer: YT.Player | undefined = undefined;
    private _videoRef: HTMLVideoElement | null = null; // <video> ref
    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
    private _youtubeIframeId: number = -1;
    private _youtubeContentCreated = false;
    private _audioPlayer: HTMLAudioElement | null = null;
    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
    private _playRegionTimer: any = null; // timeout for playback
    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
    @observable static _nativeControls: boolean; // default html controls
    @observable _marqueeing: number[] | undefined; // coords for marquee selection
    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
    @observable _screenCapture = false;
    @observable _clicking = false; // used for transition between showing/hiding timeline
    @observable _forceCreateYouTubeIFrame = false;
    @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;

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


    @computed get youtubeVideoId() {
        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
    }


    // returns the path of the audio file
    @computed get audiopath() {
        const field = Cast(this.props.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(): Presentation | null {
            const data = this.dataDoc[this.fieldKey + '-presentation'];
            return data ? JSON.parse(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._playing = false;
        
            this.props.setContentView?.(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.
            if (this.youtubeVideoId) {
                  const youtubeaspect = 400 / 315;
                  const nativeWidth = Doc.NativeWidth(this.layoutDoc);
                  const nativeHeight = Doc.NativeHeight(this.layoutDoc);
                  if (!nativeWidth || !nativeHeight) {
                        if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
                        Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
                        this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
                  }
            }
          this.player && this.setPlayheadTime(0);
          
          // if presentation data exists, pass it to ReplayMovments
          if (this.presentation != null) {
            ReplayMovements.Instance.setVideoBox(this);  
          }
      }

      componentWillUnmount() {
        this.removeCurrentlyPlaying();
        this.Pause();
          Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
          
          // tell ReplayMovements that this VideoBox is no longer active
          if (this.presentation != null) {
            ReplayMovements.Instance.removeVideoBox();  
          }
      }


      // plays video
    @action public Play = (update: boolean = true) => {
        // BUGFIXES: only run if video is not already playing & dont set isplaying to true until that async call
        if (this._playing) return;
        // requires a promise for native videoplayer to be set to true
        if (!this.player) 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;
                      console.log('trim start', this.timeline.trimStart);
                        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();
                        update && this._youtubePlayer?.playVideo();
                        this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
                  } catch (e) {
                        console.log("Video Play Exception:", e);
                  }
            }
            this.updateTimecode();
      }

      // goes to time
      @action public Seek(time: number) {
            try {
                  this._youtubePlayer?.seekTo(Math.round(time), true);
            } catch (e) {
                  console.log("Video Seek Exception:", e);
            }
            this.player && (this.player.currentTime = time);
            this._audioPlayer && (this._audioPlayer.currentTime = time);
            // TODO: revisit this and clean it
            console.log('seek', time);
            if ((this.player?.currentTime || -1) < this.rawDuration) {
                  this._finished = false;
            }
      }

      // pauses video
    @action public Pause = (update: boolean = true) => {
          if (!this._playing) return;
            this._playing = false;
            this.removeCurrentlyPlaying();
            try {
                  update && this.player?.pause();
                  update && this._audioPlayer?.pause();
                  update && this._youtubePlayer?.pauseVideo();
                  this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
                  this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
            } catch (e) {
                  console.log("Video Pause Exception:", e);
            }
            this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
            this._playTimer = undefined;
            this.updateTimecode();
            if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
      }

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

            }
            try {
                  this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
            } catch (e) {
                  console.log("Video FullScreen Exception:", e);
            }
      }


      // 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._currentTimecode || 0).toString(),
                        _isLinkButton: true
                  });
                  this.props.addDocument?.(b);
                  DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
                  Networking.PostToServer("/youtubeScreenshot", {
                        id: this.youtubeVideoId,
                        timecode: this.layoutDoc._currentTimecode
                  }).then(response => {
                        const resolved = response?.accessPaths?.agnostic?.client;
                        if (resolved) {
                              this.props.removeDocument?.(b);
                              this.createRealSummaryLink(resolved);
                        }
                  });
            } 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.rootDoc.title).replace(/[ -\.]/g, "");
                  const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
                  const filename = basename(encodedFilename);
                  VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
                        returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
            }
      }

      updateIcon = () => {
            const makeIcon = (returnedfilename: string) => {
                  this.dataDoc.icon = new ImageField(returnedfilename);
                  this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
                  this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
            };
            this.Snapshot(undefined, undefined, makeIcon);
      }

      // creates link for snapshot
      createRealSummaryLink = (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 imageSummary = 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), _isLinkButton: true,
                  _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
            });
            Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
            Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
            this.props.addDocument?.(imageSummary);
            const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
            link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
            setTimeout(() =>
                  (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
      }


      getAnchor = () => {
            const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
            const marquee = AnchorMenu.Instance.GetAnchor?.();
            return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
      }


      // sets video info on load
      videoLoad = action(() => {
            const aspect = this.player!.videoWidth / this.player!.videoHeight;
            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;
            } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
      });


      // updates video time
      @action
      updateTimecode = (e? : any) => {
          // BUGFIX: if seeking when paused, no longer finished
          // ok this is constantly fired all the time
          if (e && !this._playing && this.player && this.player.paused) {
              // console.log('updateTimecode: seeking when paused', e);
              this.player.currentTime = e.target.currentTime;
              this._finished = false;
          }
          this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
            try {
                  this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
            } catch (e) {
                  console.log("Video Timecode Exception:", e);
            }
      }


      // 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._currentTimecode),
                        time => {
                              !this._playing && (vref.currentTime = time);
                              // console.log("vref time = " + vref.currentTime)
                        }, { fireImmediately: true });
            }
      }

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


      // 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.rootDoc.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: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), 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 = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
            const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
            return !field ? <div key="loading">Loading</div> :
                  <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
                        <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
                              {this.uIButtons}
                              <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
                                    onCanPlay={this.videoLoad}
                                    controls={VideoBox._nativeControls}
                                    onPlay={() => this.Play()}
                                    onSeeked={this.updateTimecode}
                                    onPause={() => this.Pause() }
                                    onClick={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>;
      }


      @action youtubeIframeLoaded = (e: any) => {
            if (!this._youtubeContentCreated) {
                  this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
                  return;
            }
            else this._youtubeContentCreated = false;

            this.loadYouTube(e.target);
      }

      loadYouTube = (iframe: any) => {
            let started = true;
            const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
                  if (started && event.data === YT.PlayerState.PLAYING) {
                        started = false;
                        this._youtubePlayer?.unMute();
                        //this.Pause();
                        return;
                  }
                  if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
                  if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
            });
            const onYoutubePlayerReady = (event: any) => {
                  this._disposers.reactionDisposer?.();
                  this._disposers.youtubeReactionDisposer?.();
                  this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
                  this._disposers.youtubeReactionDisposer = reaction(
                        () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
                        (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
            };
            if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
            else {
                  (YT as any)?.ready(() => {
                        this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
                              events: {
                                    'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
                                    'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
                              }
                        });
                  });
            }
      }


      // for play button

    onPlayDown = () => {
          console.log('onPlayDown', this._playing);
            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.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
                              // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));

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


      // removes video from currently playing display
      @action
      removeCurrentlyPlaying = () => {
            if (CollectionStackedTimeline.CurrentlyPlaying) {
                  const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
                  index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
            }
      }

      // adds video to currently playing display
      @action
      addCurrentlyPlaying = () => {
            if (!CollectionStackedTimeline.CurrentlyPlaying) {
                  CollectionStackedTimeline.CurrentlyPlaying = [];
            }
            if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
                  CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
            }
      }


      @computed get youtubeContent() {
            this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
            this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
            const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
            const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
            return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
                  onPointerLeave={this.updateTimecode}
                  onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
                  src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
      }


      // 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._currentTimecode);
            docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
            return this.addDocument(doc);
      }
    
    @action
    private setPlaying = (b: boolean) => {
        this._playing = b;
    }


      // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
      @action
      playFrom = async (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
            clearTimeout(this._playRegionTimer);
            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);
                    // BUGFIX: video.play is async, so we need to wait for it to finish
                    console.log('playFrom');
                        this.player.play().then(() => {
                            this._audioPlayer?.play();
                            this.setPlaying(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;
                                        // if (this.player) {
                                        //     this.player.currentTime = 0;
                                        //     this.updateTimecode();
                                        // }
                                        
                                    }
                                    // 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) {
                  return { height: "100%" };
            }
            else {
                  return { width: "100%" };
            }
      }


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


      // plays link
      playLink = (doc: Doc) => {
            const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
            const endTime = this.timeline?.anchorEnd(doc);
            if (startTime !== undefined) {
                  if (!this.layoutDoc.dontAutoPlayFollowedLinks) 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 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
                  setupMoveUpEvents(this, e, action(e => {
                        MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
                        this._marqueeing = [e.clientX, e.clientY];
                        return true;
                  }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
            }
      }

      // ends marquee selection
      @action
      finishMarquee = () => {
            this._marqueeing = undefined;
            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._currentTimecode = time;

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

      playing = () => this._playing;

      contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];

      scaling = () => this.props.scaling?.() || 1;

      panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
      panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 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);
      }

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

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


      // renders video controls
      @computed get uIButtons() {
            const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
            return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
                  <div className="videobox-button"
                        title={this._playing ? "play" : "pause"}
                        onPointerDown={this.onPlayDown}>
                        <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
                  </div>

                  {this.timeline && <div className="timecode-controls">
                        <div className="timecode-current">
                              {formatTime(curTime)}
                        </div>

                        {this._fullScreen || this.heightPercent === 100 ?
                              <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={(e: React.PointerEvent) => { e.stopPropagation(); }}
                                          onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
                                    />
                              </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 && <div className="videobox-button"
                        title={"show timeline"}
                        onPointerDown={this.onTimelineHdlDown}>
                        <FontAwesomeIcon icon="eye" />
                  </div>}

                  {!this._fullScreen && <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 show-slider"
                        title={this._muted ? "unmute" : "mute"}
                        onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
                        <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
                  </div>
                  <input type="range" 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 &&
                        <>
                              <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)); }}
                              />
                        </>}
            </div>;
      }

      // renders CollectionStackedTimeline
      @computed get renderTimeline() {
            return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
                  <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
                        fieldKey={this.annotationKey}
                        dictationKey={this.fieldKey + "-dictation"}
                        mediaPath={this.audiopath}
                        renderDepth={this.props.renderDepth + 1}
                        startTag={"_timecodeToShow" /* videoStart */}
                        endTag={"_timecodeToHide" /* videoEnd */}
                        bringToFront={emptyFunction}
                        CollectionView={undefined}
                        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>;
      }

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

      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.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
                  }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
                  <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 {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
                                    renderDepth={this.props.renderDepth + 1}
                                    fieldKey={this.annotationKey}
                                    CollectionView={undefined}
                                    isAnnotationOverlay={true}
                                    annotationLayerHostsContent={true}
                                    PanelWidth={this.panelWidth}
                                    PanelHeight={this.panelHeight}
                                    ScreenToLocalTransform={this.screenToLocalTransform}
                                    docFilters={this.timelineDocFilter}
                                    select={emptyFunction}
                                    scaling={returnOne}
                                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                                    removeDocument={this.removeDocument}
                                    moveDocument={this.moveDocument}
                                    addDocument={this.addDocWithTimecode}>
                                    {this.contentFunc}
                              </CollectionFreeFormView>
                        </div>
                        {this.annotationLayer}
                        {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
                              <MarqueeAnnotator
                                    rootDoc={this.rootDoc}
                                    scrollTop={0}
                                    down={this._marqueeing}
                                    scaling={this.marqueeFitScaling}
                                    docView={this.props.docViewPath().slice(-1)[0]}
                                    containerOffset={this.marqueeOffset}
                                    addDocument={this.addDocWithTimecode}
                                    finishMarquee={this.finishMarquee}
                                    savedAnnotations={this.savedAnnotations}
                                    annotationLayer={this._annotationLayer.current}
                                    mainCont={this._mainCont.current}
                              />}
                        {this.renderTimeline}
                  </div>
            </div >);
      }
}

VideoBox._nativeControls = false;