#include <cstring>

#include "../../src/ErrorSimulator.hpp"
#include "../Tests.hpp"
#include "core/Test.hpp"
#include "core/ToString.hpp"
#include "core/Utility.hpp"

static const float eps = 0.0001f;

static void testPopCount() {
    TEST(4, Core::popCount(0xF));
    TEST(0, Core::popCount(0x0));
    TEST(2, Core::popCount(0x6));
    TEST(7, Core::popCount(0x7F));
    TEST(3, Core::popCount(0x2A));
    TEST(32, Core::popCount(0xFFFF'FFFF));
    TEST(64, Core::popCount(0xFFFF'FFFF'FFFF'FFFF));
    TEST(44, Core::popCount(0xFFFF'0FFF'FFFF));
}

static void testIf() {
    TEST_TRUE((Core::IsSame<Core::If<true, int, double>, int>));
    TEST_TRUE((Core::IsSame<Core::If<false, int, double>, double>));
}

static void testZeroRellocate() {
    void* buffer = Core::reallocateRaw(nullptr, 16);
    TEST_NOT_NULL(buffer);
    buffer = Core::reallocateRaw(buffer, 0);
    TEST_NULL(buffer);
}

static void testMemoryInfoList() {
    void* a = Core::allocateRaw(8);
    void* b = Core::allocateRaw(8);
    void* c = Core::allocateRaw(8);
    void* d = Core::allocateRaw(8);
    Core::deallocateRaw(b); // remove middle element
    Core::deallocateRaw(a); // remove first
    Core::deallocateRaw(d); // remove last
    Core::deallocateRaw(c); // remove single
    Core::deallocateRaw(nullptr);
}

static void testZeroAllocate() {
    constexpr size_t n = 1024 * 1024;
    void* a = Core::allocateRaw(n);
    memset(a, 0, n);
    void* b = Core::zeroAllocateRaw(n);
    TEST_TRUE(memcmp(a, b, n) == 0);
    Core::deallocateRaw(a);
    Core::deallocateRaw(b);
}

static void testInterpolate() {
    TEST_FLOAT(7.5f, Core::interpolate(5.0f, 10.0f, 0.5f), eps);
    TEST_FLOAT(-2.0, Core::interpolate(-10.0f, 10.0f, 0.4f), eps);
    TEST_FLOAT(10.0f, Core::interpolate(-3.0f, 10.0f, 1.0f), eps);
    TEST_FLOAT(7.0f, Core::interpolate(7.0f, 10.0f, 0.0f), eps);
    TEST_FLOAT(6.0f, Core::interpolate(0.0f, 10.0f, 0.6f), eps);
}

static void testRadianToDegree() {
    TEST_FLOAT(45.0f, Core::radianToDegree(Core::PI * 0.25f), eps);
    TEST_FLOAT(90.0f, Core::radianToDegree(Core::PI * 0.5f), eps);
    TEST_FLOAT(180.0f, Core::radianToDegree(Core::PI), eps);
    TEST_FLOAT(360.0f, Core::radianToDegree(Core::PI * 2.0f), eps);
}

static void testDegreeToRadian() {
    TEST_FLOAT(Core::PI * 0.25f, Core::degreeToRadian(45.0f), eps);
    TEST_FLOAT(Core::PI * 0.5f, Core::degreeToRadian(90.0f), eps);
    TEST_FLOAT(Core::PI, Core::degreeToRadian(180.0f), eps);
    TEST_FLOAT(Core::PI * 2.0f, Core::degreeToRadian(360.0f), eps);
}

static void testSleep(i64 nanos) {
    i64 time = -Core::getNanos();
    TEST_FALSE(Core::sleepNanos(nanos));
    time += Core::getNanos();
    TEST_TRUE(time >= nanos && time <= (nanos * 13) / 10);
}

static void testSleepMillis(i64 millis) {
    i64 time = -Core::getNanos();
    TEST_FALSE(Core::sleepMillis(millis));
    time += Core::getNanos();
    i64 nanos = millis * 1'000'000;
    TEST_TRUE(time >= nanos && time <= (nanos * 13) / 10);
}

typedef struct {
    int i;
    i64 d;
} SwapTest;

static void testSwap() {
    SwapTest a = {3, 20};
    SwapTest b = {7, 30};
    Core::swap(a, b);
    TEST(7, a.i);
    TEST(3, b.i);
    TEST(30l, a.d);
    TEST(20l, b.d);
}

static void testFail() {
#ifdef ERROR_SIMULATOR
    failStepThrow = 1;
    TEST(-1l, Core::getNanos());
    failStepThrow = 1;
    TEST_TRUE(Core::sleepMillis(5));
    failStepThrow = 0;
#endif
}

static void testSort() {
    size_t data[] = {9, 0, 3, 1, 8, 4, 6, 2, 5, 7};
    size_t n = 10;
    Core::bubbleSort(data, n);
    Core::bubbleSort(data, n);
    Core::bubbleSort(data, 0);
    for(size_t i = 0; i < n; i++) {
        TEST(data[i], i);
    }
}

static void testMinMax() {
    TEST(5, Core::min<int>(5, 7));
    TEST(7, Core::max<int>(5, 7));
    TEST(5, Core::clamp<int>(3, 5, 7));
    TEST(7, Core::clamp<int>(9, 5, 7));
    TEST(6, Core::clamp<int>(6, 5, 7));
}

static void testToString() {
    char buffer[512];
    TEST(10, formatBuffer(buffer, sizeof(buffer), "a#a##a", 1.0, 2, 3));
    TEST_STRING("a1.00a#a23", buffer);
    TEST(5, formatBuffer(buffer, sizeof(buffer), "aa##ab"));
    TEST_STRING("aa#ab", buffer);
    TEST(6, formatBuffer(buffer, 3, "aaaaaa"));
    TEST_STRING("aa", buffer);
    TEST(4, formatBuffer(buffer, 4, "a#", 456));
    TEST_STRING("a45", buffer);
    TEST(10, formatBuffer(buffer, 4, "# # #", 456, 567, 78));
    TEST_STRING("456", buffer);
    TEST(10, formatBuffer(buffer, 1, "# # #", 456, 567, 78));
    TEST_STRING("", buffer);
    TEST(10, formatBuffer(nullptr, 0, "# # #", 456ll, 567l, 78ull));
}

void testUtility(bool light) {
    testPopCount();
    testIf();
    testZeroRellocate();
    testMemoryInfoList();
    testZeroAllocate();
    testInterpolate();
    testRadianToDegree();
    testDegreeToRadian();
    testSleep(light ? 5'000'000 : 50'000'000);
    testSleep(light ? 50'000'000 : 1'300'000'000);
    testSleepMillis(light ? 5 : 50);
    testSleepMillis(light ? 50 : 1300);
    testSwap();
    testFail();
    testSort();
    testMinMax();
    testToString();
}

static void outOfMemory(void*) {
    Core::setOutOfMemoryHandler(nullptr, nullptr);
}

void testInvalidAllocate(void) {
    Core::setOutOfMemoryHandler(outOfMemory, nullptr);
    Core::allocateRaw(0xFFF'FFFF'FFFF);
    TEST_TRUE(false);
    Core::finalizeTests();
    EXIT(0);
}

void testInvalidReallocate(void) {
    Core::setOutOfMemoryHandler(outOfMemory, nullptr);
    void* p = Core::allocateRaw(0xFF);
    Core::printMemoryReport();
    Core::reallocateRaw(p, 0xFFF'FFFF'FFFF);
    TEST_TRUE(false);
    Core::finalizeTests();
    EXIT(0);
}

[[noreturn]] void testPreCanary(void) {
#ifdef CHECK_MEMORY
    char* p = static_cast<char*>(Core::allocateRaw(16));
    p[-1] = 0;
    Core::deallocateRaw(p);
    TEST_TRUE(false);
#endif
    Core::finalizeTests();
    EXIT(0);
}

[[noreturn]] void testPreCanaryNew(void) {
#ifdef CHECK_MEMORY
    volatile char* p2 = new char[3];
    volatile char* p = coreNewN(char, 16);
    delete[] p2;
    coreDeleteN(p);
    p = coreNewN(char, 16);
    p[-1] = 0;
    coreDeleteN(p);
    TEST_TRUE(false);
#endif
    Core::finalizeTests();
    EXIT(0);
}

[[noreturn]] void testPreCanaryNewArray(void) {
#ifdef CHECK_MEMORY
    volatile char* p = coreNew(char);
    p[-1] = 0;
    coreDelete(p);
    TEST_TRUE(false);
#endif
    Core::finalizeTests();
    EXIT(0);
}

[[noreturn]] void testPostCanary(void) {
#ifdef CHECK_MEMORY
    char* p = static_cast<char*>(Core::allocateRaw(16));
    p[17] = 0;
    Core::deallocateRaw(p);
    TEST_TRUE(false);
#endif
    Core::finalizeTests();
    EXIT(0);
}