// Copyright 2013 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/text_renderer.h"

#include <stddef.h>
#include <utility>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/decoder_buffer.h"
#include "media/base/demuxer.h"
#include "media/base/text_cue.h"

namespace media {

TextRenderer::TextRenderer(
    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
    const AddTextTrackCB& add_text_track_cb)
    : task_runner_(task_runner),
      add_text_track_cb_(add_text_track_cb),
      state_(kUninitialized),
      pending_read_count_(0),
      weak_factory_(this) {}

TextRenderer::~TextRenderer() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  STLDeleteValues(&text_track_state_map_);
  if (!pause_cb_.is_null())
    base::ResetAndReturn(&pause_cb_).Run();
}

void TextRenderer::Initialize(const base::Closure& ended_cb) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(!ended_cb.is_null());
  DCHECK_EQ(kUninitialized, state_)  << "state_ " << state_;
  DCHECK(text_track_state_map_.empty());
  DCHECK_EQ(pending_read_count_, 0);
  DCHECK(pending_eos_set_.empty());
  DCHECK(ended_cb_.is_null());

  ended_cb_ = ended_cb;
  state_ = kPaused;
}

void TextRenderer::StartPlaying() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_EQ(state_, kPaused) << "state_ " << state_;

  for (TextTrackStateMap::iterator itr = text_track_state_map_.begin();
       itr != text_track_state_map_.end(); ++itr) {
    TextTrackState* state = itr->second;
    if (state->read_state == TextTrackState::kReadPending) {
      DCHECK_GT(pending_read_count_, 0);
      continue;
    }

    Read(state, itr->first);
  }

  state_ = kPlaying;
}

void TextRenderer::Pause(const base::Closure& callback) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(state_ == kPlaying || state_ == kEnded) << "state_ " << state_;
  DCHECK_GE(pending_read_count_, 0);

  if (pending_read_count_ == 0) {
    state_ = kPaused;
    task_runner_->PostTask(FROM_HERE, callback);
    return;
  }

  pause_cb_ = callback;
  state_ = kPausePending;
}

void TextRenderer::Flush(const base::Closure& callback) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_EQ(pending_read_count_, 0);
  DCHECK(state_ == kPaused) << "state_ " << state_;

  for (TextTrackStateMap::iterator itr = text_track_state_map_.begin();
       itr != text_track_state_map_.end(); ++itr) {
    pending_eos_set_.insert(itr->first);
    itr->second->text_ranges_.Reset();
  }
  DCHECK_EQ(pending_eos_set_.size(), text_track_state_map_.size());
  task_runner_->PostTask(FROM_HERE, callback);
}

void TextRenderer::AddTextStream(DemuxerStream* text_stream,
                                 const TextTrackConfig& config) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(state_ != kUninitialized) << "state_ " << state_;
  DCHECK(text_track_state_map_.find(text_stream) ==
         text_track_state_map_.end());
  DCHECK(pending_eos_set_.find(text_stream) ==
         pending_eos_set_.end());

  AddTextTrackDoneCB done_cb =
      BindToCurrentLoop(base::Bind(&TextRenderer::OnAddTextTrackDone,
                                   weak_factory_.GetWeakPtr(),
                                   text_stream));

  add_text_track_cb_.Run(config, done_cb);
}

void TextRenderer::RemoveTextStream(DemuxerStream* text_stream) {
  DCHECK(task_runner_->BelongsToCurrentThread());

  TextTrackStateMap::iterator itr = text_track_state_map_.find(text_stream);
  DCHECK(itr != text_track_state_map_.end());

  TextTrackState* state = itr->second;
  DCHECK_EQ(state->read_state, TextTrackState::kReadIdle);
  delete state;
  text_track_state_map_.erase(itr);

  pending_eos_set_.erase(text_stream);
}

bool TextRenderer::HasTracks() const {
  DCHECK(task_runner_->BelongsToCurrentThread());
  return !text_track_state_map_.empty();
}

void TextRenderer::BufferReady(
    DemuxerStream* stream,
    DemuxerStream::Status status,
    const scoped_refptr<DecoderBuffer>& input) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_NE(status, DemuxerStream::kConfigChanged);

  if (status == DemuxerStream::kAborted) {
    DCHECK(!input.get());
    DCHECK_GT(pending_read_count_, 0);
    DCHECK(pending_eos_set_.find(stream) != pending_eos_set_.end());

    TextTrackStateMap::iterator itr = text_track_state_map_.find(stream);
    DCHECK(itr != text_track_state_map_.end());

    TextTrackState* state = itr->second;
    DCHECK_EQ(state->read_state, TextTrackState::kReadPending);

    --pending_read_count_;
    state->read_state = TextTrackState::kReadIdle;

    switch (state_) {
      case kPlaying:
        return;

      case kPausePending:
        if (pending_read_count_ == 0) {
          state_ = kPaused;
          base::ResetAndReturn(&pause_cb_).Run();
        }

        return;

      case kPaused:
      case kUninitialized:
      case kEnded:
        NOTREACHED();
        return;
    }

    NOTREACHED();
    return;
  }

  if (input->end_of_stream()) {
    CueReady(stream, NULL);
    return;
  }

  DCHECK_EQ(status, DemuxerStream::kOk);
  DCHECK_GE(input->side_data_size(), 2u);

  // The side data contains both the cue id and cue settings,
  // each terminated with a NUL.
  const char* id_ptr = reinterpret_cast<const char*>(input->side_data());
  size_t id_len = strlen(id_ptr);
  std::string id(id_ptr, id_len);

  const char* settings_ptr = id_ptr + id_len + 1;
  size_t settings_len = strlen(settings_ptr);
  std::string settings(settings_ptr, settings_len);

  // The cue payload is stored in the data-part of the input buffer.
  std::string text(input->data(), input->data() + input->data_size());

  scoped_refptr<TextCue> text_cue(
      new TextCue(input->timestamp(),
                  input->duration(),
                  id,
                  settings,
                  text));

  CueReady(stream, text_cue);
}

void TextRenderer::CueReady(
    DemuxerStream* text_stream,
    const scoped_refptr<TextCue>& text_cue) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_NE(state_, kUninitialized);
  DCHECK_GT(pending_read_count_, 0);
  DCHECK(pending_eos_set_.find(text_stream) != pending_eos_set_.end());

  TextTrackStateMap::iterator itr = text_track_state_map_.find(text_stream);
  DCHECK(itr != text_track_state_map_.end());

  TextTrackState* state = itr->second;
  DCHECK_EQ(state->read_state, TextTrackState::kReadPending);
  DCHECK(state->text_track);

  --pending_read_count_;
  state->read_state = TextTrackState::kReadIdle;

  switch (state_) {
    case kPlaying: {
      if (text_cue.get())
        break;

      const size_t count = pending_eos_set_.erase(text_stream);
      DCHECK_EQ(count, 1U);

      if (pending_eos_set_.empty()) {
        DCHECK_EQ(pending_read_count_, 0);
        state_ = kEnded;
        task_runner_->PostTask(FROM_HERE, ended_cb_);
        return;
      }

      DCHECK_GT(pending_read_count_, 0);
      return;
    }
    case kPausePending: {
      if (text_cue.get())
        break;

      const size_t count = pending_eos_set_.erase(text_stream);
      DCHECK_EQ(count, 1U);

      if (pending_read_count_ > 0) {
        DCHECK(!pending_eos_set_.empty());
        return;
      }

      state_ = kPaused;
      base::ResetAndReturn(&pause_cb_).Run();

      return;
    }

    case kPaused:
    case kUninitialized:
    case kEnded:
      NOTREACHED();
      return;
  }

  base::TimeDelta start = text_cue->timestamp();

  if (state->text_ranges_.AddCue(start)) {
    base::TimeDelta end = start + text_cue->duration();

    state->text_track->addWebVTTCue(start, end,
                                    text_cue->id(),
                                    text_cue->text(),
                                    text_cue->settings());
  }

  if (state_ == kPlaying) {
    Read(state, text_stream);
    return;
  }

  if (pending_read_count_ == 0) {
      DCHECK_EQ(state_, kPausePending) << "state_ " << state_;
      state_ = kPaused;
      base::ResetAndReturn(&pause_cb_).Run();
  }
}

void TextRenderer::OnAddTextTrackDone(DemuxerStream* text_stream,
                                      std::unique_ptr<TextTrack> text_track) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_NE(state_, kUninitialized);
  DCHECK(text_stream);
  DCHECK(text_track);

  std::unique_ptr<TextTrackState> state(
      new TextTrackState(std::move(text_track)));
  text_track_state_map_[text_stream] = state.release();
  pending_eos_set_.insert(text_stream);

  if (state_ == kPlaying)
    Read(text_track_state_map_[text_stream], text_stream);
}

void TextRenderer::Read(
    TextTrackState* state,
    DemuxerStream* text_stream) {
  DCHECK_NE(state->read_state, TextTrackState::kReadPending);

  state->read_state = TextTrackState::kReadPending;
  ++pending_read_count_;

  text_stream->Read(base::Bind(
      &TextRenderer::BufferReady, weak_factory_.GetWeakPtr(), text_stream));
}

TextRenderer::TextTrackState::TextTrackState(std::unique_ptr<TextTrack> tt)
    : read_state(kReadIdle), text_track(std::move(tt)) {}

TextRenderer::TextTrackState::~TextTrackState() {
}

}  // namespace media
