// Copyright 2014 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/cast/receiver/video_decoder.h"

#include <stdint.h>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/values.h"
#include "media/base/video_frame_pool.h"
#include "media/base/video_util.h"
#include "media/cast/cast_environment.h"
// VPX_CODEC_DISABLE_COMPAT excludes parts of the libvpx API that provide
// backwards compatibility for legacy applications using the library.
#define VPX_CODEC_DISABLE_COMPAT 1
#include "third_party/libvpx/source/libvpx/vpx/vp8dx.h"
#include "third_party/libvpx/source/libvpx/vpx/vpx_decoder.h"
#include "third_party/libyuv/include/libyuv/convert.h"
#include "ui/gfx/geometry/size.h"

namespace media {
namespace cast {

// Base class that handles the common problem of detecting dropped frames, and
// then invoking the Decode() method implemented by the subclasses to convert
// the encoded payload data into a usable video frame.
class VideoDecoder::ImplBase
    : public base::RefCountedThreadSafe<VideoDecoder::ImplBase> {
 public:
  ImplBase(const scoped_refptr<CastEnvironment>& cast_environment, Codec codec)
      : cast_environment_(cast_environment),
        codec_(codec),
        operational_status_(STATUS_UNINITIALIZED) {}

  OperationalStatus InitializationResult() const {
    return operational_status_;
  }

  void DecodeFrame(std::unique_ptr<EncodedFrame> encoded_frame,
                   const DecodeFrameCallback& callback) {
    DCHECK_EQ(operational_status_, STATUS_INITIALIZED);

    bool is_continuous = true;
    DCHECK(!encoded_frame->frame_id.is_null());
    if (!last_frame_id_.is_null()) {
      if (encoded_frame->frame_id > (last_frame_id_ + 1)) {
        RecoverBecauseFramesWereDropped();
        is_continuous = false;
      }
    }
    last_frame_id_ = encoded_frame->frame_id;

    const scoped_refptr<VideoFrame> decoded_frame = Decode(
        encoded_frame->mutable_bytes(),
        static_cast<int>(encoded_frame->data.size()));
    decoded_frame->set_timestamp(
        encoded_frame->rtp_timestamp.ToTimeDelta(kVideoFrequency));

    std::unique_ptr<FrameEvent> decode_event(new FrameEvent());
    decode_event->timestamp = cast_environment_->Clock()->NowTicks();
    decode_event->type = FRAME_DECODED;
    decode_event->media_type = VIDEO_EVENT;
    decode_event->rtp_timestamp = encoded_frame->rtp_timestamp;
    decode_event->frame_id = encoded_frame->frame_id;
    cast_environment_->logger()->DispatchFrameEvent(std::move(decode_event));

    cast_environment_->PostTask(
        CastEnvironment::MAIN,
        FROM_HERE,
        base::Bind(callback, decoded_frame, is_continuous));
  }

 protected:
  friend class base::RefCountedThreadSafe<ImplBase>;
  virtual ~ImplBase() {}

  virtual void RecoverBecauseFramesWereDropped() {}

  // Note: Implementation of Decode() is allowed to mutate |data|.
  virtual scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) = 0;

  const scoped_refptr<CastEnvironment> cast_environment_;
  const Codec codec_;

  // Subclass' ctor is expected to set this to STATUS_INITIALIZED.
  OperationalStatus operational_status_;

  // Pool of VideoFrames to decode incoming frames into.
  media::VideoFramePool video_frame_pool_;

 private:
  FrameId last_frame_id_;

  DISALLOW_COPY_AND_ASSIGN(ImplBase);
};

class VideoDecoder::Vp8Impl : public VideoDecoder::ImplBase {
 public:
  explicit Vp8Impl(const scoped_refptr<CastEnvironment>& cast_environment)
      : ImplBase(cast_environment, CODEC_VIDEO_VP8) {
    if (ImplBase::operational_status_ != STATUS_UNINITIALIZED)
      return;

    vpx_codec_dec_cfg_t cfg = {0};
    // TODO(miu): Revisit this for typical multi-core desktop use case.  This
    // feels like it should be 4 or 8.
    cfg.threads = 1;

    DCHECK(vpx_codec_get_caps(vpx_codec_vp8_dx()) & VPX_CODEC_CAP_POSTPROC);
    if (vpx_codec_dec_init(&context_,
                           vpx_codec_vp8_dx(),
                           &cfg,
                           VPX_CODEC_USE_POSTPROC) != VPX_CODEC_OK) {
      ImplBase::operational_status_ = STATUS_INVALID_CONFIGURATION;
      return;
    }
    ImplBase::operational_status_ = STATUS_INITIALIZED;
  }

 private:
  ~Vp8Impl() final {
    if (ImplBase::operational_status_ == STATUS_INITIALIZED)
      CHECK_EQ(VPX_CODEC_OK, vpx_codec_destroy(&context_));
  }

  scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) final {
    if (len <= 0 || vpx_codec_decode(&context_,
                                     data,
                                     static_cast<unsigned int>(len),
                                     NULL,
                                     0) != VPX_CODEC_OK) {
      return NULL;
    }

    vpx_codec_iter_t iter = NULL;
    vpx_image_t* const image = vpx_codec_get_frame(&context_, &iter);
    if (!image)
      return NULL;
    if (image->fmt != VPX_IMG_FMT_I420) {
      NOTREACHED() << "Only pixel format supported is I420, got " << image->fmt;
      return NULL;
    }
    DCHECK(vpx_codec_get_frame(&context_, &iter) == NULL)
        << "Should have only decoded exactly one frame.";

    const gfx::Size frame_size(image->d_w, image->d_h);
    // Note: Timestamp for the VideoFrame will be set in VideoReceiver.
    // |decoded_frame| will be returned to |video_frame_pool_| on destruction to
    // be reused.
    const scoped_refptr<VideoFrame> decoded_frame =
        video_frame_pool_.CreateFrame(PIXEL_FORMAT_I420, frame_size,
                                      gfx::Rect(frame_size), frame_size,
                                      base::TimeDelta());
    libyuv::I420Copy(image->planes[VPX_PLANE_Y], image->stride[VPX_PLANE_Y],
                     image->planes[VPX_PLANE_U], image->stride[VPX_PLANE_U],
                     image->planes[VPX_PLANE_V], image->stride[VPX_PLANE_V],
                     decoded_frame->visible_data(media::VideoFrame::kYPlane),
                     decoded_frame->stride(media::VideoFrame::kYPlane),
                     decoded_frame->visible_data(media::VideoFrame::kUPlane),
                     decoded_frame->stride(media::VideoFrame::kUPlane),
                     decoded_frame->visible_data(media::VideoFrame::kVPlane),
                     decoded_frame->stride(media::VideoFrame::kVPlane),
                     frame_size.width(), frame_size.height());
    return decoded_frame;
  }

  // VPX decoder context (i.e., an instantiation).
  vpx_codec_ctx_t context_;

  DISALLOW_COPY_AND_ASSIGN(Vp8Impl);
};

#ifndef OFFICIAL_BUILD
// A fake video decoder that always output 2x2 black frames.
class VideoDecoder::FakeImpl : public VideoDecoder::ImplBase {
 public:
  explicit FakeImpl(const scoped_refptr<CastEnvironment>& cast_environment)
      : ImplBase(cast_environment, CODEC_VIDEO_FAKE),
        last_decoded_id_(-1) {
    if (ImplBase::operational_status_ != STATUS_UNINITIALIZED)
      return;
    ImplBase::operational_status_ = STATUS_INITIALIZED;
  }

 private:
  ~FakeImpl() final {}

  scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) final {
    // Make sure this is a JSON string.
    if (!len || data[0] != '{')
      return NULL;
    base::JSONReader reader;
    std::unique_ptr<base::Value> values(
        reader.Read(base::StringPiece(reinterpret_cast<char*>(data), len)));
    if (!values)
      return NULL;
    base::DictionaryValue* dict = NULL;
    values->GetAsDictionary(&dict);

    bool key = false;
    int id = 0;
    int ref = 0;
    dict->GetBoolean("key", &key);
    dict->GetInteger("id", &id);
    dict->GetInteger("ref", &ref);
    DCHECK(id == last_decoded_id_ + 1);
    last_decoded_id_ = id;
    return media::VideoFrame::CreateBlackFrame(gfx::Size(2, 2));
  }

  int last_decoded_id_;

  DISALLOW_COPY_AND_ASSIGN(FakeImpl);
};
#endif

VideoDecoder::VideoDecoder(
    const scoped_refptr<CastEnvironment>& cast_environment,
    Codec codec)
    : cast_environment_(cast_environment) {
  switch (codec) {
#ifndef OFFICIAL_BUILD
    case CODEC_VIDEO_FAKE:
      impl_ = new FakeImpl(cast_environment);
      break;
#endif
    case CODEC_VIDEO_VP8:
      impl_ = new Vp8Impl(cast_environment);
      break;
    case CODEC_VIDEO_H264:
      // TODO(miu): Need implementation.
      NOTIMPLEMENTED();
      break;
    default:
      NOTREACHED() << "Unknown or unspecified codec.";
      break;
  }
}

VideoDecoder::~VideoDecoder() {}

OperationalStatus VideoDecoder::InitializationResult() const {
  if (impl_.get())
    return impl_->InitializationResult();
  return STATUS_UNSUPPORTED_CODEC;
}

void VideoDecoder::DecodeFrame(std::unique_ptr<EncodedFrame> encoded_frame,
                               const DecodeFrameCallback& callback) {
  DCHECK(encoded_frame.get());
  DCHECK(!callback.is_null());
  if (!impl_.get() || impl_->InitializationResult() != STATUS_INITIALIZED) {
    callback.Run(make_scoped_refptr<VideoFrame>(NULL), false);
    return;
  }
  cast_environment_->PostTask(CastEnvironment::VIDEO,
                              FROM_HERE,
                              base::Bind(&VideoDecoder::ImplBase::DecodeFrame,
                                         impl_,
                                         base::Passed(&encoded_frame),
                                         callback));
}

}  // namespace cast
}  // namespace media
