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

#include <endian.h>

#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h"
#include "pw_bluetooth_sapphire/internal/host/common/assert.h"
#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h"
#include "pw_bluetooth_sapphire/internal/host/common/log.h"
#include "pw_bluetooth_sapphire/internal/host/hci-spec/util.h"
#include "pw_bluetooth_sapphire/internal/host/hci/sequential_command_runner.h"
#include "pw_bluetooth_sapphire/internal/host/transport/transport.h"

namespace bt::hci {

LegacyLowEnergyAdvertiser::~LegacyLowEnergyAdvertiser() {
  // This object is probably being destroyed because the stack is shutting down,
  // in which case the HCI layer may have already been destroyed.
  if (!hci().is_alive() || !hci()->command_channel()) {
    return;
  }
  StopAdvertising();
}

EmbossCommandPacket LegacyLowEnergyAdvertiser::BuildEnablePacket(
    const DeviceAddress& address,
    pw::bluetooth::emboss::GenericEnableParam enable) {
  auto packet = hci::EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetAdvertisingEnableCommandWriter>(
      hci_spec::kLESetAdvertisingEnable);
  auto packet_view = packet.view_t();
  packet_view.advertising_enable().Write(enable);
  return packet;
}

CommandChannel::CommandPacketVariant
LegacyLowEnergyAdvertiser::BuildSetAdvertisingData(const DeviceAddress& address,
                                                   const AdvertisingData& data,
                                                   AdvFlags flags) {
  auto packet = EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetAdvertisingDataCommandWriter>(
      hci_spec::kLESetAdvertisingData);
  auto params = packet.view_t();
  const uint8_t data_length =
      static_cast<uint8_t>(data.CalculateBlockSize(/*include_flags=*/true));
  params.advertising_data_length().Write(data_length);

  MutableBufferView adv_view(params.advertising_data().BackingStorage().data(),
                             data_length);
  data.WriteBlock(&adv_view, flags);

  return packet;
}

CommandChannel::CommandPacketVariant
LegacyLowEnergyAdvertiser::BuildSetScanResponse(
    const DeviceAddress& address, const AdvertisingData& scan_rsp) {
  auto packet = EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetScanResponseDataCommandWriter>(
      hci_spec::kLESetScanResponseData);
  auto params = packet.view_t();
  const uint8_t data_length =
      static_cast<uint8_t>(scan_rsp.CalculateBlockSize());
  params.scan_response_data_length().Write(data_length);

  MutableBufferView scan_data_view(
      params.scan_response_data().BackingStorage().data(), data_length);
  scan_rsp.WriteBlock(&scan_data_view, /*flags=*/std::nullopt);

  return packet;
}

CommandChannel::CommandPacketVariant
LegacyLowEnergyAdvertiser::BuildSetAdvertisingParams(
    const DeviceAddress& address,
    pw::bluetooth::emboss::LEAdvertisingType type,
    pw::bluetooth::emboss::LEOwnAddressType own_address_type,
    AdvertisingIntervalRange interval) {
  auto packet = EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetAdvertisingParametersCommandWriter>(
      hci_spec::kLESetAdvertisingParameters);
  auto params = packet.view_t();
  params.advertising_interval_min().UncheckedWrite(interval.min());
  params.advertising_interval_max().UncheckedWrite(interval.max());
  params.adv_type().Write(type);
  params.own_address_type().Write(own_address_type);
  params.advertising_channel_map().BackingStorage().WriteUInt(
      hci_spec::kLEAdvertisingChannelAll);
  params.advertising_filter_policy().Write(
      pw::bluetooth::emboss::LEAdvertisingFilterPolicy::ALLOW_ALL);

  // We don't support directed advertising yet, so leave peer_address and
  // peer_address_type as 0x00
  // (|packet| parameters are initialized to zero above).

  return packet;
}

CommandChannel::CommandPacketVariant
LegacyLowEnergyAdvertiser::BuildUnsetAdvertisingData(
    const DeviceAddress& address) {
  return EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetAdvertisingDataCommandWriter>(
      hci_spec::kLESetAdvertisingData);
}

CommandChannel::CommandPacketVariant
LegacyLowEnergyAdvertiser::BuildUnsetScanResponse(
    const DeviceAddress& address) {
  auto packet = EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetScanResponseDataCommandWriter>(
      hci_spec::kLESetScanResponseData);
  return packet;
}

EmbossCommandPacket LegacyLowEnergyAdvertiser::BuildRemoveAdvertisingSet(
    const DeviceAddress& address) {
  auto packet = hci::EmbossCommandPacket::New<
      pw::bluetooth::emboss::LESetAdvertisingEnableCommandWriter>(
      hci_spec::kLESetAdvertisingEnable);
  auto packet_view = packet.view_t();
  packet_view.advertising_enable().Write(
      pw::bluetooth::emboss::GenericEnableParam::DISABLE);
  return packet;
}

static EmbossCommandPacket BuildReadAdvertisingTxPower() {
  return EmbossCommandPacket::New<
      pw::bluetooth::emboss::LEReadAdvertisingChannelTxPowerCommandView>(
      hci_spec::kLEReadAdvertisingChannelTxPower);
}

void LegacyLowEnergyAdvertiser::StartAdvertising(
    const DeviceAddress& address,
    const AdvertisingData& data,
    const AdvertisingData& scan_rsp,
    AdvertisingOptions options,
    ConnectionCallback connect_callback,
    ResultFunction<> result_callback) {
  fit::result<HostError> result =
      CanStartAdvertising(address, data, scan_rsp, options);
  if (result.is_error()) {
    result_callback(ToResult(result.error_value()));
    return;
  }

  if (IsAdvertising() && !IsAdvertising(address)) {
    bt_log(INFO,
           "hci-le",
           "already advertising (only one advertisement supported at a time)");
    result_callback(ToResult(HostError::kNotSupported));
    return;
  }

  if (IsAdvertising()) {
    bt_log(DEBUG, "hci-le", "updating existing advertisement");
  }

  // Midst of a TX power level read - send a cancel over the previous status
  // callback.
  if (staged_params_.has_value()) {
    auto result_cb = std::move(staged_params_.value().result_callback);
    result_cb(ToResult(HostError::kCanceled));
  }

  // If the TX Power level is requested, then stage the parameters for the read
  // operation. If there already is an outstanding TX Power Level read request,
  // return early. Advertising on the outstanding call will now use the updated
  // |staged_params_|.
  if (options.include_tx_power_level) {
    AdvertisingData data_copy;
    data.Copy(&data_copy);

    AdvertisingData scan_rsp_copy;
    scan_rsp.Copy(&scan_rsp_copy);

    staged_params_ = StagedParams{address,
                                  options.interval,
                                  options.flags,
                                  std::move(data_copy),
                                  std::move(scan_rsp_copy),
                                  std::move(connect_callback),
                                  std::move(result_callback)};

    if (starting_ && hci_cmd_runner().IsReady()) {
      return;
    }
  }

  if (!hci_cmd_runner().IsReady()) {
    bt_log(DEBUG,
           "hci-le",
           "canceling advertising start/stop sequence due to new advertising "
           "request");
    // Abort any remaining commands from the current stop sequence. If we got
    // here then the controller MUST receive our request to disable advertising,
    // so the commands that we send next will overwrite the current advertising
    // settings and re-enable it.
    hci_cmd_runner().Cancel();
  }

  starting_ = true;

  // If the TX Power Level is requested, read it from the controller, update the
  // data buf, and proceed with starting advertising.
  //
  // If advertising was canceled during the TX power level read (either
  // |starting_| was reset or the |result_callback| was moved), return early.
  if (options.include_tx_power_level) {
    auto power_cb = [this](auto, const hci::EventPacket& event) mutable {
      BT_ASSERT(staged_params_.has_value());
      if (!starting_ || !staged_params_.value().result_callback) {
        bt_log(
            INFO, "hci-le", "Advertising canceled during TX Power Level read.");
        return;
      }

      if (hci_is_error(event, WARN, "hci-le", "read TX power level failed")) {
        staged_params_.value().result_callback(event.ToResult());
        staged_params_ = {};
        starting_ = false;
        return;
      }

      const auto& params = event.return_params<
          hci_spec::LEReadAdvertisingChannelTxPowerReturnParams>();

      // Update the advertising and scan response data with the TX power level.
      auto staged_params = std::move(staged_params_.value());
      staged_params.data.SetTxPower(params->tx_power);
      if (staged_params.scan_rsp.CalculateBlockSize()) {
        staged_params.scan_rsp.SetTxPower(params->tx_power);
      }
      // Reset the |staged_params_| as it is no longer in use.
      staged_params_ = {};

      StartAdvertisingInternal(
          staged_params.address,
          staged_params.data,
          staged_params.scan_rsp,
          staged_params.interval,
          staged_params.flags,
          std::move(staged_params.connect_callback),
          [this, result_callback = std::move(staged_params.result_callback)](
              const Result<>& result) {
            starting_ = false;
            result_callback(result);
          });
    };

    hci()->command_channel()->SendCommand(BuildReadAdvertisingTxPower(),
                                          std::move(power_cb));
    return;
  }

  StartAdvertisingInternal(address,
                           data,
                           scan_rsp,
                           options.interval,
                           options.flags,
                           std::move(connect_callback),
                           [this, result_callback = std::move(result_callback)](
                               const Result<>& result) {
                             starting_ = false;
                             result_callback(result);
                           });
}

void LegacyLowEnergyAdvertiser::StopAdvertising() {
  LowEnergyAdvertiser::StopAdvertising();
  starting_ = false;
}

void LegacyLowEnergyAdvertiser::StopAdvertising(const DeviceAddress& address) {
  if (!hci_cmd_runner().IsReady()) {
    hci_cmd_runner().Cancel();
  }

  LowEnergyAdvertiser::StopAdvertisingInternal(address);
  starting_ = false;
}

void LegacyLowEnergyAdvertiser::OnIncomingConnection(
    hci_spec::ConnectionHandle handle,
    pw::bluetooth::emboss::ConnectionRole role,
    const DeviceAddress& peer_address,
    const hci_spec::LEConnectionParameters& conn_params) {
  static DeviceAddress identity_address =
      DeviceAddress(DeviceAddress::Type::kLEPublic, {0});

  // We use the identity address as the local address if we aren't advertising.
  // If we aren't advertising, this is obviously wrong. However, the link will
  // be disconnected in that case before it can propagate to higher layers.
  DeviceAddress local_address = identity_address;
  if (IsAdvertising()) {
    local_address = connection_callbacks().begin()->first;
  }

  CompleteIncomingConnection(
      handle, role, local_address, peer_address, conn_params);
}

}  // namespace bt::hci
