/*
 * Copyright 2021 Max Planck Institute for Software Systems, and
 * National University of Singapore
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "sims/nic/corundum_bm/corundum_bm.h"

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cassert>

// #define DEBUG 1
// #define ARMED_MODE

static nicbm::Runner *runner;

namespace corundum {

DescRing::DescRing()
    : _dmaAddr(0),
      _sizeLog(0),
      _size(0),
      _sizeMask(0),
      _index(0),
      _headPtr(0),
      _tailPtr(0),
      _currHead(0),
      _currTail(0),
      active(false),
      armed(false) {
}

DescRing::~DescRing() {
}

addr_t DescRing::dmaAddr() {
  return this->_dmaAddr;
}

size_t DescRing::sizeLog() {
  return this->_sizeLog;
}

unsigned DescRing::index() {
  return this->_index;
}

ptr_t DescRing::headPtr() {
  return this->_headPtr;
}

ptr_t DescRing::tailPtr() {
  return this->_tailPtr;
}

void DescRing::setDMALower(uint32_t addr) {
  this->_dmaAddr &= 0xFFFFFFFF00000000;
  this->_dmaAddr |= (addr_t)addr;
}

void DescRing::setDMAUpper(uint32_t addr) {
  this->_dmaAddr &= 0xFFFFFFFF;
  this->_dmaAddr |= ((addr_t)addr << 32);
}

void DescRing::setSizeLog(size_t size_log) {
  if (size_log & QUEUE_ACTIVE_MASK) {
    this->active = true;
  } else {
    this->active = false;
  }

  this->_sizeLog = size_log & 0xFF;
  this->_size = 1 << this->_sizeLog;
  this->_sizeMask = this->_size - 1;
  this->cplDma.resize(this->_size, false);
}

void DescRing::setIndex(unsigned index) {
  assert(!(index & QUEUE_CONT_MASK));
  if (index & QUEUE_ARM_MASK) {
    this->armed = true;
  }
  this->_index = index & 0xFF;
}

void DescRing::setHeadPtr(ptr_t ptr) {
  this->_headPtr = ptr;
}

void DescRing::setTailPtr(ptr_t ptr) {
  this->_tailPtr = ptr;
}

bool DescRing::empty() {
  return (this->_headPtr == this->_currTail);
}

bool DescRing::full() {
  return (this->_currHead - this->_tailPtr >= (int)this->_size);
}

bool DescRing::updatePtr(ptr_t ptr, bool head) {
  ptr_t curr_ptr = head ? this->_headPtr : this->_tailPtr;
  if (ptr != curr_ptr) {
    // out of order completion
    this->cplDma[ptr & this->_sizeMask] = true;
    return false;
  }
  /* Safe to update the pointer. Also check if any DMA is completed
   * out-of-order in front of us.
   */
  curr_ptr = ptr & this->_sizeMask;

  do {
    if (head) {
      this->_headPtr++;
    } else {
      this->_tailPtr++;
    }
    this->cplDma[curr_ptr] = false;
    curr_ptr = (curr_ptr + 1) & this->_sizeMask;
  } while (this->cplDma.at(curr_ptr));
  return true;
}

EventRing::EventRing() {
}

EventRing::~EventRing() {
}

void EventRing::dmaDone(DMAOp *op) {
  assert(op->write_);
  switch (op->type) {
    case DMA_TYPE_EVENT:
      if (updatePtr((ptr_t)op->tag, true)) {
        runner->MsiIssue(0);
      }
      delete op;
      break;
    default:
      fprintf(stderr, "Unknown DMA type %u\n", op->type);
      abort();
  }
}

void EventRing::issueEvent(unsigned type, unsigned source) {
  assert(type == EVENT_TYPE_TX_CPL || type == EVENT_TYPE_RX_CPL);
#ifdef ARMED_MODE
  if (this->armed) {
#endif
    if (full()) {
      fprintf(stderr, "Event ring is rull\n");
      return;
    }
    addr_t dma_addr =
        this->_dmaAddr + (this->_currHead & this->_sizeMask) * EVENT_SIZE;
    /* Issue DMA write */
    DMAOp *op = new DMAOp;
    op->type = DMA_TYPE_EVENT;
    op->dma_addr_ = dma_addr;
    op->len_ = EVENT_SIZE;
    op->ring = this;
    op->tag = this->_currHead;
    op->write_ = true;
    Event *event = (Event *)op->data_;
    memset(event, 0, sizeof(Event));
    event->type = type;
    event->source = source;
#ifdef DEBUG
    printf("corundum_bm: event ring issue dma addr %lx index %lu len %lu\n", op->dma_addr_, op->tag, op->len_);
#endif
    runner->IssueDma(*op);
    this->_currHead++;
    this->armed = false;
#ifdef ARMED_MODE
  }
#endif
}

CplRing::CplRing(EventRing *eventRing) : eventRing(eventRing) {
}

CplRing::~CplRing() {
}

void CplRing::dmaDone(DMAOp *op) {
  assert(op->write_);
  switch (op->type) {
    case DMA_TYPE_TX_CPL:
    case DMA_TYPE_RX_CPL: {
      if (updatePtr((ptr_t)op->tag, true)) {
        unsigned type =
            op->type == DMA_TYPE_TX_CPL ? EVENT_TYPE_TX_CPL : EVENT_TYPE_RX_CPL;
        this->eventRing->issueEvent(type, 0);
      }
      delete op;
      break;
    }
    default:
      fprintf(stderr, "Unknown DMA type %u\n", op->type);
      abort();
  }
}

void CplRing::complete(unsigned index, size_t len, bool tx) {
  CplData data;
  data.index = index;
  data.len = len;
  data.tx = tx;
  this->pending.push_back(data);
  while (!full() && !this->pending.empty()) {
    CplData &data = this->pending.front();
    addr_t dma_addr =
        this->_dmaAddr + (this->_currHead & this->_sizeMask) * CPL_SIZE;
    /* Issue DMA write */
    DMAOp *op = new DMAOp;
    op->type = data.tx ? DMA_TYPE_TX_CPL : DMA_TYPE_RX_CPL;
    op->dma_addr_ = dma_addr;
    op->len_ = CPL_SIZE;
    op->ring = this;
    op->tag = this->_currHead;
    op->write_ = true;
    Cpl *cpl = (Cpl *)op->data_;
    memset(cpl, 0, sizeof(Cpl));
    cpl->index = data.index;
    cpl->len = data.len;
    this->pending.pop_front();
    runner->IssueDma(*op);
    this->_currHead++;
  }
}

TxRing::TxRing(CplRing *cplRing) : txCplRing(cplRing) {
}

TxRing::~TxRing() {
}

void TxRing::setHeadPtr(ptr_t ptr) {
#ifdef DEBUG
  printf("corundum_bm: tx ring %u\n", ptr);
#endif
  DescRing::setHeadPtr(ptr);
  while (this->_currTail != this->_headPtr) {
    unsigned index = this->_currTail & this->_sizeMask;
    addr_t dma_addr = this->_dmaAddr + index * DESC_SIZE;
    /* Issue DMA read */
    DMAOp *op = new DMAOp;
    op->type = DMA_TYPE_DESC;
    op->dma_addr_ = dma_addr;
    op->len_ = DESC_SIZE;
    op->ring = this;
    op->tag = this->_currTail;
    op->write_ = false;
#ifdef DEBUG
  printf("corundum_bm: tx issue dma addr %lx index %lu len %lu\n", op->dma_addr_, op->tag, op->len_);
#endif
    runner->IssueDma(*op);
    this->_currTail++;
  }
}

void TxRing::dmaDone(DMAOp *op) {
  switch (op->type) {
    case DMA_TYPE_DESC: {
      assert(!op->write_);
      Desc *desc = (Desc *)op->data_;
#ifdef DEBUG
      printf("corundum_bm: tx dma desc done addr %lx index %lu len %u\n", desc->addr, op->tag, desc->len);
#endif
      op->type = DMA_TYPE_MEM;
      op->dma_addr_ = desc->addr;
      op->len_ = desc->len;
      op->write_ = false;
      runner->IssueDma(*op);
      break;
    }
    case DMA_TYPE_MEM:
      assert(!op->write_);
#ifdef DEBUG
      printf("corundum_bm: tx dma memory done index %lu len %lu\n", op->tag, op->len_);
#endif
      runner->EthSend(op->data_, op->len_);
      updatePtr((ptr_t)op->tag, false);
      this->txCplRing->complete(op->tag, op->len_, true);
      delete op;
      break;
    default:
      fprintf(stderr, "Unknown DMA type %d\n", op->type);
      abort();
  }
}

RxRing::RxRing(CplRing *cplRing) : rxCplRing(cplRing) {
}

RxRing::~RxRing() {
}

void RxRing::dmaDone(DMAOp *op) {
  switch (op->type) {
    case DMA_TYPE_DESC: {
      assert(!op->write_);
      Desc *desc = (Desc *)op->data_;
#ifdef DEBUG
      printf("corundum_bm: rx dma desc done addr %lx index %lu len %lu\n", desc->addr, op->tag, op->rx_data->len);
#endif
      op->type = DMA_TYPE_MEM;
      op->dma_addr_ = desc->addr;
      op->len_ = op->rx_data->len;
      memcpy((void *)op->data_, (void *)op->rx_data->data, op->len_);
      delete op->rx_data;
      op->write_ = true;
      runner->IssueDma(*op);
      break;
    }
    case DMA_TYPE_MEM:
      assert(op->write_);
#ifdef DEBUG
      printf("corundum_bm: rx dma memory done index %lu len %lu\n", op->tag, op->len_);
#endif
      updatePtr((ptr_t)op->tag, false);
      this->rxCplRing->complete(op->tag, op->len_, false);
      delete op;
      break;
    default:
      fprintf(stderr, "Unknown DMA type %u\n", op->type);
      abort();
  }
}

void RxRing::rx(RxData *rx_data) {
  if (empty()) {
    delete rx_data;
    return;
  }
  addr_t dma_addr =
      this->_dmaAddr + (this->_currTail & this->_sizeMask) * DESC_SIZE;
  /* Issue DMA read */
  DMAOp *op = new DMAOp;
  op->type = DMA_TYPE_DESC;
  op->dma_addr_ = dma_addr;
  op->len_ = DESC_SIZE;
  op->ring = this;
  op->rx_data = rx_data;
  op->tag = this->_currTail;
  op->write_ = false;
#ifdef DEBUG
  printf("corundum_bm: rx issue dma addr %lx index %lu len %lu\n", op->dma_addr_, op->tag, op->len_);
#endif
  runner->IssueDma(*op);
  this->_currTail++;
}

Port::Port()
    : _id(0),
      _features(0),
      _mtu(0),
      _schedCount(0),
      _schedOffset(0),
      _schedStride(0),
      _schedType(0),
      _rssMask(0),
      _schedEnable(false),
      _queueEnable(false) {
}

Port::~Port() {
}

unsigned Port::id() {
  return this->_id;
}

unsigned Port::features() {
  return this->_features;
}

size_t Port::mtu() {
  return this->_mtu;
}

size_t Port::schedCount() {
  return this->_schedCount;
}

addr_t Port::schedOffset() {
  return this->_schedOffset;
}

addr_t Port::schedStride() {
  return this->_schedStride;
}

unsigned Port::schedType() {
  return this->_schedType;
}

unsigned Port::rssMask() {
  return this->_rssMask;
}

void Port::setId(unsigned id) {
  this->_id = id;
}

void Port::setFeatures(unsigned features) {
  this->_features =
      features & (IF_FEATURE_RSS | IF_FEATURE_PTP_TS | IF_FEATURE_TX_CSUM |
                  IF_FEATURE_RX_CSUM | IF_FEATURE_RX_HASH);
}

void Port::setMtu(size_t mtu) {
  this->_mtu = mtu;
}

void Port::setSchedCount(size_t count) {
  this->_schedCount = count;
}

void Port::setSchedOffset(addr_t offset) {
  this->_schedOffset = offset;
}

void Port::setSchedStride(addr_t stride) {
  this->_schedStride = stride;
}

void Port::setSchedType(unsigned type) {
  this->_schedType = type;
}

void Port::setRssMask(unsigned mask) {
  this->_rssMask = mask;
}

void Port::schedEnable() {
  this->_schedEnable = true;
}

void Port::schedDisable() {
  this->_schedEnable = false;
}

void Port::queueEnable() {
  this->_queueEnable = true;
}

void Port::queueDisable() {
  this->_queueEnable = false;
}

Corundum::Corundum()
    : txRing(&this->txCplRing),
      txCplRing(&this->eventRing),
      rxRing(&this->rxCplRing),
      rxCplRing(&this->eventRing),
      features(0) {
  this->port.setId(0);
  this->port.setFeatures(this->features);
  this->port.setMtu(2048);
  this->port.setSchedCount(1);
  this->port.setSchedOffset(0x100000);
  this->port.setSchedStride(0x100000);
  this->port.setSchedType(0);
  this->port.setRssMask(0);
  this->port.schedDisable();
}

Corundum::~Corundum() {
}

reg_t Corundum::RegRead(uint8_t bar, addr_t addr) {
  switch (addr) {
    case REG_FW_ID:
      return 32;
    case REG_FW_VER:
      return 1;
    case REG_BOARD_ID:
      return 0x43215678;
    case REG_BOARD_VER:
      return 1;
    case REG_PHC_COUNT:
      return 1;
    case REG_PHC_OFFSET:
      return 0x200;
    case REG_PHC_STRIDE:
      return 0x80;
    case REG_IF_COUNT:
      return 1;
    case REG_IF_STRIDE:
      return 0x80000;
    case REG_IF_CSR_OFFSET:
      return 0x80000;
    case PHC_REG_FEATURES:
      return 0x1;
    case PHC_REG_PTP_CUR_SEC_L:
      return 0x0;
    case PHC_REG_PTP_CUR_SEC_H:
      return 0x0;
    case IF_REG_IF_ID:
      return 0;
    case IF_REG_IF_FEATURES:
      return this->features;
    case IF_REG_EVENT_QUEUE_COUNT:
      return 1;
    case IF_REG_EVENT_QUEUE_OFFSET:
      return 0x100000;
    case IF_REG_TX_QUEUE_COUNT:
      return 1;
    case IF_REG_TX_QUEUE_OFFSET:
      return 0x200000;
    case IF_REG_TX_CPL_QUEUE_COUNT:
      return 1;
    case IF_REG_TX_CPL_QUEUE_OFFSET:
      return 0x400000;
    case IF_REG_RX_QUEUE_COUNT:
      return 1;
    case IF_REG_RX_QUEUE_OFFSET:
      return 0x600000;
    case IF_REG_RX_CPL_QUEUE_COUNT:
      return 1;
    case IF_REG_RX_CPL_QUEUE_OFFSET:
      return 0x700000;
    case IF_REG_PORT_COUNT:
      return 1;
    case IF_REG_PORT_OFFSET:
      return 0x800000;
    case IF_REG_PORT_STRIDE:
      return 0x200000;
    case EVENT_QUEUE_HEAD_PTR_REG:
      return this->eventRing.headPtr();
    case TX_QUEUE_ACTIVE_LOG_SIZE_REG:
      return this->txRing.sizeLog();
    case TX_QUEUE_TAIL_PTR_REG:
      return this->txRing.tailPtr();
    case TX_CPL_QUEUE_HEAD_PTR_REG:
      return this->txCplRing.headPtr();
    case RX_QUEUE_TAIL_PTR_REG:
      return this->rxRing.tailPtr();
    case RX_CPL_QUEUE_HEAD_PTR_REG:
      return this->rxCplRing.headPtr();
    case PORT_REG_PORT_ID:
      return this->port.id();
    case PORT_REG_PORT_FEATURES:
      return this->port.features();
    case PORT_REG_PORT_MTU:
      return this->port.mtu();
    case PORT_REG_SCHED_COUNT:
      return this->port.schedCount();
    case PORT_REG_SCHED_OFFSET:
      return this->port.schedOffset();
    case PORT_REG_SCHED_STRIDE:
      return this->port.schedStride();
    case PORT_REG_SCHED_TYPE:
      return this->port.schedType();
    default:
      fprintf(stderr, "Unknown register read %lx\n", addr);
      abort();
  }
}

void Corundum::RegWrite(uint8_t bar, uint64_t addr, reg_t val) {
  switch (addr) {
    case REG_FW_ID:
    case REG_FW_VER:
    case REG_BOARD_ID:
    case REG_BOARD_VER:
    case REG_PHC_COUNT:
    case REG_PHC_OFFSET:
    case REG_PHC_STRIDE:
    case REG_IF_COUNT:
    case REG_IF_STRIDE:
    case REG_IF_CSR_OFFSET:
    case PHC_REG_FEATURES:
    case PHC_REG_PTP_SET_FNS:
    case PHC_REG_PTP_SET_NS:
    case PHC_REG_PTP_SET_SEC_L:
    case PHC_REG_PTP_SET_SEC_H:
      break;
    case EVENT_QUEUE_BASE_ADDR_REG:
      this->eventRing.setDMALower(val);
      break;
    case EVENT_QUEUE_BASE_ADDR_REG + 4:
      this->eventRing.setDMAUpper(val);
      break;
    case EVENT_QUEUE_ACTIVE_LOG_SIZE_REG:
      this->eventRing.setSizeLog(val);
      break;
    case EVENT_QUEUE_INTERRUPT_INDEX_REG:
      this->eventRing.setIndex(val);
      break;
    case EVENT_QUEUE_HEAD_PTR_REG:
      this->eventRing.setHeadPtr(val);
      break;
    case EVENT_QUEUE_TAIL_PTR_REG:
      this->eventRing.setTailPtr(val);
      break;
    case TX_QUEUE_BASE_ADDR_REG:
      this->txRing.setDMALower(val);
      break;
    case TX_QUEUE_BASE_ADDR_REG + 4:
      this->txRing.setDMAUpper(val);
      break;
    case TX_QUEUE_ACTIVE_LOG_SIZE_REG:
      this->txRing.setSizeLog(val);
      break;
    case TX_QUEUE_CPL_QUEUE_INDEX_REG:
      this->txRing.setIndex(val);
      break;
    case TX_QUEUE_HEAD_PTR_REG:
      this->txRing.setHeadPtr(val);
      break;
    case TX_QUEUE_TAIL_PTR_REG:
      this->txRing.setTailPtr(val);
      break;
    case TX_CPL_QUEUE_BASE_ADDR_REG:
      this->txCplRing.setDMALower(val);
      break;
    case TX_CPL_QUEUE_BASE_ADDR_REG + 4:
      this->txCplRing.setDMAUpper(val);
      break;
    case TX_CPL_QUEUE_ACTIVE_LOG_SIZE_REG:
      this->txCplRing.setSizeLog(val);
      break;
    case TX_CPL_QUEUE_INTERRUPT_INDEX_REG:
      this->txCplRing.setIndex(val);
      break;
    case TX_CPL_QUEUE_HEAD_PTR_REG:
      this->txCplRing.setHeadPtr(val);
      break;
    case TX_CPL_QUEUE_TAIL_PTR_REG:
      this->txCplRing.setTailPtr(val);
      break;
    case RX_QUEUE_BASE_ADDR_REG:
      this->rxRing.setDMALower(val);
      break;
    case RX_QUEUE_BASE_ADDR_REG + 4:
      this->rxRing.setDMAUpper(val);
      break;
    case RX_QUEUE_ACTIVE_LOG_SIZE_REG:
      this->rxRing.setSizeLog(val);
      break;
    case RX_QUEUE_CPL_QUEUE_INDEX_REG:
      this->rxRing.setIndex(val);
      break;
    case RX_QUEUE_HEAD_PTR_REG:
      this->rxRing.setHeadPtr(val);
      break;
    case RX_QUEUE_TAIL_PTR_REG:
      this->rxRing.setTailPtr(val);
      break;
    case RX_CPL_QUEUE_BASE_ADDR_REG:
      this->rxCplRing.setDMALower(val);
      break;
    case RX_CPL_QUEUE_BASE_ADDR_REG + 4:
      this->rxCplRing.setDMAUpper(val);
      break;
    case RX_CPL_QUEUE_ACTIVE_LOG_SIZE_REG:
      this->rxCplRing.setSizeLog(val);
      break;
    case RX_CPL_QUEUE_INTERRUPT_INDEX_REG:
      this->rxCplRing.setIndex(val);
      break;
    case RX_CPL_QUEUE_HEAD_PTR_REG:
      this->rxCplRing.setHeadPtr(val);
      break;
    case RX_CPL_QUEUE_TAIL_PTR_REG:
      this->rxCplRing.setTailPtr(val);
      break;
    case PORT_REG_SCHED_ENABLE:
      if (val) {
        this->port.schedEnable();
      } else {
        this->port.schedDisable();
      }
      break;
    case PORT_REG_RSS_MASK:
      this->port.setRssMask(val);
      break;
    case PORT_QUEUE_ENABLE:
      if (val) {
        this->port.queueEnable();
      } else {
        this->port.queueDisable();
      }
      break;
    default:
      fprintf(stderr, "Unknown register write %lx\n", addr);
      abort();
  }
}

void Corundum::SetupIntro(struct SimbricksProtoPcieDevIntro &di) {
  di.bars[0].len = 1 << 24;
  di.bars[0].flags = SIMBRICKS_PROTO_PCIE_BAR_64;
  di.pci_vendor_id = 0x5543;
  di.pci_device_id = 0x1001;
  di.pci_class = 0x02;
  di.pci_subclass = 0x00;
  di.pci_revision = 0x00;
  di.pci_msi_nvecs = 32;
}

void Corundum::DmaComplete(nicbm::DMAOp &op) {
  DMAOp *op_ = reinterpret_cast<DMAOp *>(&op);
  op_->ring->dmaDone(op_);
}

void Corundum::EthRx(uint8_t port, const void *data, size_t len) {
  RxData *rx_data = new RxData;
  memcpy((void *)rx_data->data, data, len);
  rx_data->len = len;
  rxRing.rx(rx_data);
}

}  // namespace corundum

int main(int argc, char *argv[]) {
  corundum::Corundum dev;
  runner = new nicbm::Runner(dev);
  return runner->RunMain(argc, argv);
}
