#include "ImtqPollingTask.h"

#include <fcntl.h>
#include <fsfw/tasks/SemaphoreFactory.h>
#include <fsfw/tasks/TaskFactory.h>
#include <fsfw/timemanager/Stopwatch.h>
#include <fsfw_hal/linux/UnixFileGuard.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>

#include "fsfw/FSFW.h"

ImtqPollingTask::ImtqPollingTask(object_id_t imtqPollingTask, std::atomic_uint16_t& i2cFatalErrors)
    : SystemObject(imtqPollingTask), i2cFatalErrors(i2cFatalErrors) {
  semaphore = SemaphoreFactory::instance()->createBinarySemaphore();
  semaphore->acquire();
  ipcLock = MutexFactory::instance()->createMutex();
  bufLock = MutexFactory::instance()->createMutex();
}

ReturnValue_t ImtqPollingTask::performOperation(uint8_t operationCode) {
  while (true) {
    ipcLock->lockMutex();
    state = InternalState::IDLE;
    ipcLock->unlockMutex();
    semaphore->acquire();

    comStatus = returnvalue::OK;
    // Stopwatch watch;
    switch (currentRequest.requestType) {
      case imtq::RequestType::MEASURE_NO_ACTUATION: {
        // Measured to take 24 ms for debug and release builds.
        // Stopwatch watch;
        handleMeasureStep();
        break;
      }
      case imtq::RequestType::ACTUATE: {
        handleActuateStep();
        break;
      }
      default: {
        break;
      }
    };
  }
  return returnvalue::OK;
}

void ImtqPollingTask::handleMeasureStep() {
  size_t replyLen = 0;
  uint8_t* replyPtr;
  ImtqRepliesDefault replies(replyBuf.data());
  // If some startup handling is added later, set configured after it was done once.
  if (performStartup) {
    // Set integration time for the MGM.
    cmdBuf[0] = imtq::CC::SET_PARAM;
    size_t dummy = 0;
    SerializeAdapter::serialize(&imtq::param::INTEGRATION_TIME_SELECT, cmdBuf.data() + 1, &dummy,
                                cmdBuf.size(), SerializeIF::Endianness::LITTLE);
    cmdBuf[3] = currentRequest.integrationTimeSel;
    cmdLen = 4;
    ReturnValue_t result = performI2cFullRequest(replyBuf.data(), 5);
    if (result != returnvalue::OK) {
      comStatus = imtq::STARTUP_CFG_ERROR;
    }
    if (replyBuf[0] != imtq::CC::SET_PARAM) {
      sif::error << "ImtqPollingTask: First byte of reply not equal to sent CC" << std::endl;
      comStatus = imtq::STARTUP_CFG_ERROR;
    }
    if (replyBuf[4] != currentRequest.integrationTimeSel) {
      sif::error << "ImtqPollingTask: Integration time configuration failed" << std::endl;
      comStatus = imtq::STARTUP_CFG_ERROR;
    }
    currentIntegrationTimeMs =
        imtq::integrationTimeFromSelectValue(currentRequest.integrationTimeSel);
    performStartup = false;
  }
  replies.setConfigured();

  // Can be used later to verify correct timing (e.g. all data has been read)
  clearReadFlagsDefault(replies);
  auto i2cCmdExecMeasure = [&](imtq::CC::CC cc) {
    ccToReplyPtrMeasure(replies, cc, &replyPtr, replyLen);
    return i2cCmdExecDefault(cc, replyPtr, replyLen, imtq::MGM_MEASUREMENT_LOW_LEVEL_ERROR);
  };

  cmdLen = 1;
  cmdBuf[0] = imtq::CC::GET_SYSTEM_STATE;
  if (i2cCmdExecMeasure(imtq::CC::GET_SYSTEM_STATE) != returnvalue::OK) {
    return;
  }

  ignoreNextActuateRequest =
      (replies.getSystemState()[2] == static_cast<uint8_t>(imtq::mode::SELF_TEST));
  if (ignoreNextActuateRequest) {
    // Do not command anything until self-test is done.
    return;
  }

  if (currentRequest.specialRequest != imtq::SpecialRequest::NONE) {
    auto executeSelfTest = [&](imtq::selfTest::Axis axis) {
      cmdBuf[0] = imtq::CC::SELF_TEST_CMD;
      cmdBuf[1] = axis;
      return i2cCmdExecMeasure(imtq::CC::SELF_TEST_CMD);
    };
    // If a self-test is already ongoing, ignore the request.
    if (replies.getSystemState()[2] != static_cast<uint8_t>(imtq::mode::SELF_TEST)) {
      switch (currentRequest.specialRequest) {
        case (imtq::SpecialRequest::DO_SELF_TEST_POS_X): {
          executeSelfTest(imtq::selfTest::Axis::X_POSITIVE);
          break;
        }
        case (imtq::SpecialRequest::DO_SELF_TEST_NEG_X): {
          executeSelfTest(imtq::selfTest::Axis::X_NEGATIVE);
          break;
        }
        case (imtq::SpecialRequest::DO_SELF_TEST_POS_Y): {
          executeSelfTest(imtq::selfTest::Axis::Y_POSITIVE);
          break;
        }
        case (imtq::SpecialRequest::DO_SELF_TEST_NEG_Y): {
          executeSelfTest(imtq::selfTest::Axis::Y_NEGATIVE);
          break;
        }
        case (imtq::SpecialRequest::DO_SELF_TEST_POS_Z): {
          executeSelfTest(imtq::selfTest::Axis::Z_POSITIVE);
          break;
        }
        case (imtq::SpecialRequest::DO_SELF_TEST_NEG_Z): {
          executeSelfTest(imtq::selfTest::Axis::Z_NEGATIVE);
          break;
        }
        case (imtq::SpecialRequest::GET_SELF_TEST_RESULT): {
          cmdBuf[0] = imtq::CC::GET_SELF_TEST_RESULT;
          i2cCmdExecMeasure(imtq::CC::GET_SELF_TEST_RESULT);
          break;
        }
        default: {
          // Should never happen
          break;
        }
      }
      // We are done. Only request self test or results here.
      return;
    }
  }

  // The I2C IP core on EIVE sometimes glitches out. Send start MTM measurement twice.
  cmdBuf[0] = imtq::CC::START_MTM_MEASUREMENT;
  if (i2cCmdExecMeasure(imtq::CC::START_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  cmdBuf[0] = imtq::CC::START_MTM_MEASUREMENT;
  if (i2cCmdExecMeasure(imtq::CC::START_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  // Takes a bit of time to take measurements. Subtract a bit because of the delay of previous
  // commands.
  TaskFactory::delayTask(currentIntegrationTimeMs + MGM_READ_BUFFER_TIME_MS);

  cmdBuf[0] = imtq::CC::GET_RAW_MTM_MEASUREMENT;
  if (i2cCmdExecMeasure(imtq::CC::GET_RAW_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  bool mgmMeasurementTooOld = false;
  // See p.39 of the iMTQ user manual. If the NEW bit of the STAT bitfield is not set, we probably
  // have old data. Which can be really bad for ACS. And everything.
  if ((replyPtr[2] >> 7) == 0) {
    replyPtr[0] = false;
    mgmMeasurementTooOld = true;
  }

  cmdBuf[0] = imtq::CC::GET_ENG_HK_DATA;
  if (i2cCmdExecMeasure(imtq::CC::GET_ENG_HK_DATA) != returnvalue::OK) {
    return;
  }

  cmdBuf[0] = imtq::CC::GET_CAL_MTM_MEASUREMENT;
  if (i2cCmdExecMeasure(imtq::CC::GET_CAL_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  if (mgmMeasurementTooOld) {
    sif::error << "IMTQ: MGM measurement too old" << std::endl;
  }
  return;
}

void ImtqPollingTask::handleActuateStep() {
  uint8_t* replyPtr = nullptr;
  size_t replyLen = 0;
  // No point when self-test mode is active.
  if (ignoreNextActuateRequest) {
    return;
  }
  ImtqRepliesWithTorque replies(replyBufActuation.data());
  // Can be used later to verify correct timing (e.g. all data has been read)
  clearReadFlagsWithTorque(replies);
  auto i2cCmdExecActuate = [&](imtq::CC::CC cc) {
    ccToReplyPtrActuate(replies, cc, &replyPtr, replyLen);
    return i2cCmdExecDefault(cc, replyPtr, replyLen, imtq::ACTUATE_CMD_LOW_LEVEL_ERROR);
  };
  buildDipoleCommand();
  if (i2cCmdExecActuate(imtq::CC::START_ACTUATION_DIPOLE) != returnvalue::OK) {
    return;
  }

  TaskFactory::delayTask(10);

  cmdLen = 1;
  // The I2C IP core on EIVE sometimes glitches out. Send start MTM measurement twice.
  cmdBuf[0] = imtq::CC::START_MTM_MEASUREMENT;
  if (i2cCmdExecActuate(imtq::CC::START_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  cmdBuf[0] = imtq::CC::START_MTM_MEASUREMENT;
  if (i2cCmdExecActuate(imtq::CC::START_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }

  TaskFactory::delayTask(currentIntegrationTimeMs + MGM_READ_BUFFER_TIME_MS);

  cmdBuf[0] = imtq::CC::GET_RAW_MTM_MEASUREMENT;
  if (i2cCmdExecActuate(imtq::CC::GET_RAW_MTM_MEASUREMENT) != returnvalue::OK) {
    return;
  }
  bool measurementWasTooOld = false;
  // See p.39 of the iMTQ user manual. If the NEW bit of the STAT bitfield is not set, we probably
  // have old data. Which can be really bad for ACS. And everything.
  if ((replyPtr[2] >> 7) == 0) {
    measurementWasTooOld = true;
    replyPtr[0] = false;
  }

  cmdBuf[0] = imtq::CC::GET_ENG_HK_DATA;
  if (i2cCmdExecActuate(imtq::CC::GET_ENG_HK_DATA) != returnvalue::OK) {
    return;
  }

  if (measurementWasTooOld) {
    sif::error << "IMTQ: MGM measurement too old" << std::endl;
  }
  return;
}

ReturnValue_t ImtqPollingTask::initialize() { return returnvalue::OK; }

ReturnValue_t ImtqPollingTask::initializeInterface(CookieIF* cookie) {
  i2cCookie = dynamic_cast<I2cCookie*>(cookie);
  if (i2cCookie == nullptr) {
    sif::error << "ImtqPollingTask::initializeInterface: Invalid I2C cookie" << std::endl;
    return returnvalue::FAILED;
  }
  i2cDev = i2cCookie->getDeviceFile().c_str();
  i2cAddr = i2cCookie->getAddress();
  return returnvalue::OK;
}

ReturnValue_t ImtqPollingTask::sendMessage(CookieIF* cookie, const uint8_t* sendData,
                                           size_t sendLen) {
  const auto* imtqReq = reinterpret_cast<const imtq::Request*>(sendData);
  if (sendLen != sizeof(imtq::Request)) {
    return returnvalue::FAILED;
  }
  {
    MutexGuard mg(ipcLock);
    if (state != InternalState::IDLE) {
      return returnvalue::FAILED;
    }
    state = InternalState::IS_BUSY;
    if (currentRequest.mode != imtqReq->mode) {
      if (imtqReq->mode == acs::SimpleSensorMode::NORMAL) {
        performStartup = true;
      }
    }
    std::memcpy(&currentRequest, imtqReq, sendLen);
  }
  semaphore->release();

  return returnvalue::OK;
}

ReturnValue_t ImtqPollingTask::getSendSuccess(CookieIF* cookie) { return returnvalue::OK; }

ReturnValue_t ImtqPollingTask::requestReceiveMessage(CookieIF* cookie, size_t requestLen) {
  return returnvalue::OK;
}

void ImtqPollingTask::ccToReplyPtrMeasure(ImtqRepliesDefault& replies, imtq::CC::CC cc,
                                          uint8_t** replyBuf, size_t& replyLen) {
  replyLen = imtq::getReplySize(cc);
  switch (cc) {
    case (imtq::CC::CC::GET_ENG_HK_DATA): {
      *replyBuf = replies.engHk;
      break;
    }
    case (imtq::CC::CC::SOFTWARE_RESET): {
      *replyBuf = replies.swReset;
      break;
    }
    case (imtq::CC::CC::GET_SYSTEM_STATE): {
      *replyBuf = replies.systemState;
      break;
    }
    case (imtq::CC::CC::START_MTM_MEASUREMENT): {
      *replyBuf = replies.startMtmMeasurement;
      break;
    }
    case (imtq::CC::CC::GET_RAW_MTM_MEASUREMENT): {
      *replyBuf = replies.rawMgmMeasurement;
      break;
    }
    case (imtq::CC::CC::GET_CAL_MTM_MEASUREMENT): {
      *replyBuf = replies.calibMgmMeasurement;
      break;
    }
    default: {
      *replyBuf = replies.specialRequestReply;
      break;
    }
  }
}

void ImtqPollingTask::ccToReplyPtrActuate(ImtqRepliesWithTorque& replies, imtq::CC::CC cc,
                                          uint8_t** replyBuf, size_t& replyLen) {
  replyLen = imtq::getReplySize(cc);
  switch (cc) {
    case (imtq::CC::CC::START_ACTUATION_DIPOLE): {
      *replyBuf = replies.dipoleActuation;
      break;
    }
    case (imtq::CC::CC::GET_ENG_HK_DATA): {
      *replyBuf = replies.engHk;
      break;
    }
    case (imtq::CC::CC::START_MTM_MEASUREMENT): {
      *replyBuf = replies.startMtmMeasurement;
      break;
    }
    case (imtq::CC::CC::GET_RAW_MTM_MEASUREMENT): {
      *replyBuf = replies.rawMgmMeasurement;
      break;
    }
    default: {
      *replyBuf = nullptr;
      replyLen = 0;
      break;
    }
  }
}
size_t ImtqPollingTask::getExchangeBufLen(imtq::SpecialRequest specialRequest) {
  size_t baseLen = ImtqRepliesDefault::BASE_LEN;
  switch (specialRequest) {
    case (imtq::SpecialRequest::NONE):
    case (imtq::SpecialRequest::DO_SELF_TEST_POS_X):
    case (imtq::SpecialRequest::DO_SELF_TEST_NEG_X):
    case (imtq::SpecialRequest::DO_SELF_TEST_POS_Y):
    case (imtq::SpecialRequest::DO_SELF_TEST_NEG_Y):
    case (imtq::SpecialRequest::DO_SELF_TEST_POS_Z):
    case (imtq::SpecialRequest::DO_SELF_TEST_NEG_Z): {
      break;
    }
    case (imtq::SpecialRequest::GET_SELF_TEST_RESULT): {
      baseLen += imtq::replySize::SELF_TEST_RESULTS;
      break;
    }
  }
  return baseLen;
}

void ImtqPollingTask::buildDipoleCommand() {
  cmdBuf[0] = imtq::CC::CC::START_ACTUATION_DIPOLE;
  uint8_t* serPtr = cmdBuf.data() + 1;
  size_t serLen = 0;
  for (uint8_t idx = 0; idx < 3; idx++) {
    SerializeAdapter::serialize(&currentRequest.dipoles[idx], &serPtr, &serLen, cmdBuf.size(),
                                SerializeIF::Endianness::LITTLE);
  }
  SerializeAdapter::serialize(&currentRequest.torqueDuration, &serPtr, &serLen, cmdBuf.size(),
                              SerializeIF::Endianness::LITTLE);
  // sif::debug << "Dipole X: " << dipoles[0] << std::endl;
  // sif::debug << "Torqeu Dur: " << torqueDuration << std::endl;
  cmdLen = 1 + serLen;
}

ReturnValue_t ImtqPollingTask::readReceivedMessage(CookieIF* cookie, uint8_t** buffer,
                                                   size_t* size) {
  imtq::Request currentRequest;
  {
    MutexGuard mg(ipcLock);
    std::memcpy(&currentRequest, &this->currentRequest, sizeof(currentRequest));
  }

  size_t replyLen = 0;
  {
    MutexGuard mg(bufLock);
    if (currentRequest.requestType == imtq::RequestType::MEASURE_NO_ACTUATION) {
      replyLen = getExchangeBufLen(currentRequest.specialRequest);
      memcpy(exchangeBuf.data(), replyBuf.data(), replyLen);
    } else if (currentRequest.requestType == imtq::RequestType::ACTUATE) {
      replyLen = ImtqRepliesWithTorque::BASE_LEN;
      memcpy(exchangeBuf.data(), replyBufActuation.data(), replyLen);
    } else {
      *size = 0;
    }
  }
  {
    MutexGuard mg(ipcLock);
    this->currentRequest.requestType = imtq::RequestType::DO_NOTHING;
  }
  *buffer = exchangeBuf.data();
  *size = replyLen;
  return comStatus;
}

void ImtqPollingTask::clearReadFlagsDefault(ImtqRepliesDefault& replies) {
  replies.calibMgmMeasurement[0] = false;
  replies.rawMgmMeasurement[0] = false;
  replies.systemState[0] = false;
  replies.specialRequestReply[0] = false;
  replies.engHk[0] = false;
}

ReturnValue_t ImtqPollingTask::i2cCmdExecDefault(imtq::CC::CC cc, uint8_t* replyPtr,
                                                 size_t replyLen, ReturnValue_t comErrIfFails) {
  ReturnValue_t res = performI2cFullRequest(replyPtr + 1, replyLen);
  if (res != returnvalue::OK) {
    sif::error << "IMTQ: I2C transaction for command 0x" << std::hex << std::setw(2) << cc
               << " failed" << std::dec << std::endl;
    comStatus = comErrIfFails;
    return returnvalue::FAILED;
  }
  if (replyPtr[1] != cc) {
    sif::warning << "IMTQ: Unexpected CC 0x" << std::hex << std::setw(2)
                 << static_cast<int>(replyPtr[1]) << " for command 0x" << cc << std::dec
                 << std::endl;
    comStatus = comErrIfFails;
    return returnvalue::FAILED;
  }
  replyPtr[0] = true;
  return returnvalue::OK;
}

void ImtqPollingTask::clearReadFlagsWithTorque(ImtqRepliesWithTorque& replies) {
  replies.dipoleActuation[0] = false;
  replies.engHk[0] = false;
  replies.rawMgmMeasurement[0] = false;
  replies.startMtmMeasurement[0] = false;
}

ReturnValue_t ImtqPollingTask::performI2cFullRequest(uint8_t* reply, size_t replyLen) {
  int fd = 0;
  if (cmdLen == 0 or reply == nullptr) {
    sif::error << "ImtqPollingTask: Command lenght is zero or reply PTR is invalid" << std::endl;
    return returnvalue::FAILED;
  }

  {
    UnixFileGuard fileHelper(i2cDev, fd, O_RDWR, "ImtqPollingTask::performI2cFullRequest");
    if (fileHelper.getOpenResult() != returnvalue::OK) {
      return fileHelper.getOpenResult();
    }
    if (ioctl(fd, I2C_SLAVE, i2cAddr) < 0) {
      sif::warning << "Opening IMTQ slave device failed with code " << errno << ": "
                   << strerror(errno) << std::endl;
      if (errno == EBUSY) {
        i2cFatalErrors++;
      }
    }

    int written = write(fd, cmdBuf.data(), cmdLen);
    if (written < 0) {
      sif::error << "IMTQ: Failed to send with error code " << errno
                 << ". Error description: " << strerror(errno) << std::endl;
      // This is a weird issue which sometimes occurs on debug builds. All I2C buses are busy
      // for all writes,
      if (errno == EBUSY) {
        i2cFatalErrors++;
      }
      return returnvalue::FAILED;
    } else if (static_cast<size_t>(written) != cmdLen) {
      sif::error << "IMTQ: Could not write all bytes" << std::endl;
      return returnvalue::FAILED;
    }
  }
#if FSFW_HAL_I2C_WIRETAPPING == 1
  sif::info << "Sent I2C data to bus " << deviceFile << ":" << std::endl;
  arrayprinter::print(sendData, sendLen);
#endif

  // wait 1 ms like specified in the datasheet. This is the time the IMTQ needs
  // to prepare a reply.
  usleep(1000);

  {
    UnixFileGuard fileHelper(i2cDev, fd, O_RDWR, "ImtqPollingTask::performI2cFullRequest");
    if (fileHelper.getOpenResult() != returnvalue::OK) {
      return fileHelper.getOpenResult();
    }
    if (ioctl(fd, I2C_SLAVE, i2cAddr) < 0) {
      sif::warning << "Opening IMTQ slave device failed with code " << errno << ": "
                   << strerror(errno) << std::endl;
    }
    MutexGuard mg(bufLock);
    int readLen = read(fd, reply, replyLen);
    if (readLen != static_cast<int>(replyLen)) {
      if (readLen < 0) {
        sif::warning << "IMTQ: Reading failed with error code " << errno << " | " << strerror(errno)
                     << std::endl;
      } else {
        sif::warning << "IMTQ: Read only" << readLen << " from " << replyLen << " bytes"
                     << std::endl;
      }
    }
  }
  if (reply[0] == 0xff or reply[1] == 0xff) {
    sif::warning << "IMTQ: No reply available after 1 millisecond";
    return NO_REPLY_AVAILABLE;
  }
  return returnvalue::OK;
}