#include <algorithm>
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <dlfcn.h>
#include <iterator>
#include <sys/ioctl.h>

namespace {
struct MailboxMessage {
  unsigned word[5];
};

struct MailboxArgument {
  const MailboxMessage *sent;
  MailboxMessage *received;
};

struct SecondaryConfigOperation {
  unsigned offset;
  unsigned value;
  unsigned read;
  unsigned write;
};

struct AttachedBuffer {
  unsigned index;
  unsigned size;
  std::uint64_t address;
};

AttachedBuffer attachedBuffers[2048]{};

volatile std::uint32_t *configurationBar() {
  FILE *maps = std::fopen("/proc/self/maps", "r");
  if (!maps) return nullptr;
  char line[1024];
  unsigned long start = 0;
  while (std::fgets(line, sizeof(line), maps)) {
    if (std::sscanf(line, "%lx-", &start) == 1 &&
        std::strstr(line, " /dev/ipu0\n")) {
      std::fclose(maps);
      return reinterpret_cast<volatile std::uint32_t *>(start);
    }
  }
  std::fclose(maps);
  return nullptr;
}

std::uint32_t readConfiguration(volatile std::uint32_t *bar,
                                std::uint32_t offset) {
  return bar[offset / sizeof(std::uint32_t)];
}

void printConfiguration(const char *phase) {
  auto *bar = configurationBar();
  if (!bar) return;
  std::fprintf(stderr,
               "IPU_CONFIG phase=%s CCSR=%08x GS1=%08x GS2=%08x "
               "EXCHWINDOWBASER=%08x\n",
               phase, readConfiguration(bar, 0x2000),
               readConfiguration(bar, 0x302c), readConfiguration(bar, 0x3030),
               readConfiguration(bar, 0x3044));
  std::fputs("IPU_CONFIG XB.DCXCR", stderr);
  for (unsigned instance = 0; instance != 16; ++instance) {
    const std::uint32_t offset = instance < 8 ? 0x5404 + instance * 0x400
                                               : 0x1b804 + (instance - 8) * 0x400;
    std::fprintf(stderr, " %u=%08x", instance,
                 readConfiguration(bar, offset));
  }
  std::fputc('\n', stderr);
}

void dumpBuffer(const AttachedBuffer &buffer) {
  const char *directory = std::getenv("IPU_IOCTL_TRACE_DUMP");
  if (!directory || !buffer.address || !buffer.size) return;
  char path[512];
  std::snprintf(path, sizeof(path), "%s/ipu-buffer-%u-%u.bin", directory,
                buffer.index, buffer.size);
  if (FILE *output = std::fopen(path, "wb")) {
    std::fwrite(reinterpret_cast<const void *>(buffer.address), 1,
                buffer.size, output);
    std::fclose(output);
  }
}

void dumpBufferAtPhase(const AttachedBuffer &buffer, const char *phase) {
  const char *directory = std::getenv("IPU_IOCTL_TRACE_DUMP");
  if (!directory || !phase || !buffer.address || !buffer.size) return;
  char path[512];
  std::snprintf(path, sizeof(path), "%s/ipu-buffer-%u-%u-%s.bin", directory,
                buffer.index, buffer.size, phase);
  if (FILE *output = std::fopen(path, "wb")) {
    std::fwrite(reinterpret_cast<const void *>(buffer.address), 1,
                buffer.size, output);
    std::fclose(output);
  }
}

void printMessage(const char *direction, const MailboxMessage *message) {
  if (!message) return;
  std::fprintf(stderr, "IPU_IOCTL mailbox %s %08x %08x %08x %08x %08x\n",
               direction, message->word[0], message->word[1], message->word[2],
               message->word[3], message->word[4]);
}

} // namespace

extern "C" {
std::uint64_t ipuIoctlTraceBufferAddresses[2048]{};
std::uint32_t ipuIoctlTraceBufferSizes[2048]{};
}

extern "C" void ipuIoctlTraceSnapshot(const char *phase) {
  printConfiguration(phase);
  for (const auto &buffer : attachedBuffers)
    dumpBufferAtPhase(buffer, phase);
}

extern "C" int ioctl(int fd, unsigned long request, ...) {
  using Ioctl = int (*)(int, unsigned long, ...);
  static const auto realIoctl = reinterpret_cast<Ioctl>(
      dlsym(RTLD_NEXT, "ioctl"));
  unsigned long argument = 0;
  va_list arguments;
  va_start(arguments, request);
  if (_IOC_DIR(request) != _IOC_NONE || _IOC_SIZE(request) != 0)
    argument = va_arg(arguments, unsigned long);
  va_end(arguments);

  const unsigned number = _IOC_NR(request);
  if (_IOC_TYPE(request) == 0xed && number == 11) {
    const auto *buffer = reinterpret_cast<const AttachedBuffer *>(argument);
    if (buffer) {
      if (buffer->index < std::size(attachedBuffers))
        attachedBuffers[buffer->index] = *buffer;
      if (buffer->index < std::size(ipuIoctlTraceBufferAddresses)) {
        ipuIoctlTraceBufferAddresses[buffer->index] = buffer->address;
        ipuIoctlTraceBufferSizes[buffer->index] = buffer->size;
      }
      std::fprintf(stderr,
                   "IPU_IOCTL buffer-attach index=%u size=%u address=%p head=",
                   buffer->index, buffer->size,
                   reinterpret_cast<void *>(buffer->address));
      const auto *bytes = reinterpret_cast<const unsigned char *>(
          static_cast<std::uintptr_t>(buffer->address));
      for (unsigned i = 0; i < std::min(buffer->size, 32u); ++i)
        std::fprintf(stderr, "%02x", bytes[i]);
      std::fputc('\n', stderr);
    }
  } else if (_IOC_TYPE(request) == 0xed && number == 12) {
    std::fprintf(stderr, "IPU_IOCTL buffer-detach index=%lu\n", argument);
    if (argument < std::size(attachedBuffers)) {
      dumpBuffer(attachedBuffers[argument]);
      attachedBuffers[argument] = {};
      ipuIoctlTraceBufferAddresses[argument] = 0;
      ipuIoctlTraceBufferSizes[argument] = 0;
    }
  } else if (_IOC_TYPE(request) == 0xed && number == 31) {
    const auto *mailbox = reinterpret_cast<const MailboxArgument *>(argument);
    printMessage("send", mailbox ? mailbox->sent : nullptr);
  } else if (_IOC_TYPE(request) == 0xed && number == 46) {
    const auto *operation =
        reinterpret_cast<const SecondaryConfigOperation *>(argument);
    if (operation)
      std::fprintf(stderr,
                   "IPU_IOCTL secondary offset=%08x value=%08x read=%u write=%u\n",
                   operation->offset, operation->value, operation->read,
                   operation->write);
  } else if (_IOC_TYPE(request) == 0xed && number == 47) {
    const auto *ipuId = reinterpret_cast<const unsigned *>(argument);
    std::fprintf(stderr, "IPU_IOCTL secondary-ipuid=%u\n",
                 ipuId ? *ipuId : ~0u);
  } else if (_IOC_TYPE(request) == 0xed &&
             (number == 10 || number == 13 || number == 14 || number == 34 ||
              number == 35 || number == 36 || number == 44 || number == 45)) {
    std::fprintf(stderr, "IPU_IOCTL number=%u argument=%#lx\n", number,
                 argument);
  }

  const int result = realIoctl(fd, request, argument);
  if (_IOC_TYPE(request) == 0xed && number == 31) {
    const auto *mailbox = reinterpret_cast<const MailboxArgument *>(argument);
    printMessage("recv", mailbox ? mailbox->received : nullptr);
  }
  return result;
}
