#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

#include <jpeglib.h>
#include <argtable2.h>

const char * input_file;
const char * output_file;
const char * filter;
const char * axis;
const char * direction;
double times;

/* raw image */
JSAMPARRAY row_pointers = NULL;

/* dimensions */
JDIMENSION width;
JDIMENSION height;
int num_components;
J_COLOR_SPACE color_space;
int quality = 75;

void abort_(const char * s, ...)
{
    va_list args;
    va_start(args, s);
    vfprintf(stderr, s, args);
    fprintf(stderr, "\n");
    va_end(args);
    abort();
}

void flip_horizontal() // odbicie pionowe (axis x) - zamiana wierszy
{
    if (color_space != JCS_RGB) return;

    for (JDIMENSION y = 0; y < height / 2; y++) {
        JSAMPROW temp = row_pointers[y];
        row_pointers[y] = row_pointers[height - 1 - y];
        row_pointers[height - 1 - y] = temp;
    }
}

void flip_vertical() // odbicie poziome (axis y) - odbicie pikseli w wierszu
{
    if (color_space != JCS_RGB) return;

    for (JDIMENSION y = 0; y < height; y++) {
        for (JDIMENSION x = 0; x < width / 2; x++) {
            for (int c = 0; c < num_components; c++) {
                JSAMPLE tmp = row_pointers[y][x * num_components + c];
                row_pointers[y][x * num_components + c] = row_pointers[y][(width - 1 - x) * num_components + c];
                row_pointers[y][(width - 1 - x) * num_components + c] = tmp;
            }
        }
    }
}

void rotate_left() // obrót o 90 stopni w lewo (przeciwnie do ruchu wskazówek zegara)
{
    if (color_space != JCS_RGB) return;

    size_t rowbytes = width * num_components;
    JSAMPARRAY new_row_pointers = (JSAMPARRAY) malloc(sizeof(JSAMPROW) * width);
    for (JDIMENSION i = 0; i < width; i++) {
        new_row_pointers[i] = (JSAMPROW) malloc(rowbytes);
    }

    for (JDIMENSION y = 0; y < height; y++) {
        for (JDIMENSION x = 0; x < width; x++) {
            for (int c = 0; c < num_components; c++) {
                // (x,y) => (width-1 - x, y)
                new_row_pointers[width - 1 - x][y * num_components + c] = row_pointers[y][x * num_components + c];
            }
        }
    }

    for (JDIMENSION y = 0; y < height; y++) {
        free(row_pointers[y]);
    }
    free(row_pointers);

    JSAMPARRAY tmp = row_pointers;
    row_pointers = new_row_pointers;
    tmp = NULL;

    JDIMENSION old_width = width;
    width = height;
    height = old_width;
}

void rotate_right() // obrót o 90 stopni w prawo (zgodnie z ruchem wskazówek zegara)
{
    if (color_space != JCS_RGB) return;

    size_t rowbytes = width * num_components;
    JSAMPARRAY new_row_pointers = (JSAMPARRAY) malloc(sizeof(JSAMPROW) * width);
    for (JDIMENSION i = 0; i < width; i++) {
        new_row_pointers[i] = (JSAMPROW) malloc(rowbytes);
    }

    for (JDIMENSION y = 0; y < height; y++) {
        for (JDIMENSION x = 0; x < width; x++) {
            for (int c = 0; c < num_components; c++) {
                // (x,y) => (y, height -1 - x)
                new_row_pointers[x][(height - 1 - y) * num_components + c] = row_pointers[y][x * num_components + c];
            }
        }
    }

    for (JDIMENSION y = 0; y < height; y++) {
        free(row_pointers[y]);
    }
    free(row_pointers);

    JSAMPARRAY tmp = row_pointers;
    row_pointers = new_row_pointers;
    tmp = NULL;

    JDIMENSION old_width = width;
    width = height;
    height = old_width;
}

void process_file()
{
    if(strcmp(filter, "flip") == 0){
        if(axis == NULL){
            abort_("Error: --axis parameter required for flip filter");
        }
        if(strcmp(axis, "x") == 0){
            flip_horizontal();
        } else if(strcmp(axis, "y") == 0){
            flip_vertical();
        } else {
            abort_("Error: invalid --axis value. Use x or y");
        }
    }
    else if(strcmp(filter, "rotate") == 0){
        if(direction == NULL){
            abort_("Error: --direction parameter required for rotate filter");
        }
        if(strcmp(direction, "left") == 0){
            rotate_left();
        } else if(strcmp(direction, "right") == 0){
            rotate_right();
        } else {
            abort_("Error: invalid --direction value. Use left or right");
        }
    }
    else {
        abort_("Error: unsupported filter %s", filter);
    }
}

void read_jpeg_file(const char *filename)
{
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    FILE *infile = fopen(filename, "rb");

    if (!infile) {
        abort_("Error opening input jpeg file %s!\n", filename);
    }

    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&cinfo);
    jpeg_stdio_src(&cinfo, infile);
    jpeg_read_header(&cinfo, TRUE);

    jpeg_start_decompress(&cinfo);
    width = cinfo.output_width;
    height = cinfo.output_height;
    num_components = cinfo.output_components;
    color_space = cinfo.out_color_space;

    size_t rowbytes = width * num_components;
    row_pointers = (JSAMPARRAY) malloc(sizeof(JSAMPROW) * height);
    for (JDIMENSION y = 0; y < height; y++) {
        row_pointers[y] = (JSAMPROW) malloc(rowbytes);
    }

    JDIMENSION y = 0;
    while (cinfo.output_scanline < cinfo.image_height) {
        JDIMENSION read = jpeg_read_scanlines(&cinfo, &row_pointers[y], 1);
        y += read;
    }

    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);
    fclose(infile);
}

void write_jpeg_file(const char *filename)
{
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    FILE *outfile = fopen(filename, "wb");

    if (!outfile) {
        abort_("Error opening output jpeg file %s!\n", filename);
    }

    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    jpeg_stdio_dest(&cinfo, outfile);

    cinfo.image_width = width;
    cinfo.image_height = height;
    cinfo.input_components = num_components;
    cinfo.in_color_space = color_space;

    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, quality, TRUE);

    jpeg_start_compress(&cinfo, TRUE);

    JDIMENSION y = 0;
    while (cinfo.next_scanline < cinfo.image_height) {
        JDIMENSION written = jpeg_write_scanlines(&cinfo, &row_pointers[y], 1);
        y += written;
    }

    jpeg_finish_compress(&cinfo);
    jpeg_destroy_compress(&cinfo);
    fclose(outfile);

    for (JDIMENSION y = 0; y < height; y++) {
        free(row_pointers[y]);
    }
    free(row_pointers);
}

int main(int argc, char *argv[])
{
    struct arg_file  *input = arg_file1(NULL, NULL, "<input>", "Input JPEG file");
    struct arg_file  *output = arg_file1(NULL, NULL, "<output>", "Output JPEG file");
    struct arg_str   *filter_arg = arg_str1(NULL, "filter", "<filter>", "Filter to apply (flip or rotate)");
    struct arg_str   *axis_arg = arg_str0(NULL, "axis", "<axis>", "Axis for flip (x or y)");
    struct arg_str   *direction_arg = arg_str0(NULL, "direction", "<direction>", "Direction for rotate (left or right)");
    struct arg_dbl   *times_arg = arg_dbl0(NULL, "times", "<times>", "Number of times to apply filter (unused)");
    struct arg_end   *end = arg_end(20);

    void *argtable[] = {input, output, filter_arg, axis_arg, direction_arg, times_arg, end};

    if (arg_nullcheck(argtable) != 0) {
        fprintf(stderr, "Insufficient memory\n");
        return 1;
    }

    int nerrors = arg_parse(argc, argv, argtable);
    if (nerrors != 0) {
        arg_print_errors(stderr, end, argv[0]);
        fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
        return 1;
    }

    input_file = input->filename[0];
    output_file = output->filename[0];
    filter = filter_arg->sval[0];
    axis = axis_arg->count > 0 ? axis_arg->sval[0] : NULL;
    direction = direction_arg->count > 0 ? direction_arg->sval[0] : NULL;

    read_jpeg_file(input_file);
    process_file();
    write_jpeg_file(output_file);

    arg_freetable(argtable, sizeof(argtable) / sizeof(argtable[0]));
    return 0;
}
