import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
import { DateField } from '../../../fields/DateField';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
import { formatTime } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocUtils } from '../../documents/DocUtils';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { DocViewUtils } from '../DocViewUtils';
import { PinDocView, PinProps } from '../PinFuncs';
import './AudioBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { OpenWhere } from './OpenWhere';

/**
 * AudioBox
 * Main component: AudioBox.tsx
 * Supporting Components: CollectionStackedTimeline, AudioWaveform
 *
 * AudioBox is a node that supports the recording and playback of audio files in Dash.
 * When an audio file is importeed into Dash, it is immediately rendered as an AudioBox document.
 * When a blank AudioBox node is created in Dash, audio recording controls are displayed and the user can start a recording which can be paused or stopped, and can use dictation to create a text transcript.
 * Recording is done using the MediaDevices API to access the user's device microphone (see recordAudioAnnotation below)
 * CollectionStackedTimeline handles AudioBox and VideoBox shared behavior, but AudioBox handles playing, pausing, etc because it contains <audio> element
 * User can trim audio: nondestructive, just sets new bounds for playback and rendering timelin
 */

// used as a wrapper class for MediaStream from MediaDevices API
// declare class MediaRecorder {
//     constructor(e: unknown); // whatever MediaRecorder has
// }

export enum mediaState {
    PendingRecording = 'pendingRecording',
    Recording = 'recording',
    Paused = 'paused',
    Playing = 'playing',
}

@observer
export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(AudioBox, fieldKey);
    }

    constructor(props: FieldViewProps) {
        super(props);
        makeObservable(this);
    }

    static topControlsHeight = 30; // height of upper controls above timeline
    static bottomControlsHeight = 20; // height of lower controls below timeline

    _dropDisposer?: DragManager.DragDropDisposer;
    _disposers: { [name: string]: IReactionDisposer } = {};
    _ele: HTMLAudioElement | null = null; // <audio> ref
    _recorder: Opt<MediaRecorder>; // MediaRecorder
    _recordStart = 0;
    _pauseStart = 0; // time when recording is paused (used to keep track of recording timecodes)
    _pausedTime = 0;
    _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
    _play: NodeJS.Timeout | null = null; // timeout for playback

    @observable _stackedTimeline: CollectionStackedTimeline | null | undefined = undefined; // CollectionStackedTimeline ref
    @observable _finished: boolean = false; // has playback reached end of clip
    @observable _volume: number = 1;
    @observable _muted: boolean = false;
    @observable _paused: boolean = false; // is recording paused
    // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded
    @computed get recordingStart() {
        return DateCast(this.dataDoc[this.fieldKey + '_recordingStart'])?.date.getTime();
    }
    @computed get rawDuration() {
        return NumCast(this.dataDoc[`${this.fieldKey}_duration`]);
    } // bcz: shouldn't be needed since it's computed from audio element
    // mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^
    // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly

    @computed get miniPlayer() {
        return this._props.PanelHeight() < 50;
    } // used to collapse timeline when node is shrunk
    @computed get links() {
        return Doc.Links(this.dataDoc);
    }
    @computed get mediaState() {
        return this.dataDoc.mediaState as mediaState;
    }
    set mediaState(value) {
        this.dataDoc.mediaState = value;
    }
    @computed get path() {
        // returns the path of the audio file
        const path = Cast(this.Document[this.fieldKey], AudioField, null)?.url.href || '';
        return path === nullAudio ? '' : path;
    }

    @computed get timeline() {
        return this._stackedTimeline;
    } // returns CollectionStackedTimeline ref

    componentWillUnmount() {
        this.removeCurrentlyPlaying();
        this._dropDisposer?.();
        Object.values(this._disposers).forEach(disposer => disposer?.());

        this.mediaState === mediaState.Recording && this.stopRecording();
    }

    @action
    componentDidMount() {
        this._props.setContentViewBox?.(this);
        if (this.path) {
            this.mediaState = mediaState.Paused;
            this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
        } else {
            this.mediaState = undefined as unknown as mediaState;
        }
    }

    getLinkData(l: Doc) {
        let la1 = l.link_anchor_1 as Doc;
        let la2 = l.link_anchor_2 as Doc;
        const linkTime = this.timeline?.anchorStart(la2) || this.timeline?.anchorStart(la1) || 0;
        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
            la1 = l.link_anchor_2 as Doc;
            la2 = l.link_anchor_1 as Doc;
        }
        return { la1, la2, linkTime };
    }

    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
        const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
        const anchor = addAsAnnotation
            ? CollectionStackedTimeline.createAnchor(
                  this.Document,
                  this.dataDoc,
                  this.annotationKey,
                  this._ele?.currentTime || Cast(this.Document._layout_currentTimecode, 'number', null) || (this.mediaState === mediaState.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
                  undefined,
                  undefined,
                  addAsAnnotation
              ) || this.Document
            : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });

        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document);
        return anchor;
    };

    // updates timecode and shows it in timeline, follows links at time
    @action
    timecodeChanged = () => {
        if (this.mediaState !== mediaState.Recording && this._ele) {
            this.links
                .map(l => this.getLinkData(l))
                .forEach(({ la1, linkTime }) => {
                    if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) {
                        Doc.linkFollowHighlight(la1);
                    }
                });
            this.layoutDoc._layout_currentTimecode = this._ele.currentTime;
            this.timeline?.scrollToTime(NumCast(this.layoutDoc._layout_currentTimecode));
        }
    };

    // 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) => {
        this._play && clearTimeout(this._play); // abort any previous clip ending
        if (isNaN(this._ele?.duration ?? Number.NaN)) {
            // audio element isn't loaded yet... wait 1/2 second and try again
            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
        } else if (this.timeline && this._ele) {
            // trimBounds override requested playback bounds
            const end = Math.min(this.timeline.trimEnd, endTime ?? this.timeline.trimEnd);
            const start = Math.max(this.timeline.trimStart, seekTimeInSeconds);
            // checks if times are within clip range
            if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
                this._ele.currentTime = start;
                this._ele.play();
                this.mediaState = mediaState.Playing;
                this.addCurrentlyPlaying();
                this._play = 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();
                    },
                    (end - start) * 1000
                );
            } else {
                this.Pause();
            }
        }
    };

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

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

    // update the recording time
    updateRecordTime = () => {
        if (this.mediaState === mediaState.Recording) {
            setTimeout(this.updateRecordTime, 30);
            if (!this._paused) {
                this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
            }
        }
    };

    // starts recording
    recordAudioAnnotation = async () => {
        this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        this._recorder = new MediaRecorder(this._stream);
        this.dataDoc[this.fieldKey + '_recordingStart'] = new DateField();
        DocViewUtils.ActiveRecordings.push(this);
        this._recorder.ondataavailable = async (e: BlobEvent) => {
            const file: Blob & { name?: string; lastModified?: number; webkitRelativePath?: string } = e.data;
            file.name = '';
            file.lastModified = 0;
            file.webkitRelativePath = '';
            const [{ result }] = await Networking.UploadFilesToServer({ file: file as Blob & { name: string; lastModified: number; webkitRelativePath: string } });
            if (!(result instanceof Error)) {
                this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
            }
        };
        this._recordStart = new Date().getTime();
        runInAction(() => {
            this.mediaState = mediaState.Recording;
        });
        setTimeout(this.updateRecordTime);
        this._recorder.start();
        setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour
    };

    // stops recording
    @action
    stopRecording = () => {
        if (this._recorder) {
            this._recorder.stop();
            this._recorder = undefined;
            const now = new Date().getTime();
            this._paused && (this._pausedTime += now - this._pauseStart);
            this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000;
            this.mediaState = mediaState.Paused;
            this._stream?.getAudioTracks()[0].stop();
            const ind = DocViewUtils.ActiveRecordings.indexOf(this);
            ind !== -1 && DocViewUtils.ActiveRecordings.splice(ind, 1);
        }
    };

    // context menu
    specificContextMenu = (): void => {
        const funcs: ContextMenuProps[] = [];
        funcs.push({
            description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
            event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore
            icon: 'expand-arrows-alt',
        });
        funcs.push({
            description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered',
            event: () => { this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks}, // prettier-ignore
            icon: 'expand-arrows-alt',
        });
        funcs.push({
            description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
            event: () => { this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks; }, // prettier-ignore
            icon: 'expand-arrows-alt',
        });
        funcs.push({
            description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : 'Auto') + ' play anchors onClick',
            event: () => { this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors; }, // prettier-ignore
            icon: 'expand-arrows-alt',
        });
        ContextMenu.Instance?.addItem({
            description: 'Options...',
            subitems: funcs,
            icon: 'asterisk',
        });
    };

    // button for starting and stopping the recording
    Record = (e: React.PointerEvent) => {
        e.button === 0 &&
            !e.ctrlKey &&
            setupMoveUpEvents(
                this,
                e,
                returnFalse,
                returnFalse,
                action(() => {
                    this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
                }),
                false
            );
    };

    // for play button
    Play = () => {
        if (this.timeline && this._ele) {
            const eleTime = this._ele.currentTime;

            // if curr timecode outside of trim bounds, set it to start
            let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;

            // restarts clip if reached end on last play
            if (this._finished) {
                this._finished = false;
                start = this.timeline.trimStart;
            }

            this.playFrom(start, this.timeline.trimEnd, true);
        }
    };

    IsPlaying = () => this.mediaState === mediaState.Playing;
    TogglePause = () => {
        if (this.mediaState === mediaState.Paused) this.Play();
        else this.pause();
    };
    // pause playback without removing from the playback list to allow user to play it again.
    @action
    pause = () => {
        if (this._ele) {
            this._ele.pause();
            this.mediaState = mediaState.Paused;

            // if paused in the middle of playback, prevents restart on next play
            if (!this._finished && this._play) clearTimeout(this._play);
        }
    };
    // pause playback and remove from playback list
    @action
    Pause = () => {
        this.pause();
        this.removeCurrentlyPlaying();
    };

    // for dictation button, creates a text document for dictation
    onFile = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            returnFalse,
            returnFalse,
            action(() => {
                const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
                const textField = Doc.LayoutFieldKey(newDoc);
                const newDocData = newDoc[DocData];
                newDocData[`${textField}_recordingSource`] = this.dataDoc;
                newDocData[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`);
                newDocData.mediaState = ComputedField.MakeFunction(`this.${textField}_recordingSource.mediaState`);
                if (Doc.IsInMyOverlay(this.Document)) {
                    newDoc.overlayX = this.Document.x;
                    newDoc.overlayY = NumCast(this.Document.y) + NumCast(this.layoutDoc._height);
                    Doc.AddToMyOverlay(newDoc);
                } else {
                    this._props.addDocTab(newDoc, OpenWhere.addRight);
                }
            }),
            false
        );
    };

    // sets <audio> ref for updating time
    setRef = (e: HTMLAudioElement | null) => {
        e?.addEventListener('timeupdate', this.timecodeChanged);
        e?.addEventListener('ended', () => {
            this._finished = true;
            this.Pause();
        });
        this._ele = e;
    };

    // pause the time during recording phase
    recordPause = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            returnFalse,
            returnFalse,
            action(() => {
                this._pauseStart = new Date().getTime();
                this._paused = true;
                this._recorder?.pause();
            }),
            false
        );
    };

    // continue the recording
    recordPlay = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            returnFalse,
            returnFalse,
            action(() => {
                this._paused = false;
                this._pausedTime += new Date().getTime() - this._pauseStart;
                this._recorder?.resume();
            }),
            false
        );
    };

    // plays link
    playLink = (link: Doc /* , options: FocusViewOptions */) => {
        if (link.annotationOn === this.Document) {
            if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
                this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
            } else {
                this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = this.timeline?.anchorStart(link) || 0;
            }
        } else {
            this.links
                .filter(l => l.link_anchor_1 === link || l.link_anchor_2 === link)
                .forEach(l => {
                    const { la1, la2 } = this.getLinkData(l);
                    const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2);
                    const endTime = this.timeline?.anchorEnd(la1) || this.timeline?.anchorEnd(la2);
                    if (startTime !== undefined) {
                        if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
                            this.playFrom(startTime, endTime);
                        } else {
                            this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = startTime;
                        }
                    }
                });
        }
    };

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

    timelineScreenToLocal = () => this.ScreenToLocalBoxXf().translate(0, -AudioBox.topControlsHeight);

    setPlayheadTime = (time: number) => {
        this._ele!.currentTime /* = this.layoutDoc._layout_currentTimecode */ = time;
    };

    playing = () => this.mediaState === mediaState.Playing;

    isActiveChild = () => this._isAnyChildContentActive;

    // timeline dimensions
    timelineWidth = () => this._props.PanelWidth();
    timelineHeight = () => this._props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight);

    // ends trim, hides trim controls and displays new clip
    @undoBatch
    finishTrim = () => {
        this.Pause();
        this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.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
    onResetPointerDown = (e: React.PointerEvent) => {
        e.stopPropagation();
        this.timeline &&
            setupMoveUpEvents(
                this,
                e,
                returnFalse,
                returnFalse,
                action(() => {
                    if (this.timeline?.IsTrimming !== TrimScope.None) {
                        this.timeline?.CancelTrimming();
                    } else {
                        this.beginEndtime = this.timeline?.trimEnd;
                        this.beginStarttime = this.timeline?.trimStart;
                        this.startTrim(TrimScope.All);
                    }
                })
            );
    };

    beginEndtime: number | undefined;
    beginStarttime: number | undefined;

    // for trim button, double click displays full clip, single displays curr trim bounds
    onClipPointerDown = (e: React.PointerEvent) => {
        e.stopPropagation();
        this.beginEndtime = this.timeline?.trimEnd;
        this.beginStarttime = this.timeline?.trimStart;
        this.timeline &&
            setupMoveUpEvents(
                this,
                e,
                returnFalse,
                returnFalse,
                action((moveEv: 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 zoom slider, sets timeline waveform zoom
    zoom = (zoom: number) => {
        this.timeline?.setZoom(zoom);
    };

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

    // toggles audio muted
    @action
    toggleMute = () => {
        if (this._ele) {
            this._muted = !this._muted;
            this._ele.muted = this._muted;
        }
    };

    setupTimelineDrop = (r: HTMLDivElement | null) => {
        if (r && this.timeline) {
            this._dropDisposer?.();
            this._dropDisposer = DragManager.MakeDropTarget(r, (e, de) => de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData), this.layoutDoc);
        }
    };

    // UI for recording, initially displayed when new audio created in Dash
    @computed get recordingControls() {
        return (
            <div className="audiobox-recorder">
                <div className="audiobox-dictation" onPointerDown={this.onFile}>
                    <FontAwesomeIcon size="2x" icon="file-alt" />
                </div>
                {[mediaState.Recording, mediaState.Playing].includes(this.mediaState) ? (
                    <div className="recording-controls" onClick={e => e.stopPropagation()}>
                        <div className="record-button" onPointerDown={this.Record}>
                            <FontAwesomeIcon size="2x" icon="stop" />
                        </div>
                        <div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}>
                            <FontAwesomeIcon size="2x" icon={this._paused ? 'play' : 'pause'} />
                        </div>
                        <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode)))}</div>
                    </div>
                ) : (
                    <div className="audiobox-start-record" onPointerDown={this.Record}>
                        <FontAwesomeIcon icon="microphone" />
                        RECORD
                    </div>
                )}
            </div>
        );
    }

    // UI for playback, displayed for imported or recorded clips, hides timeline and collapses controls when node is shrunk vertically
    @computed get playbackControls() {
        return (
            <div
                className="audiobox-file"
                style={{
                    flexDirection: this.miniPlayer ? 'row' : 'column',
                    justifyContent: this.miniPlayer ? 'flex-start' : 'space-between',
                }}>
                <div className="audiobox-controls">
                    <div className="controls-left">
                        <div
                            className="audiobox-button"
                            title={this.mediaState === mediaState.Paused ? 'play' : 'pause'}
                            onPointerDown={e => {
                                e.stopPropagation();
                                this.mediaState === mediaState.Paused ? this.Play() : this.Pause();
                            }}>
                            <FontAwesomeIcon icon={this.mediaState === mediaState.Paused ? 'play' : 'pause'} size="1x" />
                        </div>

                        {!this.miniPlayer && (
                            <>
                                <Tooltip title={<>trim audio</>}>
                                    <div className="audiobox-button" onPointerDown={this.onClipPointerDown}>
                                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size="1x" />
                                    </div>
                                </Tooltip>
                                {this.timeline?.IsTrimming === TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? null : (
                                    <Tooltip title={this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}>
                                        <div className="audiobox-button" onPointerDown={this.onResetPointerDown}>
                                            <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size="1x" />
                                        </div>
                                    </Tooltip>
                                )}
                            </>
                        )}
                    </div>
                    <div className="controls-right">
                        <div
                            className="audiobox-button"
                            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 => e.stopPropagation()}
                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
                        />
                    </div>
                </div>

                <div className="audiobox-playback" style={{ width: this.miniPlayer ? 0 : '100%' }}>
                    <div className="audiobox-timeline">{this.renderTimeline}</div>
                </div>

                {this.audio}

                <div className="audiobox-timecodes">
                    <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
                    {this.miniPlayer ? (
                        <div />
                    ) : (
                        <div className="bottom-controls-middle">
                            <FontAwesomeIcon icon="search-plus" />
                            <input
                                type="range"
                                step="0.1"
                                min="1"
                                max="5"
                                value={this.timeline?._zoomFactor ?? 1}
                                className="toolbar-slider"
                                id="zoom-slider"
                                onPointerDown={e => e.stopPropagation()}
                                onChange={e => this.zoom(Number(e.target.value))}
                            />
                        </div>
                    )}

                    <div className="timecode-duration">{this.timeline && formatTime(Math.round(this.timeline.clipDuration))}</div>
                </div>
            </div>
        );
    }

    // gets CollectionStackedTimeline
    @computed get renderTimeline() {
        return (
            <CollectionStackedTimeline
                ref={action((r: CollectionStackedTimeline | null) => {
                    this._stackedTimeline = r;
                })}
                // eslint-disable-next-line react/jsx-props-no-spreading
                {...this._props}
                dataFieldKey={this.fieldKey}
                fieldKey={this.annotationKey}
                dictationKey={this.fieldKey + '_dictation'}
                mediaPath={this.path}
                renderDepth={this._props.renderDepth + 1}
                startTag={'_timecodeToShow' /* audioStart */}
                endTag={'_timecodeToHide' /* audioEnd */}
                playFrom={this.playFrom}
                setTime={this.setPlayheadTime}
                playing={this.playing}
                whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
                moveDocument={this.moveDocument}
                addDocument={this.addDocument}
                removeDocument={this.removeDocument}
                ScreenToLocalTransform={this.timelineScreenToLocal}
                Play={this.Play}
                Pause={this.Pause}
                isContentActive={this._props.isContentActive}
                isAnyChildContentActive={this.isAnyChildContentActive}
                playLink={this.playLink}
                PanelWidth={this.timelineWidth}
                PanelHeight={this.timelineHeight}
                rawDuration={this.rawDuration}
            />
        );
    }

    // returns the html audio element
    @computed get audio() {
        return (
            <audio
                ref={this.setRef}
                className={`audiobox-control${this._props.isContentActive() ? '-interactive' : ''}`}
                onLoadedData={action(() => {
                    this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration);
                })}>
                <source src={this.path} type="audio/mpeg" />
                Not supported.
            </audio>
        );
    }

    render() {
        return (
            <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none' }}>
                {!this.path ? this.recordingControls : this.playbackControls}
            </div>
        );
    }
}

Docs.Prototypes.TemplateMap.set(DocumentType.AUDIO, {
    layout: { view: AudioBox, dataField: 'data' },
    options: { acl: '', _height: 100, _layout_fitWidth: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillVolumeUpFill' },
});
