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

#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <cstring>

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

using namespace hal;

static wpi::mutex digitalI2COnBoardMutex;
static wpi::mutex digitalI2CMXPMutex;

static uint8_t i2COnboardObjCount{0};
static uint8_t i2CMXPObjCount{0};
static int i2COnBoardHandle{-1};
static int i2CMXPHandle{-1};

static HAL_DigitalHandle i2CMXPDigitalHandle1{HAL_kInvalidHandle};
static HAL_DigitalHandle i2CMXPDigitalHandle2{HAL_kInvalidHandle};

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

extern "C" {

void HAL_InitializeI2C(HAL_I2CPort port, int32_t* status) {
  hal::init::CheckInit();
  initializeDigital(status);
  if (*status != 0) return;

  if (port < 0 || port > 1) {
    // Set port out of range error here
    return;
  }

  if (port == HAL_I2C_kOnboard) {
    std::scoped_lock lock(digitalI2COnBoardMutex);
    i2COnboardObjCount++;
    if (i2COnboardObjCount > 1) return;
    int handle = open("/dev/i2c-2", O_RDWR);
    if (handle < 0) {
      std::printf("Failed to open onboard i2c bus: %s\n", std::strerror(errno));
      return;
    }
    i2COnBoardHandle = handle;
  } else {
    std::scoped_lock lock(digitalI2CMXPMutex);
    i2CMXPObjCount++;
    if (i2CMXPObjCount > 1) return;
    if ((i2CMXPDigitalHandle1 = HAL_InitializeDIOPort(
             HAL_GetPort(24), false, status)) == HAL_kInvalidHandle) {
      return;
    }
    if ((i2CMXPDigitalHandle2 = HAL_InitializeDIOPort(
             HAL_GetPort(25), false, status)) == HAL_kInvalidHandle) {
      HAL_FreeDIOPort(i2CMXPDigitalHandle1);  // free the first port allocated
      return;
    }
    digitalSystem->writeEnableMXPSpecialFunction(
        digitalSystem->readEnableMXPSpecialFunction(status) | 0xC000, status);
    int handle = open("/dev/i2c-1", O_RDWR);
    if (handle < 0) {
      std::printf("Failed to open MXP i2c bus: %s\n", std::strerror(errno));
      return;
    }
    i2CMXPHandle = handle;
  }
}

int32_t HAL_TransactionI2C(HAL_I2CPort port, int32_t deviceAddress,
                           const uint8_t* dataToSend, int32_t sendSize,
                           uint8_t* dataReceived, int32_t receiveSize) {
  if (port < 0 || port > 1) {
    // Set port out of range error here
    return -1;
  }

  struct i2c_msg msgs[2];
  msgs[0].addr = deviceAddress;
  msgs[0].flags = 0;
  msgs[0].len = sendSize;
  msgs[0].buf = const_cast<uint8_t*>(dataToSend);
  msgs[1].addr = deviceAddress;
  msgs[1].flags = I2C_M_RD;
  msgs[1].len = receiveSize;
  msgs[1].buf = dataReceived;

  struct i2c_rdwr_ioctl_data rdwr;
  rdwr.msgs = msgs;
  rdwr.nmsgs = 2;

  if (port == HAL_I2C_kOnboard) {
    std::scoped_lock lock(digitalI2COnBoardMutex);
    return ioctl(i2COnBoardHandle, I2C_RDWR, &rdwr);
  } else {
    std::scoped_lock lock(digitalI2CMXPMutex);
    return ioctl(i2CMXPHandle, I2C_RDWR, &rdwr);
  }
}

int32_t HAL_WriteI2C(HAL_I2CPort port, int32_t deviceAddress,
                     const uint8_t* dataToSend, int32_t sendSize) {
  if (port < 0 || port > 1) {
    // Set port out of range error here
    return -1;
  }

  struct i2c_msg msg;
  msg.addr = deviceAddress;
  msg.flags = 0;
  msg.len = sendSize;
  msg.buf = const_cast<uint8_t*>(dataToSend);

  struct i2c_rdwr_ioctl_data rdwr;
  rdwr.msgs = &msg;
  rdwr.nmsgs = 1;

  if (port == HAL_I2C_kOnboard) {
    std::scoped_lock lock(digitalI2COnBoardMutex);
    return ioctl(i2COnBoardHandle, I2C_RDWR, &rdwr);
  } else {
    std::scoped_lock lock(digitalI2CMXPMutex);
    return ioctl(i2CMXPHandle, I2C_RDWR, &rdwr);
  }
}

int32_t HAL_ReadI2C(HAL_I2CPort port, int32_t deviceAddress, uint8_t* buffer,
                    int32_t count) {
  if (port < 0 || port > 1) {
    // Set port out of range error here
    return -1;
  }

  struct i2c_msg msg;
  msg.addr = deviceAddress;
  msg.flags = I2C_M_RD;
  msg.len = count;
  msg.buf = buffer;

  struct i2c_rdwr_ioctl_data rdwr;
  rdwr.msgs = &msg;
  rdwr.nmsgs = 1;

  if (port == HAL_I2C_kOnboard) {
    std::scoped_lock lock(digitalI2COnBoardMutex);
    return ioctl(i2COnBoardHandle, I2C_RDWR, &rdwr);
  } else {
    std::scoped_lock lock(digitalI2CMXPMutex);
    return ioctl(i2CMXPHandle, I2C_RDWR, &rdwr);
  }
}

void HAL_CloseI2C(HAL_I2CPort port) {
  if (port < 0 || port > 1) {
    // Set port out of range error here
    return;
  }

  if (port == HAL_I2C_kOnboard) {
    std::scoped_lock lock(digitalI2COnBoardMutex);
    if (i2COnboardObjCount-- == 0) {
      close(i2COnBoardHandle);
    }
  } else {
    std::scoped_lock lock(digitalI2CMXPMutex);
    if (i2CMXPObjCount-- == 0) {
      close(i2CMXPHandle);
    }
    HAL_FreeDIOPort(i2CMXPDigitalHandle1);
    HAL_FreeDIOPort(i2CMXPDigitalHandle2);
  }
}

}  // extern "C"
