// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/base/android/media_codec_loop.h"

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/timestamp_constants.h"

namespace media {

// Declaring these as constexpr variables doesn't work in windows -- they
// always are 0.  The exception is FromMicroseconds, which doesn't do any
// conversion.  However, declaring these as constexpr functions seesm to work
// fine everywhere.  We care that this works in windows because our unit tests
// run on non-android platforms.
constexpr base::TimeDelta DecodePollDelay() {
  return base::TimeDelta::FromMilliseconds(10);
}

constexpr base::TimeDelta NoWaitTimeout() {
  return base::TimeDelta::FromMicroseconds(0);
}

constexpr base::TimeDelta IdleTimerTimeout() {
  return base::TimeDelta::FromSeconds(1);
}

MediaCodecLoop::InputData::InputData() {}

MediaCodecLoop::InputData::InputData(const InputData& other)
    : memory(other.memory),
      length(other.length),
      key_id(other.key_id),
      iv(other.iv),
      subsamples(other.subsamples),
      presentation_time(other.presentation_time),
      is_eos(other.is_eos),
      is_encrypted(other.is_encrypted) {}

MediaCodecLoop::InputData::~InputData() {}

MediaCodecLoop::MediaCodecLoop(
    int sdk_int,
    Client* client,
    std::unique_ptr<MediaCodecBridge> media_codec,
    scoped_refptr<base::SingleThreadTaskRunner> task_runner)
    : state_(STATE_READY),
      client_(client),
      media_codec_(std::move(media_codec)),
      pending_input_buf_index_(kInvalidBufferIndex),
      sdk_int_(sdk_int),
      weak_factory_(this) {
  if (task_runner)
    io_timer_.SetTaskRunner(task_runner);
  // TODO(liberato): should this DCHECK?
  if (media_codec_ == nullptr)
    SetState(STATE_ERROR);
}

MediaCodecLoop::~MediaCodecLoop() {
  io_timer_.Stop();
}

void MediaCodecLoop::SetTestTickClock(base::TickClock* test_tick_clock) {
  test_tick_clock_ = test_tick_clock;
}

void MediaCodecLoop::OnKeyAdded() {
  if (state_ == STATE_WAITING_FOR_KEY)
    SetState(STATE_READY);

  DoPendingWork();
}

bool MediaCodecLoop::TryFlush() {
  // We do not clear the input queue here.  It depends on the caller.
  // For decoder reset, then it is appropriate.  Otherwise, the requests might
  // simply be sent to us later, such as on a format change.

  // STATE_DRAINED seems like it allows flush, but it causes test failures.
  // crbug.com/624878
  if (state_ == STATE_ERROR || state_ == STATE_DRAINED)
    return false;

  if (CodecNeedsFlushWorkaround())
    return false;

  // Actually try to flush!
  io_timer_.Stop();

  if (media_codec_->Flush() != MEDIA_CODEC_OK) {
    // TODO(liberato): we might not want to notify the client about this.
    SetState(STATE_ERROR);
    return false;
  }

  SetState(STATE_READY);
  return true;
}

void MediaCodecLoop::DoPendingWork() {
  if (state_ == STATE_ERROR)
    return;

  bool did_work = false, did_input = false, did_output = false;
  do {
    did_input = ProcessOneInputBuffer();
    did_output = ProcessOneOutputBuffer();
    if (did_input || did_output)
      did_work = true;
  } while (did_input || did_output);

  // TODO(liberato): add "start_timer" for AVDA.
  ManageTimer(did_work);
}

bool MediaCodecLoop::ProcessOneInputBuffer() {
  if (state_ != STATE_READY)
    return false;

  // We can only queue a buffer if there is input from the client, or if we
  // tried previously but had to wait for a key.  In the latter case, MediaCodec
  // already has the data.
  if (pending_input_buf_index_ == kInvalidBufferIndex &&
      !client_->IsAnyInputPending()) {
    return false;
  }

  // DequeueInputBuffer() may set STATE_ERROR.
  InputBuffer input_buffer = DequeueInputBuffer();

  if (input_buffer.index == kInvalidBufferIndex)
    return false;

  // EnqueueInputBuffer() may change the state.
  EnqueueInputBuffer(input_buffer);
  return state_ != STATE_ERROR;
}

MediaCodecLoop::InputBuffer MediaCodecLoop::DequeueInputBuffer() {
  DVLOG(2) << __FUNCTION__;

  // Do not dequeue a new input buffer if we failed with MEDIA_CODEC_NO_KEY.
  // That status does not return the input buffer back to the pool of
  // available input buffers. We have to reuse it later when calling
  // MediaCodec's QueueSecureInputBuffer().
  if (pending_input_buf_index_ != kInvalidBufferIndex) {
    InputBuffer result(pending_input_buf_index_, true);
    pending_input_buf_index_ = kInvalidBufferIndex;
    return result;
  }

  int input_buf_index = kInvalidBufferIndex;

  media::MediaCodecStatus status =
      media_codec_->DequeueInputBuffer(NoWaitTimeout(), &input_buf_index);
  switch (status) {
    case media::MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER:
      break;

    case media::MEDIA_CODEC_ERROR:
      DLOG(ERROR) << __FUNCTION__
                  << ": MEDIA_CODEC_ERROR from DequeInputBuffer";
      SetState(STATE_ERROR);
      break;

    case media::MEDIA_CODEC_OK:
      break;

    default:
      NOTREACHED() << "Unknown DequeueInputBuffer status " << status;
      SetState(STATE_ERROR);
      break;
  }

  return InputBuffer(input_buf_index, false);
}

void MediaCodecLoop::EnqueueInputBuffer(const InputBuffer& input_buffer) {
  DCHECK_NE(input_buffer.index, kInvalidBufferIndex);

  InputData input_data;
  if (input_buffer.is_pending) {
    // A pending buffer is already filled with data, no need to copy it again.
    input_data = pending_input_buf_data_;
  } else {
    input_data = client_->ProvideInputData();
  }

  if (input_data.is_eos) {
    media_codec_->QueueEOS(input_buffer.index);
    SetState(STATE_DRAINING);
    client_->OnInputDataQueued(true);
    return;
  }

  media::MediaCodecStatus status = MEDIA_CODEC_OK;

  if (input_data.is_encrypted) {
    // Note that input_data might not have a valid memory ptr if this is a
    // re-send of a buffer that was sent before decryption keys arrived.

    status = media_codec_->QueueSecureInputBuffer(
        input_buffer.index, input_data.memory, input_data.length,
        input_data.key_id, input_data.iv, input_data.subsamples,
        input_data.presentation_time);

  } else {
    status = media_codec_->QueueInputBuffer(
        input_buffer.index, input_data.memory, input_data.length,
        input_data.presentation_time);
  }

  switch (status) {
    case MEDIA_CODEC_ERROR:
      DLOG(ERROR) << __FUNCTION__
                  << ": MEDIA_CODEC_ERROR from QueueInputBuffer";
      client_->OnInputDataQueued(false);
      // Transition to the error state after running the completion cb, to keep
      // it in order if the client chooses to flush its queue.
      SetState(STATE_ERROR);
      break;

    case MEDIA_CODEC_NO_KEY:
      // Do not call the completion cb here.  It will be called when we retry
      // after getting the key.
      pending_input_buf_index_ = input_buffer.index;
      pending_input_buf_data_ = input_data;
      // MediaCodec has a copy of the data already.  When we call again, be sure
      // to send in nullptr for the source.  Note that the client doesn't
      // guarantee that the pointer will remain valid after we return anyway.
      pending_input_buf_data_.memory = nullptr;
      SetState(STATE_WAITING_FOR_KEY);
      // Do not call OnInputDataQueued yet.
      break;

    case MEDIA_CODEC_OK:
      client_->OnInputDataQueued(true);
      break;

    default:
      NOTREACHED() << "Unknown Queue(Secure)InputBuffer status " << status;
      client_->OnInputDataQueued(false);
      SetState(STATE_ERROR);
      break;
  }
}

bool MediaCodecLoop::ProcessOneOutputBuffer() {
  // TODO(liberato): When merging AVDA, we will also have to ask the client if
  // it can accept another output buffer.

  if (state_ == STATE_ERROR)
    return false;

  OutputBuffer out;
  MediaCodecStatus status = media_codec_->DequeueOutputBuffer(
      NoWaitTimeout(), &out.index, &out.offset, &out.size, &out.pts,
      &out.is_eos, &out.is_key_frame);

  bool did_work = false;
  switch (status) {
    case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED:
      // Output buffers are replaced in MediaCodecBridge, nothing to do.
      did_work = true;
      break;

    case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED:
      if (!client_->OnOutputFormatChanged())
        SetState(STATE_ERROR);
      did_work = state_ != STATE_ERROR;
      break;

    case MEDIA_CODEC_OK:
      // We got the decoded frame or EOS.
      if (out.is_eos) {
        // Set state STATE_DRAINED after we have received EOS frame at the
        // output. media_decoder_job.cc says: once output EOS has occurred, we
        // should not be asked to decode again.
        DCHECK_EQ(state_, STATE_DRAINING);
        SetState(STATE_DRAINED);

        DCHECK_NE(out.index, kInvalidBufferIndex);
        DCHECK(media_codec_);

        media_codec_->ReleaseOutputBuffer(out.index, false);

        client_->OnDecodedEos(out);
      } else {
        if (!client_->OnDecodedFrame(out))
          SetState(STATE_ERROR);
      }

      did_work = true;
      break;

    case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER:
      // Nothing to do.
      break;

    case MEDIA_CODEC_ERROR:
      DLOG(ERROR) << __FUNCTION__
                  << ": MEDIA_CODEC_ERROR from DequeueOutputBuffer";
      SetState(STATE_ERROR);
      break;

    default:
      NOTREACHED() << "Unknown DequeueOutputBuffer status " << status;
      SetState(STATE_ERROR);
      break;
  }

  return did_work;
}

void MediaCodecLoop::ManageTimer(bool did_work) {
  bool should_be_running = true;

  // One might also use DefaultTickClock, but then ownership becomes harder.
  base::TimeTicks now = (test_tick_clock_ ? test_tick_clock_->NowTicks()
                                          : base::TimeTicks::Now());
  if (did_work || idle_time_begin_ == base::TimeTicks()) {
    idle_time_begin_ = now;
  } else {
    // Make sure that we have done work recently enough, else stop the timer.
    if (now - idle_time_begin_ > IdleTimerTimeout())
      should_be_running = false;
  }

  if (should_be_running && !io_timer_.IsRunning()) {
    io_timer_.Start(FROM_HERE, DecodePollDelay(), this,
                    &MediaCodecLoop::DoPendingWork);
  } else if (!should_be_running && io_timer_.IsRunning()) {
    io_timer_.Stop();
  }
}

void MediaCodecLoop::SetState(State new_state) {
  const State old_state = state_;
  state_ = new_state;
  if (old_state != new_state && new_state == STATE_ERROR)
    client_->OnCodecLoopError();
}

MediaCodecBridge* MediaCodecLoop::GetCodec() const {
  return media_codec_.get();
}

bool MediaCodecLoop::CodecNeedsFlushWorkaround() const {
  // Return true if and only if Flush() isn't supported / doesn't work.
  // Prior to JellyBean-MR2, flush() had several bugs (b/8125974, b/8347958) so
  // we have to completely destroy and recreate the codec there.
  // TODO(liberato): MediaCodecUtil implements the same function.  We should
  // call that one, except that it doesn't compile outside of android right now.
  return sdk_int_ < 18;
}

// static
const char* MediaCodecLoop::AsString(State state) {
#define RETURN_STRING(x) \
  case x:                \
    return #x;

  switch (state) {
    RETURN_STRING(STATE_READY);
    RETURN_STRING(STATE_WAITING_FOR_KEY);
    RETURN_STRING(STATE_DRAINING);
    RETURN_STRING(STATE_DRAINED);
    RETURN_STRING(STATE_ERROR);
  }
#undef RETURN_STRING

  NOTREACHED() << "Unknown state " << state;
  return nullptr;
}

}  // namespace media
