/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2020 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 <QGuiApplication>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QResizeEvent>
#include <QScreen>
#include <QtMath>
#include <QVBoxLayout>

#include "audio/audiomanager.h"
#include "common/power.h"
#include "common/ratiodialog.h"
#include "common/timecodefunctions.h"
#include "config/config.h"
#include "project/item/sequence/sequence.h"
#include "project/project.h"
#include "render/rendermanager.h"
#include "task/taskmanager.h"
#include "widget/menu/menu.h"
#include "window/mainwindow/mainwindow.h"

namespace olive {

QVector<ViewerWidget*> ViewerWidget::instances_;

const int kMaxPreQueueSize = 16;

ViewerWidget::ViewerWidget(QWidget *parent) :
  TimeBasedWidget(false, true, parent),
  playback_speed_(0),
  frame_cache_job_time_(0),
  color_menu_enabled_(true),
  override_color_manager_(nullptr),
  time_changed_from_timer_(false),
  pause_autocache_during_playback_(false),
  prequeuing_(false)
{
  // Set up main layout
  QVBoxLayout* layout = new QVBoxLayout(this);
  layout->setMargin(0);

  // Set up stacked widget to allow switching away from the viewer widget
  stack_ = new QStackedWidget();
  layout->addWidget(stack_);

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

  display_widget_ = new ViewerDisplayWidget();
  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(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_);

  // Create waveform view when audio is connected and video isn't
  waveform_view_ = new AudioWaveformView();
  PassWheelEventsToScrollBar(waveform_view_);
  stack_->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::TimeChanged, this, &ViewerWidget::TimeChangedFromWaveform);
  connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);

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

  SetAutoMaxScrollBar(true);

  instances_.append(this);
}

ViewerWidget::~ViewerWidget()
{
  QList<ViewerWindow*> windows = windows_;

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

void ViewerWidget::TimeChangedEvent(const int64_t &i)
{
  if (!time_changed_from_timer_) {
    Pause();
  }

  controls_->SetTime(i);

  {
    qint64 waveform_time;

    if (waveform_view_->timebase() != this->timebase()) {
      waveform_time = Timecode::rescale_timestamp(i, this->timebase(), waveform_view_->timebase());
    } else {
      waveform_time = i;
    }

    waveform_view_->SetTime(waveform_time);
  }

  if (GetConnectedNode() && last_time_ != i) {
    rational time_set = Timecode::timestamp_to_time(i, timebase());

    if (!IsPlaying()) {
      UpdateTextureFromNode(time_set);

      PushScrubbedAudio();
    }

    if (!pause_autocache_during_playback_ || !IsPlaying()) {
      auto_cacher_.SetPlayhead(time_set);
    }

    display_widget_->SetTime(time_set);
  }

  last_time_ = i;
}

void ViewerWidget::ConnectNodeInternal(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::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
  connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
  connect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange);
  connect(n->texture_input(), &NodeInput::InputConnected, this, &ViewerWidget::UpdateStack);
  connect(n->texture_input(), &NodeInput::InputDisconnected, this, &ViewerWidget::UpdateStack);

  InterlacingChangedSlot(n->video_params().interlacing());

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

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

  ColorManager* using_manager;
  if (override_color_manager_) {
    using_manager = override_color_manager_;
  } else if (n->parent()) {
    using_manager = static_cast<Sequence*>(n->parent())->project()->color_manager();
  } else {
    qWarning() << "Failed to find a suitable color manager for the connected viewer node";
    using_manager = nullptr;
  }

  auto_cacher_.SetColorManager(using_manager);

  display_widget_->ConnectColorManager(using_manager);
  foreach (ViewerWindow* window, windows_) {
    window->display_widget()->ConnectColorManager(using_manager);
  }

  UpdateStack();

  waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache());
  if (GetConnectedTimelinePoints()) {
    waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints());
  }

  UpdateRendererVideoParameters();
  UpdateRendererAudioParameters();

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

void ViewerWidget::DisconnectNodeInternal(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::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
  disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
  disconnect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange);
  disconnect(n->texture_input(), &NodeInput::InputConnected, this, &ViewerWidget::UpdateStack);
  disconnect(n->texture_input(), &NodeInput::InputDisconnected, this, &ViewerWidget::UpdateStack);

  ruler()->SetPlaybackCache(nullptr);

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

  display_widget_->DisconnectColorManager();
  foreach (ViewerWindow* window, windows_) {
    window->display_widget()->DisconnectColorManager();
  }
  auto_cacher_.SetColorManager(nullptr);

  waveform_view_->SetViewer(nullptr);
  waveform_view_->ConnectTimelinePoints(nullptr);

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

void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n)
{
  auto_cacher_.SetViewerNode(n);
}

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

  waveform_view_->SetScale(s);
}

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

  UpdateMinimumScale();
}

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

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

void ViewerWidget::ConnectViewerNode(ViewerOutput *node, ColorManager* color_manager)
{
  override_color_manager_ = color_manager;

  TimeBasedWidget::ConnectViewerNode(node);
}

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

void ViewerWidget::SetMatrix(const QMatrix4x4 &mat)
{
  display_widget_->SetMatrixZoom(mat);
  foreach (ViewerWindow* vw, windows_) {
    vw->display_widget()->SetMatrixZoom(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();
    }
  }

  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()->video_params());
    vw->display_widget()->SetDeinterlacing(vw->display_widget()->IsDeinterlacing());
  }

  vw->display_widget()->SetImage(display_widget_->last_loaded_buffer());

  windows_.append(vw);
}

void ViewerWidget::ForceUpdate()
{
  // Hack that forces the viewer to update
  UpdateTextureFromNode(GetTime());
}

void ViewerWidget::SetAutoCacheEnabled(bool e)
{
  auto_cacher_.SetPaused(!e);
}

void ViewerWidget::CacheEntireSequence()
{
  auto_cacher_.ForceCacheRange(TimeRange(rational(), GetConnectedNode()->video_frame_cache()->GetLength()));
}

void ViewerWidget::CacheSequenceInOut()
{
  if (GetConnectedTimelinePoints() && GetConnectedTimelinePoints()->workarea()->enabled()) {
    auto_cacher_.ForceCacheRange(GetConnectedTimelinePoints()->workarea()->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);
}

FramePtr ViewerWidget::DecodeCachedImage(const QString &fn, const rational& time) const
{
  FramePtr frame = GetConnectedNode()->video_frame_cache()->LoadCacheFrame(fn);

  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 &fn, const rational& time) const
{
  ticket->Start();
  ticket->Finish(QVariant::fromValue(DecodeCachedImage(fn, time)), false);
}

bool ViewerWidget::ShouldForceWaveform() const
{
  return GetConnectedNode()
      && !GetConnectedNode()->texture_input()->IsConnected()
      && GetConnectedNode()->samples_input()->IsConnected();
}

void ViewerWidget::UpdateTextureFromNode(const rational& time)
{
  bool frame_exists_at_time = FrameExistsAtTime(time);

  // Check playback queue for a frame
  if (IsPlaying()) {

    // We still run the playback queue even when FrameExistsAtTime returns false because we might be
    // playing backwards and about to start showing frames, so the queue should be prepared for
    // that.
    while (!playback_queue_.empty()) {

      const ViewerPlaybackFrame& pf = playback_queue_.front();

      if (pf.timestamp == time) {

        // Frame was in queue, no need to decode anything
        SetDisplayImage(pf.frame, true);
        return;

      } else {

        // Skip this frame
        PopOldestFrameFromPlaybackQueue();

      }
    }

    // Only show warning if frame actually exists
    if (frame_exists_at_time) {
      qWarning() << "Playback queue failed to keep up";
    }

  }

  if (!frame_exists_at_time) {
    // There is definitely no frame here, we can immediately flip to showing nothing
    nonqueue_watchers_.clear();
    SetDisplayImage(nullptr, false);
    return;
  }

  // Frame was not in queue, will require rendering or decoding from cache
  RenderTicketWatcher* watcher = new RenderTicketWatcher();
  connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame);
  nonqueue_watchers_.append(watcher);
  watcher->SetTicket(GetFrame(time, true));
}

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 if that's been set as an option)
  foreach (ViewerWidget* viewer, instances_) {
    if (viewer != this) {
      viewer->Pause();
    }

    if (pause_autocache_during_playback_) {
      viewer->auto_cacher_.ClearVideoQueue();
    }
  }

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

  playback_speed_ = speed;
  play_in_to_out_only_ = in_to_out_only;

  playback_queue_next_frame_ = ruler()->GetTime();

  controls_->ShowPauseButton();

  // Attempt to fill playback queue
  if (stack_->currentWidget() == sizer_) {
    prequeue_length_ = DeterminePlaybackQueueSize();

    if (prequeue_length_ > 0) {
      prequeuing_ = true;

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

  if (!prequeuing_) {
    FinishPlayPreprocess();
  }
}

void ViewerWidget::PauseInternal()
{
  if (IsPlaying()) {
    AudioManager::instance()->StopOutput();
    playback_speed_ = 0;
    controls_->ShowPlayButton();

    disconnect(display_widget_, &ViewerDisplayWidget::frameSwapped, this, &ViewerWidget::ForceUpdate);

    foreach (ViewerWindow* window, windows_) {
      window->Pause();
    }

    playback_queue_.clear();
    playback_backup_timer_.stop();
  }

  prequeuing_ = false;
}

void ViewerWidget::PushScrubbedAudio()
{
  if (!IsPlaying() && Config::Current()["AudioScrubbing"].toBool()) {
    // Get audio src device from renderer
    AudioPlaybackCache::PlaybackDevice* audio_src = GetConnectedNode()->audio_playback_cache()->CreatePlaybackDevice();

    if (audio_src->open(QIODevice::ReadOnly)) {
      const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters();

      // FIXME: Hardcoded scrubbing interval (20ms)
      int size_of_sample = params.time_to_bytes(rational(20, 1000));

      // Push audio
      audio_src->seek(params.time_to_bytes(GetTime()));
      QByteArray frame_audio = audio_src->read(size_of_sample);
      AudioManager::instance()->SetOutputParams(params);
      AudioManager::instance()->PushToOutput(frame_audio);

      audio_src->close();
    }

    delete audio_src;
  }
}

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)) {
    QByteArray hash = GetConnectedNode()->video_frame_cache()->GetHash(time);

    if (!hash.isEmpty()) {
      return GetConnectedNode()->video_frame_cache()->CachePathName(hash);
    }
  }

  return QString();
}

bool ViewerWidget::FrameExistsAtTime(const rational &time)
{
  return GetConnectedNode()
      && ((time >= 0 && time < GetConnectedNode()->video_frame_cache()->GetLength())
          || GetConnectedNode()->video_frame_cache()->GetLength().isNull());
}

void ViewerWidget::SetDisplayImage(FramePtr frame, bool main_only)
{
  display_widget_->SetImage(frame);

  if (!main_only) {
    foreach (ViewerWindow* vw, windows_) {
      vw->display_widget()->SetImage(frame);
    }
  }

  emit LoadedBuffer(frame.get());
}

void ViewerWidget::RequestNextFrameForQueue()
{
  rational next_time = Timecode::timestamp_to_time(playback_queue_next_frame_,
                                                   timebase());

  playback_queue_next_frame_ += playback_speed_;

  RenderTicketWatcher* watcher = new RenderTicketWatcher();
  connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrameForQueue);
  watcher->SetTicket(GetFrame(next_time, false));
}

RenderTicketPtr ViewerWidget::GetFrame(const rational &t, bool clear_render_queue)
{
  QByteArray cached_hash = GetConnectedNode()->video_frame_cache()->GetHash(t);

  QString cache_fn = GetConnectedNode()->video_frame_cache()->CachePathName(cached_hash);

  if (cached_hash.isEmpty() || !QFileInfo::exists(cache_fn)) {
    // Frame hasn't been cached, start render job
    if (clear_render_queue) {
      auto_cacher_.ClearVideoQueue();
    }

    return auto_cacher_.GetSingleFrame(t);
  } else {
    // Frame has been cached, grab the frame
    RenderTicketPtr ticket = std::make_shared<RenderTicket>();
    ticket->setProperty("time", QVariant::fromValue(t));
    QtConcurrent::run(this, &ViewerWidget::DecodeCachedImage, ticket, cache_fn, t);

    return ticket;
  }
}

void ViewerWidget::FinishPlayPreprocess()
{
  int64_t playback_start_time = ruler()->GetTime();

  AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache();
  AudioManager::instance()->SetOutputParams(audio_cache->GetParameters());
  AudioManager::instance()->StartOutput(audio_cache,
                                        audio_cache->GetParameters().time_to_bytes(GetTime()),
                                        playback_speed_);

  playback_timer_.Start(playback_start_time, playback_speed_, timebase_dbl());

  foreach (ViewerWindow* window, windows_) {
    window->Play(playback_start_time, playback_speed_, timebase());
  }

  connect(display_widget_, &ViewerDisplayWidget::frameSwapped, this, &ViewerWidget::ForceUpdate);

  playback_backup_timer_.setInterval(qFloor(timebase_dbl()));
  playback_backup_timer_.start();

  PlaybackTimerUpdate();
}

int ViewerWidget::DeterminePlaybackQueueSize()
{
  int64_t end_ts;

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

  int remaining_frames = (end_ts - GetTimestamp()) * playback_speed_;

  return qMin(kMaxPreQueueSize, remaining_frames);
}

void ViewerWidget::PopOldestFrameFromPlaybackQueue()
{
  playback_queue_.pop_front();

  if (int(playback_queue_.size()) < DeterminePlaybackQueueSize()) {
    RequestNextFrameForQueue();
  }
}

void ViewerWidget::UpdateStack()
{
  rational new_tb;

  if (ShouldForceWaveform()) {
    // If we have a node AND video is disconnected AND audio is connected, show waveform view
    stack_->setCurrentWidget(waveform_view_);
    //new_tb = GetConnectedNode()->audio_params().time_base();
  } else {
    // Otherwise show regular display
    stack_->setCurrentWidget(sizer_);

    /*if (GetConnectedNode()) {
      new_tb = GetConnectedNode()->video_params().time_base();
    }*/
  }

  /*if (new_tb != timebase()) {
    SetTimebase(new_tb);
  }*/
}

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

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()
{
  windows_.removeOne(static_cast<ViewerWindow*>(sender()));
}

void ViewerWidget::ContextMenuScopeTriggered(QAction *action)
{
  emit RequestScopePanel(static_cast<ScopePanel::Type>(action->data().toInt()));
}

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

  if (!ticket->WasCancelled()) {
    FramePtr frame = ticket->Get().value<FramePtr>();

    if (nonqueue_watchers_.contains(ticket)) {
      while (!nonqueue_watchers_.isEmpty()) {
        if (nonqueue_watchers_.takeFirst() == ticket) {
          break;
        }
      }

      SetDisplayImage(frame, false);
    }
  }

  delete ticket;
}

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

  if (!watcher->WasCancelled()) {
    FramePtr frame = watcher->Get().value<FramePtr>();

    // Ignore this signal if we've paused now
    if (IsPlaying() || prequeuing_) {
      playback_queue_.AppendTimewise({frame->timestamp(), frame}, playback_speed_);

      foreach (ViewerWindow* window, windows_) {
        window->queue()->AppendTimewise({frame->timestamp(), frame}, playback_speed_);
      }

      if (prequeuing_ && int(playback_queue_.size()) == prequeue_length_) {
        prequeuing_ = false;
        FinishPlayPreprocess();
      }
    }
  }

  delete watcher;
}

void ViewerWidget::ShowContextMenu(const QPoint &pos)
{
  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);
      }

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

    {
      // Deinterlace Option
      if (GetConnectedNode()->video_params().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();

    {
      // Scopes
      Menu* scopes_menu = new Menu(tr("Scopes"), &menu);
      menu.addMenu(scopes_menu);

      for (int i=0;i<ScopePanel::kTypeCount;i++) {
        QAction* scope_action = scopes_menu->addAction(ScopePanel::TypeToName(static_cast<ScopePanel::Type>(i)));
        scope_action->setData(i);
      }

      connect(scopes_menu, &Menu::triggered, this, &ViewerWidget::ContextMenuScopeTriggered);
    }

    menu.addSeparator();

    {
      Menu* cache_menu = new Menu(tr("Cache"), &menu);
      menu.addMenu(cache_menu);

      // Auto-cache
      QAction* autocache_action = cache_menu->addAction(tr("Auto-Cache"));
      autocache_action->setCheckable(true);
      autocache_action->setChecked(!auto_cacher_.IsPaused());
      connect(autocache_action, &QAction::triggered, this, &ViewerWidget::SetAutoCacheEnabled);

      cache_menu->addSeparator();

      // Stop auto-cache while playing
      QAction* pause_autocache_while_playing = cache_menu->addAction(tr("Pause Auto-Cache During Playback"));
      pause_autocache_while_playing->setCheckable(true);
      pause_autocache_while_playing->setChecked(pause_autocache_during_playback_);
      connect(pause_autocache_while_playing, &QAction::triggered, this, [this](bool e){
        pause_autocache_during_playback_ = e;
      });

      cache_menu->addSeparator();

      // 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* show_waveform_action = menu.addAction(tr("Show Audio Waveform"));
    show_waveform_action->setCheckable(true);
    show_waveform_action->setChecked(stack_->currentWidget() == waveform_view_);
    show_waveform_action->setEnabled(!ShouldForceWaveform());
    connect(show_waveform_action, &QAction::triggered, this, &ViewerWidget::ManualSwitchToWaveform);
  }

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

void ViewerWidget::Play(bool in_to_out_only)
{
  if (in_to_out_only) {
    if (GetConnectedTimelinePoints()
        && GetConnectedTimelinePoints()->workarea()->enabled()) {
      // Jump to in point
      SetTimeAndSignal(Timecode::time_to_timestamp(GetConnectedTimelinePoints()->workarea()->in(), timebase()));
    } else {
      in_to_out_only = false;
    }
  }

  PlayInternal(1, in_to_out_only);
}

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

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

  auto_cacher_.SetPlayhead(GetTime());
}

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)
{
  display_widget_->SetSignalCursorColorEnabled(e);

  foreach (ViewerWindow* vw, windows_) {
    vw->display_widget()->SetSignalCursorColorEnabled(e);
  }
}

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

  controls_->SetTimebase(timebase);

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

void ViewerWidget::PlaybackTimerUpdate()
{
  int64_t current_time = playback_timer_.GetTimestampNow();

  int64_t min_time, max_time;

  {
    if ((play_in_to_out_only_ || Config::Current()["Loop"].toBool())
        && GetConnectedTimelinePoints()
        && GetConnectedTimelinePoints()->workarea()->enabled()) {

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

    } else {

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

    }
  }

  if ((playback_speed_ < 0 && current_time <= min_time)
      || (playback_speed_ > 0 && current_time >= max_time)) {

    // Determine which timestamp we tripped
    int64_t tripped_time;

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

    if (Config::Current()[QStringLiteral("Loop")].toBool()) {

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

      // Cache the current speed
      int current_speed = playback_speed_;

      // Jump to the other side and keep playing at the same speed
      SetTimeAndSignal(opposing_time);
      PlayInternal(current_speed, play_in_to_out_only_);

    } else {

      // Pause at the boundary
      SetTimeAndSignal(tripped_time);

    }

  } else {

    // Sets time, wrapping in this bool ensures we don't pause from setting the time
    time_changed_from_timer_ = true;
    SetTimeAndSignal(current_time);
    time_changed_from_timer_ = false;

  }

  if (!isVisible()) {
    while (!playback_queue_.empty() && playback_queue_.front().timestamp != GetTime()) {
      PopOldestFrameFromPlaybackQueue();
    }
  }
}

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(Timecode::time_to_timestamp(length, timebase()));
    UpdateMinimumScale();

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

    last_length_ = length;
  }
}

void ViewerWidget::InterlacingChangedSlot(VideoParams::Interlacing interlacing)
{
  // Automatically set a "sane" deinterlacing option
  display_widget_->SetDeinterlacing(interlacing != VideoParams::kInterlaceNone);

  foreach (ViewerWindow* vw, windows_) {
    vw->display_widget()->SetDeinterlacing(interlacing != VideoParams::kInterlaceNone);
  }
}

void ViewerWidget::UpdateRendererVideoParameters()
{
  display_widget_->SetVideoParams(GetConnectedNode()->video_params());
  foreach (ViewerWindow* window, windows_) {
    window->display_widget()->SetVideoParams(GetConnectedNode()->video_params());
  }
}

void ViewerWidget::UpdateRendererAudioParameters()
{
}

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 (GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) {
    QMetaObject::invokeMethod(this, "ForceUpdate", Qt::QueuedConnection);
  }
}

void ViewerWidget::ManualSwitchToWaveform(bool e)
{
  if (e) {
    stack_->setCurrentWidget(waveform_view_);
  } else {
    stack_->setCurrentWidget(sizer_);
  }
}

void ViewerWidget::TimeChangedFromWaveform(qint64 t)
{
  if (waveform_view_->timebase() != this->timebase()) {
    // Transform time to our timebase
    t = Timecode::rescale_timestamp(t, waveform_view_->timebase(), this->timebase());
  }

  SetTimeAndSignal(t);
}

void ViewerWidget::ViewerShiftedRange(const rational &from, const rational &to)
{
  if (GetTime() >= qMin(from, to)) {
    QMetaObject::invokeMethod(this, "ForceUpdate", Qt::QueuedConnection);
  }
}

}
