// Copyright 2015 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 "content/browser/media/android/media_session.h"

#include "base/android/jni_android.h"
#include "content/browser/media/android/media_session_observer.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "jni/MediaSession_jni.h"

namespace content {

DEFINE_WEB_CONTENTS_USER_DATA_KEY(MediaSession);

MediaSession::PlayerIdentifier::PlayerIdentifier(MediaSessionObserver* observer,
                                                 int player_id)
    : observer(observer),
      player_id(player_id) {
}

bool MediaSession::PlayerIdentifier::operator==(
    const PlayerIdentifier& other) const {
  return this->observer == other.observer && this->player_id == other.player_id;
}

size_t MediaSession::PlayerIdentifier::Hash::operator()(
    const PlayerIdentifier& player_identifier) const {
  size_t hash = BASE_HASH_NAMESPACE::hash<MediaSessionObserver*>()(
      player_identifier.observer);
  hash += BASE_HASH_NAMESPACE::hash<int>()(player_identifier.player_id);
  return hash;
}

// static
bool content::MediaSession::RegisterMediaSession(JNIEnv* env) {
  return RegisterNativesImpl(env);
}

// static
MediaSession* MediaSession::Get(WebContents* web_contents) {
  MediaSession* session = FromWebContents(web_contents);
  if (!session) {
    CreateForWebContents(web_contents);
    session = FromWebContents(web_contents);
    session->Initialize();
  }
  return session;
}

MediaSession::~MediaSession() {
  DCHECK(players_.empty());
  DCHECK(audio_focus_state_ == State::INACTIVE);
}

bool MediaSession::AddPlayer(MediaSessionObserver* observer,
                             int player_id,
                             Type type) {
  // If the audio focus is already granted and is of type Content, there is
  // nothing to do. If it is granted of type Transient the requested type is
  // also transient, there is also nothing to do. Otherwise, the session needs
  // to request audio focus again.
  if (audio_focus_state_ == State::ACTIVE &&
      (audio_focus_type_ == Type::Content || audio_focus_type_ == type)) {
    players_.insert(PlayerIdentifier(observer, player_id));
    return true;
  }

  State old_audio_focus_state = audio_focus_state_;
  audio_focus_state_ = RequestSystemAudioFocus(type) ? State::ACTIVE
                                                     : State::INACTIVE;
  audio_focus_type_ = type;

  if (audio_focus_state_ != State::ACTIVE)
    return false;

  // The session should be reset if a player is starting while all players are
  // suspended.
  if (old_audio_focus_state != State::ACTIVE)
    players_.clear();

  players_.insert(PlayerIdentifier(observer, player_id));
  UpdateWebContents();

  return true;
}

void MediaSession::RemovePlayer(MediaSessionObserver* observer,
                                int player_id) {
  auto it = players_.find(PlayerIdentifier(observer, player_id));
  if (it != players_.end())
    players_.erase(it);

  AbandonSystemAudioFocusIfNeeded();
}

void MediaSession::RemovePlayers(MediaSessionObserver* observer) {
  for (auto it = players_.begin(); it != players_.end();) {
    if (it->observer == observer)
      players_.erase(it++);
    else
      ++it;
  }

  AbandonSystemAudioFocusIfNeeded();
}

void MediaSession::OnSuspend(JNIEnv* env, jobject obj, jboolean temporary) {
  if (audio_focus_state_ != State::ACTIVE)
    return;

  OnSuspendInternal(SuspendType::SYSTEM);
  if (!temporary)
    audio_focus_state_ = State::INACTIVE;
  UpdateWebContents();
}

void MediaSession::OnResume(JNIEnv* env, jobject obj) {
  if (audio_focus_state_ != State::SUSPENDED)
    return;

  OnResumeInternal(SuspendType::SYSTEM);
  UpdateWebContents();
}

void MediaSession::Resume() {
  DCHECK(IsSuspended());

  // Request audio focus again in case we lost it because another app started
  // playing while the playback was paused.
  audio_focus_state_ = RequestSystemAudioFocus(audio_focus_type_)
                           ? State::ACTIVE
                           : State::INACTIVE;
  if (audio_focus_state_ != State::ACTIVE)
    return;

  OnResumeInternal(SuspendType::UI);
}

void MediaSession::Suspend() {
  DCHECK(!IsSuspended());

  OnSuspendInternal(SuspendType::UI);
}

bool MediaSession::IsSuspended() const {
  // TODO(mlamouri): should be == State::SUSPENDED.
  return audio_focus_state_ != State::ACTIVE;
}

bool MediaSession::IsControllable() const {
  // Only content type media session can be controllable unless it is inactive.
  return audio_focus_state_ != State::INACTIVE &&
         audio_focus_type_ == Type::Content;
}

void MediaSession::ResetJavaRefForTest() {
  j_media_session_.Reset();
}

bool MediaSession::IsActiveForTest() const {
  return audio_focus_state_ == State::ACTIVE;
}

MediaSession::Type MediaSession::audio_focus_type_for_test() const {
  return audio_focus_type_;
}

void MediaSession::RemoveAllPlayersForTest() {
  players_.clear();
  AbandonSystemAudioFocusIfNeeded();
}

void MediaSession::OnSuspendInternal(SuspendType type) {
  audio_focus_state_ = State::SUSPENDED;
  suspend_type_ = type;

  for (const auto& it : players_)
    it.observer->OnSuspend(it.player_id);
}

void MediaSession::OnResumeInternal(SuspendType type) {
  if (suspend_type_ != type && type != SuspendType::UI)
    return;

  audio_focus_state_ = State::ACTIVE;

  for (const auto& it : players_)
    it.observer->OnResume(it.player_id);
}

MediaSession::MediaSession(WebContents* web_contents)
    : WebContentsObserver(web_contents),
      audio_focus_state_(State::INACTIVE),
      audio_focus_type_(Type::Transient) {}

void MediaSession::Initialize() {
  JNIEnv* env = base::android::AttachCurrentThread();
  DCHECK(env);
  j_media_session_.Reset(Java_MediaSession_createMediaSession(
      env,
      base::android::GetApplicationContext(),
      reinterpret_cast<intptr_t>(this)));
}

bool MediaSession::RequestSystemAudioFocus(Type type) {
  // During tests, j_media_session_ might be null.
  if (j_media_session_.is_null())
    return true;

  JNIEnv* env = base::android::AttachCurrentThread();
  DCHECK(env);
  return Java_MediaSession_requestAudioFocus(env, j_media_session_.obj(),
                                             type == Type::Transient);
}

void MediaSession::AbandonSystemAudioFocusIfNeeded() {
  if (audio_focus_state_ == State::INACTIVE || !players_.empty())
    return;

  // During tests, j_media_session_ might be null.
  if (!j_media_session_.is_null()) {
    JNIEnv* env = base::android::AttachCurrentThread();
    DCHECK(env);
    Java_MediaSession_abandonAudioFocus(env, j_media_session_.obj());
  }

  audio_focus_state_ = State::INACTIVE;
  UpdateWebContents();
}

void MediaSession::UpdateWebContents() {
  static_cast<WebContentsImpl*>(web_contents())->OnMediaSessionStateChanged();
}

}  // namespace content
