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

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

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


/* we will be using this uninitialized pointer later to store raw, uncompressed image */
JSAMPARRAY row_pointers = NULL;


/* dimensions of the image we want to write */
JDIMENSION width;
JDIMENSION height;
int num_components;
int quality = 75;
J_COLOR_SPACE color_space;


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


/* ===== 1. FLIP: reflection over x or y axis ===== */
void flip()
{
  JDIMENSION x, y;
  size_t rowbytes = width * num_components;

  if (strcmp(axis, "x") == 0) {
    /* axis x -> reflect vertically: swap rows top<->bottom */
    for (y = 0; y < height / 2; y++) {
      JSAMPROW tmp = row_pointers[y];
      row_pointers[y] = row_pointers[height - 1 - y];
      row_pointers[height - 1 - y] = tmp;
    }
  }
  else if (strcmp(axis, "y") == 0) {
    /* axis y -> reflect horizontally: swap pixels left<->right within each row */
    for (y = 0; y < height; y++) {
      JSAMPROW row = row_pointers[y];
      for (x = 0; x < width / 2; x++) {
        JSAMPROW left  = &(row[x * num_components]);
        JSAMPROW right = &(row[(width - 1 - x) * num_components]);
        int c;
        for (c = 0; c < num_components; c++) {
          JSAMPLE t = left[c];
          left[c] = right[c];
          right[c] = t;
        }
      }
    }
  }
  else {
    abort_("flip: unknown axis '%s' (expected 'x' or 'y')", axis);
  }

  (void)rowbytes;
}


/* ===== 2. ROTATE: 90 degrees left or right ===== */
void rotate()
{
  JDIMENSION x, y;
  JDIMENSION new_width  = height;
  JDIMENSION new_height = width;
  size_t new_rowbytes = new_width * num_components;

  /* allocate new buffer */
  JSAMPARRAY new_rows = (JSAMPARRAY) malloc(sizeof(JSAMPROW) * new_height);
  if (!new_rows) abort_("rotate: malloc failed");
  for (y = 0; y < new_height; y++) {
    new_rows[y] = (JSAMPROW) malloc(new_rowbytes);
    if (!new_rows[y]) abort_("rotate: malloc failed");
  }

  if (strcmp(direction, "right") == 0) {
    /* rotate 90 clockwise: new[x][height-1-y] = old[y][x] */
    for (y = 0; y < height; y++) {
      JSAMPROW src_row = row_pointers[y];
      for (x = 0; x < width; x++) {
        JSAMPROW src = &(src_row[x * num_components]);
        JSAMPROW dst = &(new_rows[x][(height - 1 - y) * num_components]);
        int c;
        for (c = 0; c < num_components; c++) dst[c] = src[c];
      }
    }
  }
  else if (strcmp(direction, "left") == 0) {
    /* rotate 90 counter-clockwise: new[width-1-x][y] = old[y][x] */
    for (y = 0; y < height; y++) {
      JSAMPROW src_row = row_pointers[y];
      for (x = 0; x < width; x++) {
        JSAMPROW src = &(src_row[x * num_components]);
        JSAMPROW dst = &(new_rows[width - 1 - x][y * num_components]);
        int c;
        for (c = 0; c < num_components; c++) dst[c] = src[c];
      }
    }
  }
  else {
    abort_("rotate: unknown direction '%s' (expected 'left' or 'right')", direction);
  }

  /* free old buffer */
  for (y = 0; y < height; y++) free(row_pointers[y]);
  free(row_pointers);

  /* replace with new buffer and swap dimensions */
  row_pointers = new_rows;
  width  = new_width;
  height = new_height;
}


void process_file()
{
  if (strcmp(filter, "flip") == 0) {
    flip();
  }
  else if (strcmp(filter, "rotate") == 0) {
    rotate();
  }
}


/**
* read_jpeg_file Reads from a jpeg file on disk specified by filename and saves into the
* raw_image buffer in an uncompressed format.
*/
void read_jpeg_file( const char *filename )
{
  struct jpeg_decompress_struct cinfo;
  struct jpeg_error_mgr jerr;
  int y;

  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.out_color_components;
  color_space = cinfo.out_color_space;

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

  y=0;
  JSAMPARRAY tmp = row_pointers;
  while( cinfo.output_scanline < cinfo.image_height )
  {
    y = jpeg_read_scanlines( &cinfo, tmp, 1 );
    tmp +=y;
  }
  jpeg_finish_decompress( &cinfo );
  jpeg_destroy_decompress( &cinfo );
  fclose( infile );
}


/**
* write_jpeg_file Writes the raw image data stored in the raw_image buffer
* to a jpeg image with default compression and smoothing options in the file
* specified by *filename.
*/
void write_jpeg_file( const char *filename )
{
  struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  int y;
  JSAMPARRAY tmp;

  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 );
  tmp = row_pointers;
  while( cinfo.next_scanline < cinfo.image_height )
  {
    y = jpeg_write_scanlines( &cinfo, tmp, 1 );
    tmp +=y;
  }
  jpeg_finish_compress( &cinfo );
  jpeg_destroy_compress( &cinfo );
  fclose( outfile );

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



int main(int argc, char **argv){
  // Options
  struct arg_file *input_file_arg  = arg_file1("i", "in-file",   "<input>",     "Input JPEG File");
  struct arg_file *output_file_arg = arg_file1("o", "out-file",  "<output>",    "Output JPEG File");
  struct arg_str  *filter_arg      = arg_str1 ("f", "filter",    "<filter>",    "Filter (flip|rotate)");
  struct arg_str  *axis_arg        = arg_str0 ("a", "axis",      "<axis>",      "Axis for flip (x|y)");
  struct arg_str  *direction_arg   = arg_str0 ("d", "direction", "<direction>", "Direction for rotate (left|right)");
  struct arg_dbl  *times_arg       = arg_dbl0 ("t", "times",     "<times>",     "Multiplyer");
  struct arg_lit  *help            = arg_lit0 ("h", "help",                     "print this help and exit");
  struct arg_end  *end             = arg_end(10);

  int nerrors;

  void *argtable[] = {input_file_arg, output_file_arg, filter_arg, axis_arg, direction_arg, times_arg, help, end};

  if (arg_nullcheck(argtable) != 0) printf("error: insufficient memory\n");

  /* default values for optional parameters */
  times_arg->dval[0] = 1;
  axis_arg->sval[0] = "x";
  direction_arg->sval[0] = "right";

  nerrors = arg_parse(argc, argv, argtable);

  if (help->count > 0){
     printf("Usage: geometry");
     arg_print_syntax(stdout, argtable,"\n");
     arg_print_glossary(stdout, argtable,"  %-25s %s\n");
     arg_freetable(argtable, sizeof(argtable)/sizeof(argtable[0]));
     return 0;
  }

  if (nerrors==0){
     input_file  = input_file_arg->filename[0];
     output_file = output_file_arg->filename[0];
     filter      = filter_arg->sval[0];
     axis        = axis_arg->sval[0];
     direction   = direction_arg->sval[0];
     times       = times_arg->dval[0];
  }
  else{
     arg_print_errors(stderr, end, "geometry");
     arg_print_glossary(stderr, argtable, " %-25s %s\n");
     arg_freetable(argtable, sizeof(argtable)/sizeof(argtable[0]));
     return 1;
  }

   read_jpeg_file(input_file);
   process_file();
   write_jpeg_file(output_file);
   arg_freetable(argtable, sizeof(argtable)/sizeof(argtable[0]));
   return 0;
}
