// Copyright 2023 The Pigweed Authors
//
// 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
//
//     https://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.

#include "pw_bluetooth_sapphire/internal/host/hci/low_energy_advertiser.h"

#include "pw_bluetooth_sapphire/internal/host/hci/sequential_command_runner.h"

namespace bt::hci {

LowEnergyAdvertiser::LowEnergyAdvertiser(hci::Transport::WeakPtr hci)
    : hci_(std::move(hci)),
      hci_cmd_runner_(std::make_unique<SequentialCommandRunner>(
          hci_->command_channel()->AsWeakPtr())) {}

fit::result<HostError> LowEnergyAdvertiser::CanStartAdvertising(
    const DeviceAddress& address,
    const AdvertisingData& data,
    const AdvertisingData& scan_rsp,
    const AdvertisingOptions& options) const {
  BT_ASSERT(address.type() != DeviceAddress::Type::kBREDR);

  if (options.anonymous) {
    bt_log(WARN, "hci-le", "anonymous advertising not supported");
    return fit::error(HostError::kNotSupported);
  }

  // If the TX Power Level is requested, ensure both buffers have enough space.
  size_t size_limit = GetSizeLimit();
  if (options.include_tx_power_level) {
    size_limit -= kTLVTxPowerLevelSize;
  }

  if (size_t size = data.CalculateBlockSize(/*include_flags=*/true);
      size > size_limit) {
    bt_log(WARN,
           "hci-le",
           "advertising data too large (actual: %zu, max: %zu)",
           size,
           size_limit);
    return fit::error(HostError::kAdvertisingDataTooLong);
  }

  if (size_t size = scan_rsp.CalculateBlockSize(/*include_flags=*/false);
      size > size_limit) {
    bt_log(WARN,
           "hci-le",
           "scan response too large (actual: %zu, max: %zu)",
           size,
           size_limit);
    return fit::error(HostError::kScanResponseTooLong);
  }

  return fit::ok();
}

void LowEnergyAdvertiser::StartAdvertisingInternal(
    const DeviceAddress& address,
    const AdvertisingData& data,
    const AdvertisingData& scan_rsp,
    AdvertisingIntervalRange interval,
    AdvFlags flags,
    ConnectionCallback connect_callback,
    hci::ResultFunction<> result_callback) {
  if (IsAdvertising(address)) {
    // Temporarily disable advertising so we can tweak the parameters
    EmbossCommandPacket packet = BuildEnablePacket(
        address, pw::bluetooth::emboss::GenericEnableParam::DISABLE);
    hci_cmd_runner_->QueueCommand(packet);
  }

  // Set advertising parameters
  pw::bluetooth::emboss::LEAdvertisingType type =
      pw::bluetooth::emboss::LEAdvertisingType::NOT_CONNECTABLE_UNDIRECTED;
  if (connect_callback) {
    type = pw::bluetooth::emboss::LEAdvertisingType::
        CONNECTABLE_AND_SCANNABLE_UNDIRECTED;
  } else if (scan_rsp.CalculateBlockSize() > 0) {
    type = pw::bluetooth::emboss::LEAdvertisingType::SCANNABLE_UNDIRECTED;
  }

  pw::bluetooth::emboss::LEOwnAddressType own_addr_type;
  if (address.type() == DeviceAddress::Type::kLEPublic) {
    own_addr_type = pw::bluetooth::emboss::LEOwnAddressType::PUBLIC;
  } else {
    own_addr_type = pw::bluetooth::emboss::LEOwnAddressType::RANDOM;
  }

  data.Copy(&staged_parameters_.data);
  scan_rsp.Copy(&staged_parameters_.scan_rsp);

  using PacketPtr = std::unique_ptr<CommandPacket>;

  CommandChannel::CommandPacketVariant set_adv_params_packet =
      BuildSetAdvertisingParams(address, type, own_addr_type, interval);
  if (std::holds_alternative<PacketPtr>(set_adv_params_packet) &&
      !std::get<PacketPtr>(set_adv_params_packet)) {
    bt_log(WARN,
           "hci-le",
           "cannot build HCI set params packet for %s",
           bt_str(address));
    result_callback(ToResult(HostError::kCanceled));
    return;
  }

  hci_cmd_runner_->QueueCommand(
      std::move(set_adv_params_packet),
      fit::bind_member<&LowEnergyAdvertiser::OnSetAdvertisingParamsComplete>(
          this));

  // In order to support use cases where advertisers use the return parameters
  // of the SetAdvertisingParams HCI command, we place the remaining advertising
  // setup HCI commands in the result callback here. SequentialCommandRunner
  // doesn't allow enqueuing commands within a callback (during a run).
  hci_cmd_runner_->RunCommands([this,
                                address,
                                flags,
                                result_callback = std::move(result_callback),
                                connect_callback = std::move(connect_callback)](
                                   hci::Result<> result) mutable {
    if (bt_is_error(result,
                    WARN,
                    "hci-le",
                    "failed to start advertising for %s",
                    bt_str(address))) {
      result_callback(result);
      return;
    }

    bool success = StartAdvertisingInternalStep2(address,
                                                 flags,
                                                 std::move(connect_callback),
                                                 std::move(result_callback));
    if (!success) {
      result_callback(ToResult(HostError::kCanceled));
    }
  });
}

bool LowEnergyAdvertiser::StartAdvertisingInternalStep2(
    const DeviceAddress& address,
    AdvFlags flags,
    ConnectionCallback connect_callback,
    hci::ResultFunction<> result_callback) {
  using PacketPtr = std::unique_ptr<CommandPacket>;

  CommandChannel::CommandPacketVariant set_adv_data_packet =
      BuildSetAdvertisingData(address, staged_parameters_.data, flags);
  if (std::holds_alternative<PacketPtr>(set_adv_data_packet) &&
      !std::get<PacketPtr>(set_adv_data_packet)) {
    bt_log(WARN,
           "hci-le",
           "cannot build HCI set advertising data packet for %s",
           bt_str(address));
    return false;
  }

  CommandChannel::CommandPacketVariant set_scan_rsp_packet =
      BuildSetScanResponse(address, staged_parameters_.scan_rsp);
  if (std::holds_alternative<PacketPtr>(set_scan_rsp_packet) &&
      !std::get<PacketPtr>(set_scan_rsp_packet)) {
    bt_log(WARN,
           "hci-le",
           "cannot build HCI set scan response data packet for %s",
           bt_str(address));
    return false;
  }

  EmbossCommandPacket enable_packet = BuildEnablePacket(
      address, pw::bluetooth::emboss::GenericEnableParam::ENABLE);

  hci_cmd_runner_->QueueCommand(std::move(set_adv_data_packet));
  hci_cmd_runner_->QueueCommand(std::move(set_scan_rsp_packet));
  hci_cmd_runner_->QueueCommand(enable_packet);

  staged_parameters_.reset();
  hci_cmd_runner_->RunCommands([this,
                                address,
                                result_callback = std::move(result_callback),
                                connect_callback = std::move(connect_callback)](
                                   Result<> result) mutable {
    if (bt_is_error(result,
                    WARN,
                    "hci-le",
                    "failed to start advertising for %s",
                    bt_str(address))) {
    } else {
      bt_log(INFO, "hci-le", "advertising enabled for %s", bt_str(address));
      connection_callbacks_.emplace(address, std::move(connect_callback));
    }

    result_callback(result);
    OnCurrentOperationComplete();
  });

  return true;
}

// We have StopAdvertising(address) so one would naturally think to implement
// StopAdvertising() by iterating through all addresses and calling
// StopAdvertising(address) on each iteration. However, such an implementation
// won't work. Each call to StopAdvertising(address) checks if the command
// runner is running, cancels any pending commands if it is, and then issues new
// ones. Called in quick succession, StopAdvertising(address) won't have a
// chance to finish its previous HCI commands before being cancelled. Instead,
// we must enqueue them all at once and then run them together.
void LowEnergyAdvertiser::StopAdvertising() {
  if (!hci_cmd_runner_->IsReady()) {
    hci_cmd_runner_->Cancel();
  }

  for (auto itr = connection_callbacks_.begin();
       itr != connection_callbacks_.end();) {
    const DeviceAddress& address = itr->first;

    bool success = EnqueueStopAdvertisingCommands(address);
    if (success) {
      itr = connection_callbacks_.erase(itr);
    } else {
      bt_log(WARN, "hci-le", "cannot stop advertising for %s", bt_str(address));
      itr++;
    }
  }

  if (hci_cmd_runner_->HasQueuedCommands()) {
    hci_cmd_runner_->RunCommands([this](hci::Result<> result) {
      bt_log(INFO, "hci-le", "advertising stopped: %s", bt_str(result));
      OnCurrentOperationComplete();
    });
  }
}

void LowEnergyAdvertiser::StopAdvertisingInternal(
    const DeviceAddress& address) {
  if (!IsAdvertising(address)) {
    return;
  }

  bool success = EnqueueStopAdvertisingCommands(address);
  if (!success) {
    bt_log(WARN, "hci-le", "cannot stop advertising for %s", bt_str(address));
    return;
  }

  hci_cmd_runner_->RunCommands([this, address](Result<> result) {
    bt_log(INFO,
           "hci-le",
           "advertising stopped for %s: %s",
           bt_str(address),
           bt_str(result));
    OnCurrentOperationComplete();
  });

  connection_callbacks_.erase(address);
}

bool LowEnergyAdvertiser::EnqueueStopAdvertisingCommands(
    const DeviceAddress& address) {
  EmbossCommandPacket disable_packet = BuildEnablePacket(
      address, pw::bluetooth::emboss::GenericEnableParam::DISABLE);

  using PacketPtr = std::unique_ptr<hci::CommandPacket>;

  hci::CommandChannel::CommandPacketVariant unset_scan_rsp_packet =
      BuildUnsetScanResponse(address);
  if (std::holds_alternative<PacketPtr>(unset_scan_rsp_packet) &&
      !std::get<PacketPtr>(unset_scan_rsp_packet)) {
    bt_log(WARN,
           "hci-le",
           "cannot build HCI unset scan rsp packet for %s",
           bt_str(address));
    return false;
  }

  hci::CommandChannel::CommandPacketVariant unset_adv_data_packet =
      BuildUnsetAdvertisingData(address);
  if (std::holds_alternative<PacketPtr>(unset_adv_data_packet) &&
      !std::get<PacketPtr>(unset_adv_data_packet)) {
    bt_log(WARN,
           "hci-le",
           "cannot build HCI unset advertising data packet for %s",
           bt_str(address));
    return false;
  }

  EmbossCommandPacket remove_packet = BuildRemoveAdvertisingSet(address);

  hci_cmd_runner_->QueueCommand(disable_packet);
  hci_cmd_runner_->QueueCommand(std::move(unset_scan_rsp_packet));
  hci_cmd_runner_->QueueCommand(std::move(unset_adv_data_packet));
  hci_cmd_runner_->QueueCommand(remove_packet);

  return true;
}

void LowEnergyAdvertiser::CompleteIncomingConnection(
    hci_spec::ConnectionHandle handle,
    pw::bluetooth::emboss::ConnectionRole role,
    const DeviceAddress& local_address,
    const DeviceAddress& peer_address,
    const hci_spec::LEConnectionParameters& conn_params) {
  // Immediately construct a Connection object. If this object goes out of scope
  // following the error checks below, it will send the a command to disconnect
  // the link.
  std::unique_ptr<LowEnergyConnection> link =
      std::make_unique<LowEnergyConnection>(
          handle, local_address, peer_address, conn_params, role, hci());

  if (!IsAdvertising(local_address)) {
    bt_log(DEBUG,
           "hci-le",
           "connection received without advertising address (role: %d, local "
           "address: %s, peer "
           "address: %s, connection parameters: %s)",
           static_cast<uint8_t>(role),
           bt_str(local_address),
           bt_str(peer_address),
           bt_str(conn_params));
    return;
  }

  if (!connection_callbacks_[local_address]) {
    bt_log(WARN,
           "hci-le",
           "connection received when not connectable (role: %d, local address: "
           "%s, peer "
           "address: %s, connection parameters: %s)",
           static_cast<uint8_t>(role),
           bt_str(local_address),
           bt_str(peer_address),
           bt_str(conn_params));
    return;
  }

  ConnectionCallback connect_callback =
      std::move(connection_callbacks_[local_address]);
  StopAdvertising(local_address);
  connect_callback(std::move(link));
}

}  // namespace bt::hci
