#include "core/math.h"
#include "guest/dreamcast.h"
#include "guest/sh4/sh4.h"
#include "retest.h"

static const uint32_t UNINITIALIZED_REG = 0xbaadf00d;

struct sh4_test {
  const char *name;
  const uint8_t *buffer;
  int buffer_size;
  int buffer_offset;
  struct sh4_context in;
  struct sh4_context out;
};

struct sh4_test_reg {
  const char *name;
  size_t offset;
  int size;
};

/* as per the notes in sh4_context.h, the fr / xf register pairs are swapped */
static struct sh4_test_reg sh4_test_regs[] = {
    {"fpscr", offsetof(struct sh4_context, fpscr), 4},
    {"r0", offsetof(struct sh4_context, r[0]), 4},
    {"r1", offsetof(struct sh4_context, r[1]), 4},
    {"r2", offsetof(struct sh4_context, r[2]), 4},
    {"r3", offsetof(struct sh4_context, r[3]), 4},
    {"r4", offsetof(struct sh4_context, r[4]), 4},
    {"r5", offsetof(struct sh4_context, r[5]), 4},
    {"r6", offsetof(struct sh4_context, r[6]), 4},
    {"r7", offsetof(struct sh4_context, r[7]), 4},
    {"r8", offsetof(struct sh4_context, r[8]), 4},
    {"r9", offsetof(struct sh4_context, r[9]), 4},
    {"r10", offsetof(struct sh4_context, r[10]), 4},
    {"r11", offsetof(struct sh4_context, r[11]), 4},
    {"r12", offsetof(struct sh4_context, r[12]), 4},
    {"r13", offsetof(struct sh4_context, r[13]), 4},
    {"r14", offsetof(struct sh4_context, r[14]), 4},
    {"r15", offsetof(struct sh4_context, r[15]), 4},
    {"fr0", offsetof(struct sh4_context, fr[1]), 4},
    {"fr1", offsetof(struct sh4_context, fr[0]), 4},
    {"fr2", offsetof(struct sh4_context, fr[3]), 4},
    {"fr3", offsetof(struct sh4_context, fr[2]), 4},
    {"fr4", offsetof(struct sh4_context, fr[5]), 4},
    {"fr5", offsetof(struct sh4_context, fr[4]), 4},
    {"fr6", offsetof(struct sh4_context, fr[7]), 4},
    {"fr7", offsetof(struct sh4_context, fr[6]), 4},
    {"fr8", offsetof(struct sh4_context, fr[9]), 4},
    {"fr9", offsetof(struct sh4_context, fr[8]), 4},
    {"fr10", offsetof(struct sh4_context, fr[11]), 4},
    {"fr11", offsetof(struct sh4_context, fr[10]), 4},
    {"fr12", offsetof(struct sh4_context, fr[13]), 4},
    {"fr13", offsetof(struct sh4_context, fr[12]), 4},
    {"fr14", offsetof(struct sh4_context, fr[15]), 4},
    {"fr15", offsetof(struct sh4_context, fr[14]), 4},
    {"xf0", offsetof(struct sh4_context, xf[1]), 4},
    {"xf1", offsetof(struct sh4_context, xf[0]), 4},
    {"xf2", offsetof(struct sh4_context, xf[3]), 4},
    {"xf3", offsetof(struct sh4_context, xf[2]), 4},
    {"xf4", offsetof(struct sh4_context, xf[5]), 4},
    {"xf5", offsetof(struct sh4_context, xf[4]), 4},
    {"xf6", offsetof(struct sh4_context, xf[7]), 4},
    {"xf7", offsetof(struct sh4_context, xf[6]), 4},
    {"xf8", offsetof(struct sh4_context, xf[9]), 4},
    {"xf9", offsetof(struct sh4_context, xf[8]), 4},
    {"xf10", offsetof(struct sh4_context, xf[11]), 4},
    {"xf11", offsetof(struct sh4_context, xf[10]), 4},
    {"xf12", offsetof(struct sh4_context, xf[13]), 4},
    {"xf13", offsetof(struct sh4_context, xf[12]), 4},
    {"xf14", offsetof(struct sh4_context, xf[15]), 4},
    {"xf15", offsetof(struct sh4_context, xf[14]), 4},
};
int sh4_num_test_regs = (int)(sizeof(sh4_test_regs) / sizeof(sh4_test_regs[0]));

static void run_sh4_test(struct dreamcast *dc, const struct sh4_test *test) {
  LOG_INFO("running %s..", test->name);

  /* load binary */
  as_memcpy_to_guest(dc->sh4->memory_if->space, 0x8c010000, test->buffer,
                     test->buffer_size);

  /* skip to the test's offset */
  sh4_reset(dc->sh4, 0x8c010000 + test->buffer_offset);

  /* setup in registers */
  for (int i = 0; i < sh4_num_test_regs; i++) {
    struct sh4_test_reg *reg = &sh4_test_regs[i];

    uint32_t input = *(uint32_t *)((uint8_t *)&test->in + reg->offset);

    if (input == UNINITIALIZED_REG) {
      continue;
    }

    *(uint32_t *)((uint8_t *)&dc->sh4->ctx + reg->offset) = input;
  }

  /* run until the function returns */
  dc_resume(dc);

  while (dc->sh4->ctx.pc) {
    dc_tick(dc, 1);
  }

  /* ensure sh4 sr is up to date before testing against it */
  sh4_implode_sr(&dc->sh4->ctx);

  /* validate out registers */
  for (int i = 0; i < sh4_num_test_regs; i++) {
    struct sh4_test_reg *reg = &sh4_test_regs[i];

    uint32_t expected = *(uint32_t *)((uint8_t *)&test->out + reg->offset);

    if (expected == UNINITIALIZED_REG) {
      continue;
    }

    uint32_t actual = *(uint32_t *)((uint8_t *)&dc->sh4->ctx + reg->offset);
    CHECK_EQ(expected, actual, "%s expected 0x%08x, actual 0x%08x", reg->name,
             expected, actual);
  }
}

TEST(sh4_x64) {
  struct dreamcast *dc = dc_create(NULL);
  CHECK_NOTNULL(dc);

/* clang-format off */
#define INIT_CONTEXT(fpscr, r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11,                    \
                     r12, r13, r14, r15, fr0, fr1, fr2, fr3, fr4, fr5, fr6,                      \
                     fr7, fr8, fr9, fr10, fr11, fr12, fr13, fr14, fr15, xf0,                     \
                     xf1, xf2, xf3, xf4, xf5, xf6, xf7, xf8, xf9, xf10, xf11,                    \
                     xf12, xf13, xf14, xf15)                                                     \
  {                                                                                              \
    {r0, r1, r2,  r3,  r4,  r5,  r6,  r7, r8, r9, r10, r11, r12, r13, r14, r15},                 \
    {0},                                                                                         \
    {fr1, fr0, fr3,  fr2,  fr5,  fr4,  fr7,  fr6, fr9, fr8, fr11, fr10, fr13, fr12, fr15, fr14}, \
    {xf1, xf0, xf3,  xf2,  xf5,  xf4,  xf7,  xf6, xf9, xf8, xf11, xf10, xf13, xf12, xf15, xf14}, \
    0, 0, 0, 0, 0, 0, 0, fpscr,                                                                  \
    0, 0, 0,                                                                                     \
    0, 0, 0,                                                                                     \
    0, 0, 0,                                                                                     \
    0,                                                                                           \
    {{0}, {0}},                                                                                  \
    0, 0, 0,                                                                                     \
    {0},                                                                                         \
  }
#define TEST_SH4(name, buffer, buffer_size, buffer_offset,                                                                                                                 \
  fpscr_in,                                                                                                                                                                \
  r0_in,  r1_in,  r2_in,  r3_in,  r4_in,  r5_in,   r6_in,   r7_in,  r8_in,  r9_in,  r10_in,  r11_in,  r12_in,  r13_in,  r14_in,  r15_in,                                   \
  fr0_in, fr1_in, fr2_in, fr3_in, fr4_in, fr5_in,  fr6_in,  fr7_in, fr8_in, fr9_in, fr10_in, fr11_in, fr12_in, fr13_in, fr14_in, fr15_in,                                  \
  xf0_in, xf1_in, xf2_in, xf3_in, xf4_in, xf5_in,  xf6_in,  xf7_in, xf8_in, xf9_in, xf10_in, xf11_in, xf12_in, xf13_in, xf14_in, xf15_in,                                  \
  fpscr_out,                                                                                                                                                               \
  r0_out,  r1_out,  r2_out,  r3_out,  r4_out,  r5_out,   r6_out,   r7_out,  r8_out,  r9_out,  r10_out,  r11_out,  r12_out,  r13_out,  r14_out,  r15_out,                   \
  fr0_out, fr1_out, fr2_out, fr3_out, fr4_out, fr5_out,  fr6_out,  fr7_out, fr8_out, fr9_out, fr10_out, fr11_out, fr12_out, fr13_out, fr14_out, fr15_out,                  \
  xf0_out, xf1_out, xf2_out, xf3_out, xf4_out, xf5_out,  xf6_out,  xf7_out, xf8_out, xf9_out, xf10_out, xf11_out, xf12_out, xf13_out, xf14_out, xf15_out)                  \
  {                                                                                                                                                                        \
    static struct sh4_test test_##name = {                                                                                                                                 \
      #name, buffer, buffer_size, buffer_offset,                                                                                                                           \
      INIT_CONTEXT(fpscr_in,                                                                                                                                               \
                   r0_in,  r1_in,  r2_in,  r3_in,  r4_in,  r5_in,   r6_in,   r7_in,  r8_in,  r9_in,  r10_in,  r11_in,  r12_in,  r13_in,  r14_in,  r15_in,                  \
                   fr0_in, fr1_in, fr2_in, fr3_in, fr4_in, fr5_in,  fr6_in,  fr7_in, fr8_in, fr9_in, fr10_in, fr11_in, fr12_in, fr13_in, fr14_in, fr15_in,                 \
                   xf0_in, xf1_in, xf2_in, xf3_in, xf4_in, xf5_in,  xf6_in,  xf7_in, xf8_in, xf9_in, xf10_in, xf11_in, xf12_in, xf13_in, xf14_in, xf15_in),                \
      INIT_CONTEXT(fpscr_out,                                                                                                                                              \
                   r0_out,  r1_out,  r2_out,  r3_out,  r4_out,  r5_out,   r6_out,   r7_out,  r8_out,  r9_out,  r10_out,  r11_out,  r12_out,  r13_out,  r14_out,  r15_out,  \
                   fr0_out, fr1_out, fr2_out, fr3_out, fr4_out, fr5_out,  fr6_out,  fr7_out, fr8_out, fr9_out, fr10_out, fr11_out, fr12_out, fr13_out, fr14_out, fr15_out, \
                   xf0_out, xf1_out, xf2_out, xf3_out, xf4_out, xf5_out,  xf6_out,  xf7_out, xf8_out, xf9_out, xf10_out, xf11_out, xf12_out, xf13_out, xf14_out, xf15_out) \
    };                                                                                                                                                                     \
    run_sh4_test(dc, &test_##name);                                                                                                                                        \
  }
#include "test_sh4.inc"
#undef TEST_SH4
  /* clang-format on */

  dc_destroy(dc);
}
