// 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/presentation/presentation_service_impl.h"

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

#include "base/logging.h"
#include "base/stl_util.h"
#include "content/browser/presentation/presentation_type_converters.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/presentation_session_message.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/frame_navigate_params.h"
#include "content/public/common/presentation_constants.h"

namespace content {

namespace {

const int kInvalidRequestSessionId = -1;

int GetNextRequestSessionId() {
  static int next_request_session_id = 0;
  return ++next_request_session_id;
}

// Converts a PresentationSessionMessage |input| to a SessionMessage.
// |input|: The message to convert.
// |pass_ownership|: If true, function may reuse strings or buffers from
//     |input| without copying. |input| can be freely modified.
blink::mojom::SessionMessagePtr ToMojoSessionMessage(
    content::PresentationSessionMessage* input,
    bool pass_ownership) {
  DCHECK(input);
  blink::mojom::SessionMessagePtr output(blink::mojom::SessionMessage::New());
  if (input->is_binary()) {
    // binary data
    DCHECK(input->data);
    output->type = blink::mojom::PresentationMessageType::ARRAY_BUFFER;
    if (pass_ownership) {
      output->data = std::move(*(input->data));
    } else {
      output->data = *(input->data);
    }
  } else {
    // string message
    output->type = blink::mojom::PresentationMessageType::TEXT;
    if (pass_ownership) {
      output->message = std::move(input->message);
    } else {
      output->message = input->message;
    }
  }
  return output;
}

std::unique_ptr<PresentationSessionMessage> GetPresentationSessionMessage(
    blink::mojom::SessionMessagePtr input) {
  std::unique_ptr<content::PresentationSessionMessage> output;
  if (input.is_null())
    return output;

  switch (input->type) {
    case blink::mojom::PresentationMessageType::TEXT: {
      // Return nullptr PresentationSessionMessage if invalid (unset |message|,
      // set |data|, or size too large).
      if (input->data || !input->message ||
          input->message->size() > content::kMaxPresentationSessionMessageSize)
        return output;

      output.reset(
          new PresentationSessionMessage(PresentationMessageType::TEXT));
      output->message = std::move(input->message.value());
      return output;
    }
    case blink::mojom::PresentationMessageType::ARRAY_BUFFER: {
      // Return nullptr PresentationSessionMessage if invalid (unset |data|, set
      // |message|, or size too large).
      if (!input->data || input->message ||
          input->data->size() > content::kMaxPresentationSessionMessageSize)
        return output;

      output.reset(new PresentationSessionMessage(
          PresentationMessageType::ARRAY_BUFFER));
      output->data.reset(
          new std::vector<uint8_t>(std::move(input->data.value())));
      return output;
    }
    case blink::mojom::PresentationMessageType::BLOB: {
      // Return nullptr PresentationSessionMessage if invalid (unset |data|, set
      // |message|, or size too large).
      if (!input->data || input->message ||
          input->data->size() > content::kMaxPresentationSessionMessageSize)
        return output;

      output.reset(
          new PresentationSessionMessage(PresentationMessageType::BLOB));
      output->data.reset(
          new std::vector<uint8_t>(std::move(input->data.value())));
      return output;
    }
  }

  NOTREACHED() << "Invalid presentation message type " << input->type;
  return output;
}

void InvokeNewSessionCallbackWithError(
    const PresentationServiceImpl::NewSessionCallback& callback) {
  callback.Run(blink::mojom::PresentationSessionInfoPtr(),
               blink::mojom::PresentationError::From(PresentationError(
                   PRESENTATION_ERROR_UNKNOWN, "Internal error")));
}

}  // namespace

PresentationServiceImpl::PresentationServiceImpl(
    RenderFrameHost* render_frame_host,
    WebContents* web_contents,
    PresentationServiceDelegate* delegate)
    : WebContentsObserver(web_contents),
      delegate_(delegate),
      start_session_request_id_(kInvalidRequestSessionId),
      weak_factory_(this) {
  DCHECK(render_frame_host);
  DCHECK(web_contents);
  CHECK(render_frame_host->IsRenderFrameLive());

  render_process_id_ = render_frame_host->GetProcess()->GetID();
  render_frame_id_ = render_frame_host->GetRoutingID();
  DVLOG(2) << "PresentationServiceImpl: "
           << render_process_id_ << ", " << render_frame_id_;
  if (delegate_)
    delegate_->AddObserver(render_process_id_, render_frame_id_, this);
}

PresentationServiceImpl::~PresentationServiceImpl() {
  if (delegate_)
    delegate_->RemoveObserver(render_process_id_, render_frame_id_);
}

// static
void PresentationServiceImpl::CreateMojoService(
    RenderFrameHost* render_frame_host,
    mojo::InterfaceRequest<blink::mojom::PresentationService> request) {
  DVLOG(2) << "CreateMojoService";
  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host);
  DCHECK(web_contents);

  // This object will be deleted when the RenderFrameHost is about to be
  // deleted (RenderFrameDeleted).
  PresentationServiceImpl* impl = new PresentationServiceImpl(
      render_frame_host,
      web_contents,
      GetContentClient()->browser()->GetPresentationServiceDelegate(
          web_contents));
  impl->Bind(std::move(request));
}

void PresentationServiceImpl::Bind(
    mojo::InterfaceRequest<blink::mojom::PresentationService> request) {
  binding_.reset(new mojo::Binding<blink::mojom::PresentationService>(
      this, std::move(request)));
}

void PresentationServiceImpl::SetClient(
    blink::mojom::PresentationServiceClientPtr client) {
  DCHECK(!client_.get());
  // TODO(imcheng): Set ErrorHandler to listen for errors.
  client_ = std::move(client);
}

void PresentationServiceImpl::ListenForScreenAvailability(const GURL& url) {
  DVLOG(2) << "ListenForScreenAvailability " << url.spec();
  if (!delegate_) {
    client_->OnScreenAvailabilityUpdated(url, false);
    return;
  }

  if (screen_availability_listeners_.count(url))
    return;

  std::unique_ptr<ScreenAvailabilityListenerImpl> listener(
      new ScreenAvailabilityListenerImpl(url, this));
  if (delegate_->AddScreenAvailabilityListener(
      render_process_id_,
      render_frame_id_,
      listener.get())) {
    screen_availability_listeners_[url] = std::move(listener);
  } else {
    DVLOG(1) << "AddScreenAvailabilityListener failed. Ignoring request.";
  }
}

void PresentationServiceImpl::StopListeningForScreenAvailability(
    const GURL& url) {
  DVLOG(2) << "StopListeningForScreenAvailability " << url.spec();
  if (!delegate_)
    return;

  auto listener_it = screen_availability_listeners_.find(url);
  if (listener_it == screen_availability_listeners_.end())
    return;

  delegate_->RemoveScreenAvailabilityListener(
      render_process_id_, render_frame_id_, listener_it->second.get());
  screen_availability_listeners_.erase(listener_it);
}

void PresentationServiceImpl::StartSession(
    const std::vector<GURL>& presentation_urls,
    const NewSessionCallback& callback) {
  DVLOG(2) << "StartSession";
  if (!delegate_) {
    callback.Run(
        blink::mojom::PresentationSessionInfoPtr(),
        blink::mojom::PresentationError::From(PresentationError(
            PRESENTATION_ERROR_NO_AVAILABLE_SCREENS, "No screens found.")));
    return;
  }

  // There is a StartSession request in progress. To avoid queueing up
  // requests, the incoming request is rejected.
  if (start_session_request_id_ != kInvalidRequestSessionId) {
    InvokeNewSessionCallbackWithError(callback);
    return;
  }

  start_session_request_id_ = GetNextRequestSessionId();
  pending_start_session_cb_.reset(new NewSessionCallbackWrapper(callback));
  delegate_->StartSession(
      render_process_id_, render_frame_id_, presentation_urls,
      base::Bind(&PresentationServiceImpl::OnStartSessionSucceeded,
                 weak_factory_.GetWeakPtr(), start_session_request_id_),
      base::Bind(&PresentationServiceImpl::OnStartSessionError,
                 weak_factory_.GetWeakPtr(), start_session_request_id_));
}

void PresentationServiceImpl::JoinSession(
    const std::vector<GURL>& presentation_urls,
    const base::Optional<std::string>& presentation_id,
    const NewSessionCallback& callback) {
  DVLOG(2) << "JoinSession";
  if (!delegate_) {
    callback.Run(blink::mojom::PresentationSessionInfoPtr(),
                 blink::mojom::PresentationError::From(PresentationError(
                     PRESENTATION_ERROR_NO_PRESENTATION_FOUND,
                     "Error joining route: No matching route")));
    return;
  }

  int request_session_id = RegisterJoinSessionCallback(callback);
  if (request_session_id == kInvalidRequestSessionId) {
    InvokeNewSessionCallbackWithError(callback);
    return;
  }
  delegate_->JoinSession(
      render_process_id_, render_frame_id_, presentation_urls,
      presentation_id.value_or(std::string()),
      base::Bind(&PresentationServiceImpl::OnJoinSessionSucceeded,
                 weak_factory_.GetWeakPtr(), request_session_id),
      base::Bind(&PresentationServiceImpl::OnJoinSessionError,
                 weak_factory_.GetWeakPtr(), request_session_id));
}

int PresentationServiceImpl::RegisterJoinSessionCallback(
    const NewSessionCallback& callback) {
  if (pending_join_session_cbs_.size() >= kMaxNumQueuedSessionRequests)
    return kInvalidRequestSessionId;

  int request_id = GetNextRequestSessionId();
  pending_join_session_cbs_[request_id].reset(
      new NewSessionCallbackWrapper(callback));
  return request_id;
}

void PresentationServiceImpl::ListenForConnectionStateChangeAndChangeState(
    const PresentationSessionInfo& connection) {
  if (delegate_) {
    delegate_->ListenForConnectionStateChange(
        render_process_id_, render_frame_id_, connection,
        base::Bind(&PresentationServiceImpl::OnConnectionStateChanged,
                   weak_factory_.GetWeakPtr(), connection));
    OnConnectionStateChanged(connection,
                             PresentationConnectionStateChangeInfo(
                                 PRESENTATION_CONNECTION_STATE_CONNECTED));
  }
}

void PresentationServiceImpl::OnStartSessionSucceeded(
    int request_session_id,
    const PresentationSessionInfo& session_info) {
  if (request_session_id != start_session_request_id_)
    return;

  CHECK(pending_start_session_cb_.get());
  pending_start_session_cb_->Run(
      blink::mojom::PresentationSessionInfo::From(session_info),
      blink::mojom::PresentationErrorPtr());
  ListenForConnectionStateChangeAndChangeState(session_info);
  pending_start_session_cb_.reset();
  start_session_request_id_ = kInvalidRequestSessionId;
}

void PresentationServiceImpl::OnStartSessionError(
    int request_session_id,
    const PresentationError& error) {
  if (request_session_id != start_session_request_id_)
    return;

  CHECK(pending_start_session_cb_.get());
  pending_start_session_cb_->Run(blink::mojom::PresentationSessionInfoPtr(),
                                 blink::mojom::PresentationError::From(error));
  pending_start_session_cb_.reset();
  start_session_request_id_ = kInvalidRequestSessionId;
}

void PresentationServiceImpl::OnJoinSessionSucceeded(
    int request_session_id,
    const PresentationSessionInfo& session_info) {
  if (RunAndEraseJoinSessionMojoCallback(
          request_session_id,
          blink::mojom::PresentationSessionInfo::From(session_info),
          blink::mojom::PresentationErrorPtr())) {
    ListenForConnectionStateChangeAndChangeState(session_info);
  }
}

void PresentationServiceImpl::OnJoinSessionError(
    int request_session_id,
    const PresentationError& error) {
  RunAndEraseJoinSessionMojoCallback(
      request_session_id, blink::mojom::PresentationSessionInfoPtr(),
      blink::mojom::PresentationError::From(error));
}

bool PresentationServiceImpl::RunAndEraseJoinSessionMojoCallback(
    int request_session_id,
    blink::mojom::PresentationSessionInfoPtr session,
    blink::mojom::PresentationErrorPtr error) {
  auto it = pending_join_session_cbs_.find(request_session_id);
  if (it == pending_join_session_cbs_.end())
    return false;

  DCHECK(it->second.get());
  it->second->Run(std::move(session), std::move(error));
  pending_join_session_cbs_.erase(it);
  return true;
}

void PresentationServiceImpl::SetDefaultPresentationUrls(
    const std::vector<GURL>& presentation_urls) {
  DVLOG(2) << "SetDefaultPresentationUrls";
  if (!delegate_)
    return;

  if (default_presentation_urls_ == presentation_urls)
    return;

  default_presentation_urls_ = presentation_urls;
  delegate_->SetDefaultPresentationUrls(
      render_process_id_, render_frame_id_, presentation_urls,
      base::Bind(&PresentationServiceImpl::OnDefaultPresentationStarted,
                 weak_factory_.GetWeakPtr()));
}

void PresentationServiceImpl::SendSessionMessage(
    blink::mojom::PresentationSessionInfoPtr session,
    blink::mojom::SessionMessagePtr session_message,
    const SendSessionMessageCallback& callback) {
  DVLOG(2) << "SendSessionMessage";
  DCHECK(!session_message.is_null());
  // send_message_callback_ should be null by now, otherwise resetting of
  // send_message_callback_ with new callback will drop the old callback.
  if (!delegate_ || send_message_callback_) {
    callback.Run(false);
    return;
  }

  send_message_callback_.reset(new SendSessionMessageCallback(callback));
  delegate_->SendMessage(
      render_process_id_, render_frame_id_,
      session.To<PresentationSessionInfo>(),
      GetPresentationSessionMessage(std::move(session_message)),
      base::Bind(&PresentationServiceImpl::OnSendMessageCallback,
                 weak_factory_.GetWeakPtr()));
}

void PresentationServiceImpl::OnSendMessageCallback(bool sent) {
  // It is possible that Reset() is invoked before receiving this callback.
  // So, always check send_message_callback_ for non-null.
  if (send_message_callback_) {
    send_message_callback_->Run(sent);
    send_message_callback_.reset();
  }
}

void PresentationServiceImpl::CloseConnection(
    const GURL& presentation_url,
    const std::string& presentation_id) {
  DVLOG(2) << "CloseConnection " << presentation_id;
  if (delegate_)
    delegate_->CloseConnection(render_process_id_, render_frame_id_,
                               presentation_id);
}

void PresentationServiceImpl::Terminate(const GURL& presentation_url,
                                        const std::string& presentation_id) {
  DVLOG(2) << "Terminate " << presentation_id;
  if (delegate_)
    delegate_->Terminate(render_process_id_, render_frame_id_, presentation_id);
}

void PresentationServiceImpl::OnConnectionStateChanged(
    const PresentationSessionInfo& connection,
    const PresentationConnectionStateChangeInfo& info) {
  DVLOG(2) << "PresentationServiceImpl::OnConnectionStateChanged "
           << "[presentation_id]: " << connection.presentation_id
           << " [state]: " << info.state;
  DCHECK(client_.get());
  if (info.state == PRESENTATION_CONNECTION_STATE_CLOSED) {
    client_->OnConnectionClosed(
        blink::mojom::PresentationSessionInfo::From(connection),
        content::PresentationConnectionCloseReasonToMojo(info.close_reason),
        info.message);
  } else {
    client_->OnConnectionStateChanged(
        blink::mojom::PresentationSessionInfo::From(connection),
        PresentationConnectionStateToMojo(info.state));
  }
}

bool PresentationServiceImpl::FrameMatches(
    content::RenderFrameHost* render_frame_host) const {
  if (!render_frame_host)
    return false;

  return render_frame_host->GetProcess()->GetID() == render_process_id_ &&
         render_frame_host->GetRoutingID() == render_frame_id_;
}

void PresentationServiceImpl::ListenForSessionMessages(
    blink::mojom::PresentationSessionInfoPtr session) {
  DVLOG(2) << "ListenForSessionMessages";
  if (!delegate_)
    return;

  PresentationSessionInfo session_info(session.To<PresentationSessionInfo>());
  delegate_->ListenForSessionMessages(
      render_process_id_, render_frame_id_, session_info,
      base::Bind(&PresentationServiceImpl::OnSessionMessages,
                 weak_factory_.GetWeakPtr(), session_info));
}

void PresentationServiceImpl::OnSessionMessages(
    const PresentationSessionInfo& session,
    const ScopedVector<PresentationSessionMessage>& messages,
    bool pass_ownership) {
  DCHECK(client_);

  DVLOG(2) << "OnSessionMessages";
  std::vector<blink::mojom::SessionMessagePtr> mojo_messages(messages.size());
  std::transform(messages.begin(), messages.end(), mojo_messages.begin(),
                 [pass_ownership](PresentationSessionMessage* message) {
                   return ToMojoSessionMessage(message, pass_ownership);
                 });

  client_->OnSessionMessagesReceived(
      blink::mojom::PresentationSessionInfo::From(session),
      std::move(mojo_messages));
}

void PresentationServiceImpl::DidNavigateAnyFrame(
    content::RenderFrameHost* render_frame_host,
    const content::LoadCommittedDetails& details,
    const content::FrameNavigateParams& params) {
  DVLOG(2) << "PresentationServiceImpl::DidNavigateAnyFrame";
  if (!FrameMatches(render_frame_host))
    return;

  std::string prev_url_host = details.previous_url.host();
  std::string curr_url_host = params.url.host();

  // If a frame navigation is in-page (e.g. navigating to a fragment in
  // same page) then we do not unregister listeners.
  DVLOG(2) << "DidNavigateAnyFrame: "
           << "prev host: " << prev_url_host << ", curr host: " << curr_url_host
           << ", details.is_in_page: " << details.is_in_page;
  if (details.is_in_page)
    return;

  // Reset if the frame actually navigated.
  Reset();
}

void PresentationServiceImpl::RenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  DVLOG(2) << "PresentationServiceImpl::RenderFrameDeleted";
  if (!FrameMatches(render_frame_host))
    return;

  // RenderFrameDeleted means the associated RFH is going to be deleted soon.
  // This object should also be deleted.
  Reset();
  delete this;
}

void PresentationServiceImpl::WebContentsDestroyed() {
  LOG(ERROR) << "PresentationServiceImpl is being deleted in "
             << "WebContentsDestroyed()! This shouldn't happen since it "
             << "should've been deleted during RenderFrameDeleted().";
  Reset();
  delete this;
}

void PresentationServiceImpl::Reset() {
  DVLOG(2) << "PresentationServiceImpl::Reset";
  if (delegate_)
    delegate_->Reset(render_process_id_, render_frame_id_);

  default_presentation_urls_.clear();

  screen_availability_listeners_.clear();

  start_session_request_id_ = kInvalidRequestSessionId;
  pending_start_session_cb_.reset();

  pending_join_session_cbs_.clear();

  if (on_session_messages_callback_.get()) {
    on_session_messages_callback_->Run(
        mojo::Array<blink::mojom::SessionMessagePtr>());
    on_session_messages_callback_.reset();
  }

  if (send_message_callback_) {
    // Run the callback with false, indicating the renderer to stop sending
    // the requests and invalidate all pending requests.
    send_message_callback_->Run(false);
    send_message_callback_.reset();
  }
}

void PresentationServiceImpl::OnDelegateDestroyed() {
  DVLOG(2) << "PresentationServiceImpl::OnDelegateDestroyed";
  delegate_ = nullptr;
  Reset();
}

void PresentationServiceImpl::OnDefaultPresentationStarted(
    const PresentationSessionInfo& connection) {
  DCHECK(client_.get());
  client_->OnDefaultSessionStarted(
      blink::mojom::PresentationSessionInfo::From(connection));
  ListenForConnectionStateChangeAndChangeState(connection);
}

PresentationServiceImpl::ScreenAvailabilityListenerImpl::
    ScreenAvailabilityListenerImpl(const GURL& availability_url,
                                   PresentationServiceImpl* service)
    : availability_url_(availability_url), service_(service) {
  DCHECK(service_);
  DCHECK(service_->client_.get());
}

PresentationServiceImpl::ScreenAvailabilityListenerImpl::
~ScreenAvailabilityListenerImpl() {
}

GURL PresentationServiceImpl::ScreenAvailabilityListenerImpl::
    GetAvailabilityUrl() const {
  return availability_url_;
}

void PresentationServiceImpl::ScreenAvailabilityListenerImpl
::OnScreenAvailabilityChanged(bool available) {
  service_->client_->OnScreenAvailabilityUpdated(availability_url_, available);
}

void PresentationServiceImpl::ScreenAvailabilityListenerImpl
::OnScreenAvailabilityNotSupported() {
  service_->client_->OnScreenAvailabilityNotSupported(availability_url_);
}

PresentationServiceImpl::NewSessionCallbackWrapper
::NewSessionCallbackWrapper(const NewSessionCallback& callback)
    : callback_(callback) {
}

PresentationServiceImpl::NewSessionCallbackWrapper
::~NewSessionCallbackWrapper() {
  if (!callback_.is_null())
    InvokeNewSessionCallbackWithError(callback_);
}

void PresentationServiceImpl::NewSessionCallbackWrapper::Run(
    blink::mojom::PresentationSessionInfoPtr session,
    blink::mojom::PresentationErrorPtr error) {
  DCHECK(!callback_.is_null());
  callback_.Run(std::move(session), std::move(error));
  callback_.Reset();
}

}  // namespace content
