// Copyright 2014 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/pairing/bluetooth_host_pairing_controller.h"

#include "base/bind.h"
#include "base/hash.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/task_runner_util.h"
#include "chromeos/system/devicetype.h"
#include "components/pairing/bluetooth_pairing_constants.h"
#include "components/pairing/pairing_api.pb.h"
#include "components/pairing/proto_decoder.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "net/base/io_buffer.h"

namespace pairing_chromeos {

namespace {
const int kReceiveSize = 16384;

std::string GetChromeOSDeviceType() {
  switch (chromeos::GetDeviceType()) {
    case chromeos::DeviceType::kChromebox:
      return std::string(kDeviceNamePrefix) + "box";
    case chromeos::DeviceType::kChromebase:
      return std::string(kDeviceNamePrefix) + "base";
    case chromeos::DeviceType::kChromebit:
      return std::string(kDeviceNamePrefix) + "bit";
    case chromeos::DeviceType::kChromebook:
      return std::string(kDeviceNamePrefix) + "book";
    default:
      return std::string(kDeviceNamePrefix) + "device";
  }
}

pairing_api::HostStatusParameters::Connectivity PairingApiConnectivityStatus(
    HostPairingController::Connectivity connectivity_status) {
  switch (connectivity_status) {
    case HostPairingController::CONNECTIVITY_UNTESTED:
      return pairing_api::HostStatusParameters::CONNECTIVITY_UNTESTED;
    case HostPairingController::CONNECTIVITY_NONE:
      return pairing_api::HostStatusParameters::CONNECTIVITY_NONE;
    case HostPairingController::CONNECTIVITY_LIMITED:
      return pairing_api::HostStatusParameters::CONNECTIVITY_LIMITED;
    case HostPairingController::CONNECTIVITY_CONNECTING:
      return pairing_api::HostStatusParameters::CONNECTIVITY_CONNECTING;
    case HostPairingController::CONNECTIVITY_CONNECTED:
      return pairing_api::HostStatusParameters::CONNECTIVITY_CONNECTED;
    default:
      NOTREACHED();
      return pairing_api::HostStatusParameters::CONNECTIVITY_UNTESTED;
  }
}

pairing_api::HostStatusParameters::UpdateStatus PairingApiUpdateStatus(
    HostPairingController::UpdateStatus update_status) {
  switch(update_status) {
    case HostPairingController::UPDATE_STATUS_UNKNOWN:
      return pairing_api::HostStatusParameters::UPDATE_STATUS_UNKNOWN;
    case HostPairingController::UPDATE_STATUS_UPDATING:
      return pairing_api::HostStatusParameters::UPDATE_STATUS_UPDATING;
    case HostPairingController::UPDATE_STATUS_REBOOTING:
      return pairing_api::HostStatusParameters::UPDATE_STATUS_REBOOTING;
    case HostPairingController::UPDATE_STATUS_UPDATED:
      return pairing_api::HostStatusParameters::UPDATE_STATUS_UPDATED;
    default:
      NOTREACHED();
      return pairing_api::HostStatusParameters::UPDATE_STATUS_UNKNOWN;
  }
}

pairing_api::HostStatusParameters::EnrollmentStatus PairingApiEnrollmentStatus(
    HostPairingController::EnrollmentStatus enrollment_status) {
  switch(enrollment_status) {
    case HostPairingController::ENROLLMENT_STATUS_UNKNOWN:
      return pairing_api::HostStatusParameters::ENROLLMENT_STATUS_UNKNOWN;
    case HostPairingController::ENROLLMENT_STATUS_ENROLLING:
      return pairing_api::HostStatusParameters::ENROLLMENT_STATUS_ENROLLING;
    case HostPairingController::ENROLLMENT_STATUS_FAILURE:
      return pairing_api::HostStatusParameters::ENROLLMENT_STATUS_FAILURE;
    case HostPairingController::ENROLLMENT_STATUS_SUCCESS:
      return pairing_api::HostStatusParameters::ENROLLMENT_STATUS_SUCCESS;
    default:
      NOTREACHED();
      return pairing_api::HostStatusParameters::ENROLLMENT_STATUS_UNKNOWN;
  }
}

std::vector<BluetoothHostPairingController::InputDeviceInfo> GetDevices() {
  std::vector<BluetoothHostPairingController::InputDeviceInfo> devices;
  if (device::InputServiceLinux::HasInstance())
    device::InputServiceLinux::GetInstance()->GetDevices(&devices);
  return devices;
}

}  // namespace

BluetoothHostPairingController::BluetoothHostPairingController(
    const scoped_refptr<base::SingleThreadTaskRunner>& file_task_runner)
    : current_stage_(STAGE_NONE),
      connectivity_status_(CONNECTIVITY_UNTESTED),
      update_status_(UPDATE_STATUS_UNKNOWN),
      enrollment_status_(ENROLLMENT_STATUS_UNKNOWN),
      proto_decoder_(new ProtoDecoder(this)),
      file_task_runner_(file_task_runner),
      ptr_factory_(this) {}

BluetoothHostPairingController::~BluetoothHostPairingController() {
  Reset();
}

void BluetoothHostPairingController::SetDelegateForTesting(
    TestDelegate* delegate) {
  delegate_ = delegate;
}

scoped_refptr<device::BluetoothAdapter>
BluetoothHostPairingController::GetAdapterForTesting() {
  return adapter_;
}

void BluetoothHostPairingController::ChangeStage(Stage new_stage) {
  if (current_stage_ == new_stage)
    return;
  VLOG(1) << "ChangeStage " << new_stage;
  current_stage_ = new_stage;
  for (Observer& observer : observers_)
    observer.PairingStageChanged(new_stage);
}

void BluetoothHostPairingController::SendHostStatus() {
  pairing_api::HostStatus host_status;

  host_status.set_api_version(kPairingAPIVersion);
  if (!enrollment_domain_.empty())
    host_status.mutable_parameters()->set_domain(enrollment_domain_);
  if (!permanent_id_.empty())
    host_status.mutable_parameters()->set_permanent_id(permanent_id_);

  // TODO(zork): Get these values from the UI. (http://crbug.com/405744)
  host_status.mutable_parameters()->set_connectivity(
      PairingApiConnectivityStatus(connectivity_status_));
  host_status.mutable_parameters()->set_update_status(
      PairingApiUpdateStatus(update_status_));
  host_status.mutable_parameters()->set_enrollment_status(
      PairingApiEnrollmentStatus(enrollment_status_));

  // TODO(zork): Get a list of other paired controllers.
  // (http://crbug.com/405757)

  int size = 0;
  scoped_refptr<net::IOBuffer> io_buffer(
      ProtoDecoder::SendHostStatus(host_status, &size));

  controller_socket_->Send(
      io_buffer, size,
      base::Bind(&BluetoothHostPairingController::OnSendComplete,
                 ptr_factory_.GetWeakPtr()),
      base::Bind(&BluetoothHostPairingController::OnSendError,
                 ptr_factory_.GetWeakPtr()));
}

void BluetoothHostPairingController::Reset() {
  if (controller_socket_.get()) {
    controller_socket_->Close();
    controller_socket_ = nullptr;
  }

  if (service_socket_.get()) {
    service_socket_->Close();
    service_socket_ = nullptr;
  }

  if (adapter_.get()) {
    if (adapter_->IsDiscoverable()) {
      adapter_->SetDiscoverable(false, base::Bind(&base::DoNothing),
                                base::Bind(&base::DoNothing));
    }

    base::PostTaskAndReplyWithResult(
        file_task_runner_.get(), FROM_HERE, base::Bind(&GetDevices),
        base::Bind(&BluetoothHostPairingController::PowerOffAdapterIfApplicable,
                   ptr_factory_.GetWeakPtr()));
  }
  ChangeStage(STAGE_NONE);
}

void BluetoothHostPairingController::OnGetAdapter(
    scoped_refptr<device::BluetoothAdapter> adapter) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(!adapter_.get());
  adapter_ = adapter;

  if (adapter_->IsPresent()) {
    SetName();
  } else {
    // Set the name once the adapter is present.
    adapter_->AddObserver(this);
  }
}

void BluetoothHostPairingController::SetName() {
  // Hash the bluetooth address and take the lower 2 bytes to create a human
  // readable device name.
  const uint32_t device_id = base::Hash(adapter_->GetAddress()) & 0xFFFF;
  device_name_ =
      base::StringPrintf("%s_%04X", GetChromeOSDeviceType().c_str(), device_id);

  adapter_->SetName(
      device_name_,
      base::Bind(&BluetoothHostPairingController::OnSetName,
                 ptr_factory_.GetWeakPtr()),
      base::Bind(&BluetoothHostPairingController::OnSetError,
                 ptr_factory_.GetWeakPtr()));
}

void BluetoothHostPairingController::OnSetName() {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (adapter_->IsPowered()) {
    was_powered_ = true;
    OnSetPowered();
  } else {
    adapter_->SetPowered(
        true,
        base::Bind(&BluetoothHostPairingController::OnSetPowered,
                   ptr_factory_.GetWeakPtr()),
        base::Bind(&BluetoothHostPairingController::OnSetError,
                   ptr_factory_.GetWeakPtr()));
  }
}

void BluetoothHostPairingController::OnSetPowered() {
  DCHECK(thread_checker_.CalledOnValidThread());
  adapter_->AddPairingDelegate(
      this, device::BluetoothAdapter::PAIRING_DELEGATE_PRIORITY_HIGH);

  device::BluetoothAdapter::ServiceOptions options;
  options.name.reset(new std::string(kPairingServiceName));

  adapter_->CreateRfcommService(
      device::BluetoothUUID(kPairingServiceUUID), options,
      base::Bind(&BluetoothHostPairingController::OnCreateService,
                 ptr_factory_.GetWeakPtr()),
      base::Bind(&BluetoothHostPairingController::OnCreateServiceError,
                 ptr_factory_.GetWeakPtr()));
}

void BluetoothHostPairingController::OnCreateService(
    scoped_refptr<device::BluetoothSocket> socket) {
  DCHECK(thread_checker_.CalledOnValidThread());
  service_socket_ = socket;

  service_socket_->Accept(
      base::Bind(&BluetoothHostPairingController::OnAccept,
                 ptr_factory_.GetWeakPtr()),
      base::Bind(&BluetoothHostPairingController::OnAcceptError,
                 ptr_factory_.GetWeakPtr()));

  adapter_->SetDiscoverable(
      true,
      base::Bind(&BluetoothHostPairingController::OnSetDiscoverable,
                 ptr_factory_.GetWeakPtr(), true),
      base::Bind(&BluetoothHostPairingController::OnSetError,
                 ptr_factory_.GetWeakPtr()));
}

void BluetoothHostPairingController::OnAccept(
    const device::BluetoothDevice* device,
    scoped_refptr<device::BluetoothSocket> socket) {
  DCHECK(thread_checker_.CalledOnValidThread());
  adapter_->SetDiscoverable(
      false,
      base::Bind(&BluetoothHostPairingController::OnSetDiscoverable,
                 ptr_factory_.GetWeakPtr(), false),
      base::Bind(&BluetoothHostPairingController::OnSetError,
                 ptr_factory_.GetWeakPtr()));

  controller_socket_ = socket;
  service_socket_ = nullptr;

  SendHostStatus();

  controller_socket_->Receive(
      kReceiveSize,
      base::Bind(&BluetoothHostPairingController::OnReceiveComplete,
                 ptr_factory_.GetWeakPtr()),
      base::Bind(&BluetoothHostPairingController::OnReceiveError,
                 ptr_factory_.GetWeakPtr()));
}

void BluetoothHostPairingController::OnSetDiscoverable(bool change_stage) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (change_stage) {
    DCHECK_EQ(current_stage_, STAGE_NONE);
    ChangeStage(STAGE_WAITING_FOR_CONTROLLER);
  }
}

void BluetoothHostPairingController::OnSendComplete(int bytes_sent) {}

void BluetoothHostPairingController::OnReceiveComplete(
    int bytes, scoped_refptr<net::IOBuffer> io_buffer) {
  DCHECK(thread_checker_.CalledOnValidThread());
  proto_decoder_->DecodeIOBuffer(bytes, io_buffer);

  if (controller_socket_.get()) {
    controller_socket_->Receive(
        kReceiveSize,
        base::Bind(&BluetoothHostPairingController::OnReceiveComplete,
                   ptr_factory_.GetWeakPtr()),
        base::Bind(&BluetoothHostPairingController::OnReceiveError,
                   ptr_factory_.GetWeakPtr()));
  }
}

void BluetoothHostPairingController::OnCreateServiceError(
    const std::string& message) {
  LOG(ERROR) << message;
  ChangeStage(STAGE_INITIALIZATION_ERROR);
}

void BluetoothHostPairingController::OnSetError() {
  adapter_->RemovePairingDelegate(this);
  ChangeStage(STAGE_INITIALIZATION_ERROR);
}

void BluetoothHostPairingController::OnAcceptError(
    const std::string& error_message) {
  LOG(ERROR) << error_message;
  ChangeStage(STAGE_CONTROLLER_CONNECTION_ERROR);
}

void BluetoothHostPairingController::OnSendError(
    const std::string& error_message) {
  LOG(ERROR) << error_message;
  if (enrollment_status_ != ENROLLMENT_STATUS_ENROLLING &&
      enrollment_status_ != ENROLLMENT_STATUS_SUCCESS) {
    ChangeStage(STAGE_CONTROLLER_CONNECTION_ERROR);
  }
}

void BluetoothHostPairingController::PowerOffAdapterIfApplicable(
    const std::vector<InputDeviceInfo>& devices) {
  bool use_bluetooth = false;
  for (const auto& device : devices) {
    if (device.type == InputDeviceInfo::TYPE_BLUETOOTH) {
      use_bluetooth = true;
      break;
    }
  }
  if (!was_powered_ && !use_bluetooth) {
    adapter_->SetPowered(
        false, base::Bind(&BluetoothHostPairingController::ResetAdapter,
                          ptr_factory_.GetWeakPtr()),
        base::Bind(&BluetoothHostPairingController::ResetAdapter,
                   ptr_factory_.GetWeakPtr()));
  } else {
    ResetAdapter();
  }
}

void BluetoothHostPairingController::ResetAdapter() {
  adapter_->RemoveObserver(this);
  adapter_ = nullptr;
  if (delegate_)
    delegate_->OnAdapterReset();
}

void BluetoothHostPairingController::OnReceiveError(
    device::BluetoothSocket::ErrorReason reason,
    const std::string& error_message) {
  LOG(ERROR) << reason << ", " << error_message;
  ChangeStage(STAGE_CONTROLLER_CONNECTION_ERROR);
}

void BluetoothHostPairingController::OnHostStatusMessage(
    const pairing_api::HostStatus& message) {
  NOTREACHED();
}

void BluetoothHostPairingController::OnConfigureHostMessage(
    const pairing_api::ConfigureHost& message) {
  ChangeStage(STAGE_SETUP_BASIC_CONFIGURATION);
  for (Observer& observer : observers_) {
    observer.ConfigureHostRequested(
        message.parameters().accepted_eula(), message.parameters().lang(),
        message.parameters().timezone(), message.parameters().send_reports(),
        message.parameters().keyboard_layout());
  }
}

void BluetoothHostPairingController::OnPairDevicesMessage(
    const pairing_api::PairDevices& message) {
  DCHECK(thread_checker_.CalledOnValidThread());
  enrollment_domain_ = message.parameters().enrolling_domain();
  ChangeStage(STAGE_ENROLLING);
  for (Observer& observer : observers_)
    observer.EnrollHostRequested(message.parameters().admin_access_token());
}

void BluetoothHostPairingController::OnCompleteSetupMessage(
    const pairing_api::CompleteSetup& message) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (current_stage_ != STAGE_ENROLLMENT_SUCCESS) {
    ChangeStage(STAGE_ENROLLMENT_ERROR);
  } else {
    // TODO(zork): Handle adding another controller. (http://crbug.com/405757)
    ChangeStage(STAGE_FINISHED);
  }
  Reset();
}

void BluetoothHostPairingController::OnErrorMessage(
    const pairing_api::Error& message) {
  NOTREACHED();
}

void BluetoothHostPairingController::OnAddNetworkMessage(
    const pairing_api::AddNetwork& message) {
  DCHECK(thread_checker_.CalledOnValidThread());
  for (Observer& observer : observers_)
    observer.AddNetworkRequested(message.parameters().onc_spec());
}

void BluetoothHostPairingController::AdapterPresentChanged(
    device::BluetoothAdapter* adapter,
    bool present) {
  DCHECK_EQ(adapter, adapter_.get());
  if (present) {
    adapter_->RemoveObserver(this);
    SetName();
  }
}

void BluetoothHostPairingController::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void BluetoothHostPairingController::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

HostPairingController::Stage BluetoothHostPairingController::GetCurrentStage() {
  return current_stage_;
}

void BluetoothHostPairingController::StartPairing() {
  DCHECK_EQ(current_stage_, STAGE_NONE);
  bool bluetooth_available =
      device::BluetoothAdapterFactory::IsBluetoothAdapterAvailable();
  if (!bluetooth_available) {
    ChangeStage(STAGE_INITIALIZATION_ERROR);
    return;
  }

  device::BluetoothAdapterFactory::GetAdapter(
      base::Bind(&BluetoothHostPairingController::OnGetAdapter,
                 ptr_factory_.GetWeakPtr()));
}

std::string BluetoothHostPairingController::GetDeviceName() {
  return device_name_;
}

std::string BluetoothHostPairingController::GetConfirmationCode() {
  DCHECK_EQ(current_stage_, STAGE_WAITING_FOR_CODE_CONFIRMATION);
  return confirmation_code_;
}

std::string BluetoothHostPairingController::GetEnrollmentDomain() {
  return enrollment_domain_;
}

void BluetoothHostPairingController::OnNetworkConnectivityChanged(
    Connectivity connectivity_status) {
  connectivity_status_ = connectivity_status;
  if (connectivity_status == CONNECTIVITY_NONE)
    ChangeStage(STAGE_SETUP_NETWORK_ERROR);
  SendHostStatus();
}

void BluetoothHostPairingController::OnUpdateStatusChanged(
    UpdateStatus update_status) {
  update_status_ = update_status;
  if (update_status == UPDATE_STATUS_UPDATED)
    ChangeStage(STAGE_WAITING_FOR_CREDENTIALS);
  SendHostStatus();
}

void BluetoothHostPairingController::OnEnrollmentStatusChanged(
    EnrollmentStatus enrollment_status) {
  DCHECK_EQ(current_stage_, STAGE_ENROLLING);
  DCHECK(thread_checker_.CalledOnValidThread());

  enrollment_status_ = enrollment_status;
  if (enrollment_status == ENROLLMENT_STATUS_SUCCESS) {
    ChangeStage(STAGE_ENROLLMENT_SUCCESS);
  } else if (enrollment_status == ENROLLMENT_STATUS_FAILURE) {
    ChangeStage(STAGE_ENROLLMENT_ERROR);
  }
  SendHostStatus();
}

void BluetoothHostPairingController::SetPermanentId(
    const std::string& permanent_id) {
  permanent_id_ = permanent_id;
}

void BluetoothHostPairingController::RequestPinCode(
    device::BluetoothDevice* device) {
  // Disallow unknown device.
  device->RejectPairing();
}

void BluetoothHostPairingController::RequestPasskey(
    device::BluetoothDevice* device) {
  // Disallow unknown device.
  device->RejectPairing();
}

void BluetoothHostPairingController::DisplayPinCode(
    device::BluetoothDevice* device,
    const std::string& pincode) {
  // Disallow unknown device.
  device->RejectPairing();
}

void BluetoothHostPairingController::DisplayPasskey(
    device::BluetoothDevice* device,
    uint32_t passkey) {
  // Disallow unknown device.
  device->RejectPairing();
}

void BluetoothHostPairingController::KeysEntered(
    device::BluetoothDevice* device,
    uint32_t entered) {
  // Disallow unknown device.
  device->RejectPairing();
}

void BluetoothHostPairingController::ConfirmPasskey(
    device::BluetoothDevice* device,
    uint32_t passkey) {
  // If a new connection is occurring, reset the stage.  This can occur if the
  // pairing times out, or a new controller connects.
  if (current_stage_ == STAGE_WAITING_FOR_CODE_CONFIRMATION)
    ChangeStage(STAGE_WAITING_FOR_CONTROLLER);

  confirmation_code_ = base::StringPrintf("%06d", passkey);
  device->ConfirmPairing();
  ChangeStage(STAGE_WAITING_FOR_CODE_CONFIRMATION);
}

void BluetoothHostPairingController::AuthorizePairing(
    device::BluetoothDevice* device) {
  // Disallow unknown device.
  device->RejectPairing();
}

}  // namespace pairing_chromeos
