/***

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

#include "timelineundocommon.h"

namespace olive {

//
// TrackRippleRemoveAreaCommand
//
TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) :
  track_(track),
  range_(range),
  allow_splitting_gaps_(false),
  splice_split_command_(nullptr)
{
  trim_out_.block = nullptr;
  trim_in_.block = nullptr;
}

TrackRippleRemoveAreaCommand::~TrackRippleRemoveAreaCommand()
{
  delete splice_split_command_;
  qDeleteAll(remove_block_commands_);
}

void TrackRippleRemoveAreaCommand::prepare()
{
  // Determine precisely what will be happening to these tracks
  Block* first_block = track_->NearestBlockBeforeOrAt(range_.in());

  if (!first_block) {
    // No blocks at this time, nothing to be done on this track
    return;
  }

  // Determine if this first block is getting trimmed or removed
  bool first_block_is_out_trimmed = first_block->in() < range_.in();
  bool first_block_is_in_trimmed = first_block->out() > range_.out();

  // Set's the block that any insert command should insert AFTER. If the first block is not
  // getting out-trimmed, that means first block is either getting removed or in-trimmed, which
  // means any insert should happen before it
  insert_previous_ = first_block_is_out_trimmed ? first_block : first_block->previous();

  // If it's getting trimmed, determine if it's actually getting spliced
  if (first_block_is_out_trimmed && first_block_is_in_trimmed) {
    if (!allow_splitting_gaps_ && dynamic_cast<GapBlock*>(first_block)) {
      // As a rule, we don't split gaps, so we just treat it as a trim of the range requested
      trim_out_ = {first_block,
                   first_block->length(),
                   first_block->length() - range_.length()};
    } else {
      // This block is getting spliced, so we'll handle that later
      splice_split_command_ = new BlockSplitCommand(first_block, range_.in());
    }
  } else {
    // It's just getting trimmed or removed, so we'll append that operation
    if (first_block_is_out_trimmed) {
      trim_out_ = {first_block,
                   first_block->length(),
                   first_block->length() - (first_block->out() - range_.in())};
    } else if (first_block_is_in_trimmed) {
      // Block is getting in trimmed
      trim_in_ = {first_block,
                 first_block->length(),
                 first_block->length() - (range_.out() - first_block->in())};
    } else {
      // We know for sure this block is within the range so it will be removed
      removals_.append(RemoveOperation({first_block, first_block->previous()}));
    }

    // If the first block is getting in trimmed, we're already at the end of our range
    if (!first_block_is_in_trimmed) {
      // Loop through the rest of the blocks and determine what to do with those
      for (Block* next=first_block->next(); next; next=next->next()) {
        bool trimming = (next->out() > range_.out());

        if (trimming) {
          trim_in_ = {next,
                      next->length(),
                      next->length() - (range_.out() - next->in())};
          break;
        } else {
          removals_.append(RemoveOperation({next, next->previous()}));

          if (next->out() == range_.out()) {
            break;
          }
        }
      }
    }
  }
}

void TrackRippleRemoveAreaCommand::redo()
{
  if (splice_split_command_) {
    // We're just splicing
    splice_split_command_->redo_now();

    // Trim the in of the split
    Block* split = splice_split_command_->new_block();
    split->set_length_and_media_in(split->length() - (range_.out() - split->in()));
  } else {
    if (trim_out_.block) {
      trim_out_.block->set_length_and_media_out(trim_out_.new_length);
    }

    if (trim_in_.block) {
      trim_in_.block->set_length_and_media_in(trim_in_.new_length);
    }

    // Perform removals
    if (!removals_.isEmpty()) {
      foreach (auto op, removals_) {
        // Ripple remove them all first
        track_->RippleRemoveBlock(op.block);
      }

      // Create undo commands for node removals where possible
      if (remove_block_commands_.isEmpty()) {
        foreach (auto op, removals_) {
          if (NodeCanBeRemoved(op.block)) {
            remove_block_commands_.append(CreateRemoveCommand(op.block));
          }
        }
      }

      foreach (UndoCommand* c, remove_block_commands_) {
        c->redo_now();
      }
    }
  }
}

void TrackRippleRemoveAreaCommand::undo()
{
  if (splice_split_command_) {
    splice_split_command_->undo_now();
  } else {
    if (trim_out_.block) {
      trim_out_.block->set_length_and_media_out(trim_out_.old_length);
    }

    if (trim_in_.block) {
      trim_in_.block->set_length_and_media_in(trim_in_.old_length);
    }

    // Un-remove any blocks
    for (int i=remove_block_commands_.size()-1; i>=0; i--) {
      remove_block_commands_.at(i)->undo_now();
    }

    foreach (auto op, removals_) {
      track_->InsertBlockAfter(op.block, op.before);
    }
  }
}

//
// TrackListRippleRemoveAreaCommand
//
void TrackListRippleRemoveAreaCommand::prepare()
{
  foreach (Track* track, list_->GetTracks()) {
    if (track->IsLocked()) {
      continue;
    }

    TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, range_);
    commands_.append(c);
    working_tracks_.append(track);
  }
}

void TrackListRippleRemoveAreaCommand::redo()
{
  foreach (TrackRippleRemoveAreaCommand* c, commands_) {
    c->redo_now();
  }
}

void TrackListRippleRemoveAreaCommand::undo()
{
  foreach (TrackRippleRemoveAreaCommand* c, commands_) {
    c->undo_now();
  }
}

//
// TimelineRippleRemoveAreaCommand
//
TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) :
  timeline_(timeline)
{
  for (int i=0; i<Track::kCount; i++) {
    add_child(new TrackListRippleRemoveAreaCommand(timeline->track_list(static_cast<Track::Type>(i)),
                                                   in,
                                                   out));
  }
}

//
// TrackListRippleToolCommand
//
TrackListRippleToolCommand::TrackListRippleToolCommand(TrackList* track_list,
                           const QHash<Track*, RippleInfo>& info,
                           const rational& ripple_movement,
                           const Timeline::MovementMode& movement_mode) :
  track_list_(track_list),
  info_(info),
  ripple_movement_(ripple_movement),
  movement_mode_(movement_mode)
{
}

void TrackListRippleToolCommand::ripple(bool redo)
{
  if (info_.isEmpty()) {
    return;
  }

  // The following variables are used to determine how much of the cache to invalidate

  // If we can shift, we will shift from the latest out before the ripple to the latest out after,
  // since those sections will be unchanged by this ripple
  rational pre_latest_out = RATIONAL_MIN;
  rational post_latest_out = RATIONAL_MIN;

  // Make timeline changes
  for (auto it=info_.cbegin(); it!=info_.cend(); it++) {
    Track* track = it.key();
    const RippleInfo& info = it.value();
    WorkingData working_data = working_data_.value(track);
    Block* b = info.block;

    // Generate block length
    rational new_block_length;
    rational operation_movement = ripple_movement_;

    if (movement_mode_ == Timeline::kTrimIn) {
      operation_movement = -operation_movement;
    }

    if (!redo) {
      operation_movement = -operation_movement;
    }

    if (b) {
      new_block_length = b->length() + operation_movement;
    }

    rational pre_shift;
    rational post_shift;

    if (info.append_gap) {

      // Rather than rippling the referenced block, we'll insert a gap and ripple with that
      GapBlock* gap = working_data.created_gap;

      if (redo) {
        if (!gap) {
          gap = new GapBlock();
          gap->set_length_and_media_out(qAbs(ripple_movement_));
          working_data.created_gap = gap;
        }

        gap->setParent(track->parent());
        track->InsertBlockBefore(gap, b);

        // As an insertion, we will shift from the gap's in to the gap's out
        pre_shift = gap->in();
        post_shift = gap->out();
        working_data.earliest_point_of_change = gap->in();
      } else {
        // As a removal, we will shift from the gap's out to the gap's in
        pre_shift = gap->out();
        post_shift = gap->in();

        track->RippleRemoveBlock(gap);
        gap->setParent(&memory_manager_);
      }

    } else if ((redo && new_block_length.isNull()) || (!redo && !b->track())) {

      // The ripple is the length of this block. We assume that for this to happen, it must have
      // been a gap that we will now remove.

      if (redo) {
        // The earliest point changes will happen is at the start of this block
        working_data.earliest_point_of_change = b->in();

        // As a removal, we will be shifting from the out point to the in point
        pre_shift = b->out();
        post_shift = b->in();

        // Remove gap from track and from graph
        working_data.removed_gap_after = b->previous();
        track->RippleRemoveBlock(b);
        b->setParent(&memory_manager_);
      } else {
        // Restore gap to graph and track
        b->setParent(track->parent());
        track->InsertBlockAfter(b, working_data.removed_gap_after);

        // The earliest point changes will happen is at the start of this block
        working_data.earliest_point_of_change = b->in();

        // As an insert, we will be shifting from the block's in point to its out point
        pre_shift = b->in();
        post_shift = b->out();
      }

    } else {

      // Store old length
      working_data.old_length = b->length();

      if (movement_mode_ == Timeline::kTrimIn) {
        // The earliest point changes will occur is in point of this bloc
        working_data.earliest_point_of_change = b->in();

        // Undo the trim in inversion we do above, this will still be inverted accurately for
        // undoing where appropriate
        rational inverted = -operation_movement;
        if (inverted > 0) {
          pre_shift = b->in() + inverted;
          post_shift = b->in();
        } else {
          pre_shift = b->in();
          post_shift = b->in() - inverted;
        }

        // Update length
        b->set_length_and_media_in(new_block_length);
      } else {
        // The earliest point changes will occur is the out point if trimming out or the in point
        // if trimming in
        working_data.earliest_point_of_change = b->out();

        // The latest out before the ripple is this block's current out point
        pre_shift = b->out();

        // Update length
        b->set_length_and_media_out(new_block_length);

        // The latest out after the ripple is this block's out point after the length change
        post_shift = b->out();
      }

    }

    working_data_.insert(it.key(), working_data);

    pre_latest_out = qMax(pre_latest_out, pre_shift);
    post_latest_out = qMax(post_latest_out, post_shift);
  }
}

//
// TimelineRippleDeleteGapsAtRegionsCommand
//
void TimelineRippleDeleteGapsAtRegionsCommand::prepare()
{
  int max_gaps = 0;
  QHash<Track*, QVector<RemovalRequest> > requested_gaps;

  // Convert regions to gaps
  for (const QPair<Track*, TimeRange> &region : qAsConst(regions_)) {
    Track *track = region.first;
    const TimeRange &range = region.second;

    GapBlock *gap = dynamic_cast<GapBlock*>(track->NearestBlockBeforeOrAt(range.in()));

    if (gap) {
      QVector<RemovalRequest> &gaps_on_track = requested_gaps[track];

      RemovalRequest this_req = {gap, range};

      // Insertion sort
      bool inserted = false;
      for (int i=0; i<gaps_on_track.size(); i++) {
        if (gaps_on_track.at(i).range.in() < range.in()) {
          gaps_on_track.insert(i, this_req);
          inserted = true;
          break;
        }
      }
      if (!inserted) {
        gaps_on_track.append(this_req);
      }

      max_gaps = qMax(max_gaps, gaps_on_track.size());
    } else {
      qWarning() << "Failed to find corresponding gap to region";
    }
  }

  // For each gap on each track, find a corresponding gap on every other track (which may include
  // a requested gap) to ripple in order to keep everything synchronized
  QHash<GapBlock*, rational> gap_lengths;
  for (int gap_index=0; gap_index<max_gaps; gap_index++) {
    rational earliest_point = RATIONAL_MAX;
    rational ripple_length = RATIONAL_MAX;
    rational latest_point = RATIONAL_MIN;

    foreach (const QVector<RemovalRequest> &gaps_on_track, requested_gaps) {
      if (gap_index < gaps_on_track.size()) {
        const RemovalRequest &gap = gaps_on_track.at(gap_index);
        earliest_point = qMin(earliest_point, gap.range.in());
        ripple_length = qMin(ripple_length, gap.range.length());
        latest_point = qMax(latest_point, gap.range.out());
      }
    }

    // Determine which gaps will be involved in this operation
    QVector<GapBlock*> gaps;

    foreach (Track* track, timeline_->GetTracks()) {
      if (track->IsLocked()) {
        continue;
      }

      const QVector<RemovalRequest> &requested_gaps_on_track = requested_gaps.value(track);
      GapBlock *gap = nullptr;
      if (gap_index < requested_gaps_on_track.size()) {
        // A requested gap was at this index, use it
        gap = requested_gaps_on_track.at(gap_index).gap;
      } else {
        // No requested gap was at this index, find one
        Block *block = track->NearestBlockAfterOrAt(earliest_point);

        if (block) {
          // Found a block, test if it's a gap
          gap = dynamic_cast<GapBlock*>(block);

          if (!gap) {
            if (block->in() == earliest_point) {
              if (block->next()) {
                gap = dynamic_cast<GapBlock*>(block->next());

                if (!gap) {
                  ripple_length = 0;
                }
              }
            } else {
              gap = dynamic_cast<GapBlock*>(block->previous());

              if (!gap) {
                ripple_length = 0;
              }
            }
          }
        } else {
          // Assume track finishes here and track won't be affected by this operation
        }
      }

      if (gap) {
        gaps.append(gap);

        if (!gap_lengths.contains(gap)) {
          gap_lengths.insert(gap, gap->length());
        }

        ripple_length = qMin(ripple_length, gap_lengths.value(gap));
      }

      if (ripple_length == 0) {
        break;
      }
    }

    if (ripple_length > 0) {
      foreach (GapBlock *gap, gaps) {
        if (gap_lengths.value(gap) == ripple_length) {
          commands_.append(new TrackRippleRemoveBlockCommand(gap->track(), gap));
        } else {
          gap_lengths[gap] -= ripple_length;
          commands_.append(new BlockResizeCommand(gap, gap_lengths.value(gap)));
        }
      }
    }
  }
}

void TimelineRippleDeleteGapsAtRegionsCommand::redo()
{
  for (auto it=commands_.cbegin(); it!=commands_.cend(); it++) {
    (*it)->redo_now();
  }
}

void TimelineRippleDeleteGapsAtRegionsCommand::undo()
{
  for (auto it=commands_.crbegin(); it!=commands_.crend(); it++) {
    (*it)->undo_now();
  }
}

}
