#include <SDL3/SDL.h>

#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

typedef struct {
    const char *input;
    RGB background;
    size_t limit;
    float minimum_score;
    uint32_t block_size;
    bool have_background;
    bool tweak;
} Options;

static void print_usage(FILE *stream) {
    fprintf(stream,
            "Usage: accent-extract IMAGE --background '#RRGGBB' [OPTIONS]\n"
            "\n"
            "Options:\n"
            "  --background COLOR  Required background colour (#RRGGBB).\n"
            "  --limit N           Pre-score candidate limit (default: 20).\n"
            "  --min-score FLOAT   Minimum score to print (default: 0.60).\n"
            "  --block-size N      Square averaging block size (default: 16).\n"
            "  --tweak             Refine output lightness and saturation.\n"
            "  --help              Show this help.\n");
}

static bool parse_positive_size(const char *text, size_t *out) {
    char *end;
    uintmax_t value;
    errno = 0;
    value = strtoumax(text, &end, 10);
    if (errno != 0 || end == text || *end != '\0' || value == 0 ||
        value > SIZE_MAX) {
        return false;
    }
    *out = (size_t)value;
    return true;
}

static bool parse_block_size(const char *text, uint32_t *out) {
    size_t value;
    if (!parse_positive_size(text, &value) || value > UINT32_MAX) {
        return false;
    }
    *out = (uint32_t)value;
    return true;
}

static bool parse_score(const char *text, float *out) {
    char *end;
    float value;
    errno = 0;
    value = strtof(text, &end);
    if (errno != 0 || end == text || *end != '\0' || !isfinite(value) ||
        value < 0.0f || value > 1.0f) {
        return false;
    }
    *out = value;
    return true;
}

static int parse_options(int argc, char **argv, Options *options) {
    int index;
    options->input = NULL;
    options->limit = 20;
    options->minimum_score = 0.60f;
    options->block_size = 16;
    options->have_background = false;
    options->tweak = false;

    for (index = 1; index < argc; ++index) {
        const char *argument = argv[index];
        if (strcmp(argument, "--help") == 0) {
            print_usage(stdout);
            return 1;
        }
        if (strcmp(argument, "--tweak") == 0) {
            options->tweak = true;
            continue;
        }
        if (strcmp(argument, "--background") == 0 ||
            strcmp(argument, "--limit") == 0 ||
            strcmp(argument, "--min-score") == 0 ||
            strcmp(argument, "--block-size") == 0) {
            const char *value;
            if (++index >= argc) {
                fprintf(stderr, "accent-extract: %s requires a value\n", argument);
                return -1;
            }
            value = argv[index];
            if (strcmp(argument, "--background") == 0) {
                if (!colour_parse_hex(value, &options->background)) {
                    fprintf(stderr, "accent-extract: invalid background colour: %s\n",
                            value);
                    return -1;
                }
                options->have_background = true;
            } else if (strcmp(argument, "--limit") == 0) {
                if (!parse_positive_size(value, &options->limit)) {
                    fprintf(stderr, "accent-extract: invalid limit: %s\n", value);
                    return -1;
                }
            } else if (strcmp(argument, "--min-score") == 0) {
                if (!parse_score(value, &options->minimum_score)) {
                    fprintf(stderr, "accent-extract: invalid minimum score: %s\n",
                            value);
                    return -1;
                }
            } else if (!parse_block_size(value, &options->block_size)) {
                fprintf(stderr, "accent-extract: invalid block size: %s\n", value);
                return -1;
            }
        } else if (argument[0] == '-') {
            fprintf(stderr, "accent-extract: unknown option: %s\n", argument);
            return -1;
        } else if (options->input != NULL) {
            fprintf(stderr, "accent-extract: only one input image is accepted\n");
            return -1;
        } else {
            options->input = argument;
        }
    }
    if (options->input == NULL) {
        fprintf(stderr, "accent-extract: no input image supplied\n");
        return -1;
    }
    if (!options->have_background) {
        fprintf(stderr, "accent-extract: --background is required\n");
        return -1;
    }
    return 0;
}

int main(int argc, char **argv) {
    Options options;
    CandidateSet candidates;
    ScoredCandidate *results = NULL;
    size_t result_count = 0;
    size_t index;
    int parsed = parse_options(argc, argv, &options);

    if (parsed != 0) {
        if (parsed < 0) {
            print_usage(stderr);
        }
        return parsed < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
    }
    if (!SDL_Init(0)) {
        fprintf(stderr, "accent-extract: SDL initialisation failed: %s\n",
                SDL_GetError());
        return EXIT_FAILURE;
    }
    candidates_init(&candidates);
    if (!image_process_file(options.input, options.block_size, &candidates)) {
        fprintf(stderr, "accent-extract: could not process '%s': %s\n",
                options.input, SDL_GetError());
        candidates_destroy(&candidates);
        SDL_Quit();
        return EXIT_FAILURE;
    }
    if (!candidates_rank(&candidates, options.limit, options.background,
                         options.minimum_score, &results, &result_count)) {
        fprintf(stderr, "accent-extract: out of memory while ranking colours\n");
        candidates_destroy(&candidates);
        SDL_Quit();
        return EXIT_FAILURE;
    }
    if (options.tweak) {
        candidates_tweak(results, result_count, options.background);
    }
    for (index = 0; index < result_count; ++index) {
        printf("#%02x%02x%02x %.3f\n", results[index].colour.r,
               results[index].colour.g, results[index].colour.b,
               (double)results[index].score);
    }
    candidates_destroy(&candidates);
    SDL_Quit();
    return EXIT_SUCCESS;
}