#include <SDL3/SDL.h>

#include <math.h>
#include <stdbool.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "candidates.h"
#include "colour.h"
#include "image.h"

static uint32_t failures = 0;

#define CHECK(expression) do {                                                \
    if (!(expression)) {                                                      \
        fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expression); \
        ++failures;                                                           \
    }                                                                         \
} while (0)

static RGB rgb(uint8_t red, uint8_t green, uint8_t blue) {
    RGB colour = {red, green, blue};
    return colour;
}

static SDL_Surface *new_surface(int width, int height) {
    SDL_Surface *surface = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32);
    CHECK(surface != NULL);
    return surface;
}

static void set_pixel(SDL_Surface *surface, int x, int y, RGB colour,
                      uint8_t alpha) {
    uint8_t *pixel = (uint8_t *)surface->pixels +
                     (size_t)y * (size_t)surface->pitch + (size_t)x * 4U;
    pixel[0] = colour.r;
    pixel[1] = colour.g;
    pixel[2] = colour.b;
    pixel[3] = alpha;
}

static void test_hex_parsing(void) {
    RGB colour = {0, 0, 0};
    CHECK(colour_parse_hex("#1eA2fF", &colour));
    CHECK(colour.r == 0x1e && colour.g == 0xa2 && colour.b == 0xff);
    CHECK(!colour_parse_hex("1ea2ff", &colour));
    CHECK(!colour_parse_hex("#123", &colour));
    CHECK(!colour_parse_hex("#gg0000", &colour));
}

static void test_linear_average(void) {
    ColourAccumulator accumulator = {0.0, 0.0, 0.0, 0.0};
    RGB average;
    colour_accumulator_add_u8(&accumulator, 0, 0, 0, 255);
    colour_accumulator_add_u8(&accumulator, 255, 255, 255, 255);
    CHECK(colour_accumulator_average(&accumulator, &average));
    CHECK(average.r >= 187 && average.r <= 188);
    CHECK(average.r == average.g && average.g == average.b);
}

static void test_partial_blocks(void) {
    SDL_Surface *surface = new_surface(17, 1);
    CandidateSet set;
    int x;
    candidates_init(&set);
    for (x = 0; x < 16; ++x) set_pixel(surface, x, 0, rgb(255, 0, 0), 255);
    set_pixel(surface, 16, 0, rgb(0, 0, 255), 255);
    CHECK(image_process_surface(surface, 16, 1, &set));
    CHECK(set.count == 2);
    CHECK(fabs(set.items[0].prevalence - 16.0) < 0.001);
    CHECK(fabs(set.items[1].prevalence - 1.0) < 0.001);
    candidates_destroy(&set);
    SDL_DestroySurface(surface);
}

static void test_transparency(void) {
    SDL_Surface *surface = new_surface(2, 1);
    CandidateSet set;
    candidates_init(&set);
    set_pixel(surface, 0, 0, rgb(255, 0, 0), 0);
    set_pixel(surface, 1, 0, rgb(0, 0, 255), 255);
    CHECK(image_process_surface(surface, 2, 1, &set));
    CHECK(set.count == 1);
    CHECK(set.items[0].colour.r == 0 && set.items[0].colour.g == 0 &&
          set.items[0].colour.b == 255);
    CHECK(fabs(set.items[0].prevalence - 1.0) < 0.001);
    candidates_destroy(&set);
    SDL_DestroySurface(surface);
}

static void test_partial_alpha(void) {
    SDL_Surface *surface = new_surface(2, 1);
    CandidateSet set;
    candidates_init(&set);
    set_pixel(surface, 0, 0, rgb(255, 0, 0), 255);
    set_pixel(surface, 1, 0, rgb(0, 0, 255), 128);
    CHECK(image_process_surface(surface, 2, 1, &set));
    CHECK(set.count == 1);
    CHECK(set.items[0].colour.r > set.items[0].colour.b);
    CHECK(set.items[0].colour.b > 100);
    CHECK(fabs(set.items[0].prevalence - (1.0 + 128.0 / 255.0)) < 0.001);
    candidates_destroy(&set);
    SDL_DestroySurface(surface);
}

static void test_arena(void) {
    Arena arena;
    uint32_t *first;
    max_align_t *aligned;
    arena_init(&arena);
    first = arena_allocate(&arena, 32, sizeof(*first));
    aligned = arena_allocate(&arena, 1, sizeof(*aligned));
    CHECK(first != NULL && aligned != NULL);
    CHECK((uintptr_t)aligned % _Alignof(max_align_t) == 0);
    CHECK(arena_allocate(&arena, SIZE_MAX, 2) == NULL);
    arena_destroy(&arena);
}

static void test_deduplication(void) {
    CandidateSet set;
    candidates_init(&set);
    CHECK(candidates_add(&set, rgb(255, 0, 0), 2.0));
    CHECK(candidates_add(&set, rgb(250, 5, 5), 3.0));
    CHECK(set.count == 1);
    CHECK(fabs(set.items[0].prevalence - 5.0) < 0.001);
    CHECK(candidates_add(&set, rgb(0, 0, 255), 1.0));
    CHECK(set.count == 2);
    candidates_destroy(&set);
}

static void test_candidate_ranking(void) {
    CandidateSet set;
    ScoredCandidate *results = NULL;
    size_t count = 0;
    size_t index;
    candidates_init(&set);
    CHECK(candidates_add(&set, rgb(255, 80, 80), 30.0));
    CHECK(candidates_add(&set, rgb(80, 180, 255), 20.0));
    CHECK(candidates_add(&set, rgb(100, 255, 120), 10.0));
    CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 0.0f, &results, &count));
    CHECK(count == 3);
    for (index = 1; index < count; ++index) {
        CHECK(results[index - 1].score >= results[index].score);
    }
    results = NULL;
    CHECK(candidates_rank(&set, 1, rgb(30, 30, 30), 0.0f, &results, &count));
    CHECK(count == 1);
    CHECK(results[0].colour.r == 255 && results[0].colour.g == 80);
    results = NULL;
    CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 1.01f,
                          &results, &count));
    CHECK(count == 0 && results == NULL);
    candidates_destroy(&set);
}

static void test_chromatic_candidate_admission(void) {
    CandidateSet set;
    ScoredCandidate *results = NULL;
    size_t count = 0;
    uint32_t value;

    candidates_init(&set);
    set.merge_distance = 0.001;
    for (value = 24; value <= 216; value += 8) {
        CHECK(candidates_add(&set, rgb((uint8_t)value, (uint8_t)value,
                                      (uint8_t)value),
                             1000.0 - (double)value));
    }
    CHECK(candidates_add(&set, rgb(150, 90, 90), 2.0));
    CHECK(candidates_add(&set, rgb(190, 55, 65), 1.0));
    CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 0.0f,
                          &results, &count));
    CHECK(count == 3);
    CHECK((results[0].colour.r == 190 && results[0].colour.g == 55) ||
          (results[1].colour.r == 190 && results[1].colour.g == 55) ||
          (results[2].colour.r == 190 && results[2].colour.g == 55));
    candidates_destroy(&set);
}

static void test_final_tweak(void) {
    CandidateSet set;
    ScoredCandidate *results = NULL;
    size_t count = 0;
    const RGB background = {0x1e, 0x1e, 0x1e};
    RGB original;
    float original_score;
    candidates_init(&set);
    CHECK(candidates_add(&set, rgb(0x7a, 0xa2, 0xf7), 10.0));
    CHECK(candidates_rank(&set, 1, background, 0.0f, &results, &count));
    CHECK(count == 1);
    original = results[0].colour;
    original_score = results[0].score;
    candidates_tweak(results, count, background);
    CHECK(results[0].score >= original_score);
    CHECK(fabs((double)results[0].score -
               (double)accent_score(results[0].colour, background)) < 0.000001);
    CHECK(results[0].colour.r != original.r ||
          results[0].colour.g != original.g ||
          results[0].colour.b != original.b);
    candidates_destroy(&set);
}

static bool write_bytes(const char *path, const uint8_t *bytes,
                        size_t length) {
    FILE *file = fopen(path, "wb");
    bool success;
    if (file == NULL) return false;
    success = fwrite(bytes, 1, length, file) == length;
    success = fclose(file) == 0 && success;
    return success;
}

static void test_static_input(void) {
    static const uint8_t ppm[] = {
        'P', '6', '\n', '2', ' ', '1', '\n', '2', '5', '5', '\n',
        255, 0, 0, 255, 0, 0
    };
    const char *path = "/tmp/accent-extract-static-test.data";
    CandidateSet set;
    CHECK(write_bytes(path, ppm, sizeof(ppm)));
    candidates_init(&set);
    CHECK(image_process_file(path, 16, &set));
    CHECK(set.count == 1);
    CHECK(set.items[0].colour.r == 255 && set.items[0].colour.g == 0);
    CHECK(fabs(set.items[0].prevalence - 2.0) < 0.001);
    candidates_destroy(&set);
    CHECK(remove(path) == 0);
}

static void test_duration_weighting(void) {
    SDL_Surface *red = new_surface(1, 1);
    SDL_Surface *blue = new_surface(1, 1);
    CandidateSet set;
    candidates_init(&set);
    set_pixel(red, 0, 0, rgb(255, 0, 0), 255);
    set_pixel(blue, 0, 0, rgb(0, 0, 255), 255);
    CHECK(image_process_surface(red, 16, 20, &set));
    CHECK(image_process_surface(blue, 16, 200, &set));
    CHECK(set.count == 2);
    CHECK(fabs(set.items[0].prevalence - 20.0) < 0.001);
    CHECK(fabs(set.items[1].prevalence - 200.0) < 0.001);
    candidates_destroy(&set);
    SDL_DestroySurface(red);
    SDL_DestroySurface(blue);
}

static void test_farbfeld(void) {
    static const uint8_t farbfeld[] = {
        'f','a','r','b','f','e','l','d', 0,0,0,2, 0,0,0,1,
        0xff,0xff, 0,0, 0,0, 0xff,0xff,
        0,0, 0,0, 0xff,0xff, 0xff,0xff
    };
    const char *path = "/tmp/accent-extract-farbfeld-test.unknown";
    CandidateSet set;
    CHECK(write_bytes(path, farbfeld, sizeof(farbfeld)));
    candidates_init(&set);
    CHECK(image_process_file(path, 1, &set));
    CHECK(set.count == 2);
    CHECK(set.items[0].colour.r == 255 && set.items[0].colour.b == 0);
    CHECK(set.items[1].colour.r == 0 && set.items[1].colour.b == 255);
    candidates_destroy(&set);
    CHECK(remove(path) == 0);
}

int main(void) {
    CHECK(SDL_Init(0));
    test_hex_parsing();
    test_linear_average();
    test_partial_blocks();
    test_transparency();
    test_partial_alpha();
    test_arena();
    test_deduplication();
    test_candidate_ranking();
    test_chromatic_candidate_admission();
    test_final_tweak();
    test_static_input();
    test_duration_weighting();
    test_farbfeld();
    SDL_Quit();
    if (failures != 0) {
        fprintf(stderr, "%" PRIu32 " test(s) failed\n", failures);
        return EXIT_FAILURE;
    }
    puts("all tests passed");
    return EXIT_SUCCESS;
}