#include "hardwaretests.h"

#include <cassert>
#include <cfenv>
#include <common/bit_cast.h>
#include <common/floatutils.h>
#include <common/log.h>
#include <common/strutils.h>
#include <filesystem>
#include <fstream>
#include <libcpu/cpu.h>
#include <libcpu/mem.h>
#include <libcpu/espresso/espresso_disassembler.h>
#include <libcpu/espresso/espresso_instructionset.h>
#include <libcpu/espresso/espresso_registerformats.h>

using namespace espresso;

static const auto TEST_FPSCR = false;
static const auto TEST_FPSCR_FR = false;
static const auto TEST_FPSCR_UX = false;
static const auto TEST_FMADDSUB = false;

namespace hwtest
{

static void
printTestField(InstructionField field, Instruction instr, RegisterState *input, RegisterState *output, cpu::CoreRegs *state)
{
   auto printGPR = [&](uint32_t reg) {
      assert(reg >= GPR_BASE);

      gLog->debug("r{:02d}    =         {:08X}         {:08X}         {:08X}", reg,
                  input->gpr[reg - GPR_BASE],
                  output->gpr[reg - GPR_BASE],
                  state->gpr[reg]);
   };

   auto printFPR = [&](uint32_t reg) {
      assert(reg >= FPR_BASE);

      gLog->debug("f{:02d}    = {:16e} {:16e} {:16e}", reg,
                  input->fr[reg - FPR_BASE],
                  output->fr[reg - FPR_BASE],
                  state->fpr[reg].value);

      gLog->debug("         {:16X} {:16X} {:16X}",
                  bit_cast<uint64_t>(input->fr[reg - FPR_BASE]),
                  bit_cast<uint64_t>(output->fr[reg - FPR_BASE]),
                  bit_cast<uint64_t>(state->fpr[reg].value));
   };

   switch (field) {
   case InstructionField::rA:
      printGPR(instr.rA);
      break;
   case InstructionField::rB:
      printGPR(instr.rB);
      break;
   case InstructionField::rD:
      printGPR(instr.rS);
      break;
   case InstructionField::rS:
      printGPR(instr.rS);
      break;
   case InstructionField::frA:
      printFPR(instr.frA);
      break;
   case InstructionField::frB:
      printFPR(instr.frB);
      break;
   case InstructionField::frC:
      printFPR(instr.frC);
      break;
   case InstructionField::frD:
      printFPR(instr.frD);
      break;
   case InstructionField::frS:
      printFPR(instr.frS);
      break;
   case InstructionField::XERC:
      gLog->debug("xer.ca =         {:08X}         {:08X}         {:08X}", input->xer.ca, output->xer.ca, state->xer.ca);
      break;
   case InstructionField::XERSO:
      gLog->debug("xer.so =         {:08X}         {:08X}         {:08X}", input->xer.so, output->xer.so, state->xer.so);
      break;
   case InstructionField::FPSCR:
      gLog->debug("fpscr =          {:08X}         {:08x}         {:08X}", input->fpscr.value, output->fpscr.value, state->fpscr.value);
      break;
   default:
      break;
   }
}

#define CompareFPSCRField(field) \
   if (result.field != expected.field) { \
      gLog->debug("fpscr." #field " = input {} expected {} found {}", input.field, expected.field, result.field); \
      failed = true; \
   }

// Compare all individual fields in fpscr
static bool
compareFPSCR(FloatingPointStatusAndControlRegister input,
             FloatingPointStatusAndControlRegister expected,
             FloatingPointStatusAndControlRegister result)
{
   auto failed = false;
   CompareFPSCRField(rn);
   CompareFPSCRField(ni);
   CompareFPSCRField(xe);
   CompareFPSCRField(ze);
   CompareFPSCRField(ue);
   CompareFPSCRField(oe);
   CompareFPSCRField(ve);
   CompareFPSCRField(vxcvi);
   CompareFPSCRField(vxsqrt);
   CompareFPSCRField(vxsoft);
   CompareFPSCRField(fprf);
   CompareFPSCRField(fi);
   CompareFPSCRField(fr);
   CompareFPSCRField(vxvc);
   CompareFPSCRField(vximz);
   CompareFPSCRField(vxzdz);
   CompareFPSCRField(vxidi);
   CompareFPSCRField(vxisi);
   CompareFPSCRField(vxsnan);
   CompareFPSCRField(xx);
   CompareFPSCRField(zx);
   CompareFPSCRField(ux);
   CompareFPSCRField(ox);
   CompareFPSCRField(vx);
   CompareFPSCRField(fex);
   CompareFPSCRField(fx);
   return failed;
}

int runTests(const std::string &path)
{
   uint32_t testsFailed = 0, testsPassed = 0;
   auto baseAddress = cpu::VirtualAddress { 0x02000000u };
   auto basePhysicalAddress = cpu::PhysicalAddress { 0x50000000u };
   auto codeSize = 2048u;

   cpu::allocateVirtualAddress(baseAddress, codeSize);
   cpu::mapMemory(baseAddress, basePhysicalAddress, codeSize, cpu::MapPermission::ReadWrite);

   Instruction bclr = encodeInstruction(InstructionID::bclr);
   bclr.bo = 0x1f;
   mem::write(baseAddress.getAddress() + 4, bclr.value);

   auto ec = std::error_code { };
   for (auto itr = std::filesystem::directory_iterator { "/tests", ec }; itr != end(itr); ++itr) {
      std::ifstream file(itr->path().string(), std::ifstream::in | std::ifstream::binary);
      cereal::BinaryInputArchive cerealInput(file);
      TestFile testFile;

      // Parse test file with cereal
      testFile.name = itr->path().filename().string();
      cerealInput(testFile);

      // Run tests
      for (auto &test : testFile.tests) {
         bool failed = false;

         if (!TEST_FMADDSUB) {
            auto data = espresso::decodeInstruction(test.instr);
            switch (data->id) {
            case InstructionID::fmadd:
            case InstructionID::fmadds:
            case InstructionID::fmsub:
            case InstructionID::fmsubs:
            case InstructionID::fnmadd:
            case InstructionID::fnmadds:
            case InstructionID::fnmsub:
            case InstructionID::fnmsubs:
               failed = true;
               break;
            }
            if (failed) {
               continue;
            }
         }

         // Setup core state from test input
         cpu::CoreRegs *state = cpu::this_core::state();
         memset(state, 0, sizeof(cpu::CoreRegs));
         state->cia = 0;
         state->nia = baseAddress.getAddress();
         state->xer = test.input.xer;
         state->cr = test.input.cr;
         state->fpscr = test.input.fpscr;
         state->ctr = test.input.ctr;

         for (auto i = 0; i < 4; ++i) {
            state->gpr[i + 3] = test.input.gpr[i];
            state->fpr[i + 1].paired0 = test.input.fr[i];
         }

         // Execute test
         mem::write(baseAddress.getAddress(), test.instr.value);
         cpu::clearInstructionCache();
         cpu::this_core::executeSub();

         // Check XER (all bits)
         if (state->xer.value != test.output.xer.value) {
            gLog->error("Test failed, xer expected {:08X} found {:08X}", test.output.xer.value, state->xer.value);
            failed = true;
         }

         // Check Condition Register (all bits)
         if (state->cr.value != test.output.cr.value) {
            gLog->error("Test failed, cr expected {:08X} found {:08X}", test.output.cr.value, state->cr.value);
            failed = true;
         }

         // Check FPSCR
         if (TEST_FPSCR) {
            if (!TEST_FPSCR_FR) {
               state->fpscr.fr = 0;
               test.output.fpscr.fr = 0;
            }

            if (!TEST_FPSCR_UX) {
               state->fpscr.ux = 0;
               test.output.fpscr.ux = 0;
            }

            auto state_fpscr = state->fpscr.value;
            auto test_fpscr = test.output.fpscr.value;

            if (state_fpscr != test_fpscr) {
               gLog->error("Test failed, fpscr {:08X} found {:08X}", test.output.fpscr.value, state->fpscr.value);
               compareFPSCR(test.input.fpscr, state->fpscr, test.output.fpscr);
               failed = true;
            }
         }

         // Check CTR
         if (state->ctr != test.output.ctr) {
            gLog->error("Test failed, ctr expected {:08X} found {:08X}", test.output.ctr, state->ctr);
            failed = true;
         }

         // Check all GPR
         for (auto i = 0; i < 4; ++i) {
            auto reg = i + hwtest::GPR_BASE;
            auto value = state->gpr[reg];
            auto expected = test.output.gpr[i];

            if (value != expected) {
               gLog->error("Test failed, r{} expected {:08X} found {:08X}", reg, expected, value);
               failed = true;
            }
         }

         // Check all FPR
         for (auto i = 0; i < 4; ++i) {
            auto reg = i + hwtest::FPR_BASE;
            auto value = state->fpr[reg].value;
            auto expected = test.output.fr[i];

            if (!is_nan(value) && !is_nan(expected) && !is_infinity(value) && !is_infinity(expected)) {
               double dval = value / expected;

               if (dval < 0.999 || dval > 1.001) {
                  gLog->error("Test failed, f{} expected {:16f} found {:16f}", reg, expected, value);
                  failed = true;
               }
            } else {
               if (is_nan(value) && is_nan(expected)) {
                  auto bits = get_float_bits(value);
                  bits.sign = get_float_bits(expected).sign;
                  value = bits.v;
               }

               if (bit_cast<uint64_t>(value) != bit_cast<uint64_t>(expected)) {
                  gLog->error("Test failed, f{} expected {:16X} found {:16X}", reg, bit_cast<uint64_t>(expected), bit_cast<uint64_t>(value));
                  failed = true;
               }
            }
         }

         if (failed) {
            Disassembly dis;

            // Print disassembly
            disassemble(test.instr, dis, baseAddress.getAddress());
            gLog->debug(disassemblyToText(dis));

            // Print all test fields
            gLog->debug("{:08x}            Input         Hardware           Interp", test.instr.value);

            for (auto field : dis.instruction->read) {
               printTestField(field, test.instr, &test.input, &test.output, state);
            }

            for (auto field : dis.instruction->write) {
               printTestField(field, test.instr, &test.input, &test.output, state);
            }

            for (auto field : dis.instruction->flags) {
               printTestField(field, test.instr, &test.input, &test.output, state);
            }

            gLog->debug("");
            ++testsFailed;
         } else {
            ++testsPassed;
         }
      }
   }

   if (testsFailed) {
      gLog->error("Failed {} of {} tests.", testsFailed, testsFailed + testsPassed);
   }

   return testsFailed;
}

} // namespace hwtest
