// 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 "components/cast_channel/keep_alive_delegate.h"

#include <string>
#include <utility>

#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/values.h"
#include "components/cast_channel/cast_channel_enum.h"
#include "components/cast_channel/cast_socket.h"
#include "components/cast_channel/logger.h"
#include "components/cast_channel/proto/cast_channel.pb.h"
#include "net/base/net_errors.h"

namespace cast_channel {
namespace {

const char kHeartbeatNamespace[] = "urn:x-cast:com.google.cast.tp.heartbeat";
const char kPingSenderId[] = "chrome";
const char kPingReceiverId[] = "receiver-0";
const char kTypeNodeId[] = "type";

// Parses the JSON-encoded payload of |message| and returns the value in the
// "type" field or the empty string if the parse fails or the field is not
// found.
std::string ParseForPayloadType(const CastMessage& message) {
  std::unique_ptr<base::Value> parsed_payload(
      base::JSONReader::Read(message.payload_utf8()));
  base::DictionaryValue* payload_as_dict;
  if (!parsed_payload || !parsed_payload->GetAsDictionary(&payload_as_dict))
    return std::string();
  std::string type_string;
  if (!payload_as_dict->GetString(kTypeNodeId, &type_string))
    return std::string();
  return type_string;
}

}  // namespace

// static
const char KeepAliveDelegate::kHeartbeatPingType[] = "PING";

// static
const char KeepAliveDelegate::kHeartbeatPongType[] = "PONG";

using ::cast_channel::ChannelError;

// static
CastMessage KeepAliveDelegate::CreateKeepAliveMessage(
    const char* message_type) {
  CastMessage output;
  output.set_protocol_version(CastMessage::CASTV2_1_0);
  output.set_source_id(kPingSenderId);
  output.set_destination_id(kPingReceiverId);
  output.set_namespace_(kHeartbeatNamespace);
  base::DictionaryValue type_dict;
  type_dict.SetString(kTypeNodeId, message_type);
  if (!base::JSONWriter::Write(type_dict, output.mutable_payload_utf8())) {
    LOG(ERROR) << "Failed to serialize dictionary.";
    return output;
  }
  output.set_payload_type(
      CastMessage::PayloadType::CastMessage_PayloadType_STRING);
  return output;
}

KeepAliveDelegate::KeepAliveDelegate(
    CastSocket* socket,
    scoped_refptr<Logger> logger,
    std::unique_ptr<CastTransport::Delegate> inner_delegate,
    base::TimeDelta ping_interval,
    base::TimeDelta liveness_timeout)
    : started_(false),
      socket_(socket),
      logger_(logger),
      inner_delegate_(std::move(inner_delegate)),
      liveness_timeout_(liveness_timeout),
      ping_interval_(ping_interval) {
  DCHECK(ping_interval_ < liveness_timeout_);
  DCHECK(inner_delegate_);
  DCHECK(socket_);
  ping_message_ = CreateKeepAliveMessage(kHeartbeatPingType);
  pong_message_ = CreateKeepAliveMessage(kHeartbeatPongType);
}

KeepAliveDelegate::~KeepAliveDelegate() {}

void KeepAliveDelegate::SetTimersForTest(
    std::unique_ptr<base::Timer> injected_ping_timer,
    std::unique_ptr<base::Timer> injected_liveness_timer) {
  ping_timer_ = std::move(injected_ping_timer);
  liveness_timer_ = std::move(injected_liveness_timer);
}

void KeepAliveDelegate::Start() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!started_);

  VLOG(1) << "Starting keep-alive timers.";
  VLOG(1) << "Ping timeout: " << ping_interval_;
  VLOG(1) << "Liveness timeout: " << liveness_timeout_;

  // Use injected mock timers, if provided.
  if (!ping_timer_) {
    ping_timer_.reset(new base::Timer(true, false));
  }
  if (!liveness_timer_) {
    liveness_timer_.reset(new base::Timer(true, false));
  }

  ping_timer_->Start(
      FROM_HERE, ping_interval_,
      base::Bind(&KeepAliveDelegate::SendKeepAliveMessage,
                 base::Unretained(this), ping_message_, kHeartbeatPingType));
  liveness_timer_->Start(
      FROM_HERE, liveness_timeout_,
      base::Bind(&KeepAliveDelegate::LivenessTimeout, base::Unretained(this)));

  started_ = true;
  inner_delegate_->Start();
}

void KeepAliveDelegate::ResetTimers() {
  DCHECK(started_);
  ping_timer_->Reset();
  liveness_timer_->Reset();
}

void KeepAliveDelegate::SendKeepAliveMessage(const CastMessage& message,
                                             const char* message_type) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  VLOG(2) << "Sending " << message_type;
  socket_->transport()->SendMessage(
      message, base::Bind(&KeepAliveDelegate::SendKeepAliveMessageComplete,
                          base::Unretained(this), message_type));
}

void KeepAliveDelegate::SendKeepAliveMessageComplete(const char* message_type,
                                                     int rv) {
  VLOG(2) << "Sending " << message_type << " complete, rv=" << rv;
  if (rv != net::OK) {
    // An error occurred while sending the ping response.
    VLOG(1) << "Error sending " << message_type;
    logger_->LogSocketEventWithRv(socket_->id(), ChannelEvent::PING_WRITE_ERROR,
                                  rv);
    OnError(ChannelError::CAST_SOCKET_ERROR);
  }
}

void KeepAliveDelegate::LivenessTimeout() {
  OnError(ChannelError::PING_TIMEOUT);
  Stop();
}

// CastTransport::Delegate interface.
void KeepAliveDelegate::OnError(ChannelError error_state) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  VLOG(1) << "KeepAlive::OnError: "
          << ::cast_channel::ChannelErrorToString(error_state);
  inner_delegate_->OnError(error_state);
  Stop();
}

void KeepAliveDelegate::OnMessage(const CastMessage& message) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  VLOG(2) << "KeepAlive::OnMessage : " << message.payload_utf8();

  if (started_)
    ResetTimers();

  // PING and PONG messages are intercepted and handled by KeepAliveDelegate
  // here. All other messages are passed through to |inner_delegate_|.
  const std::string payload_type = ParseForPayloadType(message);
  if (payload_type == kHeartbeatPingType) {
    VLOG(2) << "Received PING.";
    if (started_)
      SendKeepAliveMessage(pong_message_, kHeartbeatPongType);
  } else if (payload_type == kHeartbeatPongType) {
    VLOG(2) << "Received PONG.";
  } else {
    inner_delegate_->OnMessage(message);
  }
}

void KeepAliveDelegate::Stop() {
  if (started_) {
    started_ = false;
    ping_timer_->Stop();
    liveness_timer_->Stop();
  }
}

}  // namespace cast_channel
