#include "PlocSupvHelper.h"

#include <filesystem>
#include <fstream>

#include "OBSWConfig.h"
#ifdef XIPHOS_Q7S
#include "bsp_q7s/memory/FilesystemHelper.h"
#include "bsp_q7s/memory/SdCardManager.h"
#endif

#include "fsfw/globalfunctions/CRC.h"
#include "fsfw/tasks/TaskFactory.h"
#include "fsfw/timemanager/Countdown.h"
#include "mission/utility/Filenaming.h"
#include "mission/utility/ProgressPrinter.h"
#include "mission/utility/Timestamp.h"

PlocSupvHelper::PlocSupvHelper(object_id_t objectId) : SystemObject(objectId) {}

PlocSupvHelper::~PlocSupvHelper() {}

ReturnValue_t PlocSupvHelper::initialize() {
#ifdef XIPHOS_Q7S
  sdcMan = SdCardManager::instance();
  if (sdcMan == nullptr) {
    sif::warning << "PlocSupvHelper::initialize: Invalid SD Card Manager" << std::endl;
    return RETURN_FAILED;
  }
#endif
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::performOperation(uint8_t operationCode) {
  ReturnValue_t result = RETURN_OK;
  semaphore.acquire();
  while (true) {
    switch (internalState) {
      case InternalState::IDLE: {
        semaphore.acquire();
        break;
      }
      case InternalState::UPDATE: {
        result = performUpdate();
        if (result == RETURN_OK) {
          triggerEvent(SUPV_UPDATE_SUCCESSFUL, result);
        } else if (result == PROCESS_TERMINATED) {
          // Event already triggered
        } else {
          triggerEvent(SUPV_UPDATE_FAILED, result);
        }
        internalState = InternalState::IDLE;
        break;
      }
      case InternalState::CONTINUE_UPDATE: {
        result = continueUpdate();
        if (result == RETURN_OK) {
          triggerEvent(SUPV_CONTINUE_UPDATE_SUCCESSFUL, result);
        } else if (result == PROCESS_TERMINATED) {
          // Event already triggered
        } else {
          triggerEvent(SUPV_CONTINUE_UPDATE_FAILED, result);
        }
        internalState = InternalState::IDLE;
        break;
      }
      case InternalState::REQUEST_EVENT_BUFFER: {
        result = performEventBufferRequest();
        if (result == RETURN_OK) {
          triggerEvent(SUPV_EVENT_BUFFER_REQUEST_SUCCESSFUL, result);
        } else if (result == PROCESS_TERMINATED) {
          // Event already triggered
          break;
        } else {
          triggerEvent(SUPV_EVENT_BUFFER_REQUEST_FAILED, result);
        }
        internalState = InternalState::IDLE;
        break;
      }
      default:
        sif::debug << "PlocSupvHelper::performOperation: Invalid state" << std::endl;
        break;
    }
  }
}

ReturnValue_t PlocSupvHelper::setComIF(UartComIF* uartComIF_) {
  if (uartComIF_ == nullptr) {
    sif::warning << "PlocSupvHelper::initialize: Provided invalid uart com if" << std::endl;
    return RETURN_FAILED;
  }
  uartComIF = uartComIF_;
  return RETURN_OK;
}

void PlocSupvHelper::setComCookie(CookieIF* comCookie_) { comCookie = comCookie_; }

ReturnValue_t PlocSupvHelper::startUpdate(std::string file, uint8_t memoryId,
                                          uint32_t startAddress) {
  ReturnValue_t result = RETURN_OK;
#ifdef XIPHOS_Q7S
  result = FilesystemHelper::checkPath(file);
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::startUpdate: File " << file << " does not exist" << std::endl;
    return result;
  }
  result = FilesystemHelper::fileExists(file);
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::startUpdate: The file " << file << " does not exist"
                 << std::endl;
    return result;
  }
#endif
#ifdef TE0720_1CFA
  if (not std::filesystem::exists(file)) {
    sif::warning << "PlocSupvHelper::startUpdate: The file " << file << " does not exist"
                 << std::endl;
    return RETURN_FAILED;
  }
#endif
  update.file = file;
  update.length = getFileSize(update.file);
  update.memoryId = memoryId;
  update.startAddress = startAddress;
  update.remainingSize = update.length;
  update.bytesWritten = 0;
  update.packetNum = 1;
  update.sequenceCount = 1;
  internalState = InternalState::UPDATE;
  uartComIF->flushUartTxAndRxBuf(comCookie);
  semaphore.release();
  return result;
}

void PlocSupvHelper::initiateUpdateContinuation() {
  internalState = InternalState::CONTINUE_UPDATE;
  semaphore.release();
}

ReturnValue_t PlocSupvHelper::startEventbBufferRequest(std::string path) {
#ifdef XIPHOS_Q7S
  ReturnValue_t result = FilesystemHelper::checkPath(path);
  if (result != RETURN_OK) {
    return result;
  }
#endif
  if (not std::filesystem::exists(path)) {
    return PATH_NOT_EXISTS;
  }
  eventBufferReq.path = path;
  internalState = InternalState::REQUEST_EVENT_BUFFER;
  uartComIF->flushUartTxAndRxBuf(comCookie);
  semaphore.release();
  return RETURN_OK;
}

void PlocSupvHelper::stopProcess() { terminate = true; }

ReturnValue_t PlocSupvHelper::performUpdate() {
  ReturnValue_t result = RETURN_OK;
  result = calcImageCrc();
  if (result != RETURN_OK) {
    return result;
  }
  result = selectMemory();
  if (result != RETURN_OK) {
    return result;
  }
  result = prepareUpdate();
  if (result != RETURN_OK) {
    return result;
  }
  result = eraseMemory();
  if (result != RETURN_OK) {
    return result;
  }
  result = writeUpdatePackets();
  if (result != RETURN_OK) {
    return result;
  }
  result = handleCheckMemoryCommand();
  if (result != RETURN_OK) {
    return result;
  }
  return result;
}

ReturnValue_t PlocSupvHelper::continueUpdate() {
  ReturnValue_t result = prepareUpdate();
  if (result != RETURN_OK) {
    return result;
  }
  result = writeUpdatePackets();
  if (result != RETURN_OK) {
    return result;
  }
  result = handleCheckMemoryCommand();
  if (result != RETURN_OK) {
    return result;
  }
  return result;
}

ReturnValue_t PlocSupvHelper::writeUpdatePackets() {
  ReturnValue_t result = RETURN_OK;
#if OBSW_DEBUG_PLOC_SUPERVISOR == 1
  ProgressPrinter progressPrinter("Supervisor update", update.length,
                                  ProgressPrinter::HALF_PERCENT);
#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */
  uint8_t tempData[supv::WriteMemory::CHUNK_MAX];
  std::ifstream file(update.file, std::ifstream::binary);
  uint16_t dataLength = 0;
  supv::SequenceFlags seqFlags;
  while (update.remainingSize > 0) {
    if (terminate) {
      terminate = false;
      triggerEvent(TERMINATED_UPDATE_PROCEDURE);
      return PROCESS_TERMINATED;
    }
    if (update.remainingSize > supv::WriteMemory::CHUNK_MAX) {
      dataLength = supv::WriteMemory::CHUNK_MAX;
    } else {
      dataLength = static_cast<uint16_t>(update.remainingSize);
    }
    if (file.is_open()) {
      file.seekg(update.bytesWritten, file.beg);
      file.read(reinterpret_cast<char*>(tempData), dataLength);
      if (!file) {
        sif::warning << "PlocSupvHelper::performUpdate: Read only " << file.gcount() << " of "
                     << dataLength << " bytes" << std::endl;
        sif::info << "PlocSupvHelper::performUpdate: Failed when trying to read byte "
                  << update.bytesWritten << std::endl;
      }
    } else {
      return FILE_CLOSED_ACCIDENTALLY;
    }
    if (update.bytesWritten == 0) {
      seqFlags = supv::SequenceFlags::FIRST_PKT;
    } else if (update.remainingSize == 0) {
      seqFlags = supv::SequenceFlags::LAST_PKT;
    } else {
      seqFlags = supv::SequenceFlags::CONTINUED_PKT;
    }
    supv::WriteMemory packet(seqFlags, update.sequenceCount++, update.memoryId,
                             update.startAddress + update.bytesWritten, dataLength, tempData);
    result = handlePacketTransmission(packet);
    if (result != RETURN_OK) {
      update.sequenceCount--;
      triggerEvent(WRITE_MEMORY_FAILED, update.packetNum);
      return result;
    }
    update.remainingSize -= dataLength;
    update.packetNum += 1;
    update.bytesWritten += dataLength;
#if OBSW_DEBUG_PLOC_SUPERVISOR == 1
    progressPrinter.print(update.bytesWritten);
#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */
  }
  return result;
}

ReturnValue_t PlocSupvHelper::performEventBufferRequest() {
  using namespace supv;
  ReturnValue_t result = RETURN_OK;
  RequestLoggingData packet(RequestLoggingData::Sa::REQUEST_EVENT_BUFFERS);
  result = sendCommand(packet);
  if (result != RETURN_OK) {
    return result;
  }
  result = handleAck();
  if (result != RETURN_OK) {
    return result;
  }
  result = handleEventBufferReception();
  if (result != RETURN_OK) {
    return result;
  }
  result = handleExe();
  if (result != RETURN_OK) {
    return result;
  }
  return result;
}

ReturnValue_t PlocSupvHelper::selectMemory() {
  ReturnValue_t result = RETURN_OK;
  supv::MPSoCBootSelect packet(update.memoryId);
  result = handlePacketTransmission(packet);
  if (result != RETURN_OK) {
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::prepareUpdate() {
  ReturnValue_t result = RETURN_OK;
  supv::ApidOnlyPacket packet(supv::APID_PREPARE_UPDATE);
  result = handlePacketTransmission(packet, PREPARE_UPDATE_EXECUTION_REPORT);
  if (result != RETURN_OK) {
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::eraseMemory() {
  ReturnValue_t result = RETURN_OK;
  supv::EraseMemory eraseMemory(update.memoryId, update.startAddress, update.length);
  result = handlePacketTransmission(eraseMemory, supv::recv_timeout::ERASE_MEMORY);
  if (result != RETURN_OK) {
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::handlePacketTransmission(SpacePacket& packet,
                                                       uint32_t timeoutExecutionReport) {
  ReturnValue_t result = RETURN_OK;
  result = sendCommand(packet);
  if (result != RETURN_OK) {
    return result;
  }
  result = handleAck();
  if (result != RETURN_OK) {
    return result;
  }
  result = handleExe(timeoutExecutionReport);
  if (result != RETURN_OK) {
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::sendCommand(SpacePacket& packet) {
  ReturnValue_t result = RETURN_OK;
  rememberApid = packet.getAPID();
  result = uartComIF->sendMessage(comCookie, packet.getWholeData(), packet.getFullSize());
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::sendCommand: Failed to send command" << std::endl;
    triggerEvent(SUPV_SENDING_COMMAND_FAILED, result, static_cast<uint32_t>(internalState));
    return result;
  }
  return result;
}

ReturnValue_t PlocSupvHelper::handleAck() {
  ReturnValue_t result = RETURN_OK;
  supv::AcknowledgmentReport ackReport;
  result = handleTmReception(&ackReport, supv::SIZE_ACK_REPORT);
  if (result != RETURN_OK) {
    triggerEvent(ACK_RECEPTION_FAILURE, result, static_cast<uint32_t>(rememberApid));
    sif::warning << "PlocSupvHelper::handleAck: Error in reception of acknowledgment report"
                 << std::endl;
    return result;
  }
  result = ackReport.checkApid();
  if (result != RETURN_OK) {
    if (result == SupvReturnValuesIF::RECEIVED_ACK_FAILURE) {
      triggerEvent(SUPV_ACK_FAILURE_REPORT, static_cast<uint32_t>(ackReport.getRefApid()));
    } else if (result == SupvReturnValuesIF::INVALID_APID) {
      triggerEvent(SUPV_ACK_INVALID_APID, static_cast<uint32_t>(rememberApid));
    }
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::handleExe(uint32_t timeout) {
  ReturnValue_t result = RETURN_OK;
  supv::ExecutionReport exeReport;
  result = handleTmReception(&exeReport, supv::SIZE_EXE_REPORT, timeout);
  if (result != RETURN_OK) {
    triggerEvent(EXE_RECEPTION_FAILURE, result, static_cast<uint32_t>(rememberApid));
    sif::warning << "PlocSupvHelper::handleExe: Error in reception of execution report"
                 << std::endl;
    return result;
  }
  result = exeReport.checkApid();
  if (result != RETURN_OK) {
    if (result == SupvReturnValuesIF::RECEIVED_EXE_FAILURE) {
      triggerEvent(SUPV_EXE_FAILURE_REPORT, static_cast<uint32_t>(exeReport.getRefApid()));
    } else if (result == SupvReturnValuesIF::INVALID_APID) {
      triggerEvent(SUPV_EXE_INVALID_APID, static_cast<uint32_t>(rememberApid));
    }
    return result;
  }
  return RETURN_OK;
}

ReturnValue_t PlocSupvHelper::handleTmReception(supv::TmPacket* tmPacket, size_t remainingBytes,
                                                uint32_t timeout) {
  ReturnValue_t result = RETURN_OK;
  size_t readBytes = 0;
  size_t currentBytes = 0;
  Countdown countdown(timeout);
  while (!countdown.hasTimedOut()) {
    result = receive(tmPacket->getWholeData() + readBytes, &currentBytes, remainingBytes);
    if (result != RETURN_OK) {
      return result;
    }
    readBytes += currentBytes;
    remainingBytes = remainingBytes - currentBytes;
    if (remainingBytes == 0) {
      break;
    }
  }
  if (remainingBytes != 0) {
    sif::warning << "PlocSupvHelper::handleTmReception: Failed to read " << std::dec
                 << remainingBytes << " bytes" << std::endl;
    return RETURN_FAILED;
  }
  result = tmPacket->checkCrc();
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::handleTmReception: CRC check failed" << std::endl;
    return result;
  }
  return result;
}

ReturnValue_t PlocSupvHelper::receive(uint8_t* data, size_t* readBytes, size_t requestBytes) {
  ReturnValue_t result = RETURN_OK;
  uint8_t* buffer = nullptr;
  result = uartComIF->requestReceiveMessage(comCookie, requestBytes);
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::receive: Failed to request reply" << std::endl;
    triggerEvent(SUPV_HELPER_REQUESTING_REPLY_FAILED, result,
                 static_cast<uint32_t>(static_cast<uint32_t>(internalState)));
    return RETURN_FAILED;
  }
  result = uartComIF->readReceivedMessage(comCookie, &buffer, readBytes);
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::receive: Failed to read received message" << std::endl;
    triggerEvent(SUPV_HELPER_READING_REPLY_FAILED, result, static_cast<uint32_t>(internalState));
    return RETURN_FAILED;
  }
  if (*readBytes > 0) {
    std::memcpy(data, buffer, *readBytes);
  } else {
    TaskFactory::delayTask(40);
  }
  return result;
}

ReturnValue_t PlocSupvHelper::calcImageCrc() {
  ReturnValue_t result = RETURN_OK;
#ifdef XIPHOS_Q7S
  result = FilesystemHelper::checkPath(update.file);
#endif
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::calcImageCrc: File " << update.file << " does not exist"
                 << std::endl;
    return result;
  }
  std::ifstream file(update.file, std::ifstream::binary);
  uint16_t remainder = CRC16_INIT;
  uint8_t input;
#if OBSW_DEBUG_PLOC_SUPERVISOR == 1
  ProgressPrinter progress("Supervisor update crc calculation", update.length,
                           ProgressPrinter::ONE_PERCENT);
#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */
  uint32_t byteCount = 0;
  for (byteCount = 0; byteCount < update.length; byteCount++) {
    file.seekg(byteCount, file.beg);
    file.read(reinterpret_cast<char*>(&input), 1);
    remainder = CRC::crc16ccitt(&input, sizeof(input), remainder);
#if OBSW_DEBUG_PLOC_SUPERVISOR == 1
    progress.print(byteCount);
#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */
  }
#if OBSW_DEBUG_PLOC_SUPERVISOR == 1
  progress.print(byteCount);
#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */
  file.close();
  update.crc = remainder;
  return result;
}

ReturnValue_t PlocSupvHelper::handleCheckMemoryCommand() {
  ReturnValue_t result = RETURN_OK;
  // Verification of update write procedure
  supv::CheckMemory packet(update.memoryId, update.startAddress, update.length);
  result = sendCommand(packet);
  if (result != RETURN_OK) {
    return result;
  }
  result = handleAck();
  if (result != RETURN_OK) {
    return result;
  }
  supv::UpdateStatusReport updateStatusReport;
  result = handleTmReception(&updateStatusReport,
                             static_cast<size_t>(updateStatusReport.getNominalSize()),
                             supv::recv_timeout::UPDATE_STATUS_REPORT);
  if (result != RETURN_OK) {
    sif::warning
        << "PlocSupvHelper::handleCheckMemoryCommand: Failed to receive update status report"
        << std::endl;
    return result;
  }
  result = handleExe(CRC_EXECUTION_TIMEOUT);
  if (result != RETURN_OK) {
    return result;
  }
  result = updateStatusReport.parseDataField();
  if (result != RETURN_OK) {
    return result;
  }
  result = updateStatusReport.verifycrc(update.crc);
  if (result != RETURN_OK) {
    sif::warning << "PlocSupvHelper::handleCheckMemoryCommand: CRC failure. Expected CRC 0x"
                 << std::hex << update.crc << " but received CRC 0x" << updateStatusReport.getCrc()
                 << std::endl;
    return result;
  }
  return result;
}

uint32_t PlocSupvHelper::getFileSize(std::string filename) {
  std::ifstream file(filename, std::ifstream::binary);
  file.seekg(0, file.end);
  uint32_t size = file.tellg();
  file.close();
  return size;
}

ReturnValue_t PlocSupvHelper::handleEventBufferReception() {
  ReturnValue_t result = RETURN_OK;
  std::string filename = Filenaming::generateAbsoluteFilename(
      eventBufferReq.path, eventBufferReq.filename, timestamping);
  std::ofstream file(filename, std::ios_base::app | std::ios_base::out);
  uint32_t packetsRead = 0;
  size_t requestLen = 0;
  supv::TmPacket tmPacket;
  for (packetsRead = 0; packetsRead < NUM_EVENT_BUFFER_PACKETS; packetsRead++) {
    if (terminate) {
      triggerEvent(SUPV_EVENT_BUFFER_REQUEST_TERMINATED, packetsRead - 1);
      file.close();
      return PROCESS_TERMINATED;
    }
    if (packetsRead == NUM_EVENT_BUFFER_PACKETS - 1) {
      requestLen = SIZE_EVENT_BUFFER_LAST_PACKET;
    } else {
      requestLen = SIZE_EVENT_BUFFER_FULL_PACKET;
    }
    result = handleTmReception(&tmPacket, requestLen);
    if (result != RETURN_OK) {
      sif::debug << "PlocSupvHelper::handleEventBufferReception: Failed while trying to read packet"
                 << " " << packetsRead + 1 << std::endl;
      file.close();
      return result;
    }
    uint16_t apid = tmPacket.getAPID();
    if (apid != supv::APID_MRAM_DUMP_TM) {
      sif::warning << "PlocSupvHelper::handleEventBufferReception: Did not expect space packet "
                   << "with APID 0x" << std::hex << apid << std::endl;
      file.close();
      return EVENT_BUFFER_REPLY_INVALID_APID;
    }
    file.write(reinterpret_cast<const char*>(tmPacket.getPacketData()),
               tmPacket.getPayloadDataLength());
  }
  return result;
}