// Copyright 2024 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_i2c_mcuxpresso/i3c_initiator.h"

#include <mutex>

#include "lib/stdcompat/utility.h"
#include "pw_bytes/span.h"
#include "pw_log/log.h"
#include "pw_status/status.h"
#include "pw_status/try.h"

using cpp23::to_underlying;

namespace pw::i2c {
namespace {
pw::Status HalStatusToPwStatus(status_t status) {
  switch (status) {
    case kStatus_Success:
      return pw::OkStatus();
    case kStatus_I3C_Timeout:
      return pw::Status::DeadlineExceeded();
    case kStatus_I3C_Nak:
    case kStatus_I3C_Busy:
    case kStatus_I3C_IBIWon:
    case kStatus_I3C_WriteAbort:
      return pw::Status::Unavailable();
    case kStatus_I3C_HdrParityError:
    case kStatus_I3C_CrcError:
    case kStatus_I3C_MsgError:
      return pw::Status::DataLoss();
    default:
      return pw::Status::Unknown();
  }
}

constexpr uint32_t kI3cInitOpenDrainBaudRate = 2000000;
constexpr uint32_t kI3cInitPushPullBaudRate = 4000000;
constexpr bool kI3cInitEnableOpenDrainHigh = false;
constexpr uint8_t kBroadcastAddressRaw = 0x7E;
constexpr pw::i2c::Address kBroadcastAddress =
    pw::i2c::Address::SevenBit<kBroadcastAddressRaw>();
std::array kDisecBuffer = {std::byte{0x0b}};
}  // namespace

// inclusive-language: disable
void I3cMcuxpressoInitiator::Enable() {
  std::lock_guard lock(mutex_);
  if (enabled_) {
    return;
  }

  i3c_master_config_t masterConfig;
  I3C_MasterGetDefaultConfig(&masterConfig);

  masterConfig.baudRate_Hz.i2cBaud = config_.i2c_baud_rate;
  masterConfig.baudRate_Hz.i3cOpenDrainBaud = config_.i3c_open_drain_baud_rate;
  masterConfig.baudRate_Hz.i3cPushPullBaud = config_.i3c_push_pull_baud_rate;
  masterConfig.enableOpenDrainStop = config_.enable_open_drain_stop;
  masterConfig.enableOpenDrainHigh = config_.enable_open_drain_high;

  I3C_MasterInit(base_, &masterConfig, CLOCK_GetI3cClkFreq());
  enabled_ = true;
}

void I3cMcuxpressoInitiator::Disable() {
  std::lock_guard lock(mutex_);
  if (!enabled_) {
    return;
  }

  I3C_MasterDeinit(base_);
  enabled_ = false;
}

pw::Status I3cMcuxpressoInitiator::SetDynamicAddressList(
    pw::span<const uint8_t> dynamic_address_list) {
  if (i3c_dynamic_address_list_.has_value()) {
    PW_LOG_ERROR("i3c_dynamic_address_list_ can only be set once");
    return pw::Status::AlreadyExists();
  }

  pw::Vector<uint8_t, I3C_MAX_DEVCNT> address_list_temp;
  size_t dynamic_address_num = dynamic_address_list.size();
  if (dynamic_address_num > I3C_MAX_DEVCNT) {
    PW_LOG_WARN("Only the first %d dynamic addresses are accepted",
                I3C_MAX_DEVCNT);
    dynamic_address_num = I3C_MAX_DEVCNT;
  }
  address_list_temp.resize(dynamic_address_num);
  std::copy(dynamic_address_list.begin(),
            dynamic_address_list.begin() + dynamic_address_num,
            address_list_temp.begin());
  i3c_dynamic_address_list_.emplace(address_list_temp);

  return pw::OkStatus();
}

pw::Status I3cMcuxpressoInitiator::Initialize() {
  std::lock_guard lock(mutex_);
  i3c_master_config_t masterConfig;

  if (!i3c_dynamic_address_list_.has_value() ||
      i3c_dynamic_address_list_->empty()) {
    PW_LOG_ERROR("Cannot initialize the bus without dynamic address");
    return pw::Status::FailedPrecondition();
  }
  // Initialize I3C master with low I3C speed to match I3C timing requirement
  // (mipi_I3C-Basic_specification_v1-1-1 section 6.2 Table 86 I3C Open Drain
  // Timing Parameters)
  I3C_MasterGetDefaultConfig(&masterConfig);
  masterConfig.baudRate_Hz.i2cBaud = config_.i2c_baud_rate;
  masterConfig.baudRate_Hz.i3cOpenDrainBaud = kI3cInitOpenDrainBaudRate;
  masterConfig.baudRate_Hz.i3cPushPullBaud = kI3cInitPushPullBaudRate;
  masterConfig.enableOpenDrainStop = config_.enable_open_drain_stop;
  masterConfig.enableOpenDrainHigh = kI3cInitEnableOpenDrainHigh;
  I3C_MasterInit(base_, &masterConfig, CLOCK_GetI3cClkFreq());

  // Broadcast RSTDAA
  // TODO: b/312487906 - First broadcast CCC receives NANK randomly on random
  // devices.
  if (pw::Status status = DoTransferCcc(I3cCccAction::kWrite,
                                        I3cCcc::kRstdaaBroadcast,
                                        kBroadcastAddress,
                                        pw::ByteSpan());
      !(status.ok())) {
    if (status != pw::Status::Unavailable()) {
      return status;
    }
    PW_LOG_WARN("Failed to broadcast first CCC, trying again...");
    PW_TRY(DoTransferCcc(I3cCccAction::kWrite,
                         I3cCcc::kRstdaaBroadcast,
                         kBroadcastAddress,
                         pw::ByteSpan()));
  }
  // Broadcast DISEC 0x0b
  PW_TRY(DoTransferCcc(I3cCccAction::kWrite,
                       I3cCcc::kDisecBroadcast,
                       kBroadcastAddress,
                       kDisecBuffer));
  // DAA
  status_t hal_status = I3C_MasterProcessDAA(base_,
                                             i3c_dynamic_address_list_->data(),
                                             i3c_dynamic_address_list_->size());
  if (hal_status != kStatus_Success) {
    PW_LOG_ERROR("Failed to initialize the I3C bus...");
  }

  // Re-initialize I3C master with user provided speed.
  I3C_MasterGetDefaultConfig(&masterConfig);
  masterConfig.baudRate_Hz.i2cBaud = config_.i2c_baud_rate;
  masterConfig.baudRate_Hz.i3cOpenDrainBaud = config_.i3c_open_drain_baud_rate;
  masterConfig.baudRate_Hz.i3cPushPullBaud = config_.i3c_push_pull_baud_rate;
  masterConfig.enableOpenDrainStop = config_.enable_open_drain_stop;
  masterConfig.enableOpenDrainHigh = config_.enable_open_drain_high;
  I3C_MasterInit(base_, &masterConfig, CLOCK_GetI3cClkFreq());

  return HalStatusToPwStatus(hal_status);
}

pw::Status I3cMcuxpressoInitiator::DoTransferCcc(I3cCccAction rnw,
                                                 I3cCcc ccc_id,
                                                 pw::i2c::Address address,
                                                 pw::ByteSpan buffer) {
  status_t status;
  i3c_master_transfer_t transfer;

  if (!enabled_) {
    return pw::Status::FailedPrecondition();
  }

  if (!(to_underlying(ccc_id) & kCccDirectBit)) {  // broadcast
    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = kBroadcastAddressRaw;
    transfer.direction = kI3C_Write;
    transfer.subaddress = static_cast<uint32_t>(ccc_id);
    transfer.subaddressSize = 1;
    transfer.data = buffer.data();
    transfer.dataSize = buffer.size();
    transfer.busType = kI3C_TypeI3CSdr;
    status = I3C_MasterTransferBlocking(base_, &transfer);
  } else {  // direct
    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = kBroadcastAddressRaw;
    transfer.direction = kI3C_Write;
    transfer.subaddress = static_cast<uint32_t>(ccc_id);
    transfer.subaddressSize = 1;
    transfer.data = nullptr;
    transfer.dataSize = 0;
    transfer.busType = kI3C_TypeI3CSdr;
    status = I3C_MasterTransferBlocking(base_, &transfer);
    if (status != kStatus_Success) {
      return HalStatusToPwStatus(status);
    }

    transfer.flags = kI3C_TransferRepeatedStartFlag;
    transfer.slaveAddress = uint32_t{address.GetSevenBit()};
    transfer.direction = (rnw == I3cCccAction::kWrite) ? kI3C_Write : kI3C_Read;
    transfer.subaddress = 0;
    transfer.subaddressSize = 0;
    transfer.data = buffer.data();
    transfer.dataSize = buffer.size();
    transfer.busType = kI3C_TypeI3CSdr;
    status |= I3C_MasterTransferBlocking(base_, &transfer);
  }
  return HalStatusToPwStatus(status);
}

pw::Status I3cMcuxpressoInitiator::DoWriteReadFor(
    pw::i2c::Address address,
    pw::ConstByteSpan tx_buffer,
    pw::ByteSpan rx_buffer,
    pw::chrono::SystemClock::duration) {
  std::lock_guard lock(mutex_);
  status_t status;
  i3c_master_transfer_t transfer;

  if (!enabled_) {
    return pw::Status::FailedPrecondition();
  }

  if (std::find(i3c_dynamic_address_list_->begin(),
                i3c_dynamic_address_list_->end(),
                address.GetSevenBit()) == i3c_dynamic_address_list_->end()) {
    transfer.busType = kI3C_TypeI2C;
  } else {
    transfer.busType = kI3C_TypeI3CSdr;
  }

  if (!tx_buffer.empty() && rx_buffer.empty()) {  // write only
    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = address.GetSevenBit();
    transfer.direction = kI3C_Write;
    transfer.subaddress = 0;
    transfer.subaddressSize = 0;
    transfer.data = const_cast<std::byte*>(tx_buffer.data());
    transfer.dataSize = tx_buffer.size();
    transfer.ibiResponse = kI3C_IbiRespNack;
    status = I3C_MasterTransferBlocking(base_, &transfer);
  } else if (tx_buffer.empty() && !rx_buffer.empty()) {  // read only
    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = address.GetSevenBit();
    transfer.direction = kI3C_Read;
    transfer.subaddress = 0;
    transfer.subaddressSize = 0;
    transfer.data = rx_buffer.data();
    transfer.dataSize = rx_buffer.size();
    transfer.dataSize = rx_buffer.size();
    transfer.ibiResponse = kI3C_IbiRespNack;
    status = I3C_MasterTransferBlocking(base_, &transfer);
  } else if (!tx_buffer.empty() && !rx_buffer.empty()) {  // write and read
    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = address.GetSevenBit();
    transfer.direction = kI3C_Write;
    transfer.subaddress = 0;
    transfer.subaddressSize = 0;
    transfer.data = const_cast<std::byte*>(tx_buffer.data());
    transfer.dataSize = tx_buffer.size();
    transfer.ibiResponse = kI3C_IbiRespNack;
    status = I3C_MasterTransferBlocking(base_, &transfer);
    if (status != kStatus_Success) {
      return HalStatusToPwStatus(status);
    }

    transfer.flags = kI3C_TransferDefaultFlag;
    transfer.slaveAddress = address.GetSevenBit();
    transfer.direction = kI3C_Read;
    transfer.subaddress = 0;
    transfer.subaddressSize = 0;
    transfer.data = rx_buffer.data();
    transfer.dataSize = rx_buffer.size();
    transfer.ibiResponse = kI3C_IbiRespNack;
    status = I3C_MasterTransferBlocking(base_, &transfer);
  } else {
    return pw::Status::InvalidArgument();
  }

  return HalStatusToPwStatus(status);
}
// inclusive-language: enable

}  // namespace pw::i2c
