// 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 "remoting/client/jni/chromoting_jni_runtime.h"

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/memory/singleton.h"
#include "base/stl_util.h"
#include "base/synchronization/waitable_event.h"
#include "google_apis/google_api_keys.h"
#include "jni/JniInterface_jni.h"
#include "media/base/yuv_convert.h"
#include "remoting/base/url_request_context.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::ToJavaByteArray;

namespace {

const int kBytesPerPixel = 4;

}  // namespace

namespace remoting {

bool RegisterJni(JNIEnv* env) {
  return remoting::RegisterNativesImpl(env);
}

// Implementation of stubs defined in JniInterface_jni.h. These are the entry
// points for JNI calls from Java into C++.

static void LoadNative(JNIEnv* env, jclass clazz, jobject context) {
  base::android::ScopedJavaLocalRef<jobject> context_activity(env, context);
  base::android::InitApplicationContext(env, context_activity);

  // The google_apis functions check the command-line arguments to make sure no
  // runtime API keys have been specified by the environment. Unfortunately, we
  // neither launch Chromium nor have a command line, so we need to prevent
  // them from DCHECKing out when they go looking.
  CommandLine::Init(0, NULL);

  // Create the singleton now so that the Chromoting threads will be set up.
  remoting::ChromotingJniRuntime::GetInstance();
}

static jstring GetApiKey(JNIEnv* env, jclass clazz) {
  return env->NewStringUTF(google_apis::GetAPIKey().c_str());
}

static jstring GetClientId(JNIEnv* env, jclass clazz) {
  return env->NewStringUTF(
      google_apis::GetOAuth2ClientID(google_apis::CLIENT_REMOTING).c_str());
}

static jstring GetClientSecret(JNIEnv* env, jclass clazz) {
  return env->NewStringUTF(
      google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_REMOTING).c_str());
}

static void Connect(JNIEnv* env,
                    jclass clazz,
                    jstring username,
                    jstring authToken,
                    jstring hostJid,
                    jstring hostId,
                    jstring hostPubkey,
                    jstring pairId,
                    jstring pairSecret) {
  remoting::ChromotingJniRuntime::GetInstance()->ConnectToHost(
      ConvertJavaStringToUTF8(env, username).c_str(),
      ConvertJavaStringToUTF8(env, authToken).c_str(),
      ConvertJavaStringToUTF8(env, hostJid).c_str(),
      ConvertJavaStringToUTF8(env, hostId).c_str(),
      ConvertJavaStringToUTF8(env, hostPubkey).c_str(),
      ConvertJavaStringToUTF8(env, pairId).c_str(),
      ConvertJavaStringToUTF8(env, pairSecret).c_str());
}

static void Disconnect(JNIEnv* env, jclass clazz) {
  remoting::ChromotingJniRuntime::GetInstance()->DisconnectFromHost();
}

static void AuthenticationResponse(JNIEnv* env,
                                   jclass clazz,
                                   jstring pin,
                                   jboolean createPair) {
  remoting::ChromotingJniRuntime::GetInstance()->session()->ProvideSecret(
      ConvertJavaStringToUTF8(env, pin).c_str(), createPair);
}

static void ScheduleRedraw(JNIEnv* env, jclass clazz) {
  remoting::ChromotingJniRuntime::GetInstance()->session()->RedrawDesktop();
}

static void MouseAction(JNIEnv* env,
                        jclass clazz,
                        jint x,
                        jint y,
                        jint whichButton,
                        jboolean buttonDown) {
  // Button must be within the bounds of the MouseEvent_MouseButton enum.
  DCHECK(whichButton >= 0 && whichButton < 5);

  remoting::ChromotingJniRuntime::GetInstance()->session()->PerformMouseAction(
      x,
      y,
      static_cast<remoting::protocol::MouseEvent_MouseButton>(whichButton),
      buttonDown);
}

static void MouseWheelDeltaAction(JNIEnv* env,
                                  jclass clazz,
                                  jint delta_x,
                                  jint delta_y) {
  remoting::ChromotingJniRuntime::GetInstance()
      ->session()
      ->PerformMouseWheelDeltaAction(delta_x, delta_y);
}

static void KeyboardAction(JNIEnv* env,
                           jclass clazz,
                           jint keyCode,
                           jboolean keyDown) {
  remoting::ChromotingJniRuntime::GetInstance()
      ->session()
      ->PerformKeyboardAction(keyCode, keyDown);
}

// ChromotingJniRuntime implementation.

// static
ChromotingJniRuntime* ChromotingJniRuntime::GetInstance() {
  return Singleton<ChromotingJniRuntime>::get();
}

ChromotingJniRuntime::ChromotingJniRuntime() {
  at_exit_manager_.reset(new base::AtExitManager());

  // On Android, the UI thread is managed by Java, so we need to attach and
  // start a special type of message loop to allow Chromium code to run tasks.
  ui_loop_.reset(new base::MessageLoopForUI());
  ui_loop_->Start();

  // TODO(solb) Stop pretending to control the managed UI thread's lifetime.
  ui_task_runner_ = new AutoThreadTaskRunner(ui_loop_->message_loop_proxy(),
                                             base::MessageLoop::QuitClosure());
  network_task_runner_ = AutoThread::CreateWithType("native_net",
                                                    ui_task_runner_,
                                                    base::MessageLoop::TYPE_IO);
  display_task_runner_ = AutoThread::Create("native_disp",
                                            ui_task_runner_);

  url_requester_ = new URLRequestContextGetter(network_task_runner_);

  // Allows later decoding of video frames.
  media::InitializeCPUSpecificYUVConversions();
}

ChromotingJniRuntime::~ChromotingJniRuntime() {
  // The singleton should only ever be destroyed on the main thread.
  DCHECK(ui_task_runner_->BelongsToCurrentThread());

  // The session must be shut down first, since it depends on our other
  // components' still being alive.
  DisconnectFromHost();

  base::WaitableEvent done_event(false, false);
  network_task_runner_->PostTask(FROM_HERE, base::Bind(
      &ChromotingJniRuntime::DetachFromVmAndSignal,
      base::Unretained(this),
      &done_event));
  done_event.Wait();
  display_task_runner_->PostTask(FROM_HERE, base::Bind(
      &ChromotingJniRuntime::DetachFromVmAndSignal,
      base::Unretained(this),
      &done_event));
  done_event.Wait();
  base::android::DetachFromVM();
}

void ChromotingJniRuntime::ConnectToHost(const char* username,
                                  const char* auth_token,
                                  const char* host_jid,
                                  const char* host_id,
                                  const char* host_pubkey,
                                  const char* pairing_id,
                                  const char* pairing_secret) {
  DCHECK(ui_task_runner_->BelongsToCurrentThread());
  DCHECK(!session_);
  session_ = new ChromotingJniInstance(this,
                                       username,
                                       auth_token,
                                       host_jid,
                                       host_id,
                                       host_pubkey,
                                       pairing_id,
                                       pairing_secret);
}

void ChromotingJniRuntime::DisconnectFromHost() {
  DCHECK(ui_task_runner_->BelongsToCurrentThread());
  if (session_) {
    session_->Cleanup();
    session_ = NULL;
  }
}

void ChromotingJniRuntime::ReportConnectionStatus(
    protocol::ConnectionToHost::State state,
    protocol::ErrorCode error) {
  DCHECK(ui_task_runner_->BelongsToCurrentThread());

  JNIEnv* env = base::android::AttachCurrentThread();
  Java_JniInterface_reportConnectionStatus(env, state, error);
}

void ChromotingJniRuntime::DisplayAuthenticationPrompt(bool pairing_supported) {
  DCHECK(ui_task_runner_->BelongsToCurrentThread());

  JNIEnv* env = base::android::AttachCurrentThread();
  Java_JniInterface_displayAuthenticationPrompt(env, pairing_supported);
}

void ChromotingJniRuntime::CommitPairingCredentials(const std::string& host,
                                                    const std::string& id,
                                                    const std::string& secret) {
  DCHECK(ui_task_runner_->BelongsToCurrentThread());

  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jstring> j_host = ConvertUTF8ToJavaString(env, host);
  ScopedJavaLocalRef<jbyteArray> j_id = ToJavaByteArray(
      env, reinterpret_cast<const uint8*>(id.data()), id.size());
  ScopedJavaLocalRef<jbyteArray> j_secret = ToJavaByteArray(
      env, reinterpret_cast<const uint8*>(secret.data()), secret.size());

  Java_JniInterface_commitPairingCredentials(
      env, j_host.obj(), j_id.obj(), j_secret.obj());
}

base::android::ScopedJavaLocalRef<jobject> ChromotingJniRuntime::NewBitmap(
    webrtc::DesktopSize size) {
  JNIEnv* env = base::android::AttachCurrentThread();
  return Java_JniInterface_newBitmap(env, size.width(), size.height());
}

void ChromotingJniRuntime::UpdateFrameBitmap(jobject bitmap) {
  DCHECK(display_task_runner_->BelongsToCurrentThread());

  JNIEnv* env = base::android::AttachCurrentThread();
  Java_JniInterface_setVideoFrame(env, bitmap);
}

void ChromotingJniRuntime::UpdateCursorShape(
    const protocol::CursorShapeInfo& cursor_shape) {
  DCHECK(display_task_runner_->BelongsToCurrentThread());

  // const_cast<> is safe as long as the Java updateCursorShape() method copies
  // the data out of the buffer without mutating it, and doesn't keep any
  // reference to the buffer afterwards. Unfortunately, there seems to be no way
  // to create a read-only ByteBuffer from a pointer-to-const.
  char* data = string_as_array(const_cast<std::string*>(&cursor_shape.data()));
  int cursor_total_bytes =
      cursor_shape.width() * cursor_shape.height() * kBytesPerPixel;

  JNIEnv* env = base::android::AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> buffer(env,
      env->NewDirectByteBuffer(data, cursor_total_bytes));
  Java_JniInterface_updateCursorShape(env,
                                      cursor_shape.width(),
                                      cursor_shape.height(),
                                      cursor_shape.hotspot_x(),
                                      cursor_shape.hotspot_y(),
                                      buffer.obj());
}

void ChromotingJniRuntime::RedrawCanvas() {
  DCHECK(display_task_runner_->BelongsToCurrentThread());

  JNIEnv* env = base::android::AttachCurrentThread();
  Java_JniInterface_redrawGraphicsInternal(env);
}

void ChromotingJniRuntime::DetachFromVmAndSignal(base::WaitableEvent* waiter) {
  base::android::DetachFromVM();
  waiter->Signal();
}

}  // namespace remoting