/***

  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 "timelinewidget.h"

#include <cfloat>
#include <QSplitter>
#include <QVBoxLayout>
#include <QtMath>

#include "core.h"
#include "common/range.h"
#include "dialog/sequence/sequence.h"
#include "dialog/speedduration/speeddurationdialog.h"
#include "node/block/transition/transition.h"
#include "node/nodeundo.h"
#include "node/project/serializer/serializer.h"
#include "task/project/import/import.h"
#include "timeline/timelineundogeneral.h"
#include "timeline/timelineundopointer.h"
#include "timeline/timelineundoripple.h"
#include "timeline/timelineundoworkarea.h"
#include "tool/add.h"
#include "tool/beam.h"
#include "tool/edit.h"
#include "tool/pointer.h"
#include "tool/razor.h"
#include "tool/record.h"
#include "tool/ripple.h"
#include "tool/rolling.h"
#include "tool/slide.h"
#include "tool/slip.h"
#include "tool/trackselect.h"
#include "tool/transition.h"
#include "tool/zoom.h"
#include "tool/tool.h"
#include "trackview/trackview.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "widget/nodeparamview/nodeparamview.h"
#include "widget/timeruler/timeruler.h"

namespace olive {

#define super TimeBasedWidget

TimelineWidget::TimelineWidget(QWidget *parent) :
  super(true, true, parent),
  rubberband_(QRubberBand::Rectangle, this),
  active_tool_(nullptr),
  use_audio_time_units_(false),
  subtitle_show_command_(nullptr),
  subtitle_tentative_track_(nullptr)
{
  QVBoxLayout* vert_layout = new QVBoxLayout(this);
  vert_layout->setSpacing(0);
  vert_layout->setContentsMargins(0, 0, 0, 0);

  QHBoxLayout* ruler_and_time_layout = new QHBoxLayout();
  vert_layout->addLayout(ruler_and_time_layout);

  timecode_label_ = new RationalSlider();
  timecode_label_->SetAlignment(Qt::AlignCenter);
  timecode_label_->SetDisplayType(RationalSlider::kTime);
  timecode_label_->setVisible(false);
  timecode_label_->SetMinimum(0);
  ruler_and_time_layout->addWidget(timecode_label_);

  ruler_and_time_layout->addWidget(ruler());

  ruler()->setFocusPolicy(Qt::TabFocus);
  QWidget::setTabOrder(ruler(), timecode_label_);

  // Create list of TimelineViews - these MUST correspond to the ViewType enum

  view_splitter_ = new QSplitter(Qt::Vertical);
  vert_layout->addWidget(view_splitter_);

  // Video view
  views_.append(AddTimelineAndTrackView(Qt::AlignBottom));

  // Audio view
  views_.append(AddTimelineAndTrackView(Qt::AlignTop));

  // Subtitle view
  views_.append(AddTimelineAndTrackView(Qt::AlignTop));

  // Create tools
  tools_.resize(olive::Tool::kCount);
  tools_.fill(nullptr);

  tools_.replace(olive::Tool::kPointer, new PointerTool(this));
  tools_.replace(olive::Tool::kTrackSelect, new TrackSelectTool(this));
  tools_.replace(olive::Tool::kEdit, new EditTool(this));
  tools_.replace(olive::Tool::kRipple, new RippleTool(this));
  tools_.replace(olive::Tool::kRolling, new RollingTool(this));
  tools_.replace(olive::Tool::kRazor, new RazorTool(this));
  tools_.replace(olive::Tool::kSlip, new SlipTool(this));
  tools_.replace(olive::Tool::kSlide, new SlideTool(this));
  tools_.replace(olive::Tool::kZoom, new ZoomTool(this));
  tools_.replace(olive::Tool::kTransition, new TransitionTool(this));
  tools_.replace(olive::Tool::kRecord, new RecordTool(this));
  tools_.replace(olive::Tool::kAdd, new AddTool(this));

  import_tool_ = new ImportTool(this);

  // We add this to the list to make deleting all tools easier, it should never get accessed through the list under
  // normal circumstances (but *technically* its index would be Tool::kCount)
  tools_.append(import_tool_);

  // Global scrollbar
  connect(views_.first()->view()->horizontalScrollBar(), &QScrollBar::rangeChanged, scrollbar(), &QScrollBar::setRange);
  vert_layout->addWidget(scrollbar());

  foreach (TimelineAndTrackView* tview, views_) {
    TimelineView* view = tview->view();

    view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    view->SetSnapService(this);
    view->SetSelectionList(&selections_);
    view->SetGhostList(&ghost_items_);

    view_splitter_->addWidget(tview);

    ConnectTimelineView(view);

    connect(view, &TimelineView::customContextMenuRequested, this, &TimelineWidget::ShowContextMenu);

    connect(view, &TimelineView::MousePressed, this, &TimelineWidget::ViewMousePressed);
    connect(view, &TimelineView::MouseMoved, this, &TimelineWidget::ViewMouseMoved);
    connect(view, &TimelineView::MouseReleased, this, &TimelineWidget::ViewMouseReleased);
    connect(view, &TimelineView::MouseDoubleClicked, this, &TimelineWidget::ViewMouseDoubleClicked);
    connect(view, &TimelineView::DragEntered, this, &TimelineWidget::ViewDragEntered);
    connect(view, &TimelineView::DragMoved, this, &TimelineWidget::ViewDragMoved);
    connect(view, &TimelineView::DragLeft, this, &TimelineWidget::ViewDragLeft);
    connect(view, &TimelineView::DragDropped, this, &TimelineWidget::ViewDragDropped);

    connect(tview->splitter(), &QSplitter::splitterMoved, this, &TimelineWidget::UpdateHorizontalSplitters);
  }

  // Split viewer 50/50
  QList<int> view_sizes;
  view_sizes.reserve(views_.size());
  view_sizes.append(height()/2);  // Video
  view_sizes.append(height()/2);  // Audio
  view_sizes.append(0);           // Subtitle (hidden by default)
  view_splitter_->setSizes(view_sizes);

  // Video and audio are not collapsible, subtitle is
  view_splitter_->setCollapsible(Track::kVideo, false);
  view_splitter_->setCollapsible(Track::kAudio, false);
  view_splitter_->setCollapsible(Track::kSubtitle, true);

  // FIXME: Magic number
  SetScale(90.0);

  SetAutoSetTimebase(false);

  connect(Core::instance(), &Core::ToolChanged, this, &TimelineWidget::ToolChanged);
  connect(Core::instance(), &Core::AddableObjectChanged, this, &TimelineWidget::AddableObjectChanged);

  signal_block_change_timer_ = new QTimer(this);
  signal_block_change_timer_->setInterval(1);
  signal_block_change_timer_->setSingleShot(true);
  connect(signal_block_change_timer_, &QTimer::timeout, this, [this]{
    signal_block_change_timer_->stop();

    if (OLIVE_CONFIG("SelectAlsoSeeks").toBool()) {
      rational start = RATIONAL_MAX;
      for (Block *b : selected_blocks_) {
        start = std::min(start, b->in());
      }
      if (start != RATIONAL_MAX) {
        GetConnectedNode()->SetPlayhead(start);
      }
    }

    emit BlockSelectionChanged(selected_blocks_);
  });
}

TimelineWidget::~TimelineWidget()
{
  // Ensure no blocks are selected before any child widgets are destroyed (prevents corrupt ViewSelectionChanged() signal)
  ConnectViewerNode(nullptr);

  Clear();

  qDeleteAll(tools_);

  delete subtitle_show_command_;
}

void TimelineWidget::Clear()
{
  // Emit that we've deselected any selected blocks
  SignalDeselectedAllBlocks();

  // Set null timebase
  SetTimebase(0);
}

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

  timecode_label_->SetTimebase(timebase);

  timecode_label_->setVisible(!timebase.isNull());

  UpdateViewTimebases();
}

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

  // Update timecode label size
  UpdateTimecodeWidthFromSplitters(views_.first()->splitter());
}

void TimelineWidget::TimeChangedEvent(const rational &t)
{
  if (OLIVE_CONFIG("SeekAlsoSelects").toBool()) {
    TimelineWidgetSelections sels;

    QVector<Block*> new_blocks;

    for (auto it=sequence()->GetTracks().cbegin(); it!=sequence()->GetTracks().cend(); it++) {
      Track *track = *it;
      if (track->IsLocked()) {
        continue;
      }

      Block *b = track->VisibleBlockAtTime(sequence()->GetPlayhead());
      if (!b || dynamic_cast<GapBlock*>(b)) {
        continue;
      }

      new_blocks.push_back(b);
      sels[track->ToReference()].insert(b->range());
    }

    if (selected_blocks_ != new_blocks) {
      selected_blocks_ = new_blocks;
      SetSelections(sels, false);
      SignalBlockSelectionChange();
    }
  }
}

void TimelineWidget::ScaleChangedEvent(const double &scale)
{
  super::ScaleChangedEvent(scale);

  foreach (TimelineAndTrackView* view, views_) {
    view->view()->SetScale(scale);
  }

  if (rubberband_.isVisible()) {
    QMetaObject::invokeMethod(this, &TimelineWidget::ForceUpdateRubberBand, Qt::QueuedConnection);
  }
}

void TimelineWidget::ConnectNodeEvent(ViewerOutput *n)
{
  Sequence* s = static_cast<Sequence*>(n);

  connect(s, &Sequence::TrackAdded, this, &TimelineWidget::AddTrack);
  connect(s, &Sequence::TrackRemoved, this, &TimelineWidget::RemoveTrack);
  connect(s, &Sequence::FrameRateChanged, this, &TimelineWidget::FrameRateChanged);
  connect(s, &Sequence::SampleRateChanged, this, &TimelineWidget::SampleRateChanged);

  connect(timecode_label_, &RationalSlider::ValueChanged, s, &Sequence::SetPlayhead);
  connect(s, &Sequence::PlayheadChanged, timecode_label_, &RationalSlider::SetValue);
  timecode_label_->SetValue(s->GetPlayhead());

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

  SetTimebase(n->GetVideoParams().frame_rate_as_time_base());

  for (int i=0;i<views_.size();i++) {
    Track::Type track_type = static_cast<Track::Type>(i);
    TimelineView* view = views_.at(i)->view();
    TrackList* track_list = s->track_list(track_type);
    TrackView* track_view = views_.at(i)->track_view();

    track_view->ConnectTrackList(track_list);
    view->ConnectTrackList(track_list);

    // Defer to the track to make all the block UI items necessary
    const QVector<Track*> tracks = s->track_list(track_type)->GetTracks();
    foreach (Track* track, tracks) {
      AddTrack(track);
    }
  }
}

void TimelineWidget::DisconnectNodeEvent(ViewerOutput *n)
{
  Sequence* s = static_cast<Sequence*>(n);

  disconnect(s, &Sequence::TrackAdded, this, &TimelineWidget::AddTrack);
  disconnect(s, &Sequence::TrackRemoved, this, &TimelineWidget::RemoveTrack);
  disconnect(s, &Sequence::FrameRateChanged, this, &TimelineWidget::FrameRateChanged);
  disconnect(s, &Sequence::SampleRateChanged, this, &TimelineWidget::SampleRateChanged);

  disconnect(timecode_label_, &RationalSlider::ValueChanged, s, &Sequence::SetPlayhead);

  DeselectAll();

  foreach (Track* track, s->GetTracks()) {
    RemoveTrack(track);
  }

  ruler()->SetPlaybackCache(nullptr);

  SetTimebase(0);

  Clear();

  foreach (TimelineAndTrackView* tview, views_) {
    tview->track_view()->DisconnectTrackList();
    tview->view()->ConnectTrackList(nullptr);
  }
}

void TimelineWidget::SendCatchUpScrollEvent()
{
  super::SendCatchUpScrollEvent();

  if (rubberband_.isVisible()) {
    this->ForceUpdateRubberBand();
  }
}

void TimelineWidget::SelectAll()
{
  QVector<Block*> newly_selected_blocks;

  foreach (Block* block, added_blocks_) {
    if (!selected_blocks_.contains(block)) {
      newly_selected_blocks.append(block);
      AddSelection(block);
    }
  }

  SignalSelectedBlocks(newly_selected_blocks, false);
}

void TimelineWidget::DeselectAll()
{
  // Clear selections
  selections_.clear();

  // Update all viewports
  UpdateViewports();

  // Clear list and emit signal
  SignalDeselectedAllBlocks();
}

void TimelineWidget::RippleToIn()
{
  RippleTo(Timeline::kTrimIn);
}

void TimelineWidget::RippleToOut()
{
  RippleTo(Timeline::kTrimOut);
}

void TimelineWidget::EditToIn()
{
  EditTo(Timeline::kTrimIn);
}

void TimelineWidget::EditToOut()
{
  EditTo(Timeline::kTrimOut);
}

void TimelineWidget::SplitAtPlayhead()
{
  if (!GetConnectedNode()) {
    return;
  }

  const rational &playhead_time = GetConnectedNode()->GetPlayhead();

  QVector<Block*> selected_blocks = GetSelectedBlocks();

  // Prioritize blocks that are selected and overlap the playhead
  QVector<Block*> blocks_to_split;
  QVector<bool> block_is_selected;

  bool some_blocks_are_selected = false;

  // Get all blocks at the playhead
  foreach (Track* track, sequence()->GetTracks()) {
    if (track->IsLocked()) {
      continue;
    }

    Block* b = track->BlockContainingTime(playhead_time);

    if (dynamic_cast<ClipBlock*>(b)) {
      bool selected = false;

      // See if this block is selected
      foreach (Block* item, selected_blocks) {
        if (item == b) {
          some_blocks_are_selected = true;
          selected = true;
          break;
        }
      }

      blocks_to_split.append(b);
      block_is_selected.append(selected);
    }
  }

  // If some blocks are selected, we prioritize those and don't split the blocks that aren't
  if (some_blocks_are_selected) {
    for (int i=0;i<block_is_selected.size();i++) {
      if (!block_is_selected.at(i)) {
        blocks_to_split.removeAt(i);
        block_is_selected.removeAt(i);
        i--;
      }
    }
  }

  if (!blocks_to_split.isEmpty()) {
    Core::instance()->undo_stack()->push(new BlockSplitPreservingLinksCommand(blocks_to_split, {playhead_time}), tr("Split Clips At Playhead"));
  }
}

void TimelineWidget::ReplaceBlocksWithGaps(const QVector<Block *> &blocks,
                                           bool remove_from_graph,
                                           MultiUndoCommand *command,
                                           bool handle_transitions)
{
  foreach (Block* b, blocks) {
    if (dynamic_cast<GapBlock*>(b)) {
      // No point in replacing a gap with a gap, and TrackReplaceBlockWithGapCommand will clear
      // up any extraneous gaps
      continue;
    }

    Track* original_track = b->track();

    command->add_child(new TrackReplaceBlockWithGapCommand(original_track, b, handle_transitions));

    if (remove_from_graph) {
      command->add_child(new NodeRemoveWithExclusiveDependenciesAndDisconnect(b));
    }
  }
}

void TimelineWidget::DeleteSelected(bool ripple)
{
  if (ruler()->HasItemsSelected()) {
    ruler()->DeleteSelected();
    return;
  }

  QVector<Block*> selected_list = GetSelectedBlocks();

  // No-op if nothing is selected
  if (selected_list.isEmpty()) {
    return;
  }

  QVector<Block*> clips_to_delete;
  QVector<TransitionBlock*> transitions_to_delete;

  bool all_gaps = true;

  foreach (Block* b, selected_list) {
    if (!dynamic_cast<GapBlock*>(b)) {
      all_gaps = false;
    }

    if (dynamic_cast<ClipBlock*>(b)) {
      clips_to_delete.append(b);
    } else if (dynamic_cast<TransitionBlock*>(b)) {
      transitions_to_delete.append(static_cast<TransitionBlock*>(b));
    }
  }

  if (all_gaps) {
    ripple = true;
  }

  MultiUndoCommand* command = new MultiUndoCommand();

  // Remove all selections
  command->add_child(new SetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections()));

  // For transitions, remove them but extend their attached blocks to fill their place
  foreach (TransitionBlock* transition, transitions_to_delete) {
    TransitionRemoveCommand *trc = new TransitionRemoveCommand(transition, true);

    // Perform the transition removal now so that replacing blocks with gaps below won't get confused
    trc->redo_now();

    command->add_child(trc);
  }

  // Replace clips with gaps (effectively deleting them)
  ReplaceBlocksWithGaps(clips_to_delete, true, command, false);

  // Insert ripple command now that it's all cleaned up gaps
  TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
  rational new_playhead = RATIONAL_MAX;
  if (ripple) {
    TimelineRippleDeleteGapsAtRegionsCommand::RangeList range_list;

    foreach (Block* b, selected_list) {
      range_list.append({b->track(), b->range()});
      new_playhead = qMin(new_playhead, b->in());
    }

    ripple_command = new TimelineRippleDeleteGapsAtRegionsCommand(sequence(), range_list);
    command->add_child(ripple_command);
  }

  Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));

  // Ensures any current drag operations are cancelled
  ClearGhosts();

  if (ripple_command && ripple_command->HasCommands() && new_playhead != RATIONAL_MAX) {
    GetConnectedNode()->SetPlayhead(new_playhead);
  }
}

void TimelineWidget::IncreaseTrackHeight()
{
  if (!GetConnectedNode()) {
    return;
  }

  // Increase the height of each track by one "unit"
  foreach (Track* t, sequence()->GetTracks()) {
    t->SetTrackHeight(t->GetTrackHeight() + Track::kTrackHeightInterval);
  }
}

void TimelineWidget::DecreaseTrackHeight()
{
  if (!GetConnectedNode()) {
    return;
  }

  // Decrease the height of each track by one "unit"
  foreach (Track* t, sequence()->GetTracks()) {
    t->SetTrackHeight(qMax(t->GetTrackHeight() - Track::kTrackHeightInterval, Track::kTrackHeightMinimum));
  }
}

void TimelineWidget::InsertFootageAtPlayhead(const QVector<ViewerOutput*>& footage)
{
  auto command = new MultiUndoCommand();
  import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), true, command, 0, true);
  Core::instance()->undo_stack()->push(command, tr("Inserted Footage At Playhead"));
}

void TimelineWidget::OverwriteFootageAtPlayhead(const QVector<ViewerOutput *> &footage)
{
  auto command = new MultiUndoCommand();
  import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), false, command, 0, true);
  Core::instance()->undo_stack()->push(command, tr("Overwrote Footage At Playhead"));
}

void TimelineWidget::ToggleLinksOnSelected()
{
  QVector<Node*> blocks;
  bool link = true;

  foreach (Block* item, GetSelectedBlocks()) {
    // Only clips can be linked
    if (!dynamic_cast<ClipBlock*>(item)) {
      continue;
    }

    // Prioritize unlinking, if any block has links, assume we're unlinking
    if (link && item->HasLinks()) {
      link = false;
    }

    blocks.append(item);
  }

  if (blocks.isEmpty()) {
    return;
  }

  Core::instance()->undo_stack()->push(new NodeLinkManyCommand(blocks, link), tr("Linked Clips"));
}

void TimelineWidget::AddDefaultTransitionsToSelected()
{
  QVector<ClipBlock*> blocks;

  foreach (Block* item, GetSelectedBlocks()) {
    // Only clips can be linked
    if (ClipBlock *clip = dynamic_cast<ClipBlock*>(item)) {
      blocks.append(clip);
    }
  }

  if (!blocks.isEmpty()) {
    Core::instance()->undo_stack()->push(new TimelineAddDefaultTransitionCommand(blocks, timebase()), tr("Added Default Transitions"));
  }
}

bool TimelineWidget::CopySelected(bool cut)
{
  if (super::CopySelected(cut)) {
    return true;
  }

  if (!GetConnectedNode() || selected_blocks_.isEmpty()) {
    return false;
  }

  QVector<Node*> selected_nodes;

  foreach (Block* block, selected_blocks_) {
    selected_nodes.append(block);

    QVector<Node*> deps = block->GetDependencies();

    foreach (Node* d, deps) {
      if (!selected_nodes.contains(d)) {
        selected_nodes.append(d);
      }
    }
  }

  ProjectSerializer::SaveData sdata(ProjectSerializer::kOnlyClips);
  sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes);

  // Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere
  rational earliest_in = RATIONAL_MAX;
  ProjectSerializer::SerializedProperties properties;

  foreach (Block* block, selected_blocks_) {
    earliest_in = qMin(earliest_in, block->in());
  }

  foreach (Block* block, selected_blocks_) {
    properties[block][QStringLiteral("in")] = QString::fromStdString((block->in() - earliest_in).toString());
    properties[block][QStringLiteral("track")] = block->track()->ToReference().ToString();
  }

  sdata.SetProperties(properties);

  ProjectSerializer::Copy(sdata);

  if (cut) {
    DeleteSelected();
  }

  return true;
}

bool TimelineWidget::Paste()
{
  // TimeRuler gets first chance (markers, etc.)
  if (super::Paste()) {
    return true;
  }

  // Ensure we have a connected node
  if (!GetConnectedNode()) {
    return false;
  }

  // Attempt regular clip pasting
  if (PasteInternal(false)) {
    return true;
  }

  // Give last chance to NodeParamView
  return NodeParamView::Paste(this, std::bind(&TimelineWidget::GenerateExistingPasteMap, this, std::placeholders::_1));
}

void TimelineWidget::PasteInsert()
{
  PasteInternal(true);
}

void TimelineWidget::DeleteInToOut(bool ripple)
{
  if (!GetConnectedNode()
      || !GetConnectedNode()->GetWorkArea()->enabled()) {
    return;
  }

  MultiUndoCommand* command = new MultiUndoCommand();

  if (ripple) {

    command->add_child(new TimelineRippleRemoveAreaCommand(
                         sequence(),
                         GetConnectedNode()->GetWorkArea()->in(),
                         GetConnectedNode()->GetWorkArea()->out()));

  } else {
    QVector<Track*> unlocked_tracks = sequence()->GetUnlockedTracks();

    foreach (Track* track, unlocked_tracks) {
      GapBlock* gap = new GapBlock();

      gap->set_length_and_media_out(GetConnectedNode()->GetWorkArea()->length());

      command->add_child(new NodeAddCommand(static_cast<Project*>(track->parent()),
                                            gap));

      command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track->type()),
                                                    track->Index(),
                                                    gap,
                                                    GetConnectedNode()->GetWorkArea()->in()));
    }
  }

  // Clear workarea after this
  command->add_child(new WorkareaSetEnabledCommand(GetConnectedNode()->project(),
                                                   GetConnectedNode()->GetWorkArea(),
                                                   false));

  if (ripple) {
    GetConnectedNode()->SetPlayhead(GetConnectedNode()->GetWorkArea()->in());
  }

  Core::instance()->undo_stack()->push(command, tr("Deleted In To Out"));
}

void TimelineWidget::ToggleSelectedEnabled()
{
  QVector<Block*> items = GetSelectedBlocks();

  if (items.isEmpty()) {
    return;
  }

  MultiUndoCommand* command = new MultiUndoCommand();

  foreach (Block* i, items) {
    command->add_child(new BlockEnableDisableCommand(i,
                                                     !i->is_enabled()));
  }

  Core::instance()->undo_stack()->push(command, tr("Toggled Clips Enabled"));
}

void TimelineWidget::SetColorLabel(int index)
{
  MultiUndoCommand *command = new MultiUndoCommand();

  foreach (Block* b, selected_blocks_) {
    command->add_child(new NodeOverrideColorCommand(b, index));
  }

  Core::instance()->undo_stack()->push(command, tr("Set Colors of %1 Clips").arg(selected_blocks_.size()));
}

void TimelineWidget::NudgeLeft()
{
  if (GetConnectedNode()) {
    NudgeInternal(-timebase());
  }
}

void TimelineWidget::NudgeRight()
{
  if (GetConnectedNode()) {
    NudgeInternal(timebase());
  }
}

void TimelineWidget::MoveInToPlayhead()
{
  MoveToPlayheadInternal(false);
}

void TimelineWidget::MoveOutToPlayhead()
{
  MoveToPlayheadInternal(true);
}

void TimelineWidget::ShowSpeedDurationDialogForSelectedClips()
{
  QVector<ClipBlock*> clips;

  foreach (Block *b, selected_blocks_) {
    ClipBlock *c = dynamic_cast<ClipBlock*>(b);
    if (c) {
      clips.append(c);
    }
  }

  if (!clips.isEmpty()) {
    SpeedDurationDialog sdd(clips, timebase(), this);
    sdd.exec();
  }
}

void TimelineWidget::RecordingCallback(const QString &filename, const TimeRange &time, const Track::Reference &track)
{
  ProjectImportTask task(GetConnectedNode()->project()->root(), {filename});
  task.Start();

  auto subimport_command = task.GetCommand();

  if (task.GetImportedFootage().empty()) {
    qCritical() << "Failed to import recorded audio file" << filename;
    delete subimport_command;
  } else {
    subimport_command->redo_now();

    auto import_command = new MultiUndoCommand();
    import_command->add_child(subimport_command);

    import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, import_command, track.index());
    Core::instance()->undo_stack()->push(import_command, tr("Recorded Audio Clip"));
  }
}

void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord)
{
  foreach (TimelineAndTrackView* tview, views_) {
    tview->view()->EnableRecordingOverlay(coord);
  }
}

void TimelineWidget::DisableRecordingOverlay()
{
  foreach (TimelineAndTrackView* tview, views_) {
    tview->view()->DisableRecordingOverlay();
  }
}

void TimelineWidget::AddTentativeSubtitleTrack()
{
  if (!subtitle_show_command_) {
    // Determine if we need to do anything
    QList<int> sz = view_splitter_->sizes();
    bool should_adjust_splitter = (sz[Track::kSubtitle] == 0);
    bool should_add_sub_track = (sequence() && sequence()->track_list(Track::kSubtitle)->GetTrackCount() == 0);

    if (should_adjust_splitter || should_add_sub_track) {
      // Create command
      subtitle_show_command_ = new MultiUndoCommand();

      if (should_adjust_splitter) {
        sz[Track::kSubtitle] = height() / Track::kCount;
        subtitle_show_command_->add_child(new SetSplitterSizesCommand(view_splitter_, sz));
      }

      if (should_add_sub_track) {
        TimelineAddTrackCommand *track_add_cmd = new TimelineAddTrackCommand(sequence()->track_list(Track::kSubtitle));
        subtitle_tentative_track_ = track_add_cmd->track();
        subtitle_show_command_->add_child(track_add_cmd);
      }

      subtitle_show_command_->redo_now();
    }
  }
}

void TimelineWidget::NestSelectedClips()
{
  if (!GetConnectedNode()) {
    return;
  }

  QVector<Block*> blocks = this->selected_blocks_;
  if (blocks.empty()) {
    return;
  }

  QVector<Track::Reference> tracks(blocks.size());
  QVector<TimeRange> times(blocks.size());
  QVector<int> track_offset(Track::kCount, INT_MAX);
  rational start_time = RATIONAL_MAX;
  rational end_time = RATIONAL_MIN;
  for (int i=0; i<blocks.size(); i++) {
    Block *b = blocks.at(i);

    Track::Reference tf = b->track()->ToReference();;
    tracks[i] = tf;
    times[i] = b->range();

    int &to = track_offset[tf.type()];
    to = std::min(to, tf.index());

    start_time = std::min(start_time, b->in());
    end_time = std::max(end_time, b->out());
  }

  auto move_to_nest_command = new MultiUndoCommand();

  // Remove blocks from this sequence
  ReplaceBlocksWithGaps(blocks, false, move_to_nest_command);

  // Create new sequence
  Project *project = this->GetConnectedNode()->project();
  Sequence *nest = Core::CreateNewSequenceForProject(tr("Nested Sequence %1"), project);
  nest->SetVideoParams(GetConnectedNode()->GetVideoParams());
  nest->SetAudioParams(GetConnectedNode()->GetAudioParams());
  move_to_nest_command->add_child(new NodeAddCommand(project, nest));

  // Add to same folder
  move_to_nest_command->add_child(new FolderAddChild(this->GetConnectedNode()->folder(), nest));

  // Place blocks in new sequence
  for (int i=0; i<blocks.size(); i++) {
    Block *b = blocks.at(i);

    const TimeRange &range = times.at(i);
    Track::Reference track = tracks.at(i);

    move_to_nest_command->add_child(new TrackPlaceBlockCommand(nest->track_list(track.type()),
                                                  track.index() - track_offset.at(track.type()),
                                                  b, range.in() - start_time));
  }

  // Do this command now, because we later do checks and actions that rely on these having been done
  move_to_nest_command->redo_now();

  auto meta_command = new MultiUndoCommand();
  meta_command->add_child(move_to_nest_command);

  // Find first free track index
  bool empty = false;
  int index = -1;
  while (!empty) {
    index++;
    empty = true;
    for (int i=0; i<Track::kCount; i++) {
      if (track_offset.at(i) == INT_MAX) {
        // No clips on this track
        continue;
      }

      TrackList *list = sequence()->track_list(static_cast<Track::Type>(i));
      if (index < list->GetTrackCount() && !list->GetTrackAt(index)->IsRangeFree(TimeRange(start_time, end_time))) {
        empty = false;
        break;
      }
    }
  }

  // Place new sequence in this sequence
  import_tool_->PlaceAt({nest}, start_time, false, meta_command, index);

  Core::instance()->undo_stack()->push(meta_command, tr("Nested Clips"));
}

void TimelineWidget::ClearTentativeSubtitleTrack()
{
  if (subtitle_show_command_) {
    subtitle_show_command_->undo_now();
    delete subtitle_show_command_;
    subtitle_show_command_ = nullptr;
    subtitle_tentative_track_ = nullptr;
  }
}

void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, MultiUndoCommand *command)
{
  for (int i=0;i<Track::kCount;i++) {
    command->add_child(new TrackListInsertGaps(sequence()->track_list(static_cast<Track::Type>(i)),
                                               earliest_point,
                                               insert_length));
  }
}

Track *TimelineWidget::GetTrackFromReference(const Track::Reference &ref) const
{
  return sequence()->track_list(ref.type())->GetTrackAt(ref.index());
}

int TimelineWidget::GetTrackY(const Track::Reference &ref)
{
  return views_.at(ref.type())->view()->GetTrackY(ref.index());
}

int TimelineWidget::GetTrackHeight(const Track::Reference &ref)
{
  return views_.at(ref.type())->view()->GetTrackHeight(ref.index());
}

void TimelineWidget::CenterOn(qreal scene_pos)
{
  scrollbar()->setValue(qRound(scene_pos - scrollbar()->width()/2));
}

void TimelineWidget::ClearGhosts()
{
  if (!ghost_items_.isEmpty()) {
    foreach (TimelineViewGhostItem* ghost, ghost_items_) {
      delete ghost;
    }

    ghost_items_.clear();
  }

  HideSnaps();
}

TimelineTool *TimelineWidget::GetActiveTool()
{
  return tools_.at(Core::instance()->tool());
}

void TimelineWidget::ViewMousePressed(TimelineViewMouseEvent *event)
{
  active_tool_ = GetActiveTool();

  if (GetConnectedNode() && active_tool_ != nullptr) {
    active_tool_->MousePress(event);
    UpdateViewports();
  }

  if (event->GetButton() != Qt::LeftButton) {
    // Suspend tool immediately if the cursor isn't the primary button
    active_tool_->MouseRelease(event);
    UpdateViewports();
    active_tool_ = nullptr;
  }
}

void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event)
{
  if (GetConnectedNode()) {
    if (active_tool_) {
      active_tool_->MouseMove(event);

      UpdateViewports();

      SetCatchUpScrollValue(event->GetScreenPos().x());
    } else {
      // Mouse is not down, attempt a hover event
      TimelineTool* hover_tool = GetActiveTool();

      if (hover_tool) {
        hover_tool->HoverMove(event);
      }
    }
  }
}


void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event)
{
  StopCatchUpScrollTimer();

  if (active_tool_) {
    if (GetConnectedNode()) {
      active_tool_->MouseRelease(event);
      UpdateViewports();
    }

    active_tool_ = nullptr;
  }
}

void TimelineWidget::ViewMouseDoubleClicked(TimelineViewMouseEvent *event)
{
  // kHand tool will return nullptr
  if (!GetActiveTool()) {
    // Only kHand should return a nullptr
    Q_ASSERT(Core::instance()->tool() == olive::Tool::kHand);
    return;
  }
  if (GetConnectedNode()) {
    GetActiveTool()->MouseDoubleClick(event);
    UpdateViewports();
  }
}

void TimelineWidget::ViewDragEntered(TimelineViewMouseEvent *event)
{
  import_tool_->DragEnter(event);
  UpdateViewports();
}

void TimelineWidget::ViewDragMoved(TimelineViewMouseEvent *event)
{
  import_tool_->DragMove(event);
  UpdateViewports();

  SetCatchUpScrollValue(event->GetScreenPos().x());
}

void TimelineWidget::ViewDragLeft(QDragLeaveEvent *event)
{
  StopCatchUpScrollTimer();

  import_tool_->DragLeave(event);
  UpdateViewports();
}

void TimelineWidget::ViewDragDropped(TimelineViewMouseEvent *event)
{
  StopCatchUpScrollTimer();

  import_tool_->DragDrop(event);
  UpdateViewports();
}

void TimelineWidget::AddBlock(Block *block)
{
  // Set up clip with view parameters (clip item will automatically size its rect accordingly)
  if (!added_blocks_.contains(block)) {
    connect(block, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated);
    connect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated);
    connect(block, &Block::ColorChanged, this, &TimelineWidget::BlockUpdated);
    connect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated);
    connect(block, &Block::PreviewChanged, this, &TimelineWidget::BlockUpdated);

    added_blocks_.append(block);

    if (selections_[block->track()->ToReference()].contains(block->range()) && !selected_blocks_.contains(block)) {
      selected_blocks_.append(block);
    }
  }
}

void TimelineWidget::RemoveBlock(Block *block)
{
  // Disconnect all signals
  disconnect(block, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated);
  disconnect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated);
  disconnect(block, &Block::ColorChanged, this, &TimelineWidget::BlockUpdated);
  disconnect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated);
  disconnect(block, &Block::PreviewChanged, this, &TimelineWidget::BlockUpdated);

  // Take item from map
  added_blocks_.removeOne(block);

  // If selected, deselect it
  int select_index = selected_blocks_.indexOf(block);
  if (select_index > -1) {
    selected_blocks_.removeAt(select_index);
    RemoveSelection(block);

    SignalBlockSelectionChange();
  }
}

void TimelineWidget::AddTrack(Track *track)
{
  foreach (Block* b, track->Blocks()) {
    AddBlock(b);
  }

  connect(track, &Track::IndexChanged, this, &TimelineWidget::TrackUpdated);
  connect(track, &Track::IndexChanged, this, &TimelineWidget::TrackIndexChanged);
  connect(track, &Track::BlocksRefreshed, this, &TimelineWidget::TrackUpdated);
  connect(track, &Track::TrackHeightChanged, this, &TimelineWidget::TrackUpdated);
  connect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock);
  connect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock);
}

void TimelineWidget::RemoveTrack(Track *track)
{
  disconnect(track, &Track::IndexChanged, this, &TimelineWidget::TrackUpdated);
  disconnect(track, &Track::IndexChanged, this, &TimelineWidget::TrackIndexChanged);
  disconnect(track, &Track::BlocksRefreshed, this, &TimelineWidget::TrackUpdated);
  disconnect(track, &Track::TrackHeightChanged, this, &TimelineWidget::TrackUpdated);
  disconnect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock);
  disconnect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock);

  RemoveSelection(TimeRange(0, RATIONAL_MAX), track->ToReference());

  foreach (Block* b, track->Blocks()) {
    RemoveBlock(b);
  }
}

void TimelineWidget::TrackUpdated()
{
  UpdateViewports(static_cast<Track*>(sender())->type());
}

void TimelineWidget::BlockUpdated()
{
  UpdateViewports(static_cast<Block*>(sender())->track()->type());
}

void TimelineWidget::UpdateHorizontalSplitters()
{
  QSplitter* sender_splitter = static_cast<QSplitter*>(sender());

  foreach (TimelineAndTrackView* tview, views_) {
    QSplitter* recv_splitter = tview->splitter();

    if (recv_splitter != sender_splitter) {
      recv_splitter->blockSignals(true);
      recv_splitter->setSizes(sender_splitter->sizes());
      recv_splitter->blockSignals(false);
    }
  }

  UpdateTimecodeWidthFromSplitters(sender_splitter);
}

void TimelineWidget::UpdateTimecodeWidthFromSplitters(QSplitter* s)
{
  timecode_label_->setFixedWidth(s->sizes().first() + s->handleWidth());
}

void TimelineWidget::ShowContextMenu()
{
  Menu menu(this);

  QVector<Block*> selected = GetSelectedBlocks();

  if (!selected.isEmpty()) {
    MenuShared::instance()->AddItemsForEditMenu(&menu, true);

    menu.addSeparator();

    MenuShared::instance()->AddColorCodingMenu(&menu);

    menu.addSeparator();

    if (ClipBlock *clip = dynamic_cast<ClipBlock*>(selected.first())) {
      {
        Menu *cache_menu = new Menu(tr("Cache"), &menu);
        menu.addMenu(cache_menu);

        QAction *autocache_action = cache_menu->addAction(tr("Auto-Cache"));
        autocache_action->setCheckable(true);
        autocache_action->setChecked(clip->IsAutocaching());
        connect(autocache_action, &QAction::triggered, this, &TimelineWidget::SetSelectedClipsAutocaching);

        cache_menu->addSeparator();

        auto cache_clip = cache_menu->addAction(tr("Cache All"));
        connect(cache_clip, &QAction::triggered, this, &TimelineWidget::CacheClips);

        auto cache_inout = cache_menu->addAction(tr("Cache In/Out"));
        connect(cache_inout, &QAction::triggered, this, &TimelineWidget::CacheClipsInOut);

        auto cache_discard = cache_menu->addAction(tr("Discard"));
        connect(cache_discard, &QAction::triggered, this, &TimelineWidget::CacheDiscard);
      }

      if (clip->connected_viewer()) {
        QAction *reveal_in_footage_viewer = menu.addAction(tr("Reveal in Footage Viewer"));
        reveal_in_footage_viewer->setData(reinterpret_cast<quintptr>(clip->connected_viewer()));
        reveal_in_footage_viewer->setProperty("range", QVariant::fromValue(clip->media_range()));
        connect(reveal_in_footage_viewer, &QAction::triggered, this, &TimelineWidget::RevealInFootageViewer);

        QAction *reveal_in_project = menu.addAction(tr("Reveal in Project"));
        reveal_in_project->setData(reinterpret_cast<quintptr>(clip->connected_viewer()));
        connect(reveal_in_project, &QAction::triggered, this, &TimelineWidget::RevealInProject);

        if (Sequence *sequence = dynamic_cast<Sequence*>(clip->connected_viewer())) {
          QAction *multicam_enabled = menu.addAction(tr("Multi-Cam"));
          multicam_enabled->setCheckable(true);

          MultiCamNode *mcn = nullptr;
          auto paths = clip->FindWaysNodeArrivesHere(sequence);

          for (const NodeInput &i : paths) {
            if ((mcn = dynamic_cast<MultiCamNode*>(i.node()))) {
              break;
            }
          }

          multicam_enabled->setChecked(mcn);

          connect(multicam_enabled, &QAction::triggered, this, &TimelineWidget::MulticamEnabledTriggered);
        }
      }
    }

    menu.addSeparator();

    QAction* properties_action = menu.addAction(tr("Properties"));
    connect(properties_action, &QAction::triggered, this, &TimelineWidget::ShowSpeedDurationDialogForSelectedClips);
  }

  if (selected.isEmpty()) {

    QAction* toggle_audio_units = menu.addAction(tr("Use Audio Time Units"));
    toggle_audio_units->setCheckable(true);
    toggle_audio_units->setChecked(use_audio_time_units_);
    connect(toggle_audio_units, &QAction::triggered, this, &TimelineWidget::SetUseAudioTimeUnits);

    {
      Menu *thumbnail_menu = new Menu(tr("Show Thumbnails"), &menu);
      menu.addMenu(thumbnail_menu);

      thumbnail_menu->AddActionWithData(tr("Disabled"), Timeline::kThumbnailOff, OLIVE_CONFIG("TimelineThumbnailMode"));
      thumbnail_menu->AddActionWithData(tr("Only At In Points"), Timeline::kThumbnailInOut, OLIVE_CONFIG("TimelineThumbnailMode"));
      thumbnail_menu->AddActionWithData(tr("Enabled"), Timeline::kThumbnailOn, OLIVE_CONFIG("TimelineThumbnailMode"));

      connect(thumbnail_menu, &Menu::triggered, this, &TimelineWidget::SetViewThumbnailsEnabled);
    }

    QAction* show_waveforms = menu.addAction(tr("Show Waveforms"));
    show_waveforms->setCheckable(true);
    show_waveforms->setChecked(OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled);
    connect(show_waveforms, &QAction::triggered, this, &TimelineWidget::SetViewWaveformsEnabled);

    menu.addSeparator();

    QAction* properties_action = menu.addAction(tr("Properties"));
    connect(properties_action, &QAction::triggered, this, &TimelineWidget::ShowSequenceDialog);
  }

  menu.exec(QCursor::pos());
}

void TimelineWidget::DeferredScrollAction()
{
  scrollbar()->setValue(deferred_scroll_value_);
}

void TimelineWidget::ShowSequenceDialog()
{
  if (!GetConnectedNode()) {
    return;
  }

  SequenceDialog sd(sequence(), SequenceDialog::kExisting, this);
  sd.exec();
}

void TimelineWidget::SetUseAudioTimeUnits(bool use)
{
  use_audio_time_units_ = use;

  // Update timebases
  UpdateViewTimebases();
}

void TimelineWidget::ToolChanged()
{
  HideSnaps();
  SetViewBeamCursor(TimelineCoordinate(0, Track::kNone, -1));
  SetViewTransitionOverlay(nullptr, nullptr);

  AddableObjectChanged();
}

void TimelineWidget::AddableObjectChanged()
{
  // Special cast for subtitle adding - ensure section is visible
  if (Core::instance()->tool() == Tool::kAdd && Core::instance()->GetSelectedAddableObject() == Tool::kAddableSubtitle) {
    AddTentativeSubtitleTrack();
  } else {
    ClearTentativeSubtitleTrack();
  }
}

void TimelineWidget::SetViewWaveformsEnabled(bool e)
{
  OLIVE_CONFIG("TimelineWaveformMode") = e ? Timeline::kWaveformsEnabled : Timeline::kWaveformsDisabled;
  UpdateViewports();
}

void TimelineWidget::SetViewThumbnailsEnabled(QAction *action)
{
  OLIVE_CONFIG("TimelineThumbnailMode") = action->data();
  UpdateViewports();
}

void TimelineWidget::FrameRateChanged()
{
  SetTimebase(GetConnectedNode()->GetVideoParams().frame_rate_as_time_base());
}

void TimelineWidget::SampleRateChanged()
{
  UpdateViewTimebases();
}

void TimelineWidget::TrackIndexChanged(int old, int now)
{
  Track* track = static_cast<Track*>(sender());

  Track::Reference old_ref(track->type(), old);
  Track::Reference new_ref(track->type(), now);

  auto track_selections = selections_.take(old_ref);
  if (!track_selections.isEmpty()) {
    selections_.insert(new_ref, track_selections);
  }
}

void TimelineWidget::SignalBlockSelectionChange()
{
  signal_block_change_timer_->stop();
  signal_block_change_timer_->start();
}

void TimelineWidget::RevealInFootageViewer()
{
  QAction *a = static_cast<QAction*>(sender());

  ViewerOutput *item_to_reveal = reinterpret_cast<ViewerOutput*>(a->data().value<quintptr>());
  TimeRange r = a->property("range").value<TimeRange>();

  emit RevealViewerInFootageViewer(item_to_reveal, r);
}

void TimelineWidget::RevealInProject()
{
  QAction *a = static_cast<QAction*>(sender());

  ViewerOutput *item_to_reveal = reinterpret_cast<ViewerOutput*>(a->data().value<quintptr>());

  emit RevealViewerInProject(item_to_reveal);
}

void TimelineWidget::RenameSelectedBlocks()
{
  MultiUndoCommand *command = new MultiUndoCommand();
  QVector<Node*> nodes(selected_blocks_.size());

  for (int i=0; i<nodes.size(); i++) {
    nodes[i] = selected_blocks_[i];
  }

  Core::instance()->LabelNodes(nodes);
  Core::instance()->undo_stack()->push(command, tr("Renamed %1 Clip(s)").arg(nodes.size()));
}

void TimelineWidget::TrackAboutToBeDeleted(Track *track)
{
  if (track == subtitle_tentative_track_) {
    // User is deleting the tentative subtitle track. Technically they shouldn't do this, but they
    // might if they misinterpret it as permanent. If so, we handle it cleanly by pushing our
    // command as if the action really were permanent.
    Core::instance()->undo_stack()->push(TakeSubtitleSectionCommand(), tr("Created Subtitle Track"));
  }
}

void TimelineWidget::SetSelectedClipsAutocaching(bool e)
{
  MultiUndoCommand *command = new MultiUndoCommand();

  for (Block *b : selected_blocks_) {
    if (ClipBlock *clip = dynamic_cast<ClipBlock*>(b)) {
      command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(clip, ClipBlock::kAutoCacheInput)), e));
    }
  }

  Core::instance()->undo_stack()->push(command, e ? tr("Enabled Auto-Caching On %1 Clip(s)").arg(selected_blocks_.size()) : tr("Disabled Auto-Caching On %1 Clip(s)").arg(selected_blocks_.size()));
}

void TimelineWidget::CacheClips()
{
  for (Block *b : selected_blocks_) {
    if (ClipBlock *clip = dynamic_cast<ClipBlock*>(b)) {
      clip->RequestInvalidatedFromConnected(true);
    }
  }
}

void TimelineWidget::CacheClipsInOut()
{
  if (!this->sequence() || !this->sequence()->GetWorkArea()->enabled()) {
    return;
  }

  TimeTargetObject tto;
  tto.SetTimeTarget(this->sequence());

  const TimeRange &r = this->sequence()->GetWorkArea()->range();
  for (Block *b : qAsConst(selected_blocks_)) {
    if (ClipBlock *clip = dynamic_cast<ClipBlock*>(b)) {
      if (Node *connected = clip->GetConnectedOutput(clip->kBufferIn)) {
        TimeRange adjusted = tto.GetAdjustedTime(this->sequence(), connected, r, Node::kTransformTowardsInput);
        clip->RequestInvalidatedFromConnected(true, adjusted);
      }
    }
  }
}

void TimelineWidget::CacheDiscard()
{
  if (QMessageBox::question(this, tr("Discard Cache"),
                            tr("This will discard all cache for this clip. "
                               "If the clip has auto-cache enabled, it will be recached immediately. "
                               "This cannot be undone.\n\n"
                               "Do you wish to continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
    for (Block *b : selected_blocks_) {
      if (ClipBlock *clip = dynamic_cast<ClipBlock*>(b)) {
        clip->DiscardCache();
      }
    }
  }
}

void TimelineWidget::MulticamEnabledTriggered(bool e)
{
  MultiUndoCommand *command = new MultiUndoCommand();

  for (Block *b : qAsConst(selected_blocks_)) {
    if (ClipBlock *c = dynamic_cast<ClipBlock*>(b)) {
      if (Sequence *s = dynamic_cast<Sequence*>(c->connected_viewer())) {
        if (e) {

          // Adding multicams
          // Create multicam node and add it to the graph
          MultiCamNode *n = new MultiCamNode();
          n->SetSequenceType(c->GetTrackType());
          command->add_child(new NodeAddCommand(s->parent(), n));


          // For each output the sequence has to this clip, disconnect it and
          // connect to the multicam instead
          QVector<NodeInput> inputs = c->FindWaysNodeArrivesHere(s);
          for (const NodeInput &i : inputs) {
            command->add_child(new NodeEdgeRemoveCommand(s, i));
            command->add_child(new NodeEdgeAddCommand(n, i));
          }

          command->add_child(new NodeEdgeAddCommand(s, NodeInput(n, n->kSequenceInput)));

          // Move sequence node one unit back, and place multicam in sequence's spot
          QPointF sequence_pos = c->GetNodePositionInContext(s);
          command->add_child(new NodeSetPositionCommand(s, c, sequence_pos - QPointF(1, 0)));
          command->add_child(new NodeSetPositionCommand(n, c, sequence_pos));

        } else {

          // Removing multicams
          // Locate first multicam that specifically ends up at this clip
          QVector<NodeInput> inputs = c->FindWaysNodeArrivesHere(s);
          for (const NodeInput &i : inputs) {
            if (MultiCamNode *mcn = dynamic_cast<MultiCamNode*>(i.node())) {
              for (auto it=mcn->output_connections().cbegin(); it!=mcn->output_connections().cend(); it++) {
                command->add_child(new NodeEdgeRemoveCommand(it->first, it->second));
                command->add_child(new NodeEdgeAddCommand(s, it->second));
              }

              command->add_child(new NodeRemoveAndDisconnectCommand(mcn));
            }
          }

        }
      }
    }
  }

  Core::instance()->undo_stack()->push(command, e ? tr("Multi-Cam Enabled On %1 Clip(s)").arg(selected_blocks_.size()) : tr("Multi-Cam Disabled On %1 Clip(s)").arg(selected_blocks_.size()));
}

void TimelineWidget::ForceUpdateRubberBand()
{
  if (rubberband_.isVisible()) {
    this->MoveRubberBandSelect(rubberband_enable_selecting_, rubberband_select_links_);
  }
}

void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost)
{
  ghost_items_.append(ghost);

  UpdateViewports(ghost->GetTrack().type());
}

void TimelineWidget::UpdateViewTimebases()
{
  for (int i=0;i<views_.size();i++) {
    TimelineAndTrackView* view = views_.at(i);

    if (GetConnectedNode() && use_audio_time_units_ && i == Track::kAudio) {
      view->view()->SetTimebase(GetConnectedNode()->GetAudioParams().sample_rate_as_time_base());
    } else {
      view->view()->SetTimebase(timebase());
    }
  }
}

void TimelineWidget::NudgeInternal(rational amount)
{
  if (!selected_blocks_.isEmpty()) {
    // Validate
    foreach (Block* b, selected_blocks_) {
      if (b->in() + amount < 0) {
        amount = -b->in();
      }
    }

    if (amount.isNull()) {
      return;
    }

    MultiUndoCommand *command = new MultiUndoCommand();

    foreach (Block* b, selected_blocks_) {
      command->add_child(new TrackReplaceBlockWithGapCommand(b->track(), b, false));
    }

    foreach (Block* b, selected_blocks_) {
      command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(b->track()->type()), b->track()->Index(), b, b->in() + amount));
    }

    // Nudge selections
    TimelineWidgetSelections new_sel = GetSelections();
    new_sel.ShiftTime(amount);
    command->add_child(new TimelineWidget::SetSelectionsCommand(this, new_sel, GetSelections()));

    Core::instance()->undo_stack()->push(command, tr("Nudged Clips"));
  }
}

void TimelineWidget::MoveToPlayheadInternal(bool out)
{
  if (GetConnectedNode() && !selected_blocks_.isEmpty()) {
    MultiUndoCommand *command = new MultiUndoCommand();

    // Remove each block from the graph
    QHash<Track*, rational> earliest_pts;
    foreach (Block *b, selected_blocks_) {
      command->add_child(new TrackReplaceBlockWithGapCommand(b->track(), b, false));

      rational r = earliest_pts.value(b->track(), out ? RATIONAL_MIN : RATIONAL_MAX);
      rational compare = out ? b->out() : b->in();
      if ((compare < r) == !out) {
        earliest_pts.insert(b->track(), compare);
      }
    }

    foreach (Block *b, selected_blocks_) {
      rational shift_amt = GetConnectedNode()->GetPlayhead() - earliest_pts.value(b->track());
      rational new_in = b->in() + shift_amt;
      bool can_shift = true;

      if (new_in < 0) {
        // Handle clips threatening to go below 0
        rational new_out = new_in + b->length();
        if (new_out <= 0) {
          can_shift = false;
        } else {
          command->add_child(new BlockResizeWithMediaInCommand(b, new_out));
          new_in = 0;
        }
      }

      if (can_shift) {
        command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(b->track()->type()), b->track()->Index(), b, new_in));
      }
    }

    // Shift selections
    TimelineWidgetSelections new_sel = GetSelections();
    for (auto it=new_sel.begin(); it!=new_sel.end(); it++) {
      rational track_adj = GetConnectedNode()->GetPlayhead() - earliest_pts.value(GetTrackFromReference(it.key()), GetConnectedNode()->GetPlayhead());
      if (!track_adj.isNull()) {
        it.value().shift(track_adj);
      }
    }
    command->add_child(new SetSelectionsCommand(this, new_sel, GetSelections()));

    Core::instance()->undo_stack()->push(command, tr("Moved Clip(s) To Point"));
  }
}

void TimelineWidget::SetViewBeamCursor(const TimelineCoordinate &coord)
{
  foreach (TimelineAndTrackView* tview, views_) {
    tview->view()->SetBeamCursor(coord);
  }
}

void TimelineWidget::SetViewTransitionOverlay(ClipBlock *out, ClipBlock *in)
{
  foreach (TimelineAndTrackView* tview, views_) {
    tview->view()->SetTransitionOverlay(out, in);
  }
}

void TimelineWidget::SetBlockLinksSelected(ClipBlock* block, bool selected)
{
  foreach (Block* link, block->block_links()) {
    if (selected) {
      AddSelection(link);
    } else {
      RemoveSelection(link);
    }
  }
}

void TimelineWidget::QueueScroll(int value)
{
  // (using a hacky singleShot so the scroll occurs after the scene and its scrollbars have updated)
  deferred_scroll_value_ = value;

  QTimer::singleShot(0, this, &TimelineWidget::DeferredScrollAction);
}

TimelineView *TimelineWidget::GetFirstTimelineView()
{
  return views_.first()->view();
}

rational TimelineWidget::GetTimebaseForTrackType(Track::Type type)
{
  return views_.at(type)->view()->timebase();
}

const QRect& TimelineWidget::GetRubberBandGeometry() const
{
  return rubberband_.geometry();
}

void TimelineWidget::SignalSelectedBlocks(QVector<Block *> input, bool filter)
{
  if (input.isEmpty()) {
    return;
  }

  if (filter) {
    // If filtering, remove all the blocks that are already selected
    for (int i=0; i<input.size(); i++) {
      Block* b = input.at(i);

      if (selected_blocks_.contains(b)) {
        input.removeAt(i);
        i--;
      }
    }
  }

  selected_blocks_.append(input);

  SignalBlockSelectionChange();
}

void TimelineWidget::SignalDeselectedBlocks(const QVector<Block *> &deselected_blocks)
{
  if (deselected_blocks.isEmpty()) {
    return;
  }

  foreach (Block* b, deselected_blocks) {
    selected_blocks_.removeOne(b);
  }

  SignalBlockSelectionChange();
}

void TimelineWidget::SignalDeselectedAllBlocks()
{
  if (!selected_blocks_.isEmpty()) {
    selected_blocks_.clear();
    SignalBlockSelectionChange();
  }
}

QVector<Timeline::EditToInfo> TimelineWidget::GetEditToInfo(const rational& playhead_time,
                                                            Timeline::MovementMode mode)
{
  // Get list of unlocked tracks
  QVector<Track*> tracks = sequence()->GetUnlockedTracks();

  // Create list to cache nearest times and the blocks at this point
  QVector<Timeline::EditToInfo> info_list(tracks.size());

  for (int i=0;i<tracks.size();i++) {
    Timeline::EditToInfo &info = info_list[i];

    Track* track = tracks.at(i);
    info.track = track;

    Block* b;

    // Determine what block is at this time (for "trim in", we want to catch blocks that start at
    // the time, for "trim out" we don't)
    if (mode == Timeline::kTrimIn) {
      b = track->NearestBlockBeforeOrAt(playhead_time);
    } else {
      b = track->NearestBlockBefore(playhead_time);
    }

    // If we have a block here, cache how close it is to the track
    if (b) {
      rational this_track_closest_point;

      if (mode == Timeline::kTrimIn) {
        this_track_closest_point = b->in();
      } else {
        this_track_closest_point = b->out();
      }

      info.nearest_time = this_track_closest_point;
    }

    info.nearest_block = b;
  }

  return info_list;
}

void TimelineWidget::RippleTo(Timeline::MovementMode mode)
{
  if (!GetConnectedNode()) {
    return;
  }

  rational playhead_time = GetConnectedNode()->GetPlayhead();

  QVector<Timeline::EditToInfo> tracks = GetEditToInfo(playhead_time, mode);

  if (tracks.isEmpty()) {
    return;
  }

  // Find each track's nearest point and determine the overall timeline's nearest point
  rational closest_point_to_playhead = (mode == Timeline::kTrimIn) ? RATIONAL_MIN : RATIONAL_MAX;

  foreach (const Timeline::EditToInfo& info, tracks) {
    if (info.nearest_block) {
      if (mode == Timeline::kTrimIn) {
        closest_point_to_playhead = qMax(info.nearest_time, closest_point_to_playhead);
      } else {
        closest_point_to_playhead = qMin(info.nearest_time, closest_point_to_playhead);
      }
    }
  }

  if (closest_point_to_playhead == RATIONAL_MIN || closest_point_to_playhead == RATIONAL_MAX) {
    // Assume no blocks will be acted upon
    return;
  }

  // If we're not inserting gaps and the edit point is right on the nearest in point, we enter a
  // single-frame mode where we remove one frame only
  if (closest_point_to_playhead == playhead_time) {
    if (mode == Timeline::kTrimIn) {
      playhead_time += timebase();
    } else {
      playhead_time -= timebase();
    }
  }

  // For standard rippling, we can cache here the region that will be rippled out
  rational in_ripple = qMin(closest_point_to_playhead, playhead_time);
  rational out_ripple = qMax(closest_point_to_playhead, playhead_time);

  TimelineRippleRemoveAreaCommand* c = new TimelineRippleRemoveAreaCommand(sequence(),
                                                                           in_ripple,
                                                                           out_ripple);

  Core::instance()->undo_stack()->push(c, tr("Rippled Clip(s) To Point"));

  // If we rippled, ump to where new cut is if applicable
  if (mode == Timeline::kTrimIn) {
    GetConnectedNode()->SetPlayhead(closest_point_to_playhead);
  } else if (mode == Timeline::kTrimOut && closest_point_to_playhead == GetConnectedNode()->GetPlayhead()) {
    GetConnectedNode()->SetPlayhead(playhead_time);
  }
}

void TimelineWidget::EditTo(Timeline::MovementMode mode)
{
  const rational playhead_time = GetConnectedNode()->GetPlayhead();

  // Get list of unlocked tracks
  QVector<Timeline::EditToInfo> tracks = GetEditToInfo(playhead_time, mode);

  if (tracks.isEmpty()) {
    return;
  }

  MultiUndoCommand* command = new MultiUndoCommand();

  foreach (const Timeline::EditToInfo& info, tracks) {
    if (info.nearest_block
        && !dynamic_cast<GapBlock*>(info.nearest_block)
        && info.nearest_time != playhead_time) {
      rational new_len;

      if (mode == Timeline::kTrimIn) {
        new_len = playhead_time - info.nearest_time;
      } else {
        new_len = info.nearest_time - playhead_time;
      }
      new_len = info.nearest_block->length() - new_len;

      command->add_child(new BlockTrimCommand(info.track,
                                              info.nearest_block,
                                              new_len,
                                              mode));
    }
  }

  Core::instance()->undo_stack()->push(command, tr("Cut Clip(s) To Point"));
}

void TimelineWidget::UpdateViewports(const Track::Type &type)
{
  if (type == Track::kNone) {
    foreach (TimelineAndTrackView* tview, views_) {
      tview->view()->viewport()->update();
    }
  } else {
    views_.at(type)->view()->viewport()->update();
  }
}

bool TimelineWidget::PasteInternal(bool insert)
{
  if (!GetConnectedNode()) {
    return false;
  }

  ProjectSerializer::Result res = ProjectSerializer::Paste(ProjectSerializer::kOnlyClips, GetConnectedNode()->project());
  if (res.GetLoadData().nodes.isEmpty()) {
    return false;
  }

  MultiUndoCommand *command = new MultiUndoCommand();

  Project *project = GetConnectedNode()->project();
  foreach (Node *n, res.GetLoadData().nodes) {
    command->add_child(new NodeAddCommand(project, n));
    if (n->IsItem() && !n->folder()) {
      command->add_child(new FolderAddChild(project->root(), n));
    }
  }

  for (auto it = res.GetLoadData().promised_connections.cbegin(); it != res.GetLoadData().promised_connections.cend(); it++) {
    auto oc = *it;
    command->add_child(new NodeEdgeAddCommand(oc.first, oc.second));
  }

  rational paste_start = GetConnectedNode()->GetPlayhead();

  if (insert) {
    rational paste_end = paste_start;

    for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) {
      rational length = static_cast<Block*>(it.key())->length();
      rational in = rational::fromString(it.value()[QStringLiteral("in")].toStdString());

      paste_end = qMax(paste_end, paste_start + in + length);
    }

    if (paste_end != paste_start) {
      InsertGapsAt(paste_start, paste_end - paste_start, command);
    }
  }

  for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) {
    Block *block = static_cast<Block*>(it.key());
    rational in = rational::fromString(it.value()[QStringLiteral("in")].toStdString());
    Track::Reference track = Track::Reference::FromString(it.value()[QStringLiteral("track")]);

    command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()),
                                                  track.index(),
                                                  block,
                                                  paste_start + in));
  }

  Core::instance()->undo_stack()->push(command, tr("Pasted %1 Clip(s)").arg(res.GetLoadData().properties.size()));

  return true;
}

TimelineAndTrackView *TimelineWidget::AddTimelineAndTrackView(Qt::Alignment alignment)
{
  TimelineAndTrackView *v = new TimelineAndTrackView(alignment);
  connect(v->track_view(), &TrackView::AboutToDeleteTrack, this, &TimelineWidget::TrackAboutToBeDeleted);
  return v;
}

QHash<Node *, Node *> TimelineWidget::GenerateExistingPasteMap(const ProjectSerializer::Result &r)
{
  QHash<Node *, Node *> m;

  for (Node *n : r.GetLoadData().nodes) {
    for (Block *b : qAsConst(this->selected_blocks_)) {
      for (auto it=b->GetContextPositions().cbegin(); it!=b->GetContextPositions().cend(); it++) {
        if (it.key()->id() == n->id() && !m.contains(it.key())) {
          m.insert(it.key(), n);
          break;
        }
      }
    }
  }

  return m;
}

QByteArray TimelineWidget::SaveSplitterState() const
{
  return view_splitter_->saveState();
}

void TimelineWidget::RestoreSplitterState(const QByteArray &state)
{
  view_splitter_->restoreState(state);
}

void TimelineWidget::StartRubberBandSelect(const QPoint &global_cursor_start)
{
  // Store scene positions for each view
  rubberband_scene_pos_.resize(views_.size());
  for (int i = 0; i < rubberband_scene_pos_.size(); i++) {
    TimelineView *v = views_.at(i)->view();
    rubberband_scene_pos_[i] = v->UnscalePoint(v->mapToScene(v->mapFromGlobal(global_cursor_start)));
  }

  rubberband_.show();

  // We don't touch any blocks that are already selected. If you want these to be deselected by
  // default, call DeselectAll() before calling StartRubberBandSelect()
  rubberband_old_selections_ = selections_;
}

void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_links)
{
  QPoint rubberband_now = QCursor::pos();

  TimelineView *fv = views_.first()->view();
  const QPointF &rubberband_scene_start = rubberband_scene_pos_.at(0);
  QPointF rubberband_now_scaled = fv->UnscalePoint(fv->mapToScene(fv->mapFromGlobal(rubberband_now)));

  QPoint rubberband_local_start = fv->mapTo(this, fv->mapFromScene(fv->ScalePoint(rubberband_scene_start)));
  QPoint rubberband_local_now = fv->mapTo(this, fv->mapFromScene(fv->ScalePoint(rubberband_now_scaled)));

  rubberband_.setGeometry(QRect(rubberband_local_start, rubberband_local_now).normalized());

  rubberband_enable_selecting_ = enable_selecting;
  rubberband_select_links_ = select_links;

  if (!enable_selecting) {
    return;
  }

  // Get current items in rubberband
  QVector<Block*> items_in_rubberband;

  for (int i = 0; i < views_.size(); i++) {
    TimelineView *v = views_.at(i)->view();
    QRectF r = QRectF(v->ScalePoint(rubberband_scene_pos_.at(i)), v->mapToScene(v->mapFromGlobal(rubberband_now))).normalized();
    items_in_rubberband.append(v->GetItemsAtSceneRect(r));
  }

  // Reset selection to whatever it was before
  SetSelections(rubberband_old_selections_, false);

  // Add any blocks in rubberband
  rubberband_now_selected_.clear();

  foreach (Block* b, items_in_rubberband) {
    if (dynamic_cast<GapBlock*>(b)) {
      continue;
    }

    Track* t = b->track();
    if (t->IsLocked()) {
      continue;
    }

    if (!rubberband_now_selected_.contains(b)) {
      AddSelection(b);
      rubberband_now_selected_.append(b);
    }

    ClipBlock *c = dynamic_cast<ClipBlock*>(b);
    if (c && select_links) {
      foreach (Block* link, c->block_links()) {
        if (!rubberband_now_selected_.contains(link)) {
          AddSelection(link);
          rubberband_now_selected_.append(link);
        }
      }
    }
  }
}

void TimelineWidget::EndRubberBandSelect()
{
  rubberband_.hide();

  // Emit any blocks that were newly selected
  SignalSelectedBlocks(rubberband_now_selected_);

  rubberband_now_selected_.clear();
  rubberband_old_selections_.clear();
}

void TimelineWidget::AddSelection(const TimeRange &time, const Track::Reference &track)
{
  selections_[track].insert(time);

  UpdateViewports(track.type());
}

void TimelineWidget::AddSelection(Block *item)
{
  if (item->track()) {
    AddSelection(item->range(), item->track()->ToReference());
  }
}

void TimelineWidget::RemoveSelection(const TimeRange &time, const Track::Reference &track)
{
  selections_[track].remove(time);

  UpdateViewports(track.type());
}

void TimelineWidget::RemoveSelection(Block *item)
{
  if (item->track()) {
    RemoveSelection(item->range(), item->track()->ToReference());
  }
}

void TimelineWidget::SetSelections(const TimelineWidgetSelections &s, bool process_block_changes)
{
  if (selections_ == s) {
    return;
  }

  if (!GetConnectedNode()) {
    return;
  }

  if (process_block_changes) {
    QVector<Block*> deselected;
    QVector<Block*> selected;

    foreach (Block *b, selected_blocks_) {
      if (!s[b->track()->ToReference()].contains(b->range())) {
        deselected.append(b);
      }
    }

    // NOTE: This loop could do with some optimization
    for (auto it=s.cbegin(); it!=s.cend(); it++) {
      Track *track = GetTrackFromReference(it.key());
      if (track) {
        const TimeRangeList &ranges = it.value();

        foreach (Block *b, track->Blocks()) {
          if (!selected_blocks_.contains(b) && ranges.contains(b->range())) {
            selected.append(b);
          }
        }
      }
    }

    SignalDeselectedBlocks(deselected);
    SignalSelectedBlocks(selected);
  }

  selections_ = s;

  UpdateViewports();
}

Block *TimelineWidget::GetItemAtScenePos(const TimelineCoordinate& coord)
{
  return views_.at(coord.GetTrack().type())->view()->GetItemAtScenePos(coord.GetFrame(), coord.GetTrack().index());
}

void TimelineWidget::SetSplitterSizesCommand::redo()
{
  old_sizes_ = splitter_->sizes();
  splitter_->setSizes(new_sizes_);
}

void TimelineWidget::SetSplitterSizesCommand::undo()
{
  splitter_->setSizes(old_sizes_);
}

}
