/*----------------------------------------------------------------------------*/
/* Copyright (c) 2016-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

#include "hal/SPI.h"

#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <array>
#include <atomic>
#include <cstring>

#include <wpi/mutex.h>
#include <wpi/raw_ostream.h>

#include "DigitalInternal.h"
#include "HALInitializer.h"
#include "hal/DIO.h"
#include "hal/HAL.h"
#include "hal/handles/HandlesInternal.h"

using namespace hal;

static int32_t m_spiCS0Handle{0};
static int32_t m_spiCS1Handle{0};
static int32_t m_spiCS2Handle{0};
static int32_t m_spiCS3Handle{0};
static int32_t m_spiMXPHandle{0};

static constexpr int32_t kSpiMaxHandles = 5;

// Indices 0-3 are for onboard CS0-CS2. Index 4 is for MXP.
static std::array<wpi::mutex, kSpiMaxHandles> spiHandleMutexes;
static std::array<wpi::mutex, kSpiMaxHandles> spiApiMutexes;
static std::array<wpi::mutex, kSpiMaxHandles> spiAccumulatorMutexes;

// MXP SPI does not count towards this
static std::atomic<int32_t> spiPortCount{0};

static HAL_DigitalHandle digitalHandles[9]{HAL_kInvalidHandle};

static wpi::mutex spiAutoMutex;
static int32_t spiAutoPort = kSpiMaxHandles;
static std::atomic_bool spiAutoRunning{false};
static std::unique_ptr<tDMAManager> spiAutoDMA;

static bool SPIInUseByAuto(HAL_SPIPort port) {
  // SPI engine conflicts with any other chip selects on the same SPI device.
  // There are two SPI devices: one for ports 0-3 (onboard), the other for port
  // 4 (MXP).
  if (!spiAutoRunning) return false;
  std::scoped_lock lock(spiAutoMutex);
  return (spiAutoPort >= 0 && spiAutoPort <= 3 && port >= 0 && port <= 3) ||
         (spiAutoPort == 4 && port == 4);
}

namespace hal {
namespace init {
void InitializeSPI() {}
}  // namespace init
}  // namespace hal

extern "C" {

static void CommonSPIPortInit(int32_t* status) {
  // All false cases will set
  if (spiPortCount.fetch_add(1) == 0) {
    // Have not been initialized yet
    initializeDigital(status);
    if (*status != 0) return;
    // MISO
    if ((digitalHandles[3] = HAL_InitializeDIOPort(createPortHandleForSPI(29),
                                                   false, status)) ==
        HAL_kInvalidHandle) {
      std::printf("Failed to allocate DIO 29 (MISO)\n");
      return;
    }
    // MOSI
    if ((digitalHandles[4] = HAL_InitializeDIOPort(createPortHandleForSPI(30),
                                                   false, status)) ==
        HAL_kInvalidHandle) {
      std::printf("Failed to allocate DIO 30 (MOSI)\n");
      HAL_FreeDIOPort(digitalHandles[3]);  // free the first port allocated
      return;
    }
  }
}

static void CommonSPIPortFree(void) {
  if (spiPortCount.fetch_sub(1) == 1) {
    // Clean up SPI Handles
    HAL_FreeDIOPort(digitalHandles[3]);
    HAL_FreeDIOPort(digitalHandles[4]);
  }
}

void HAL_InitializeSPI(HAL_SPIPort port, int32_t* status) {
  hal::init::CheckInit();
  if (port < 0 || port >= kSpiMaxHandles) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  int handle;
  if (HAL_GetSPIHandle(port) != 0) return;
  switch (port) {
    case HAL_SPI_kOnboardCS0:
      CommonSPIPortInit(status);
      if (*status != 0) return;
      // CS0 is not a DIO port, so nothing to allocate
      handle = open("/dev/spidev0.0", O_RDWR);
      if (handle < 0) {
        std::printf("Failed to open SPI port %d: %s\n", port,
                    std::strerror(errno));
        CommonSPIPortFree();
        return;
      }
      HAL_SetSPIHandle(HAL_SPI_kOnboardCS0, handle);
      break;
    case HAL_SPI_kOnboardCS1:
      CommonSPIPortInit(status);
      if (*status != 0) return;
      // CS1, Allocate
      if ((digitalHandles[0] = HAL_InitializeDIOPort(createPortHandleForSPI(26),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        std::printf("Failed to allocate DIO 26 (CS1)\n");
        CommonSPIPortFree();
        return;
      }
      handle = open("/dev/spidev0.1", O_RDWR);
      if (handle < 0) {
        std::printf("Failed to open SPI port %d: %s\n", port,
                    std::strerror(errno));
        CommonSPIPortFree();
        HAL_FreeDIOPort(digitalHandles[0]);
        return;
      }
      HAL_SetSPIHandle(HAL_SPI_kOnboardCS1, handle);
      break;
    case HAL_SPI_kOnboardCS2:
      CommonSPIPortInit(status);
      if (*status != 0) return;
      // CS2, Allocate
      if ((digitalHandles[1] = HAL_InitializeDIOPort(createPortHandleForSPI(27),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        std::printf("Failed to allocate DIO 27 (CS2)\n");
        CommonSPIPortFree();
        return;
      }
      handle = open("/dev/spidev0.2", O_RDWR);
      if (handle < 0) {
        std::printf("Failed to open SPI port %d: %s\n", port,
                    std::strerror(errno));
        CommonSPIPortFree();
        HAL_FreeDIOPort(digitalHandles[1]);
        return;
      }
      HAL_SetSPIHandle(HAL_SPI_kOnboardCS2, handle);
      break;
    case HAL_SPI_kOnboardCS3:
      CommonSPIPortInit(status);
      if (*status != 0) return;
      // CS3, Allocate
      if ((digitalHandles[2] = HAL_InitializeDIOPort(createPortHandleForSPI(28),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        std::printf("Failed to allocate DIO 28 (CS3)\n");
        CommonSPIPortFree();
        return;
      }
      handle = open("/dev/spidev0.3", O_RDWR);
      if (handle < 0) {
        std::printf("Failed to open SPI port %d: %s\n", port,
                    std::strerror(errno));
        CommonSPIPortFree();
        HAL_FreeDIOPort(digitalHandles[2]);
        return;
      }
      HAL_SetSPIHandle(HAL_SPI_kOnboardCS3, handle);
      break;
    case HAL_SPI_kMXP:
      initializeDigital(status);
      if (*status != 0) return;
      if ((digitalHandles[5] = HAL_InitializeDIOPort(createPortHandleForSPI(14),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        wpi::outs() << "Failed to allocate DIO 14\n";
        return;
      }
      if ((digitalHandles[6] = HAL_InitializeDIOPort(createPortHandleForSPI(15),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        wpi::outs() << "Failed to allocate DIO 15\n";
        HAL_FreeDIOPort(digitalHandles[5]);  // free the first port allocated
        return;
      }
      if ((digitalHandles[7] = HAL_InitializeDIOPort(createPortHandleForSPI(16),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        wpi::outs() << "Failed to allocate DIO 16\n";
        HAL_FreeDIOPort(digitalHandles[5]);  // free the first port allocated
        HAL_FreeDIOPort(digitalHandles[6]);  // free the second port allocated
        return;
      }
      if ((digitalHandles[8] = HAL_InitializeDIOPort(createPortHandleForSPI(17),
                                                     false, status)) ==
          HAL_kInvalidHandle) {
        wpi::outs() << "Failed to allocate DIO 17\n";
        HAL_FreeDIOPort(digitalHandles[5]);  // free the first port allocated
        HAL_FreeDIOPort(digitalHandles[6]);  // free the second port allocated
        HAL_FreeDIOPort(digitalHandles[7]);  // free the third port allocated
        return;
      }
      digitalSystem->writeEnableMXPSpecialFunction(
          digitalSystem->readEnableMXPSpecialFunction(status) | 0x00F0, status);
      handle = open("/dev/spidev1.0", O_RDWR);
      if (handle < 0) {
        std::printf("Failed to open SPI port %d: %s\n", port,
                    std::strerror(errno));
        HAL_FreeDIOPort(digitalHandles[5]);  // free the first port allocated
        HAL_FreeDIOPort(digitalHandles[6]);  // free the second port allocated
        HAL_FreeDIOPort(digitalHandles[7]);  // free the third port allocated
        HAL_FreeDIOPort(digitalHandles[8]);  // free the fourth port allocated
        return;
      }
      HAL_SetSPIHandle(HAL_SPI_kMXP, handle);
      break;
    default:
      *status = PARAMETER_OUT_OF_RANGE;
      break;
  }
}

int32_t HAL_TransactionSPI(HAL_SPIPort port, const uint8_t* dataToSend,
                           uint8_t* dataReceived, int32_t size) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return -1;
  }

  if (SPIInUseByAuto(port)) return -1;

  struct spi_ioc_transfer xfer;
  std::memset(&xfer, 0, sizeof(xfer));
  xfer.tx_buf = (__u64)dataToSend;
  xfer.rx_buf = (__u64)dataReceived;
  xfer.len = size;

  std::scoped_lock lock(spiApiMutexes[port]);
  return ioctl(HAL_GetSPIHandle(port), SPI_IOC_MESSAGE(1), &xfer);
}

int32_t HAL_WriteSPI(HAL_SPIPort port, const uint8_t* dataToSend,
                     int32_t sendSize) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return -1;
  }

  if (SPIInUseByAuto(port)) return -1;

  struct spi_ioc_transfer xfer;
  std::memset(&xfer, 0, sizeof(xfer));
  xfer.tx_buf = (__u64)dataToSend;
  xfer.len = sendSize;

  std::scoped_lock lock(spiApiMutexes[port]);
  return ioctl(HAL_GetSPIHandle(port), SPI_IOC_MESSAGE(1), &xfer);
}

int32_t HAL_ReadSPI(HAL_SPIPort port, uint8_t* buffer, int32_t count) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return -1;
  }

  if (SPIInUseByAuto(port)) return -1;

  struct spi_ioc_transfer xfer;
  std::memset(&xfer, 0, sizeof(xfer));
  xfer.rx_buf = (__u64)buffer;
  xfer.len = count;

  std::scoped_lock lock(spiApiMutexes[port]);
  return ioctl(HAL_GetSPIHandle(port), SPI_IOC_MESSAGE(1), &xfer);
}

void HAL_CloseSPI(HAL_SPIPort port) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return;
  }

  int32_t status = 0;
  HAL_FreeSPIAuto(port, &status);

  {
    std::scoped_lock lock(spiApiMutexes[port]);
    close(HAL_GetSPIHandle(port));
  }

  HAL_SetSPIHandle(port, 0);
  if (port < 4) {
    CommonSPIPortFree();
  }

  switch (port) {
    // Case 0 does not need to do anything
    case 1:
      HAL_FreeDIOPort(digitalHandles[0]);
      break;
    case 2:
      HAL_FreeDIOPort(digitalHandles[1]);
      break;
    case 3:
      HAL_FreeDIOPort(digitalHandles[2]);
      break;
    case 4:
      HAL_FreeDIOPort(digitalHandles[5]);
      HAL_FreeDIOPort(digitalHandles[6]);
      HAL_FreeDIOPort(digitalHandles[7]);
      HAL_FreeDIOPort(digitalHandles[8]);
      break;
    default:
      break;
  }
}

void HAL_SetSPISpeed(HAL_SPIPort port, int32_t speed) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return;
  }

  std::scoped_lock lock(spiApiMutexes[port]);
  ioctl(HAL_GetSPIHandle(port), SPI_IOC_WR_MAX_SPEED_HZ, &speed);
}

void HAL_SetSPIOpts(HAL_SPIPort port, HAL_Bool msbFirst,
                    HAL_Bool sampleOnTrailing, HAL_Bool clkIdleHigh) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return;
  }

  uint8_t mode = 0;
  mode |= (!msbFirst ? 8 : 0);
  mode |= (clkIdleHigh ? 2 : 0);
  mode |= (sampleOnTrailing ? 1 : 0);

  std::scoped_lock lock(spiApiMutexes[port]);
  ioctl(HAL_GetSPIHandle(port), SPI_IOC_WR_MODE, &mode);
}

void HAL_SetSPIChipSelectActiveHigh(HAL_SPIPort port, int32_t* status) {
  if (port < 0 || port >= kSpiMaxHandles) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  std::scoped_lock lock(spiApiMutexes[port]);
  if (port < 4) {
    spiSystem->writeChipSelectActiveHigh_Hdr(
        spiSystem->readChipSelectActiveHigh_Hdr(status) | (1 << port), status);
  } else {
    spiSystem->writeChipSelectActiveHigh_MXP(1, status);
  }
}

void HAL_SetSPIChipSelectActiveLow(HAL_SPIPort port, int32_t* status) {
  if (port < 0 || port >= kSpiMaxHandles) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  std::scoped_lock lock(spiApiMutexes[port]);
  if (port < 4) {
    spiSystem->writeChipSelectActiveHigh_Hdr(
        spiSystem->readChipSelectActiveHigh_Hdr(status) & ~(1 << port), status);
  } else {
    spiSystem->writeChipSelectActiveHigh_MXP(0, status);
  }
}

int32_t HAL_GetSPIHandle(HAL_SPIPort port) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return 0;
  }

  std::scoped_lock lock(spiHandleMutexes[port]);
  switch (port) {
    case 0:
      return m_spiCS0Handle;
    case 1:
      return m_spiCS1Handle;
    case 2:
      return m_spiCS2Handle;
    case 3:
      return m_spiCS3Handle;
    case 4:
      return m_spiMXPHandle;
    default:
      return 0;
  }
}

void HAL_SetSPIHandle(HAL_SPIPort port, int32_t handle) {
  if (port < 0 || port >= kSpiMaxHandles) {
    return;
  }

  std::scoped_lock lock(spiHandleMutexes[port]);
  switch (port) {
    case 0:
      m_spiCS0Handle = handle;
      break;
    case 1:
      m_spiCS1Handle = handle;
      break;
    case 2:
      m_spiCS2Handle = handle;
      break;
    case 3:
      m_spiCS3Handle = handle;
      break;
    case 4:
      m_spiMXPHandle = handle;
      break;
    default:
      break;
  }
}

void HAL_InitSPIAuto(HAL_SPIPort port, int32_t bufferSize, int32_t* status) {
  if (port < 0 || port >= kSpiMaxHandles) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (spiAutoPort != kSpiMaxHandles) {
    *status = RESOURCE_IS_ALLOCATED;
    return;
  }

  // remember the initialized port for other entry points
  spiAutoPort = port;

  // configure the correct chip select
  if (port < 4) {
    spiSystem->writeAutoSPI1Select(false, status);
    spiSystem->writeAutoChipSelect(port, status);
  } else {
    spiSystem->writeAutoSPI1Select(true, status);
    spiSystem->writeAutoChipSelect(0, status);
  }

  // configure DMA
  tDMAChannelDescriptor desc;
  spiSystem->getSystemInterface()->getDmaDescriptor(g_SpiAutoData_index, &desc);
  spiAutoDMA = std::make_unique<tDMAManager>(desc.channel, bufferSize, status);
}

void HAL_FreeSPIAuto(HAL_SPIPort port, int32_t* status) {
  if (port < 0 || port >= kSpiMaxHandles) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  std::scoped_lock lock(spiAutoMutex);
  if (spiAutoPort != port) return;
  spiAutoPort = kSpiMaxHandles;

  // disable by setting to internal clock and setting rate=0
  spiSystem->writeAutoRate(0, status);
  spiSystem->writeAutoTriggerConfig_ExternalClock(false, status);

  // stop the DMA
  spiAutoDMA->stop(status);

  spiAutoDMA.reset(nullptr);

  spiAutoRunning = false;
}

void HAL_StartSPIAutoRate(HAL_SPIPort port, double period, int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  spiAutoRunning = true;

  // start the DMA
  spiAutoDMA->start(status);

  // auto rate is in microseconds
  spiSystem->writeAutoRate(period * 1000000, status);

  // disable the external clock
  spiSystem->writeAutoTriggerConfig_ExternalClock(false, status);
}

void HAL_StartSPIAutoTrigger(HAL_SPIPort port, HAL_Handle digitalSourceHandle,
                             HAL_AnalogTriggerType analogTriggerType,
                             HAL_Bool triggerRising, HAL_Bool triggerFalling,
                             int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  spiAutoRunning = true;

  // start the DMA
  spiAutoDMA->start(status);

  // get channel routing
  bool routingAnalogTrigger = false;
  uint8_t routingChannel = 0;
  uint8_t routingModule = 0;
  if (!remapDigitalSource(digitalSourceHandle, analogTriggerType,
                          routingChannel, routingModule,
                          routingAnalogTrigger)) {
    *status = HAL_HANDLE_ERROR;
    return;
  }

  // configure external trigger and enable it
  tSPI::tAutoTriggerConfig config;
  config.ExternalClock = 1;
  config.FallingEdge = triggerFalling ? 1 : 0;
  config.RisingEdge = triggerRising ? 1 : 0;
  config.ExternalClockSource_AnalogTrigger = routingAnalogTrigger ? 1 : 0;
  config.ExternalClockSource_Module = routingModule;
  config.ExternalClockSource_Channel = routingChannel;
  spiSystem->writeAutoTriggerConfig(config, status);
}

void HAL_StopSPIAuto(HAL_SPIPort port, int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  // disable by setting to internal clock and setting rate=0
  spiSystem->writeAutoRate(0, status);
  spiSystem->writeAutoTriggerConfig_ExternalClock(false, status);

  // stop the DMA
  spiAutoDMA->stop(status);

  spiAutoRunning = false;
}

void HAL_SetSPIAutoTransmitData(HAL_SPIPort port, const uint8_t* dataToSend,
                                int32_t dataSize, int32_t zeroSize,
                                int32_t* status) {
  if (dataSize < 0 || dataSize > 32) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  if (zeroSize < 0 || zeroSize > 127) {
    *status = PARAMETER_OUT_OF_RANGE;
    return;
  }

  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  // set tx data registers
  for (int32_t i = 0; i < dataSize; ++i)
    spiSystem->writeAutoTx(i >> 2, i & 3, dataToSend[i], status);

  // set byte counts
  tSPI::tAutoByteCount config;
  config.ZeroByteCount = static_cast<unsigned>(zeroSize) & 0x7f;
  config.TxByteCount = static_cast<unsigned>(dataSize) & 0x1f;
  spiSystem->writeAutoByteCount(config, status);
}

void HAL_ForceSPIAutoRead(HAL_SPIPort port, int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  spiSystem->strobeAutoForceOne(status);
}

int32_t HAL_ReadSPIAutoReceivedData(HAL_SPIPort port, uint32_t* buffer,
                                    int32_t numToRead, double timeout,
                                    int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return 0;
  }

  size_t numRemaining = 0;
  // timeout is in ms
  spiAutoDMA->read(buffer, numToRead, timeout * 1000, &numRemaining, status);
  return numRemaining;
}

int32_t HAL_GetSPIAutoDroppedCount(HAL_SPIPort port, int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return 0;
  }

  return spiSystem->readTransferSkippedFullCount(status);
}

void HAL_ConfigureSPIAutoStall(HAL_SPIPort port, int32_t csToSclkTicks,
                               int32_t stallTicks, int32_t pow2BytesPerRead,
                               int32_t* status) {
  std::scoped_lock lock(spiAutoMutex);
  // FPGA only has one auto SPI engine
  if (port != spiAutoPort) {
    *status = INCOMPATIBLE_STATE;
    return;
  }

  tSPI::tStallConfig stallConfig;
  stallConfig.CsToSclkTicks = static_cast<uint8_t>(csToSclkTicks);
  stallConfig.StallTicks = static_cast<uint16_t>(stallTicks);
  stallConfig.Pow2BytesPerRead = static_cast<uint8_t>(pow2BytesPerRead);
  spiSystem->writeStallConfig(stallConfig, status);
}

}  // extern "C"
