// 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/blink/webmediaplayer_cast_android.h"

#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/client/gles2_interface.h"
#include "gpu/command_buffer/common/sync_token.h"
#include "media/base/android/media_common_android.h"
#include "media/base/bind_to_current_loop.h"
#include "media/blink/webmediaplayer_impl.h"
#include "media/blink/webmediaplayer_params.h"
#include "third_party/WebKit/public/platform/WebMediaPlayerClient.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkTypeface.h"

using gpu::gles2::GLES2Interface;

namespace media {

namespace {
// File-static function is to allow it to run even after WMPI is deleted.
void OnReleaseTexture(
    const base::Callback<gpu::gles2::GLES2Interface*()>& context_3d_cb,
    GLuint texture_id,
    const gpu::SyncToken& sync_token) {
  GLES2Interface* gl = context_3d_cb.Run();
  if (!gl)
    return;

  gl->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
  gl->DeleteTextures(1, &texture_id);
  // Flush to ensure that the texture gets deleted in a timely fashion.
  gl->ShallowFlushCHROMIUM();
}

GLES2Interface* GLCBShim(
    const WebMediaPlayerParams::Context3DCB& context_3d_cb) {
  return context_3d_cb.Run().gl;
}

}  // namespace

scoped_refptr<VideoFrame> MakeTextFrameForCast(
    const std::string& remote_playback_message,
    gfx::Size canvas_size,
    gfx::Size natural_size,
    const base::Callback<gpu::gles2::GLES2Interface*()>& context_3d_cb) {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(canvas_size.width(), canvas_size.height());

  // Create the canvas and draw the "Casting to <Chromecast>" text on it.
  SkCanvas canvas(bitmap);
  canvas.drawColor(SK_ColorBLACK);

  const SkScalar kTextSize(40);
  const SkScalar kMinPadding(40);

  SkPaint paint;
  paint.setAntiAlias(true);
  paint.setFilterQuality(kHigh_SkFilterQuality);
  paint.setColor(SK_ColorWHITE);
  paint.setTypeface(SkTypeface::MakeFromName(
      "sans", SkFontStyle::FromOldStyle(SkTypeface::kBold)));
  paint.setTextSize(kTextSize);

  // Calculate the vertical margin from the top
  SkPaint::FontMetrics font_metrics;
  paint.getFontMetrics(&font_metrics);
  SkScalar sk_vertical_margin = kMinPadding - font_metrics.fAscent;

  // Measure the width of the entire text to display
  size_t display_text_width = paint.measureText(remote_playback_message.c_str(),
                                                remote_playback_message.size());
  std::string display_text(remote_playback_message);

  if (display_text_width + (kMinPadding * 2) > canvas_size.width()) {
    // The text is too long to fit in one line, truncate it and append ellipsis
    // to the end.

    // First, figure out how much of the canvas the '...' will take up.
    const std::string kTruncationEllipsis("\xE2\x80\xA6");
    SkScalar sk_ellipse_width = paint.measureText(kTruncationEllipsis.c_str(),
                                                  kTruncationEllipsis.size());

    // Then calculate how much of the text can be drawn with the '...' appended
    // to the end of the string.
    SkScalar sk_max_original_text_width(canvas_size.width() -
                                        (kMinPadding * 2) - sk_ellipse_width);
    size_t sk_max_original_text_length = paint.breakText(
        remote_playback_message.c_str(), remote_playback_message.size(),
        sk_max_original_text_width);

    // Remove the part of the string that doesn't fit and append '...'.
    display_text.erase(
        sk_max_original_text_length,
        remote_playback_message.size() - sk_max_original_text_length);
    display_text.append(kTruncationEllipsis);
    display_text_width =
        paint.measureText(display_text.c_str(), display_text.size());
  }

  // Center the text horizontally.
  SkScalar sk_horizontal_margin =
      (canvas_size.width() - display_text_width) / 2.0;
  canvas.drawText(display_text.c_str(), display_text.size(),
                  sk_horizontal_margin, sk_vertical_margin, paint);

  GLES2Interface* gl = context_3d_cb.Run();

  // GPU Process crashed.
  if (!gl)
    return nullptr;
  GLuint remote_playback_texture_id = 0;
  gl->GenTextures(1, &remote_playback_texture_id);
  GLuint texture_target = GL_TEXTURE_2D;
  gl->BindTexture(texture_target, remote_playback_texture_id);
  gl->TexParameteri(texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  gl->TexParameteri(texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  gl->TexParameteri(texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  gl->TexParameteri(texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  {
    SkAutoLockPixels lock(bitmap);
    gl->TexImage2D(texture_target, 0 /* level */, GL_RGBA /* internalformat */,
                   bitmap.width(), bitmap.height(), 0 /* border */,
                   GL_RGBA /* format */, GL_UNSIGNED_BYTE /* type */,
                   bitmap.getPixels());
  }

  gpu::Mailbox texture_mailbox;
  gl->GenMailboxCHROMIUM(texture_mailbox.name);
  gl->ProduceTextureCHROMIUM(texture_target, texture_mailbox.name);
  const GLuint64 fence_sync = gl->InsertFenceSyncCHROMIUM();
  gl->Flush();

  gpu::SyncToken texture_mailbox_sync_token;
  gl->GenUnverifiedSyncTokenCHROMIUM(fence_sync,
                                     texture_mailbox_sync_token.GetData());

  gpu::MailboxHolder holders[media::VideoFrame::kMaxPlanes] = {
      gpu::MailboxHolder(texture_mailbox, texture_mailbox_sync_token,
                         texture_target)};
  return VideoFrame::WrapNativeTextures(
      media::PIXEL_FORMAT_ARGB, holders,
      media::BindToCurrentLoop(base::Bind(&OnReleaseTexture, context_3d_cb,
                                          remote_playback_texture_id)),
      canvas_size /* coded_size */, gfx::Rect(canvas_size) /* visible_rect */,
      natural_size /* natural_size */, base::TimeDelta() /* timestamp */);
}

WebMediaPlayerCast::WebMediaPlayerCast(
    WebMediaPlayerImpl* impl,
    blink::WebMediaPlayerClient* client,
    const WebMediaPlayerParams::Context3DCB& context_3d_cb)
    : webmediaplayer_(impl), client_(client), context_3d_cb_(context_3d_cb) {}

WebMediaPlayerCast::~WebMediaPlayerCast() {
  if (player_manager_) {
    if (is_player_initialized_)
      player_manager_->DestroyPlayer(player_id_);

    player_manager_->UnregisterMediaPlayer(player_id_);
  }
}

void WebMediaPlayerCast::Initialize(const GURL& url,
                                    blink::WebLocalFrame* frame,
                                    int delegate_id) {
  player_manager_->Initialize(MEDIA_PLAYER_TYPE_REMOTE_ONLY, player_id_, url,
                              frame->document().firstPartyForCookies(),
                              frame->document().url(), true, delegate_id);
  is_player_initialized_ = true;
}

void WebMediaPlayerCast::SetMediaPlayerManager(
    RendererMediaPlayerManagerInterface* media_player_manager) {
  player_manager_ = media_player_manager;
  player_id_ = player_manager_->RegisterMediaPlayer(this);
}

void WebMediaPlayerCast::requestRemotePlayback() {
  player_manager_->Seek(player_id_, base::TimeDelta::FromSecondsD(
                                        webmediaplayer_->currentTime()));
  player_manager_->RequestRemotePlayback(player_id_);
}

void WebMediaPlayerCast::requestRemotePlaybackControl() {
  player_manager_->RequestRemotePlaybackControl(player_id_);
}

void WebMediaPlayerCast::requestRemotePlaybackStop() {
  player_manager_->RequestRemotePlaybackStop(player_id_);
}

void WebMediaPlayerCast::OnMediaMetadataChanged(base::TimeDelta duration,
                                                int width,
                                                int height,
                                                bool success) {
  duration_ = duration;
}

void WebMediaPlayerCast::OnPlaybackComplete() {
  DVLOG(1) << __FUNCTION__;
  webmediaplayer_->OnRemotePlaybackEnded();
}

void WebMediaPlayerCast::OnBufferingUpdate(int percentage) {
  DVLOG(1) << __FUNCTION__;
}

void WebMediaPlayerCast::OnSeekRequest(const base::TimeDelta& time_to_seek) {
  DVLOG(1) << __FUNCTION__;
  client_->requestSeek(time_to_seek.InSecondsF());
}

void WebMediaPlayerCast::OnSeekComplete(const base::TimeDelta& current_time) {
  DVLOG(1) << __FUNCTION__;
  remote_time_at_ = base::TimeTicks::Now();
  remote_time_ = current_time;
  webmediaplayer_->OnPipelineSeeked(true);
}

void WebMediaPlayerCast::OnMediaError(int error_type) {
  DVLOG(1) << __FUNCTION__;
}

void WebMediaPlayerCast::OnVideoSizeChanged(int width, int height) {
  DVLOG(1) << __FUNCTION__;
}

void WebMediaPlayerCast::OnTimeUpdate(base::TimeDelta current_timestamp,
                                      base::TimeTicks current_time_ticks) {
  DVLOG(1) << __FUNCTION__ << " " << current_timestamp.InSecondsF();
  remote_time_at_ = current_time_ticks;
  remote_time_ = current_timestamp;
}

void WebMediaPlayerCast::OnPlayerReleased() {
  DVLOG(1) << __FUNCTION__;
}

void WebMediaPlayerCast::OnConnectedToRemoteDevice(
    const std::string& remote_playback_message) {
  DVLOG(1) << __FUNCTION__;
  remote_time_ = base::TimeDelta::FromSecondsD(webmediaplayer_->currentTime());
  is_remote_ = true;
  initializing_ = true;
  paused_ = false;
  client_->playbackStateChanged();

  remote_playback_message_ = remote_playback_message;
  webmediaplayer_->SuspendForRemote();
  client_->connectedToRemoteDevice();
}

double WebMediaPlayerCast::currentTime() const {
  base::TimeDelta ret = remote_time_;
  if (!paused_ && !initializing_) {
    ret += base::TimeTicks::Now() - remote_time_at_;
  }
  return ret.InSecondsF();
}

void WebMediaPlayerCast::play() {
  if (!paused_)
    return;

  player_manager_->Start(player_id_);
  remote_time_at_ = base::TimeTicks::Now();
  paused_ = false;
}

void WebMediaPlayerCast::pause() {
  player_manager_->Pause(player_id_, true);
}

void WebMediaPlayerCast::seek(base::TimeDelta t) {
  should_notify_time_changed_ = true;
  player_manager_->Seek(player_id_, t);
}

void WebMediaPlayerCast::OnDisconnectedFromRemoteDevice() {
  DVLOG(1) << __FUNCTION__;
  if (!paused_) {
    paused_ = true;
  }
  is_remote_ = false;
  double t = currentTime();
  if (t + media::kTimeUpdateInterval * 2 / 1000 > webmediaplayer_->duration()) {
    t = webmediaplayer_->duration();
  }
  webmediaplayer_->OnDisconnectedFromRemoteDevice(t);
}

void WebMediaPlayerCast::OnCancelledRemotePlaybackRequest() {
  DVLOG(1) << __FUNCTION__;
  client_->cancelledRemotePlaybackRequest();
}

void WebMediaPlayerCast::OnRemotePlaybackStarted() {
  client_->remotePlaybackStarted();
}

void WebMediaPlayerCast::OnDidExitFullscreen() {
  DVLOG(1) << __FUNCTION__;
}

void WebMediaPlayerCast::OnMediaPlayerPlay() {
  DVLOG(1) << __FUNCTION__ << " is_remote_ = " << is_remote_;
  initializing_ = false;
  if (is_remote_ && paused_) {
    paused_ = false;
    remote_time_at_ = base::TimeTicks::Now();
    client_->playbackStateChanged();
  }
  // Blink expects a timeChanged() in response to a seek().
  if (should_notify_time_changed_)
    client_->timeChanged();
}

void WebMediaPlayerCast::OnMediaPlayerPause() {
  DVLOG(1) << __FUNCTION__ << " is_remote_ = " << is_remote_;
  if (is_remote_ && !paused_) {
    paused_ = true;
    client_->playbackStateChanged();
  }
}

void WebMediaPlayerCast::OnRemoteRouteAvailabilityChanged(
    blink::WebRemotePlaybackAvailability availability) {
  DVLOG(1) << __FUNCTION__;
  client_->remoteRouteAvailabilityChanged(availability);
}

void WebMediaPlayerCast::SuspendAndReleaseResources() {}

bool WebMediaPlayerCast::hasVideo() const {
  return true;
}

bool WebMediaPlayerCast::paused() const {
  return paused_;
}

void WebMediaPlayerCast::SetDeviceScaleFactor(float scale_factor) {
  device_scale_factor_ = scale_factor;
}

scoped_refptr<VideoFrame> WebMediaPlayerCast::GetCastingBanner() {
  DVLOG(1) << __FUNCTION__;

  // TODO(johnme): Should redraw this frame if the layer bounds change; but
  // there seems no easy way to listen for the layer resizing (as opposed to
  // OnVideoSizeChanged, which is when the frame sizes of the video file
  // change). Perhaps have to poll (on main thread of course)?
  gfx::Size video_size_css_px = webmediaplayer_->GetCanvasSize();
  if (!video_size_css_px.width())
    return nullptr;

  // canvas_size will be the size in device pixels when pageScaleFactor == 1
  gfx::Size canvas_size(
      static_cast<int>(video_size_css_px.width() * device_scale_factor_),
      static_cast<int>(video_size_css_px.height() * device_scale_factor_));

  if (!canvas_size.width())
    return nullptr;

  return MakeTextFrameForCast(remote_playback_message_, canvas_size,
                              webmediaplayer_->naturalSize(),
                              base::Bind(&GLCBShim, context_3d_cb_));
}

void WebMediaPlayerCast::setPoster(const blink::WebURL& poster) {
  player_manager_->SetPoster(player_id_, poster);
}

}  // namespace media
