/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2019 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 "track.h"

#include <QApplication>
#include <QDebug>
#include <QFontMetrics>

#include "node/block/gap/gap.h"
#include "node/graph.h"

OLIVE_NAMESPACE_ENTER

TrackOutput::TrackOutput() :
  track_type_(Timeline::kTrackTypeNone),
  index_(-1),
  locked_(false)
{
  block_input_ = new NodeInputArray("block_in", NodeParam::kAny);
  block_input_->set_is_keyframable(false);
  AddInput(block_input_);
  connect(block_input_, &NodeInputArray::SubParamEdgeAdded, this, &TrackOutput::BlockConnected);
  connect(block_input_, &NodeInputArray::SubParamEdgeRemoved, this, &TrackOutput::BlockDisconnected);
  disconnect(block_input_, &NodeInputArray::SubParamEdgeAdded, this, &TrackOutput::InputConnectionChanged);
  disconnect(block_input_, &NodeInputArray::SubParamEdgeRemoved, this, &TrackOutput::InputConnectionChanged);

  muted_input_ = new NodeInput("muted_in", NodeParam::kBoolean);
  muted_input_->set_is_keyframable(false);
  connect(muted_input_, &NodeInput::ValueChanged, this, &TrackOutput::MutedInputValueChanged);
  AddInput(muted_input_);

  // Set default height
  track_height_ = GetDefaultTrackHeight();
}

void TrackOutput::set_track_type(const Timeline::TrackType &track_type)
{
  track_type_ = track_type;
}

const Timeline::TrackType& TrackOutput::track_type() const
{
  return track_type_;
}

Node *TrackOutput::copy() const
{
  return new TrackOutput();
}

QString TrackOutput::Name() const
{
  return tr("Track");
}

QString TrackOutput::id() const
{
  return QStringLiteral("org.olivevideoeditor.Olive.track");
}

QList<Node::CategoryID> TrackOutput::Category() const
{
  return {kCategoryTimeline};
}

QString TrackOutput::Description() const
{
  return tr("Node for representing and processing a single array of Blocks sorted by time. Also represents the end of "
            "a Sequence.");
}

QString TrackOutput::GetTrackName()
{
  if (track_name_.isEmpty()) {
    return GetDefaultTrackName(track_type_, index_);
  }

  return track_name_;
}

const int &TrackOutput::GetTrackHeight() const
{
  return track_height_;
}

void TrackOutput::SetTrackHeight(const int &height)
{
  track_height_ = height;
  emit TrackHeightChanged(track_height_);
}

void TrackOutput::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data)
{
  if (reader->name() == QStringLiteral("height")) {
    SetTrackHeight(reader->readElementText().toInt());
  } else {
    Node::LoadInternal(reader, xml_node_data);
  }
}

void TrackOutput::SaveInternal(QXmlStreamWriter *writer) const
{
  writer->writeTextElement(QStringLiteral("height"), QString::number(GetTrackHeight()));
}

void TrackOutput::Retranslate()
{
  Node::Retranslate();

  block_input_->set_name(tr("Blocks"));
  muted_input_->set_name(tr("Muted"));
}

const int &TrackOutput::Index()
{
  return index_;
}

void TrackOutput::SetIndex(const int &index)
{
  index_ = index;

  emit IndexChanged(index);
}

Block *TrackOutput::BlockContainingTime(const rational &time) const
{
  foreach (Block* block, block_cache_) {
    if (block->in() < time && block->out() > time) {
      return block;
    } else if (block->out() == time) {
      break;
    }
  }

  return nullptr;
}

Block *TrackOutput::NearestBlockBefore(const rational &time) const
{
  foreach (Block* block, block_cache_) {
    // Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block
    if (block->out() >= time) {
      return block;
    }
  }

  return nullptr;
}

Block *TrackOutput::NearestBlockBeforeOrAt(const rational &time) const
{
  foreach (Block* block, block_cache_) {
    // Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block
    if (block->out() > time) {
      return block;
    }
  }

  return nullptr;
}

Block *TrackOutput::NearestBlockAfterOrAt(const rational &time) const
{
  foreach (Block* block, block_cache_) {
    // Blocks are sorted by time, so the first Block after this time is the correct Block
    if (block->in() >= time) {
      return block;
    }
  }

  return nullptr;
}

Block *TrackOutput::NearestBlockAfter(const rational &time) const
{
  foreach (Block* block, block_cache_) {
    // Blocks are sorted by time, so the first Block after this time is the correct Block
    if (block->in() > time) {
      return block;
    }
  }

  return nullptr;
}

Block *TrackOutput::BlockAtTime(const rational &time) const
{
  if (IsMuted()) {
    return nullptr;
  }

  foreach (Block* block, block_cache_) {
    if (block
        && block->in() <= time
        && block->out() > time) {
      if (block->is_enabled()) {
        return block;
      } else {
        break;
      }
    }
  }

  return nullptr;
}

QList<Block *> TrackOutput::BlocksAtTimeRange(const TimeRange &range) const
{
  QList<Block*> list;

  if (IsMuted()) {
    return list;
  }

  foreach (Block* block, block_cache_) {
    if (block
        && block->is_enabled()
        && block->out() > range.in()
        && block->in() < range.out()) {
      list.append(block);
    }
  }

  return list;
}

const QList<Block *> &TrackOutput::Blocks() const
{
  return block_cache_;
}

void TrackOutput::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source)
{
  TimeRange limited;

  if (block_input_->sub_params().contains(from)
      && from->get_connected_node()
      && from->get_connected_node()->IsBlock()) {
    // Limit the range signal to the corresponding block
    Block* b = static_cast<Block*>(from->get_connected_node());

    if (range.out() <= b->in() || range.in() >= b->out()) {
      return;
    }

    limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out()));
  } else {
    limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length()));
  }

  Node::InvalidateCache(limited, from, source);
}

void TrackOutput::InsertBlockBefore(Block* block, Block* after)
{
  InsertBlockAtIndex(block, block_cache_.indexOf(after));
}

void TrackOutput::InsertBlockAfter(Block *block, Block *before)
{
  int before_index = block_cache_.indexOf(before);

  Q_ASSERT(before_index >= 0);

  if (before_index == block_cache_.size() - 1) {
    AppendBlock(block);
  } else {
    InsertBlockAtIndex(block, before_index + 1);
  }
}

void TrackOutput::PrependBlock(Block *block)
{
  BeginOperation();

  block_input_->Prepend();
  NodeParam::ConnectEdge(block->output(), block_input_->First());

  EndOperation();

  // Everything has shifted at this point
  InvalidateCache(TimeRange(0, track_length()), block_input_, block_input_);
}

void TrackOutput::InsertBlockAtIndex(Block *block, int index)
{
  BeginOperation();

  int insert_index = GetInputIndexFromCacheIndex(index);
  block_input_->InsertAt(insert_index);
  NodeParam::ConnectEdge(block->output(),
                         block_input_->At(insert_index));

  EndOperation();

  InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_);
}

void TrackOutput::AppendBlock(Block *block)
{
  BeginOperation();

  block_input_->Append();
  NodeParam::ConnectEdge(block->output(), block_input_->Last());

  EndOperation();

  // Invalidate area that block was added to
  InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_);
}

void TrackOutput::RippleRemoveBlock(Block *block)
{
  BeginOperation();

  rational remove_in = block->in();

  block_input_->RemoveAt(GetInputIndexFromCacheIndex(block));

  EndOperation();

  InvalidateCache(TimeRange(remove_in, track_length()), block_input_, block_input_);
}

void TrackOutput::ReplaceBlock(Block *old, Block *replace)
{
  BeginOperation();

  int index_of_old_block = GetInputIndexFromCacheIndex(old);

  NodeParam::DisconnectEdge(old->output(),
                            block_input_->At(index_of_old_block));

  NodeParam::ConnectEdge(replace->output(),
                         block_input_->At(index_of_old_block));

  EndOperation();

  if (old->length() == replace->length()) {
    InvalidateCache(TimeRange(replace->in(), replace->out()), block_input_, block_input_);
  } else {
    InvalidateCache(TimeRange(replace->in(), RATIONAL_MAX), block_input_, block_input_);
  }
}

TrackOutput *TrackOutput::TrackFromBlock(const Block *block)
{
  NodeOutput* output = block->output();

  foreach (NodeEdgePtr edge, output->edges()) {
    Node* n = edge->input()->parentNode();

    if (n->IsTrack()) {
      return static_cast<TrackOutput*>(n);
    }
  }

  return nullptr;
}

const rational &TrackOutput::track_length() const
{
  return track_length_;
}

bool TrackOutput::IsTrack() const
{
  return true;
}

int TrackOutput::GetTrackHeightIncrement()
{
  return qApp->fontMetrics().height() / 2;
}

int TrackOutput::GetDefaultTrackHeight()
{
  return qApp->fontMetrics().height() * 3;
}

int TrackOutput::GetTrackHeightMinimum()
{
  return qApp->fontMetrics().height() * 3 / 2;
}

QString TrackOutput::GetDefaultTrackName(Timeline::TrackType type, int index)
{
  // Starts tracks at 1 rather than 0
  int user_friendly_index = index+1;

  switch (type) {
  case Timeline::kTrackTypeVideo: return tr("Video %1").arg(user_friendly_index);
  case Timeline::kTrackTypeAudio: return tr("Audio %1").arg(user_friendly_index);
  case Timeline::kTrackTypeSubtitle: return tr("Subtitle %1").arg(user_friendly_index);
  case Timeline::kTrackTypeNone:
  case Timeline::kTrackTypeCount:
    break;
  }

  return tr("Track %1").arg(user_friendly_index);
}

bool TrackOutput::IsMuted() const
{
  return muted_input_->get_standard_value().toBool();
}

bool TrackOutput::IsLocked() const
{
  return locked_;
}

NodeInputArray *TrackOutput::block_input() const
{
  return block_input_;
}

void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const
{
  Block* b = BlockAtTime(time);

  // Defer to block at this time, don't add any of our own information to the hash
  if (b) {
    b->Hash(hash, time);
  }
}

void TrackOutput::SetTrackName(const QString &name)
{
  track_name_ = name;
}

void TrackOutput::SetMuted(bool e)
{
  muted_input_->set_standard_value(e);
  InvalidateCache(TimeRange(0, track_length()), block_input_, block_input_);
}

void TrackOutput::SetLocked(bool e)
{
  locked_ = e;
}

void TrackOutput::UpdateInOutFrom(int index)
{
  // Find block just before this one to find the last out point
  rational last_out = (index == 0) ? 0 : block_cache_.at(index - 1)->out();

  // Iterate through all blocks updating their in/outs
  for (int i=index; i<block_cache_.size(); i++) {
    Block* b = block_cache_.at(i);

    b->set_in(last_out);

    last_out += b->length();

    b->set_out(last_out);

    emit b->Refreshed();
  }

  // Update track length
  SetLengthInternal(last_out);
}

int TrackOutput::GetInputIndexFromCacheIndex(int cache_index)
{
  return GetInputIndexFromCacheIndex(block_cache_.at(cache_index));
}

int TrackOutput::GetInputIndexFromCacheIndex(Block *block)
{
  for (int i=0; i<block_input_->GetSize(); i++) {
    if (block_input_->At(i)->get_connected_node() == block) {
      return i;
    }
  }

  return -1;
}

void TrackOutput::SetLengthInternal(const rational &r, bool invalidate)
{
  if (r != track_length_) {
    TimeRange invalidate_range(track_length_, r);

    track_length_ = r;
    emit TrackLengthChanged();

    if (invalidate) {
      InvalidateCache(invalidate_range,
                      block_input_,
                      block_input_);
    }
  }
}

void TrackOutput::BlockConnected(NodeEdgePtr edge)
{
  QList<Block*> new_block_list;

  foreach (NodeInput* i, block_input_->sub_params()) {
    Node* connected = i->get_connected_node();

    if (connected
        && connected->IsBlock()
        && !new_block_list.contains(static_cast<Block*>(connected))) {
      Block* b = static_cast<Block*>(connected);

      if (!new_block_list.isEmpty()) {
        new_block_list.last()->set_next(b);
        b->set_previous(new_block_list.last());
      }

      new_block_list.append(b);

      if (!block_cache_.contains(b)) {
        // Make connections to this block
        connect(b, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);

        emit BlockAdded(b);
      }
    }
  }

  if (!new_block_list.isEmpty()) {
    new_block_list.first()->set_previous(nullptr);
    new_block_list.last()->set_next(nullptr);
  }

  int new_index;

  for (new_index = 0; new_index < block_cache_.size(); new_index++) {
    if (block_cache_.at(new_index) != new_block_list.at(new_index)) {
      break;
    }
  }

  block_cache_ = new_block_list;

  UpdateInOutFrom(new_index);

  InputConnectionChanged(edge);

  /*
  // Determine what node was just connected
  Node* connected_node = edge->output()->parentNode();

  // If this node is a block, we can do something with it
  if (connected_node->IsBlock()) {
    Block* connected_block = static_cast<Block*>(connected_node);

    // See where this input falls in our internal "block cache"
    Block* next = nullptr;
    for (int i=block_input_->IndexOfSubParameter(edge->input())+1; i<block_input_->GetSize(); i++) {
      Node* that_node = block_input_->At(i)->get_connected_node();

      // If we find a block, this is the block that will follow the one just connected
      if (that_node && that_node->IsBlock()) {
        next = static_cast<Block*>(that_node);
        break;
      }
    }

    int real_block_index;

    // Either insert or append depending on if we found a "next" block
    if (next) {
      // Insert block before this next block
      real_block_index = block_cache_.indexOf(next);
      block_cache_.insert(real_block_index, connected_block);

      // Update values with next
      next->set_previous(connected_block);
      connected_block->set_next(next);
    } else {
      // No "next", this block must come at the end
      real_block_index = block_cache_.size();
      block_cache_.append(connected_block);

      // Update next value
      connected_block->set_next(nullptr);
    }

    // For all blocks after the block we inserted (including it), update the "previous" and "next"
    // fields as well as the in/out values
    if (real_block_index == 0) {
      connected_block->set_previous(nullptr);
    } else {
      Block* prev = block_cache_.at(real_block_index - 1);

      connected_block->set_previous(prev);
      prev->set_next(connected_block);
    }

    UpdateInOutFrom(real_block_index);

    // Make connections to this block
    connect(connected_block, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);

    emit BlockAdded(connected_block);
  }

  InputConnectionChanged(edge);
  */
}

void TrackOutput::BlockDisconnected(NodeEdgePtr edge)
{
  Block* b = static_cast<Block*>(edge->output()->parentNode());

  if (block_cache_.contains(b)) {
    block_cache_.removeOne(b);

    Block* previous = b->previous();
    Block* next = b->next();

    if (previous) {
      previous->set_next(next);
    }

    if (next) {
      next->set_previous(previous);
    }

    b->set_previous(nullptr);
    b->set_next(nullptr);

    if (next) {
      UpdateInOutFrom(block_cache_.indexOf(next));
    } else if (block_cache_.isEmpty()) {
      SetLengthInternal(rational());
    } else {
      SetLengthInternal(block_cache_.last()->out());
    }

    disconnect(b, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);

    emit BlockRemoved(b);
  }

  /*
  // See what kind of node was just connected
  Node* connected_node = edge->output()->parentNode();

  // If this was a block, we would have put it in our block cache in BlockConnected()
  int index_of_block = block_cache_.indexOf(static_cast<Block*>(connected_node));
  if (index_of_block > -1) {
    Block* connected_block = static_cast<Block*>(connected_node);

    // Determine what index this block was in our cache and remove it
    block_cache_.removeAt(index_of_block);

    // If there were blocks following this one, update their ins/outs
    UpdateInOutFrom(index_of_block);

    // Join the previous and next blocks together
    if (connected_block->previous()) {
      connected_block->previous()->set_next(connected_block->next());
    }

    if (connected_block->next()) {
      connected_block->next()->set_previous(connected_block->previous());
    }

    disconnect(connected_block, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);

    emit BlockRemoved(connected_block);
  }
  */

  InputConnectionChanged(edge);
}

void TrackOutput::BlockLengthChanged()
{
  // Assumes sender is a Block
  Block* b = static_cast<Block*>(sender());

  rational old_out = b->out();

  UpdateInOutFrom(block_cache_.indexOf(b));

  rational new_out = b->out();

  TimeRange invalidate_region(qMin(old_out, new_out), track_length());

  InvalidateCache(invalidate_region, block_input_, block_input_);
}

void TrackOutput::MutedInputValueChanged()
{
  emit MutedChanged(IsMuted());
}

OLIVE_NAMESPACE_EXIT
