/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2022 Olive Team

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.

***/

#include "viewer.h"

#include <QDateTime>
#include <QFontDialog>
#include <QGuiApplication>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QResizeEvent>
#include <QScreen>
#include <QtMath>
#include <QVBoxLayout>

#include "audio/audiomanager.h"
#include "common/clamp.h"
#include "common/ratiodialog.h"
#include "common/timecodefunctions.h"
#include "config/config.h"
#include "core.h"
#include "node/block/gap/gap.h"
#include "node/generator/shape/shapenodebase.h"
#include "node/project/project.h"
#include "panel/multicam/multicampanel.h"
#include "panel/panelmanager.h"
#include "render/rendermanager.h"
#include "viewerpreventsleep.h"
#include "widget/audiomonitor/audiomonitor.h"
#include "widget/menu/menu.h"
#include "widget/multicam/multicamdisplay.h"
#include "widget/nodeparamview/nodeparamviewundo.h"
#include "widget/timelinewidget/tool/add.h"
#include "widget/timeruler/timeruler.h"

namespace olive {

#define super TimeBasedWidget

QVector<ViewerWidget*> ViewerWidget::instances_;

// NOTE: Hardcoded interval of size of audio chunk to render and send to the output at a time.
//       We want this to be as long as possible so the code has plenty of time to send the audio
//       while also being as short as possible so users get relatively immediate feedback when
//       changing values. 1/4 second seems to be a good middleground.
const rational ViewerWidget::kAudioPlaybackInterval = rational(1, 4);

const rational kVideoPlaybackInterval = rational(1, 2);

ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) :
  super(false, true, parent),
  playback_speed_(0),
  color_menu_enabled_(true),
  time_changed_from_timer_(false),
  prequeuing_video_(false),
  prequeuing_audio_(0),
  record_armed_(false),
  recording_(false),
  first_requeue_watcher_(nullptr),
  enable_audio_scrubbing_(true),
  waveform_mode_(kWFAutomatic),
  ignore_scrub_(0),
  multicam_panel_(nullptr)
{
  // Set up main layout
  QVBoxLayout* layout = new QVBoxLayout(this);
  layout->setMargin(0);

  // Create main OpenGL-based view and sizer
  sizer_ = new ViewerSizer();
  layout->addWidget(sizer_);

  display_widget_ = display;
  display_widget_->SetShowWidgetBackground(true);
  playback_devices_.append(display_widget_);
  connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
  connect(display_widget_, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor);
  connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged);
  connect(display_widget_, &ViewerDisplayWidget::ColorManagerChanged, this, &ViewerWidget::ColorManagerChanged);
  connect(display_widget_, &ViewerDisplayWidget::DragEntered, this, &ViewerWidget::DragEntered);
  connect(display_widget_, &ViewerDisplayWidget::Dropped, this, &ViewerWidget::Dropped);
  connect(display_widget_, &ViewerDisplayWidget::TextureChanged, this, &ViewerWidget::TextureChanged);
  connect(display_widget_, &ViewerDisplayWidget::QueueStarved, this, &ViewerWidget::QueueStarved);
  connect(display_widget_, &ViewerDisplayWidget::QueueNoLongerStarved, this, &ViewerWidget::QueueNoLongerStarved);
  connect(display_widget_, &ViewerDisplayWidget::CreateAddableAt, this, &ViewerWidget::CreateAddableAt);
  connect(sizer_, &ViewerSizer::RequestScale, display_widget_, &ViewerDisplayWidget::SetMatrixZoom);
  connect(sizer_, &ViewerSizer::RequestTranslate, display_widget_, &ViewerDisplayWidget::SetMatrixTranslate);
  connect(display_widget_, &ViewerDisplayWidget::HandDragMoved, sizer_, &ViewerSizer::HandDragMove);
  sizer_->SetWidget(display_widget_);

  // Make the display widget the first tabbable widget. While the viewer display cannot actually
  // be interacted with by tabbing, it prevents the actual first tabbable widget (the playhead
  // slider in `controls_`) from getting auto-focused any time the panel is maximized (with `)
  display_widget_->setFocusPolicy(Qt::TabFocus);

  // Create waveform view when audio is connected and video isn't
  waveform_view_ = new AudioWaveformView();
  ConnectTimelineView(waveform_view_, true);
  PassWheelEventsToScrollBar(waveform_view_);
  layout->addWidget(waveform_view_);

  // Create time ruler
  layout->addWidget(ruler());

  // Create scrollbar
  layout->addWidget(scrollbar());
  connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll);
  connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll);

  // Create lower controls
  controls_ = new PlaybackControls();
  controls_->SetTimecodeEnabled(true);
  controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
  connect(controls_, &PlaybackControls::PlayClicked, this, static_cast<void(ViewerWidget::*)()>(&ViewerWidget::Play));
  connect(controls_, &PlaybackControls::PauseClicked, this, &ViewerWidget::Pause);
  connect(controls_, &PlaybackControls::PrevFrameClicked, this, &ViewerWidget::PrevFrame);
  connect(controls_, &PlaybackControls::NextFrameClicked, this, &ViewerWidget::NextFrame);
  connect(controls_, &PlaybackControls::BeginClicked, this, &ViewerWidget::GoToStart);
  connect(controls_, &PlaybackControls::EndClicked, this, &ViewerWidget::GoToEnd);
  connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal);
  layout->addWidget(controls_);

  // FIXME: Magic number
  SetScale(48.0);

  // Ensures that seeking on the waveform view updates the time as expected
  connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);

  connect(&playback_backup_timer_, &QTimer::timeout, this, &ViewerWidget::PlaybackTimerUpdate);

  SetAutoMaxScrollBar(true);

  instances_.append(this);

  auto_cacher_ = new PreviewAutoCacher(this);
  connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, auto_cacher_, &PreviewAutoCacher::SetDisplayColorProcessor);

  UpdateWaveformViewFromMode();

  connect(Core::instance(), &Core::ColorPickerEnabled, this, &ViewerWidget::SetSignalCursorColorEnabled);
  connect(this, &ViewerWidget::CursorColor, Core::instance(), &Core::ColorPickerColorEmitted);
  connect(AudioManager::instance(), &AudioManager::OutputParamsChanged, this, &ViewerWidget::UpdateAudioProcessor);
}

ViewerWidget::~ViewerWidget()
{
  instances_.removeOne(this);

  auto windows = windows_;

  foreach (ViewerWindow* window, windows) {
    delete window;
  }
}

void ViewerWidget::TimeChangedEvent(const rational &time)
{
  if (!time_changed_from_timer_) {
    PauseInternal();
  }

  if (record_armed_) {
    DisarmRecording();
  }

  controls_->SetTime(time);
  waveform_view_->SetTime(time);

  if (GetConnectedNode() && last_time_ != time) {
    if (!IsPlaying()) {
      UpdateTextureFromNode();

      PushScrubbedAudio();

      // We don't clear the FPS timer on pause in case users want to see it immediately after, but by
      // the time a new texture is drawn, assume that the FPS no longer needs to be shown.
      display_widget_->ResetFPSTimer();
    }

    display_widget_->SetTime(time);
  }

  // Send time to auto-cacher
  auto_cacher_->SetPlayhead(time);

  last_time_ = time;
}

void ViewerWidget::ConnectNodeEvent(ViewerOutput *n)
{
  connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
  connect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
  connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
  connect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
  connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
  connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection);
  connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
  connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
  connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode);

  VideoParams vp = n->GetVideoParams();

  InterlacingChangedSlot(vp.interlacing());

  ruler()->SetPlaybackCache(n->video_frame_cache());

  SetViewerResolution(vp.width(), vp.height());
  SetViewerPixelAspect(vp.pixel_aspect_ratio());
  last_length_ = 0;
  LengthChangedSlot(n->GetLength());

  UpdateAudioProcessor();

  ColorManager* color_manager = n->project()->color_manager();

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->ConnectColorManager(color_manager);
  }

  UpdateWaveformViewFromMode();

  waveform_view_->SetViewer(GetConnectedNode());

  UpdateRendererVideoParameters();
  UpdateRendererAudioParameters();

  // Set texture to new texture (or null if no viewer node is available)
  UpdateTextureFromNode();
}

void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n)
{
  PauseInternal();

  disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
  disconnect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
  disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
  disconnect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
  disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
  disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode);
  disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
  disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
  disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode);

  timeline_selected_blocks_.clear();
  node_view_selected_.clear();
  if (multicam_panel_) {
    multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, rational::NaN);
  }

  CloseAudioProcessor();
  audio_scrub_watchers_.clear();

  SetDisplayImage(nullptr);

  ruler()->SetPlaybackCache(nullptr);

  // Effectively disables the viewer and clears the state
  SetViewerResolution(0, 0);

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->DisconnectColorManager();
  }

  waveform_view_->SetViewer(nullptr);

  // Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected
  QMetaObject::invokeMethod(this, &ViewerWidget::UpdateWaveformViewFromMode, Qt::QueuedConnection);

  SetGizmos(nullptr);
}

void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n)
{
  auto_cacher_->SetViewerNode(n);
  display_widget_->SetSubtitleTracks(dynamic_cast<Sequence*>(n));
}

void ViewerWidget::ConnectedWorkAreaChangeEvent(TimelineWorkArea *workarea)
{
  waveform_view_->SetWorkArea(workarea);
}

void ViewerWidget::ConnectedMarkersChangeEvent(TimelineMarkerList *markers)
{
  waveform_view_->SetMarkers(markers);
}

void ViewerWidget::ScaleChangedEvent(const double &s)
{
  super::ScaleChangedEvent(s);

  waveform_view_->SetScale(s);
}

void ViewerWidget::resizeEvent(QResizeEvent *event)
{
  super::resizeEvent(event);

  UpdateMinimumScale();
}

void ViewerWidget::TogglePlayPause()
{
  if (IsPlaying()) {
    Pause();
  } else {
    Play();
  }
}

bool ViewerWidget::IsPlaying() const
{
  return playback_speed_ != 0;
}

void ViewerWidget::SetColorMenuEnabled(bool enabled)
{
  color_menu_enabled_ = enabled;
}

void ViewerWidget::SetMatrix(const QMatrix4x4 &mat)
{
  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetMatrixCrop(mat);
  }
}

void ViewerWidget::SetFullScreen(QScreen *screen)
{
  if (!screen) {
    // Try to find the screen that contains the mouse cursor currently
    foreach (QScreen* test, QGuiApplication::screens()) {
      if (test->geometry().contains(QCursor::pos())) {
        screen = test;
        break;
      }
    }

    // Fallback, just use the first screen
    if (!screen) {
      screen = QGuiApplication::screens().first();
    }
  }

  if (windows_.contains(screen)) {
    ViewerWindow* vw = windows_.take(screen);
    vw->deleteLater();
    return;
  }

  ViewerWindow* vw = new ViewerWindow(this);

  vw->setGeometry(screen->geometry());
  vw->showFullScreen();
  vw->display_widget()->ConnectColorManager(color_manager());
  connect(vw, &ViewerWindow::destroyed, this, &ViewerWidget::WindowAboutToClose);
  connect(vw->display_widget(), &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);

  if (GetConnectedNode()) {
    vw->SetVideoParams(GetConnectedNode()->GetVideoParams());
    vw->display_widget()->SetDeinterlacing(vw->display_widget()->IsDeinterlacing());
  }

  vw->display_widget()->SetImage(QVariant::fromValue(display_widget()->GetCurrentTexture()));

  playback_devices_.append(vw->display_widget());

  (*vw->display_widget()->queue()) = *playback_devices_.first()->queue();
  if (IsPlaying()) {
    vw->display_widget()->Play(GetTimestamp(), playback_speed_, timebase());
  }

  windows_.insert(screen, vw);
}

void ViewerWidget::CacheEntireSequence()
{
  auto_cacher_->ForceCacheRange(TimeRange(0, GetConnectedNode()->GetVideoLength()));
}

void ViewerWidget::CacheSequenceInOut()
{
  if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) {
    auto_cacher_->ForceCacheRange(GetConnectedNode()->GetWorkArea()->range());
  } else {
    QMessageBox::warning(this,
                         tr("Error"),
                         tr("No in or out points are set to cache."),
                         QMessageBox::Ok);
  }
}

void ViewerWidget::SetGizmos(Node *node)
{
  display_widget_->SetTimeTarget(GetConnectedNode());
  display_widget_->SetGizmos(node);
}

void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track)
{
  SetTimeAndSignal(time.in());
  ArmForRecording();

  recording_callback_ = source;
  recording_range_ = time;
  recording_track_ = track;
}

void ViewerWidget::ConnectMulticamWidget(MulticamWidget *p)
{
  if (multicam_panel_) {
    disconnect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow);
  }

  multicam_panel_ = p;

  if (multicam_panel_) {
    connect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow);
  }
}

FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QUuid &cache_id, const int64_t& time)
{
  FramePtr frame = FrameHashCache::LoadCacheFrame(cache_path, cache_id, time);

  if (frame) {
    frame->set_timestamp(time);
  } else {
    qWarning() << "Tried to load cached frame from file but it was null";
  }

  return frame;
}

void ViewerWidget::DecodeCachedImage(RenderTicketPtr ticket, const QString &cache_path, const QUuid &cache_id, const int64_t& time)
{
  ticket->Start();

  FramePtr f = DecodeCachedImage(cache_path, cache_id, time);

  if (f) {
    ticket->Finish(QVariant::fromValue(f));
  } else {
    ticket->Finish();
  }
}

bool ViewerWidget::ShouldForceWaveform() const
{
  return GetConnectedNode()
      && !GetConnectedNode()->GetConnectedTextureOutput()
      && GetConnectedNode()->GetConnectedSampleOutput();
}

void ViewerWidget::SetEmptyImage()
{
  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetBlank();
  }
}

void ViewerWidget::UpdateAutoCacher()
{
  auto_cacher_->SetPlayhead(GetTime());
}

void ViewerWidget::DecrementPrequeuedAudio()
{
  prequeuing_audio_--;
  if (!prequeuing_audio_) {
    FinishPlayPreprocess();
  }
}

void ViewerWidget::ArmForRecording()
{
  controls_->StartPlayBlink();
  record_armed_ = true;
}

void ViewerWidget::DisarmRecording()
{
  controls_->StopPlayBlink();
  record_armed_ = false;
}

void ViewerWidget::UpdateAudioProcessor()
{
  if (GetConnectedNode()) {
    CloseAudioProcessor();

    AudioParams ap = GetConnectedNode()->GetAudioParams();
    AudioParams packed(OLIVE_CONFIG("AudioOutputSampleRate").toInt(),
                       OLIVE_CONFIG("AudioOutputChannelLayout").toULongLong(),
                       static_cast<AudioParams::Format>(OLIVE_CONFIG("AudioOutputSampleFormat").toInt()));

    audio_processor_.Open(ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
  }
}

void ViewerWidget::CreateAddableAt(const QRectF &f)
{
  if (Sequence *s = dynamic_cast<Sequence*>(GetConnectedNode())) {
    Track::Type type = Track::kVideo;
    int track_index = -1;
    TrackList *list = s->track_list(type);
    const rational &in = GetTime();
    rational length = OLIVE_CONFIG("DefaultStillLength").value<rational>();
    rational out = in + length;

    // Find a free track where we won't overwrite anything
    while (true) {
      track_index++;

      if (track_index >= list->GetTrackCount()) {
        // Just create a new track
        break;
      }

      Track *track = list->GetTrackAt(track_index);
      if (track->IsLocked()) {
        continue;
      }

      Block *b = track->NearestBlockBeforeOrAt(in);
      if (!b || (dynamic_cast<GapBlock*>(b) && b->out() >= out)) {
        break;
      }
    }

    MultiUndoCommand *command = new MultiUndoCommand();
    Node *clip = AddTool::CreateAddableClip(command, s, Track::Reference(type, track_index), in, length);

    if (ShapeNodeBase *shape = dynamic_cast<ShapeNodeBase*>(clip)) {
      shape->SetRect(f, s->GetVideoParams(), command);
    }

    Core::instance()->undo_stack()->pushIfHasChildren(command);
    SetGizmos(clip);
  }
}

void ViewerWidget::HandleFirstRequeueDestroy()
{
  // Extra protection to ensure we don't reference a destroyed object
  if (first_requeue_watcher_ == sender()) {
    first_requeue_watcher_ = nullptr;
  }
}

void ViewerWidget::ShowSubtitleProperties()
{
  QFont f(OLIVE_CONFIG("DefaultSubtitleFamily").toString(), OLIVE_CONFIG("DefaultSubtitleSize").toInt(), OLIVE_CONFIG("DefaultSubtitleWeight").toInt());
  QFontDialog fd(f, this);

  if (fd.exec() == QDialog::Accepted) {
    f = fd.selectedFont();
    OLIVE_CONFIG("DefaultSubtitleSize") = f.pointSize();
    OLIVE_CONFIG("DefaultSubtitleFamily") = f.family();
    OLIVE_CONFIG("DefaultSubtitleWeight") = f.weight();
    display_widget_->update();
  }
}

void ViewerWidget::DryRunFinished()
{
  RenderTicketWatcher *w = static_cast<RenderTicketWatcher*>(sender());

  if (dry_run_watchers_.contains(w)) {
    RequestNextDryRun();
  }

  delete w;
}

void ViewerWidget::RequestNextDryRun()
{
  if (IsPlaying()) {
    rational next_time = Timecode::timestamp_to_time(dry_run_next_frame_, timebase());
    if (FrameExistsAtTime(next_time)) {
      if (next_time > GetTime() + RenderManager::kDryRunInterval) {
        QTimer::singleShot(timebase().toDouble() / playback_speed_, this, &ViewerWidget::RequestNextDryRun);
      } else {
        RenderTicketWatcher *watcher = new RenderTicketWatcher(this);
        connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::DryRunFinished);
        watcher->SetTicket(GetSingleFrame(next_time, true));
        dry_run_next_frame_ += playback_speed_;
        dry_run_watchers_.append(watcher);
      }
    }
  }
}

void ViewerWidget::SaveFrameAsImage()
{
  Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), GetTime(), true);
}

void ViewerWidget::DetectMulticamNodeNow()
{
  DetectMulticamNode(GetTime());
}

void ViewerWidget::CloseAudioProcessor()
{
  audio_processor_.Close();
}

void ViewerWidget::SetWaveformMode(WaveformMode wf)
{
  waveform_mode_ = wf;
  UpdateWaveformViewFromMode();
}

void ViewerWidget::DetectMulticamNode(const rational &time)
{
  // Look for multicam node
  MultiCamNode *multicam = nullptr;
  ClipBlock *clip = nullptr;

  // Faster way to do this
  if (multicam_panel_ && multicam_panel_->isVisible()) {
    if (Sequence *s = dynamic_cast<Sequence*>(GetConnectedNode())) {
      // Prefer selected nodes
      for (Node *n : qAsConst(node_view_selected_)) {
        if ((multicam = dynamic_cast<MultiCamNode*>(n))) {
          // Found multicam, now try to find corresponding clip from selected timeline blocks
          for (Block *b : qAsConst(timeline_selected_blocks_)) {
            if (ClipBlock *c = dynamic_cast<ClipBlock*>(b)) {
              if (c->range().Contains(time) && c->ContextContainsNode(multicam)) {
                clip = c;
                break;
              }
            }
          }
          break;
        }
      }

      // Next, prefer multicam from selected block
      if (!multicam) {
        for (Block *b : qAsConst(timeline_selected_blocks_)) {
          if (b->range().Contains(time)) {
            if ((clip = dynamic_cast<ClipBlock*>(b))) {
              if ((multicam = clip->FindMulticam())) {
                break;
              }
            }
          }
        }
      }

      if (!multicam) {
        const QVector<Track*> &tracks = s->GetTracks();
        for (Track *t : tracks) {
          if (t->IsLocked()) {
            continue;
          }

          Block *b = t->NearestBlockBeforeOrAt(time);
          if ((clip = dynamic_cast<ClipBlock*>(b))) {
            if ((multicam = clip->FindMulticam())) {
              break;
            }
          }
        }
      }
    }
  }

  if (multicam) {
    if (multicam_panel_) {
      multicam_panel_->SetMulticamNode(GetConnectedNode(), multicam, clip, time);
    }
    auto_cacher()->SetMulticamNode(multicam);
  } else {
    auto_cacher()->SetMulticamNode(nullptr);
    if (multicam_panel_) {
      multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, time);
    }
  }
}

void ViewerWidget::UpdateWaveformViewFromMode()
{
  bool prefer_waveform = ShouldForceWaveform();

  sizer_->setVisible(waveform_mode_ == kWFViewerAndWaveform || waveform_mode_ == kWFViewerOnly || (waveform_mode_ == kWFAutomatic && !prefer_waveform));
  waveform_view_->setVisible(waveform_mode_ == kWFViewerAndWaveform || waveform_mode_ == kWFWaveformOnly || (waveform_mode_ == kWFAutomatic && prefer_waveform));

  waveform_view_->setSizePolicy(QSizePolicy::Expanding, waveform_mode_ == kWFViewerAndWaveform ? QSizePolicy::Maximum : QSizePolicy::Expanding);

  if (GetConnectedNode()) {
    GetConnectedNode()->SetWaveformEnabled(waveform_view_->isVisible());
  }
}

void ViewerWidget::QueueNextAudioBuffer()
{
  rational queue_end = audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_);

  // Clamp queue end by zero and the audio length
  queue_end  = clamp(queue_end, rational(0), GetConnectedNode()->GetAudioLength());
  if ((playback_speed_ > 0 && queue_end <= audio_playback_queue_time_)
      || (playback_speed_ < 0 && queue_end >= audio_playback_queue_time_)) {
    // This will queue nothing, so stop the loop here
    if (prequeuing_audio_) {
      DecrementPrequeuedAudio();
    }
    return;
  }

  RenderTicketWatcher *watcher = new RenderTicketWatcher(this);
  connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForPlayback);
  audio_playback_queue_.push_back(watcher);
  watcher->SetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end)));

  audio_playback_queue_time_ = queue_end;
}

void ViewerWidget::ReceivedAudioBufferForPlayback()
{
  while (!audio_playback_queue_.empty() && audio_playback_queue_.front()->HasResult()) {
    RenderTicketWatcher *watcher = audio_playback_queue_.front();
    audio_playback_queue_.pop_front();

    if (watcher->HasResult()) {
      SampleBuffer samples = watcher->Get().value<SampleBuffer>();
      if (samples.is_allocated()) {
        // If the samples must be reversed, reverse them now
        if (playback_speed_ < 0) {
          samples.reverse();
        }

        // Convert to packed data for audio output
        AudioProcessor::Buffer buf;
        int r = audio_processor_.Convert(samples.to_raw_ptrs().data(), samples.sample_count(), &buf);

        // TempoProcessor may have emptied the array
        if (r >= 0) {
          if (!buf.empty()) {
            const QByteArray &pack = buf.at(0);
            if (prequeuing_audio_) {
              // Add to prequeued audio buffer
              prequeued_audio_.append(pack);
            } else {
              // Push directly to audio manager
              AudioManager::instance()->PushToOutput(audio_processor_.to(), pack);
            }
          }
        } else {
          qCritical() << "Failed to process audio for playback:" << r;
        }
      }
    }

    if (prequeuing_audio_) {
      DecrementPrequeuedAudio();
    }

    delete watcher;
  }
}

void ViewerWidget::ReceivedAudioBufferForScrubbing()
{
  RenderTicketWatcher *watcher = static_cast<RenderTicketWatcher *>(sender());

  while (!audio_scrub_watchers_.empty() && audio_scrub_watchers_.front() != watcher) {
    audio_scrub_watchers_.pop_front();
  }

  if (!audio_scrub_watchers_.empty()) {
    if (watcher->HasResult()) {
      SampleBuffer samples = watcher->Get().value<SampleBuffer>();
      if (samples.is_allocated()) {
        if (samples.audio_params().channel_count() > 0) {
          AudioProcessor::Buffer buf;
          int r = audio_processor_.Convert(samples.to_raw_ptrs().data(), samples.sample_count(), &buf);

          if (r >= 0) {
            if (!buf.empty()) {
              QString error;
              const QByteArray &packed = buf.at(0);
              AudioManager::instance()->ClearBufferedOutput();
              if (!AudioManager::instance()->PushToOutput(audio_processor_.to(), packed, &error)) {
                Core::instance()->ShowStatusBarMessage(tr("Audio scrubbing failed: %1").arg(error));
              }
              AudioMonitor::PushSampleBufferOnAll(samples);
            }
          } else {
            qCritical() << "Failed to process audio for scrubbing:" << r;
          }
        }
      }
    }
  }

  delete watcher;
}

void ViewerWidget::QueueStarved()
{
  static const int kMaximumWaitTimeMs = 250;
  static const rational kMaximumWaitTime(kMaximumWaitTimeMs, 1000);
  qint64 now = QDateTime::currentMSecsSinceEpoch();

  if (!queue_starved_start_) {
    queue_starved_start_ = now;
  } else if (now > queue_starved_start_ + kMaximumWaitTimeMs) {
    if (first_requeue_watcher_) {
      if (GetTime() + kMaximumWaitTime < first_requeue_watcher_->property("time").value<rational>()) {
        // We still have time
        return;
      }
    }

    ForceRequeueFromCurrentTime();
    queue_starved_start_ = 0;
  }
}

void ViewerWidget::QueueNoLongerStarved()
{
  queue_starved_start_ = 0;
}

void ViewerWidget::ForceRequeueFromCurrentTime()
{
  // Allow half a second for requeue to complete
  static const rational kRequeueWaitTime(1);

  auto_cacher_->ClearSingleFrameRenders();
  queue_watchers_.clear();
  int queue = DeterminePlaybackQueueSize();
  playback_queue_next_frame_ = GetTimestamp() + playback_speed_ * Timecode::time_to_timestamp(kRequeueWaitTime, timebase(), Timecode::kFloor);;
  first_requeue_watcher_ = nullptr;
  for (int i=0; i<queue; i++) {
    RenderTicketWatcher *watcher = RequestNextFrameForQueue();
    if (!first_requeue_watcher_) {
      first_requeue_watcher_ = watcher;
      connect(first_requeue_watcher_, &RenderTicketWatcher::destroyed, this, &ViewerWidget::HandleFirstRequeueDestroy);
    }
  }
}

void ViewerWidget::UpdateTextureFromNode()
{
  if (!GetConnectedNode()) {
    return;
  }

  if (IsPlaying()) {
    qWarning() << "UpdateTextureFromNode called while playing";
    return;
  }

  rational time = GetTime();
  bool frame_exists_at_time = FrameExistsAtTime(time);
  bool frame_might_be_still = ViewerMightBeAStill();

  if (frame_exists_at_time || frame_might_be_still) {
    // Frame was not in queue, will require rendering or decoding from cache
    // Not playing, run a task to get the frame either from the cache or the renderer
    RenderTicketWatcher* watcher = new RenderTicketWatcher();
    watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch());
    watcher->setProperty("time", QVariant::fromValue(time));
    connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame);
    nonqueue_watchers_.append(watcher);

    // Clear queue because we want this frame more than any others
    auto_cacher_->ClearSingleFrameRenders();

    DetectMulticamNode(time);

    watcher->SetTicket(GetFrame(time));
  } else {
    // There is definitely no frame here, we can immediately flip to showing nothing
    nonqueue_watchers_.clear();
    SetEmptyImage();
    return;
  }
}

void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
{
  Q_ASSERT(speed != 0);

  if (!GetConnectedNode()) {
    // Do nothing if no viewer node is attached
    return;
  }

  if (timebase().isNull()) {
    qCritical() << "ViewerWidget can't play with an invalid timebase";
    return;
  }

  // Kindly tell all viewers to stop playing and caching so all resources can be used for playback
  foreach (ViewerWidget* viewer, instances_) {
    if (viewer != this) {
      viewer->PauseInternal();
    }
    viewer->auto_cacher_->SetThumbnailsPaused(true);
  }

  RenderManager::instance()->SetAggressiveGarbageCollection(true);

  // Disarm recording if armed
  if (record_armed_) {
    DisarmRecording();
  }

  // If the playhead is beyond the end, restart at 0
  if (!recording_) {
    rational last_frame = GetConnectedNode()->GetLength() - timebase();
    if (!in_to_out_only && GetTime() >= last_frame) {
      if (speed > 0) {
        SetTimeAndSignal(0);
      } else {
        SetTimeAndSignal(last_frame);
      }
    }
  }

  playback_speed_ = speed;
  play_in_to_out_only_ = in_to_out_only;

  playback_queue_next_frame_ = GetTimestamp() + playback_speed_;

  controls_->ShowPauseButton();

  queue_starved_start_ = 0;

  // Attempt to fill playback queue
  if (display_widget_->isVisible() || !windows_.isEmpty()) {
    prequeue_length_ = DeterminePlaybackQueueSize();

    if (prequeue_length_ > 0) {
      prequeuing_video_ = true;
      prequeue_count_ = 0;

      for (int i=0; i<prequeue_length_; i++) {
        RequestNextFrameForQueue();
      }

      dry_run_next_frame_ = playback_queue_next_frame_;
      RequestNextDryRun();
    }
  }

  AudioParams ap = GetConnectedNode()->GetAudioParams();
  if (ap.is_valid()) {
    UpdateAudioProcessor();

    AudioManager::instance()->SetOutputNotifyInterval(audio_processor_.to().time_to_bytes(kAudioPlaybackInterval));
    connect(AudioManager::instance(), &AudioManager::OutputNotify, this, &ViewerWidget::QueueNextAudioBuffer);

    static const int prequeue_count = 2;
    prequeuing_audio_ = prequeue_count; // Queue two buffers ahead of time
    audio_playback_queue_time_ = GetTime();
    for (int i=0; i<prequeue_count; i++) {
      QueueNextAudioBuffer();
    }
  }

  // Force screen to stay awake
  PreventSleep(true);
}

void ViewerWidget::PauseInternal()
{
  if (recording_) {
    AudioManager::instance()->StopRecording();
    recording_ = false;
    controls_->SetPauseButtonRecordingState(false);

    recording_callback_->DisableRecordingOverlay();
    recording_callback_->RecordingCallback(recording_filename_, recording_range_, recording_track_);
  }

  if (IsPlaying()) {
    playback_speed_ = 0;
    controls_->ShowPlayButton();

    foreach (ViewerDisplayWidget *dw, playback_devices_){
      dw->Pause();
    }

    qDeleteAll(queue_watchers_);
    queue_watchers_.clear();
    auto_cacher_->ClearSingleFrameRenders();

    playback_backup_timer_.stop();

    // Handle audio
    AudioManager::instance()->StopOutput();
    AudioMonitor::StopOnAll();
    prequeued_audio_.clear();
    disconnect(AudioManager::instance(), &AudioManager::OutputNotify, this, &ViewerWidget::QueueNextAudioBuffer);
    qDeleteAll(audio_playback_queue_);
    audio_playback_queue_.clear();
    UpdateAudioProcessor();

    foreach (ViewerWidget* viewer, instances_) {
      viewer->auto_cacher_->SetThumbnailsPaused(false);
    }

    UpdateTextureFromNode();

    RenderManager::instance()->SetAggressiveGarbageCollection(false);
  }

  prequeuing_video_ = false;
  prequeuing_audio_ = 0;
  dry_run_watchers_.clear();

  // Reset screen timeout timer
  PreventSleep(false);
}

void ViewerWidget::PushScrubbedAudio()
{
  if (!IsPlaying() && GetConnectedNode() && OLIVE_CONFIG("AudioScrubbing").toBool() && enable_audio_scrubbing_) {
    if (ignore_scrub_ > 0) {
      ignore_scrub_--;
    }

    if (ignore_scrub_ == 0) {
      // Get audio src device from renderer
      const AudioParams& params = GetConnectedNode()->GetAudioParams();

      if (params.is_valid()) {
        // NOTE: Hardcoded scrubbing interval (20ms)
        rational interval = rational(20, 1000);

        RenderTicketWatcher *watcher = new RenderTicketWatcher();
        connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForScrubbing);
        audio_scrub_watchers_.push_back(watcher);
        watcher->SetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval)));
      }
    }
  }
}

void ViewerWidget::UpdateMinimumScale()
{
  if (!GetConnectedNode()) {
    return;
  }

  if (GetConnectedNode()->GetLength().isNull()) {
    // Avoids divide by zero
    SetMinimumScale(0);
  } else {
    SetMinimumScale(static_cast<double>(ruler()->width()) / GetConnectedNode()->GetLength().toDouble());
  }
}

void ViewerWidget::SetColorTransform(const ColorTransform &transform, ViewerDisplayWidget *sender)
{
  sender->SetColorTransform(transform);
}

QString ViewerWidget::GetCachedFilenameFromTime(const rational &time)
{
  if (FrameExistsAtTime(time)) {
    return GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(time);
  }

  return QString();
}

bool ViewerWidget::FrameExistsAtTime(const rational &time)
{
  return GetConnectedNode() && time >= 0 && time < GetConnectedNode()->GetVideoLength();
}

bool ViewerWidget::ViewerMightBeAStill()
{
  return GetConnectedNode() && GetConnectedNode()->GetConnectedTextureOutput() && GetConnectedNode()->GetVideoLength().isNull();
}

void ViewerWidget::SetDisplayImage(RenderTicketPtr ticket)
{
  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    QVariant push;
    if (ticket) {
      if (dynamic_cast<MulticamDisplay*>(dw)) {
        push = ticket->property("multicam_output");
      } else {
        push = ticket->Get();
      }
    }
    dw->SetImage(push);
  }
}

RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment)
{
  RenderTicketWatcher *watcher = nullptr;

  rational next_time = Timecode::timestamp_to_time(playback_queue_next_frame_,
                                                   timebase());

  if (FrameExistsAtTime(next_time) || ViewerMightBeAStill()) {
    if (increment) {
      playback_queue_next_frame_ += playback_speed_;
    }

    watcher = new RenderTicketWatcher();
    watcher->setProperty("time", QVariant::fromValue(next_time));
    DetectMulticamNode(next_time);
    connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrameForQueue);
    queue_watchers_.append(watcher);
    watcher->SetTicket(GetFrame(next_time));
  }

  return watcher;
}

RenderTicketPtr ViewerWidget::GetFrame(const rational &t)
{
  QString cache_fn = GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(t);

  if (!QFileInfo::exists(cache_fn)) {
    // Frame hasn't been cached, start render job
    return GetSingleFrame(t);
  } else {
    // Frame has been cached, grab the frame
    RenderTicketPtr ticket = std::make_shared<RenderTicket>();
    ticket->setProperty("time", QVariant::fromValue(t));
    QtConcurrent::run(ViewerWidget::DecodeCachedImage, ticket, GetConnectedNode()->video_frame_cache()->GetCacheDirectory(), GetConnectedNode()->video_frame_cache()->GetUuid(), Timecode::time_to_timestamp(t, timebase(), Timecode::kFloor));
    return ticket;
  }
}

void ViewerWidget::FinishPlayPreprocess()
{
  // Check if we're still waiting for video or audio respectively
  if (prequeuing_video_ || prequeuing_audio_) {
    return;
  }

  int64_t playback_start_time = GetTimestamp();

  // Start audio waveform playback
  if (!prequeued_audio_.isEmpty()) {
    QString error;
    if (!AudioManager::instance()->PushToOutput(audio_processor_.to(), prequeued_audio_, &error)) {
      QMessageBox::critical(this, tr("Audio Error"), tr("Failed to start audio: %1\n\n"
                                                        "Please check your audio preferences and try again.").arg(error));
    }
    prequeued_audio_.clear();

    AudioMonitor::StartWaveformOnAll(GetConnectedNode()->GetConnectedWaveform(),
                                     GetTime(), playback_speed_);
  }

  display_widget_->ResetFPSTimer();

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->Play(playback_start_time, playback_speed_, timebase());
  }

  // This is our timer for loading the queue and setting the time
  playback_backup_timer_.setInterval(qFloor(timebase_dbl()));
  playback_backup_timer_.start();

  PlaybackTimerUpdate();
}

int ViewerWidget::DeterminePlaybackQueueSize()
{
  if (playback_speed_ == 0) {
    return 0;
  }

  int64_t end_ts;

  if (playback_speed_ > 0) {
    end_ts = Timecode::time_to_timestamp(GetConnectedNode()->GetVideoLength(),
                                         timebase());
  } else {
    end_ts = 0;
  }

  int remaining_frames = (end_ts - GetTimestamp() - 1) / playback_speed_;

  // Generate maximum queue
  int max_frames = qCeil(kVideoPlaybackInterval.toDouble() / timebase().toDouble());

  return qMin(max_frames, remaining_frames);
}

void ViewerWidget::ContextMenuSetFullScreen(QAction *action)
{
  SetFullScreen(QGuiApplication::screens().at(action->data().toInt()));
}

void ViewerWidget::ContextMenuSetPlaybackRes(QAction *action)
{
  int div = action->data().toInt();

  auto vp = GetConnectedNode()->GetVideoParams();
  vp.set_divider(div);

  auto c = new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(GetConnectedNode(), ViewerOutput::kVideoParamsInput, 0)), QVariant::fromValue(vp));
  Core::instance()->undo_stack()->push(c);
}

void ViewerWidget::ContextMenuDisableSafeMargins()
{
  context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(false));
}

void ViewerWidget::ContextMenuSetSafeMargins()
{
  context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(true));
}

void ViewerWidget::ContextMenuSetCustomSafeMargins()
{
  bool ok;

  double new_ratio = GetFloatRatioFromUser(this,
                                           tr("Safe Margins"),
                                           &ok);

  if (ok) {
    context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(true, new_ratio));
  }
}

void ViewerWidget::WindowAboutToClose()
{
  ViewerWindow *vw = static_cast<ViewerWindow*>(sender());
  windows_.remove(windows_.key(vw));
  playback_devices_.removeOne(vw->display_widget());
}

void ViewerWidget::RendererGeneratedFrame()
{
  RenderTicketWatcher* ticket = static_cast<RenderTicketWatcher*>(sender());

  if (ticket->HasResult()) {
    if (nonqueue_watchers_.contains(ticket)) {
      while (!nonqueue_watchers_.isEmpty()) {
        // Pop frames that are "old"
        if (nonqueue_watchers_.takeFirst() == ticket) {
          break;
        }
      }

      SetDisplayImage(ticket->GetTicket());
    }
  }

  delete ticket;
}

void ViewerWidget::RendererGeneratedFrameForQueue()
{
  RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(sender());

  if (queue_watchers_.contains(watcher)) {
    queue_watchers_.removeOne(watcher);

    if (watcher->HasResult()) {
      QVariant frame = watcher->Get();

      // Ignore this signal if we've paused now
      if (IsPlaying() || prequeuing_video_) {
        rational ts = watcher->property("time").value<rational>();

        foreach (ViewerDisplayWidget *dw, playback_devices_) {
          QVariant push;
          if (dynamic_cast<MulticamDisplay*>(dw)) {
            push = watcher->GetTicket()->property("multicam_output");
          } else {
            push = frame;
          }

          dw->queue()->AppendTimewise({ts, push}, playback_speed_);
        }

        if (prequeuing_video_) {
          prequeue_count_++;

          if (prequeue_count_ == prequeue_length_) {
            prequeuing_video_ = false;
            FinishPlayPreprocess();
          } else {
            // This call was mostly necessary to keep the threads busy between prequeue and playback.
            // If we only have a single render thread, it's no longer necessary.
            //RequestNextFrameForQueue();
          }
        }
      }
    }
  }

  if (first_requeue_watcher_ == watcher) {
    first_requeue_watcher_ = nullptr;
  }

  delete watcher;
}

void ViewerWidget::ShowContextMenu(const QPoint &pos)
{
  if (!GetConnectedNode()) {
    return;
  }

  Menu menu(static_cast<QWidget*>(sender()));

  context_menu_widget_ = dynamic_cast<ViewerDisplayWidget*>(sender());

  // ViewerDisplayWidget options
  if (context_menu_widget_) {
    // Color options
    if (context_menu_widget_->color_manager() && color_menu_enabled_) {
      {
        Menu* ocio_display_menu = context_menu_widget_->GetDisplayMenu(&menu);
        menu.addMenu(ocio_display_menu);
      }

      {
        Menu* ocio_view_menu = context_menu_widget_->GetViewMenu(&menu);
        menu.addMenu(ocio_view_menu);
      }

      {
        Menu* ocio_look_menu = context_menu_widget_->GetLookMenu(&menu);
        menu.addMenu(ocio_look_menu);
      }

      menu.addSeparator();
    }

    {
      // Viewer Zoom Level
      Menu* zoom_menu = new Menu(tr("Zoom"), &menu);
      menu.addMenu(zoom_menu);

      int zoom_levels[] = {10, 25, 50, 75, 100, 150, 200, 400};
      zoom_menu->addAction(tr("Fit"))->setData(0);
      for (int i=0;i<8;i++) {
        zoom_menu->addAction(tr("%1%").arg(zoom_levels[i]))->setData(zoom_levels[i]);
      }

      connect(zoom_menu, &QMenu::triggered, this, &ViewerWidget::SetZoomFromMenu);
    }

    {
      // Full Screen Menu
      Menu* full_screen_menu = new Menu(tr("Full Screen"), &menu);
      menu.addMenu(full_screen_menu);

      for (int i=0;i<QGuiApplication::screens().size();i++) {
        QScreen* s = QGuiApplication::screens().at(i);

        QAction* a = full_screen_menu->addAction(tr("Screen %1: %2x%3").arg(QString::number(i),
                                                                            QString::number(s->size().width()),
                                                                            QString::number(s->size().height())));

        a->setData(i);
        a->setCheckable(true);
        a->setChecked(windows_.contains(QGuiApplication::screens().at(i)));
      }

      connect(full_screen_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetFullScreen);
    }

    {
      // Playback Resolution Menu
      Menu *playback_res_menu = new Menu(tr("Playback Resolution"), &menu);
      menu.addMenu(playback_res_menu);

      for (int d : VideoParams::kSupportedDividers) {
        playback_res_menu->AddActionWithData(VideoParams::GetNameForDivider(d), d, GetConnectedNode()->GetVideoParams().divider());
      }

      connect(playback_res_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetPlaybackRes);
    }

    {
      // Deinterlace Option
      if (GetConnectedNode()->GetVideoParams().interlacing() != VideoParams::kInterlaceNone) {
        QAction* deinterlace_action = menu.addAction(tr("Deinterlace"));
        deinterlace_action->setCheckable(true);
        deinterlace_action->setChecked(display_widget_->IsDeinterlacing());
        connect(deinterlace_action, &QAction::triggered, display_widget_, &ViewerDisplayWidget::SetDeinterlacing);
      }
    }

    menu.addSeparator();

    /* TEMP: Hide sequence cache options. Want to see if clip caching supersedes it.
    {
      Menu* cache_menu = new Menu(tr("Cache"), &menu);
      menu.addMenu(cache_menu);

      // Cache Entire Sequence
      QAction* cache_entire_sequence = cache_menu->addAction(tr("Cache Entire Sequence"));
      connect(cache_entire_sequence, &QAction::triggered, this, &ViewerWidget::CacheEntireSequence);

      // Cache In/Out Sequence
      QAction* cache_inout_sequence = cache_menu->addAction(tr("Cache Sequence In/Out"));
      connect(cache_inout_sequence, &QAction::triggered, this, &ViewerWidget::CacheSequenceInOut);
    }*/

    menu.addSeparator();

    {
      // Safe Margins
      Menu* safe_margin_menu = new Menu(tr("Safe Margins"), &menu);
      menu.addMenu(safe_margin_menu);

      QAction* safe_margin_off = safe_margin_menu->addAction(tr("Off"));
      safe_margin_off->setCheckable(true);
      safe_margin_off->setChecked(!context_menu_widget_->GetSafeMargin().is_enabled());
      connect(safe_margin_off, &QAction::triggered, this, &ViewerWidget::ContextMenuDisableSafeMargins);

      QAction* safe_margin_on = safe_margin_menu->addAction(tr("On"));
      safe_margin_on->setCheckable(true);
      safe_margin_on->setChecked(context_menu_widget_->GetSafeMargin().is_enabled() && !context_menu_widget_->GetSafeMargin().custom_ratio());
      connect(safe_margin_on, &QAction::triggered, this, &ViewerWidget::ContextMenuSetSafeMargins);

      QAction* safe_margin_custom = safe_margin_menu->addAction(tr("Custom Aspect"));
      safe_margin_custom->setCheckable(true);
      safe_margin_custom->setChecked(context_menu_widget_->GetSafeMargin().is_enabled() && context_menu_widget_->GetSafeMargin().custom_ratio());
      connect(safe_margin_custom, &QAction::triggered, this, &ViewerWidget::ContextMenuSetCustomSafeMargins);
    }

    menu.addSeparator();
  }

  {
    QAction *stop_playback_on_last_frame = menu.addAction(tr("Stop Playback On Last Frame"));
    stop_playback_on_last_frame->setCheckable(true);
    stop_playback_on_last_frame->setChecked(OLIVE_CONFIG("StopPlaybackOnLastFrame").toBool());
    connect(stop_playback_on_last_frame, &QAction::triggered, this, [](bool e){
      OLIVE_CONFIG("StopPlaybackOnLastFrame") = e;
    });

    menu.addSeparator();
  }

  {
    auto waveform_menu = new Menu(tr("Audio Waveform"), &menu);
    menu.addMenu(waveform_menu);

    waveform_menu->AddActionWithData(tr("Automatically Show/Hide"), kWFAutomatic, waveform_mode_);
    waveform_menu->AddActionWithData(tr("Show Waveform Only"), kWFWaveformOnly, waveform_mode_);
    waveform_menu->AddActionWithData(tr("Show Both Viewer And Waveform"), kWFViewerAndWaveform, waveform_mode_);

    connect(waveform_menu, &Menu::triggered, this, &ViewerWidget::UpdateWaveformModeFromMenu);
  }

  {
    QAction* show_fps_action = menu.addAction(tr("Show FPS"));
    show_fps_action->setCheckable(true);
    show_fps_action->setChecked(display_widget_->GetShowFPS());
    connect(show_fps_action, &QAction::triggered, display_widget_, &ViewerDisplayWidget::SetShowFPS);
  }

  if (context_menu_widget_ == display_widget_) {
    auto subtitle_menu = new Menu(tr("Subtitles"), &menu);
    menu.addMenu(subtitle_menu);

    QAction* show_subtitles_action = subtitle_menu->addAction(tr("Show Subtitles"));
    show_subtitles_action->setCheckable(true);
    show_subtitles_action->setChecked(display_widget_->GetShowSubtitles());
    connect(show_subtitles_action, &QAction::triggered, display_widget_, &ViewerDisplayWidget::SetShowSubtitles);

    subtitle_menu->addSeparator();

    auto subtitle_font_properties = subtitle_menu->addAction(tr("Subtitle Properties"));
    connect(subtitle_font_properties, &QAction::triggered, this, &ViewerWidget::ShowSubtitleProperties);

    auto subtitle_antialias = subtitle_menu->addAction(tr("Use Anti-aliasing"));
    subtitle_antialias->setCheckable(true);
    subtitle_antialias->setChecked(OLIVE_CONFIG("AntialiasSubtitles").toBool());
    connect(subtitle_antialias, &QAction::triggered, this, [this](bool e){
      OLIVE_CONFIG("AntialiasSubtitles") = e;
      display_widget_->update();
    });
  }

  menu.addSeparator();

  auto save_frame_as_image = menu.addAction(tr("Save Frame As Image"));
  connect(save_frame_as_image, &QAction::triggered, this, &ViewerWidget::SaveFrameAsImage);

  menu.exec(static_cast<QWidget*>(sender())->mapToGlobal(pos));
}

void ViewerWidget::Play(bool in_to_out_only)
{
  if (in_to_out_only) {
    if (GetConnectedNode()
        && GetConnectedNode()->GetWorkArea()->enabled()) {
      // Jump to in point
      SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in());
    } else {
      in_to_out_only = false;
    }
  } else if (record_armed_) {
    DisarmRecording();

    if (GetConnectedNode()->project()->filename().isEmpty()) {
      QMessageBox::critical(this, tr("Audio Recording"), tr("Project must be saved before you can record audio."));
      return;
    }

    QDir audio_path(QFileInfo(GetConnectedNode()->project()->filename()).dir().filePath(tr("audio")));
    if (!audio_path.exists()) {
      audio_path.mkpath(QStringLiteral("."));
    }

    recording_filename_ = audio_path.filePath(QStringLiteral("%1.%2").arg(
                                                QDateTime::currentDateTime().toString("yyyy-MM-dd hh-mm-ss"),
                                                ExportFormat::GetExtension(static_cast<ExportFormat::Format>(OLIVE_CONFIG("AudioRecordingFormat").toInt())))
                                              );

    AudioParams ap(OLIVE_CONFIG("AudioRecordingSampleRate").toInt(), OLIVE_CONFIG("AudioRecordingChannelLayout").toULongLong(), static_cast<AudioParams::Format>(OLIVE_CONFIG("AudioRecordingSampleFormat").toInt()));

    EncodingParams encode_param;
    encode_param.EnableAudio(ap, static_cast<ExportCodec::Codec>(OLIVE_CONFIG("AudioRecordingCodec").toInt()));
    encode_param.SetFilename(recording_filename_);
    encode_param.set_audio_bit_rate(OLIVE_CONFIG("AudioRecordingBitRate").toInt() * 1000);

    QString error;
    if (AudioManager::instance()->StartRecording(encode_param, &error)) {
      recording_ = true;
      controls_->SetPauseButtonRecordingState(true);
      recording_callback_->EnableRecordingOverlay(TimelineCoordinate(recording_range_.in(), recording_track_));
    } else {
      QMessageBox::critical(this, tr("Audio Recording"), tr("Failed to start audio recording: %1").arg(error));
      return;
    }
  }

  PlayInternal(1, in_to_out_only);
}

void ViewerWidget::Play()
{
  Play(false);
}

void ViewerWidget::Pause()
{
  PauseInternal();
}

void ViewerWidget::ShuttleLeft()
{
  int current_speed = playback_speed_;

  if (current_speed != 0) {
    PauseInternal();
  }

  current_speed--;

  if (current_speed == 0) {
    current_speed--;
  }

  PlayInternal(current_speed, false);
}

void ViewerWidget::ShuttleStop()
{
  Pause();
}

void ViewerWidget::ShuttleRight()
{
  int current_speed = playback_speed_;

  if (current_speed != 0) {
    PauseInternal();
  }

  current_speed++;

  if (current_speed == 0) {
    current_speed++;
  }

  PlayInternal(current_speed, false);
}

void ViewerWidget::SetColorTransform(const ColorTransform &transform)
{
  SetColorTransform(transform, display_widget_);
}

void ViewerWidget::SetSignalCursorColorEnabled(bool e)
{
  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetSignalCursorColorEnabled(e);
  }
}

void ViewerWidget::TimebaseChangedEvent(const rational &timebase)
{
  super::TimebaseChangedEvent(timebase);

  controls_->SetTimebase(timebase);

  controls_->SetTime(ruler()->GetTime());
  LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->GetLength() : 0);
}

void ViewerWidget::PlaybackTimerUpdate()
{
  Q_ASSERT(playback_speed_ != 0);

  rational current_time = Timecode::timestamp_to_time(display_widget_->timer()->GetTimestampNow(), timebase());

  rational min_time, max_time;

  if (recording_ && recording_range_.out() != recording_range_.in()) {

    // Limit recording range if applicable
    min_time = recording_range_.in();
    max_time = recording_range_.out();

  } else if (play_in_to_out_only_ && GetConnectedNode()->GetWorkArea()->enabled()) {

    // If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea
    min_time = GetConnectedNode()->GetWorkArea()->in();
    max_time = GetConnectedNode()->GetWorkArea()->out();

  } else {

    // Otherwise set the bounds to the range of the sequence
    min_time = 0;
    max_time = GetConnectedNode()->GetLength();

  }

  // If we're stopping playback on the last frame rather than after it, subtract our max time
  // by one timebase unit
  if (OLIVE_CONFIG("StopPlaybackOnLastFrame").toBool()) {
    max_time = qMax(min_time, max_time - timebase());
  }

  rational time_to_set;
  bool end_of_line = false;
  bool play_after_pause = false;

  if ((!recording_ || recording_range_.out() != recording_range_.in())
      && ((playback_speed_ < 0 && current_time <= min_time)
          || (playback_speed_ > 0 && current_time >= max_time))) {

    // Determine which timestamp we tripped
    rational tripped_time;

    if (current_time <= min_time) {
      tripped_time = min_time;
    } else {
      tripped_time = max_time;
    }

    // Signal that we've reached the end of whatever range we're playing and should either pause
    // or restart playback
    end_of_line = true;

    if (OLIVE_CONFIG("Loop").toBool() && !recording_) {

      // If we're looping, jump to the other side of the workarea and continue
      time_to_set = (tripped_time == min_time) ? max_time : min_time;

      // Signal to restart playback after the pause signalled by `end_of_line`
      play_after_pause = true;

    } else {

      // Pause at the boundary we tripped
      time_to_set = tripped_time;

    }

  } else {

    // Sets time normally to whatever we calculated as the "current time"
    time_to_set = current_time;

  }

  // Set the time. By wrapping in this bool, we prevent TimeChangedEvent's default behavior of
  // pausing. Even if we pause it later with `end_of_line`, we prefer pausing after setting the time
  // so that an audio scrub event, etc. isn't sent.
  time_changed_from_timer_ = true;
  SetTimeAndSignal(time_to_set);
  time_changed_from_timer_ = false;
  if (end_of_line) {
    // Cache the current speed
    int current_speed = playback_speed_;

    PauseInternal();
    if (play_after_pause) {
      PlayInternal(current_speed, play_in_to_out_only_);
    }
  }

  if (IsPlaying()) {
    while ((int(display_widget_->queue()->size()) + queue_watchers_.size()) < DeterminePlaybackQueueSize()) {
      if (!RequestNextFrameForQueue()) {
        // Prevent infinite loop
        break;
      }
    }
  }

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->queue()->PurgeBefore(current_time, playback_speed_);
  }
}

void ViewerWidget::SetViewerResolution(int width, int height)
{
  sizer_->SetChildSize(width, height);

  foreach (ViewerWindow* vw, windows_) {
    vw->SetResolution(width, height);
  }
}

void ViewerWidget::SetViewerPixelAspect(const rational &ratio)
{
  sizer_->SetPixelAspectRatio(ratio);

  foreach (ViewerWindow* vw, windows_) {
    vw->SetPixelAspectRatio(ratio);
  }
}

void ViewerWidget::LengthChangedSlot(const rational &length)
{
  if (last_length_ != length) {
    controls_->SetEndTime(length);
    UpdateMinimumScale();

    if (length < last_length_ && GetTime() >= length) {
      UpdateTextureFromNode();
    }

    last_length_ = length;
  }
}

void ViewerWidget::InterlacingChangedSlot(VideoParams::Interlacing interlacing)
{
  // Automatically set a "sane" deinterlacing option
  bool deint = interlacing != VideoParams::kInterlaceNone;

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetDeinterlacing(deint);
  }
}

void ViewerWidget::UpdateRendererVideoParameters()
{
  VideoParams vp = GetConnectedNode()->GetVideoParams();

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetVideoParams(vp);
  }
}

void ViewerWidget::UpdateRendererAudioParameters()
{
  AudioParams ap = GetConnectedNode()->GetAudioParams();

  UpdateAudioProcessor();

  foreach (ViewerDisplayWidget *dw, playback_devices_) {
    dw->SetAudioParams(ap);
  }
}

void ViewerWidget::SetZoomFromMenu(QAction *action)
{
  sizer_->SetZoom(action->data().toInt());
}

void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range)
{
  // If our current frame is within this range, we need to update
  if (!IsPlaying() && GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) {
    QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection);
  }
}

void ViewerWidget::UpdateWaveformModeFromMenu(QAction *a)
{
  SetWaveformMode(static_cast<WaveformMode>(a->data().toInt()));
}

void ViewerWidget::DragEntered(QDragEnterEvent* event)
{
  if (event->mimeData()->formats().contains(Project::kItemMimeType)) {
    event->accept();
  }
}

void ViewerWidget::Dropped(QDropEvent *event)
{
  QByteArray mimedata = event->mimeData()->data(Project::kItemMimeType);
  QDataStream stream(&mimedata, QIODevice::ReadOnly);

  // Variables to deserialize into
  quintptr item_ptr = 0;
  QVector<Track::Reference> enabled_streams;

  while (!stream.atEnd()) {
    stream >> enabled_streams >> item_ptr;

    // We only need the one item
    break;
  }

  if (item_ptr) {
    Node* item = reinterpret_cast<Node*>(item_ptr);
    ViewerOutput* viewer = dynamic_cast<ViewerOutput*>(item);

    if (viewer) {
      ConnectViewerNode(viewer);
    }
  }
}

}
