/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "ClientHandler"

#include "cuttlefish/host/frontend/webrtc/libdevice/client_handler.h"

#include <netdb.h>
#include <openssl/rand.h>

#include <android-base/logging.h>

#include "cuttlefish/host/libs/config/cuttlefish_config.h"

namespace cuttlefish {
namespace webrtc_streaming {

// Video streams initiating in the client may be added and removed at unexpected
// times, causing the webrtc objects to be destroyed and created every time.
// This class hides away that complexity and allows to set up sinks only once.
class ClientVideoTrackImpl : public ClientVideoTrackInterface {
 public:
  void AddOrUpdateSink(rtc::VideoSinkInterface<webrtc::VideoFrame> *sink,
                       const rtc::VideoSinkWants &wants) override {
    sink_ = sink;
    wants_ = wants;
    if (video_track_) {
      video_track_->AddOrUpdateSink(sink, wants);
    }
  }

  void SetVideoTrack(webrtc::VideoTrackInterface *track) {
    video_track_ = track;
    if (sink_) {
      video_track_->AddOrUpdateSink(sink_, wants_);
    }
  }

  void UnsetVideoTrack(webrtc::VideoTrackInterface *track) {
    if (track == video_track_) {
      video_track_ = nullptr;
    }
  }

 private:
  webrtc::VideoTrackInterface* video_track_;
  rtc::VideoSinkInterface<webrtc::VideoFrame> *sink_ = nullptr;
  rtc::VideoSinkWants wants_ = {};
};

std::shared_ptr<ClientHandler> ClientHandler::Create(
    int client_id, std::shared_ptr<ConnectionObserver> observer,
    PeerConnectionBuilder &connection_builder,
    std::function<void(const Json::Value &)> send_to_client_cb,
    std::function<void(bool)> on_connection_changed_cb) {
  return std::shared_ptr<ClientHandler>(
      new ClientHandler(client_id, observer, connection_builder,
                        send_to_client_cb, on_connection_changed_cb));
}

ClientHandler::ClientHandler(
    int client_id, std::shared_ptr<ConnectionObserver> observer,
    PeerConnectionBuilder &connection_builder,
    std::function<void(const Json::Value &)> send_to_client_cb,
    std::function<void(bool)> on_connection_changed_cb)
    : client_id_(client_id),
      observer_(observer),
      send_to_client_(send_to_client_cb),
      on_connection_changed_cb_(on_connection_changed_cb),
      connection_builder_(connection_builder),
      controller_(*this, *this, *this),
      data_channels_handler_(observer),
      camera_track_(new ClientVideoTrackImpl()) {}

rtc::scoped_refptr<webrtc::RtpSenderInterface>
ClientHandler::AddTrackToConnection(
    rtc::scoped_refptr<webrtc::MediaStreamTrackInterface> track,
    rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection,
    const std::string &label) {
  if (!peer_connection) {
    return nullptr;
  }
  // Send each track as part of a different stream with the label as id
  auto err_or_sender =
      peer_connection->AddTrack(track, {label} /* stream_id */);
  if (!err_or_sender.ok()) {
    LOG(ERROR) << "Failed to add track to the peer connection";
    return nullptr;
  }
  return err_or_sender.MoveValue();
}

bool ClientHandler::AddDisplay(
    rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track,
    const std::string &label) {
  auto [it, inserted] = displays_.emplace(label, DisplayTrackAndSender{
                                                     .track = video_track,
                                                 });
  auto sender =
      AddTrackToConnection(video_track, controller_.peer_connection(), label);
  if (sender) {
    DisplayTrackAndSender &info = it->second;
    info.sender = sender;
  }
  // Succeed if the peer connection is null or the track was added
  return controller_.peer_connection() == nullptr || sender;
}

bool ClientHandler::RemoveDisplay(const std::string &label) {
  auto it = displays_.find(label);
  if (it == displays_.end()) {
    return false;
  }

  if (controller_.peer_connection()) {
    DisplayTrackAndSender &info = it->second;

    auto error = controller_.peer_connection()->RemoveTrackOrError(info.sender);
    if (!error.ok()) {
      LOG(ERROR) << "Failed to remove video track for display " << label << ": "
                 << error.message();
      return false;
    }
  }

  displays_.erase(it);
  return true;
}

bool ClientHandler::AddAudio(
    rtc::scoped_refptr<webrtc::AudioTrackInterface> audio_track,
    const std::string &label) {
  audio_streams_.emplace_back(audio_track, label);
  auto peer_connection = controller_.peer_connection();
  if (!peer_connection) {
    return true;
  }
  return AddTrackToConnection(audio_track, controller_.peer_connection(), label)
      .get();
}

ClientVideoTrackInterface* ClientHandler::GetCameraStream() {
  return camera_track_.get();
}

Result<void> ClientHandler::SendMessage(const Json::Value &msg) {
  send_to_client_(msg);
  return {};
}

Result<rtc::scoped_refptr<webrtc::PeerConnectionInterface>>
ClientHandler::Build(
    webrtc::PeerConnectionObserver &observer,
    const std::vector<webrtc::PeerConnectionInterface::IceServer>
        &per_connection_servers) {
  auto peer_connection =
      CF_EXPECT(connection_builder_.Build(observer, per_connection_servers));

  // Re-add the video and audio tracks after the peer connection has been
  // created
  for (auto &[label, info] : displays_) {
    info.sender =
        CF_EXPECT(AddTrackToConnection(info.track, peer_connection, label).get());
  }
  // Add the audio tracks to the peer connection
  for (auto &[audio_track, label] : audio_streams_) {
    // Audio channels are never removed from the connection by the device, so
    // it's ok to discard the returned sender here. The peer connection keeps
    // track of it anyways.
    CF_EXPECT(AddTrackToConnection(audio_track, peer_connection, label).get());
  }

  // libwebrtc configures the video encoder with a start bitrate of just 300kbs
  // which causes it to drop the first 4 frames it receives. Any value over 2Mbs
  // will be capped at 2Mbs when passed to the encoder by the peer_connection
  // object, so we pass the maximum possible value here.
  webrtc::BitrateSettings bitrate_settings;
  bitrate_settings.start_bitrate_bps = 2000000;  // 2Mbs
  peer_connection->SetBitrate(bitrate_settings);

  // At least one data channel needs to be created on the side that creates the
  // SDP offer (the device) for data channels to be enabled at all.
  // This channel is meant to carry control commands from the client.
  auto result = peer_connection->CreateDataChannelOrError(kControlChannelLabel,
                                                          nullptr /* config */);
  CF_EXPECTF(result.ok(), "Failed to create control data channel: {}",
             result.error().message());

  auto control_channel = result.MoveValue();
  data_channels_handler_.OnDataChannelOpen(control_channel);

  return peer_connection;
}

void ClientHandler::HandleMessage(const Json::Value &message) {
  controller_.HandleSignalingMessage(message);
}

void ClientHandler::Close() {
  // We can't simply call peer_connection_->Close() here because this method
  // could be called from one of the PeerConnectionObserver callbacks and that
  // would lead to a deadlock (Close eventually tries to destroy an object that
  // will then wait for the callback to return -> deadlock). Destroying the
  // peer_connection_ has the same effect. The only alternative is to postpone
  // that operation until after the callback returns.
  on_connection_changed_cb_(false);
}

void ClientHandler::OnConnectionStateChange(
    Result<webrtc::PeerConnectionInterface::PeerConnectionState> new_state) {
  if (!new_state.ok()) {
    LOG(ERROR) << "Connection error: " << new_state.error().FormatForEnv();
    Close();
    return;
  }
  switch (*new_state) {
    case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected:
      LOG(VERBOSE) << "Client " << client_id_ << ": WebRTC connected";
      observer_->OnConnected();
      on_connection_changed_cb_(true);
      break;
    case webrtc::PeerConnectionInterface::PeerConnectionState::kDisconnected:
      LOG(VERBOSE) << "Client " << client_id_ << ": Connection disconnected";
      Close();
      break;
    case webrtc::PeerConnectionInterface::PeerConnectionState::kFailed:
      LOG(ERROR) << "Client " << client_id_ << ": Connection failed";
      Close();
      break;
    case webrtc::PeerConnectionInterface::PeerConnectionState::kClosed:
      LOG(VERBOSE) << "Client " << client_id_ << ": Connection closed";
      Close();
      break;
    case webrtc::PeerConnectionInterface::PeerConnectionState::kNew:
      LOG(VERBOSE) << "Client " << client_id_ << ": Connection new";
      break;
    case webrtc::PeerConnectionInterface::PeerConnectionState::kConnecting:
      LOG(VERBOSE) << "Client " << client_id_ << ": Connection started";
      break;
  }
}

void ClientHandler::OnDataChannel(
    rtc::scoped_refptr<webrtc::DataChannelInterface> data_channel) {
  data_channels_handler_.OnDataChannelOpen(data_channel);
}

void ClientHandler::OnTrack(
    rtc::scoped_refptr<webrtc::RtpTransceiverInterface> transceiver) {
  auto track = transceiver->receiver()->track();
  if (track && track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) {
    // It's ok to take the raw pointer here because we make sure to unset it
    // when the track is removed
    camera_track_->SetVideoTrack(
        static_cast<webrtc::VideoTrackInterface *>(track.get()));
  }
}
void ClientHandler::OnRemoveTrack(
    rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver) {
  auto track = receiver->track();
  if (track && track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) {
    // this only unsets if the track matches the one already in store
    camera_track_->UnsetVideoTrack(
        reinterpret_cast<webrtc::VideoTrackInterface *>(track.get()));
  }
}

}  // namespace webrtc_streaming
}  // namespace cuttlefish
