#include <config.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <math.h>
#include <tiffio.h>

extern "C" {
#include <jpeglib.h>
}

#include "OutputOptions.h"
#include "GFile_TIFF.h"
#include "GFile_JPEG.h"

/* Program to convert .kdc images to .tif or .jpg format.  The name of the
 * binary and/or the name of the output file are used to determine what
 * image format the output file will be in.
 * 
 * Written by:  Chris Studholme
 * Last Update: 6-Dec-2003
 * Copyright:   GPL (http://www.fsf.org/copyleft/gpl.html)
 */

//#define __SHOW_WHITE_BALANCE

// global variables
#define TO_TIFF_PROGNAME1 "kdc2tiff"
#define TO_TIFF_PROGNAME2 "kdc2tif"
#define TO_JPEG_PROGNAME1 "kdc2jpeg"
#define TO_JPEG_PROGNAME2 "kdc2jpg"
#define TO_TIFF_EXT1 ".tiff"
#define TO_TIFF_EXT2 ".tif"
#define TO_JPEG_EXT1 ".jpeg"
#define TO_JPEG_EXT2 ".jpg"

const char* progname=0;


bool FileExists(const char* path) {
  struct stat sbuf;
  if (stat(path,&sbuf)==0)
    return true;
  if (errno==ENOENT)
    return false;
  fprintf(stderr,"error checking for file %s\n",path);
  exit(1);
}


TIFF* CreateTiffFile(OutputOptions& options) {

  TIFF* tiffimage = TIFFOpen(options.outputname,"w");
  
  TIFFSetField(tiffimage,TIFFTAG_IMAGEWIDTH,(uint32)options.outputwidth);
  TIFFSetField(tiffimage,TIFFTAG_IMAGELENGTH,(uint32)options.outputheight);
  TIFFSetField(tiffimage,TIFFTAG_BITSPERSAMPLE,(uint16)8);
  TIFFSetField(tiffimage,TIFFTAG_SAMPLESPERPIXEL,(uint16)3);
  TIFFSetField(tiffimage,TIFFTAG_PHOTOMETRIC,PHOTOMETRIC_RGB);
  TIFFSetField(tiffimage,TIFFTAG_ORIENTATION,ORIENTATION_TOPLEFT);
  TIFFSetField(tiffimage,TIFFTAG_PLANARCONFIG,PLANARCONFIG_CONTIG);
  TIFFSetField(tiffimage,TIFFTAG_FILLORDER,FILLORDER_LSB2MSB);
  // this is not correct, should use options.compresstype
  TIFFSetField(tiffimage,TIFFTAG_COMPRESSION,COMPRESSION_NONE);
  
  if (options.rowsperstrip==0) {
    options.rowsperstrip=8192/3/options.outputwidth;
    if (options.rowsperstrip<1)
      options.rowsperstrip=1;
  }
  TIFFSetField(tiffimage,TIFFTAG_ROWSPERSTRIP,(uint32)options.rowsperstrip);

  TIFFSetField(tiffimage,TIFFTAG_RESOLUTIONUNIT,RESUNIT_CENTIMETER);
  
  // print approx. 20cm x 15cm
  TIFFSetField(tiffimage,TIFFTAG_XRESOLUTION,(float)options.outputwidth/20);
  TIFFSetField(tiffimage,TIFFTAG_YRESOLUTION,(float)options.outputwidth/20);

  if (options.kdcfile) {
    if (options.kdcfile->ImageDescription!=NULL)
      TIFFSetField(tiffimage,TIFFTAG_DOCUMENTNAME,
		   options.kdcfile->ImageDescription);
    
    if (options.kdcfile->DateTime!=NULL)
      TIFFSetField(tiffimage,TIFFTAG_DATETIME,options.kdcfile->DateTime);
    
    if (options.kdcfile->Make!=NULL)
      TIFFSetField(tiffimage,TIFFTAG_MAKE,options.kdcfile->Make);
    
    if (options.kdcfile->Model!=NULL)
      TIFFSetField(tiffimage,TIFFTAG_MODEL,options.kdcfile->Model);
  }

  //  if (options.copyright!=NULL)
  //    TIFFSetField(tiffimage,TIFFTAG_COPYRIGHT,options.copyright);
  
  // TIFFTAG_ARTIST                  315     /* creator of image */
  // TIFFTAG_HOSTCOMPUTER            316     /* machine where created */
  // TIFFTAG_IMAGEDESCRIPTION        270     /* info about image */
  // TIFFTAG_SOFTWARE                305     /* name & release */

  return tiffimage;
}


void WriteTiffImage(OutputOptions& options) {

  TIFF* tiffimage = CreateTiffFile(options);

  unsigned char rgb[options.outputwidth*3];

#ifdef __SHOW_WHITE_BALANCE
  unsigned long rmean=0;
  unsigned long gmean=0;
  unsigned long bmean=0;
#endif

  // create image
  for (int y=0; y<options.outputheight; ++y) {
    options.CalculateScanLine(rgb,y);
    TIFFWriteScanline(tiffimage,rgb,y,0);

#ifdef __SHOW_WHITE_BALANCE
    for (int x=0; x<options.outputwidth; ++x) {
      rmean+=rgb[3*x+0];
      gmean+=rgb[3*x+1];
      bmean+=rgb[3*x+2];
    }
#endif
  }

#ifdef __SHOW_WHITE_BALANCE
  float npixels=options.outputwidth*options.outputheight;
  fprintf(stderr,"\nWriteTiffImage: white balance (%.1f,%.1f,%.1f)\n",
	  rmean/npixels,gmean/npixels,bmean/npixels);
#endif
  
  TIFFClose(tiffimage);
}


void WriteJpegImage(OutputOptions& options) {

  // allocate and initialize a JPEG compression object
  jpeg_compress_struct cinfo;
  jpeg_error_mgr jerr;
  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_compress(&cinfo);
 
  // specify the destination for the compressed data
  FILE* outfile;
  if ((outfile = fopen(options.outputname, "wb")) == NULL) {
    fprintf(stderr, "can't open %s\n", options.outputname);
    return;
  }
  jpeg_stdio_dest(&cinfo, outfile);
  
  // set parameters for compression
  cinfo.image_width = options.outputwidth;
  cinfo.image_height = options.outputheight;
  cinfo.input_components = 3;
  cinfo.in_color_space = JCS_RGB;
  jpeg_set_defaults(&cinfo);
  jpeg_set_quality(&cinfo,options.quality,TRUE);
  /*** set other fields here ***/
  // cinfo.density_unit =;
  // cinfo.X_density =;
  // cinfo.Y_density =;

  if (options.progressive)
    jpeg_simple_progression(&cinfo);

  jpeg_start_compress(&cinfo, TRUE);

  // comment
  if (options.kdcfile) {
    int len_needed = 64 +
      (options.kdcfile->DateTime?strlen(options.kdcfile->DateTime):0) +
      (options.kdcfile->Model?strlen(options.kdcfile->Model):0) +
      (options.copyright?strlen(options.copyright):0);
    char* comment_text = new char[len_needed];
    *comment_text=0;
    if (options.kdcfile->Model) {
      strcat(comment_text,"Model: ");
      strcat(comment_text,options.kdcfile->Model);
      strcat(comment_text,"\n");
    }
    if (options.kdcfile->DateTime) {
      strcat(comment_text,"DateTime: ");
      strcat(comment_text,options.kdcfile->DateTime);
    }
    if (options.copyright) {
      strcat(comment_text,"\n");
      strcat(comment_text,"Copyright: ");
      strcat(comment_text,options.copyright);
    }
    jpeg_write_marker(&cinfo, JPEG_COM, (const JOCTET*) comment_text, 
		      strlen(comment_text));
  }

  unsigned char rgb[options.outputwidth*3];
  JSAMPROW row_pointer[1];
  row_pointer[0] = rgb;

  // create image
  for (int y=0; y<options.outputheight; ++y) {
    options.CalculateScanLine(rgb,y);
    jpeg_write_scanlines(&cinfo,row_pointer,1);
  }

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


void Usage() {
  fprintf(stderr,"\nUSAGE\n\t%s [ global-options ] inputfile [ local-options -o outputfile1 ]\n",
	  progname);
  fprintf(stderr,"\t\t[ local-options -o outputfile2 ] ...\n");
  fprintf(stderr,"\nOPTIONS\n");
  fprintf(stderr,"\t-w #\toutput image width\n");
  fprintf(stderr,"\t-h #\toutput image height\n");
  fprintf(stderr,"\t-g #\tabsolute gamma correction\n");
  fprintf(stderr,"\t+g #\trelative gamma correction\n");
  fprintf(stderr,"\t-G\tno grain reduction (default)\n");
  fprintf(stderr,"\t+G\treduce graininess\n");
  fprintf(stderr,"\t+GG\treduce graininess (extra)\n");
  fprintf(stderr,"\t+e\tnormal contrast enhancement (default)\n");
  fprintf(stderr,"\t+E #\tadvanced contrast enhancement\n");
  fprintf(stderr,"\t-e\tno contrast enhancement\n");
  fprintf(stderr,"\t-W wht\tcolour of light source\n");
  fprintf(stderr,"\t+s\tsquare pixels (default)\n");
  fprintf(stderr,"\t-s\tallow non-square pixels\n");
  fprintf(stderr,"\t+f\tfast processing\n");
  fprintf(stderr,"\t-f\tslow processing, high quality (default)\n");
  fprintf(stderr,"\t+p\tcrop image if necessary to fit width and height (default)\n");
  fprintf(stderr,"\t-p\tno cropping, width and height are maximums\n");
  fprintf(stderr,"\t-R\tdon't rotate image (default)\n");
  fprintf(stderr,"\t+R\trotate image clockwise\n");
  fprintf(stderr,"\t+B\trotate image counter-clockwise\n");
  fprintf(stderr,"\t-C copy\tnote copyright in output file\n");

  // are we kdc2tiff or kdc2jpeg
  bool tiff=false;
  bool jpeg=false;
  if ((strcasecmp(progname,TO_TIFF_PROGNAME1)==0)||
      (strcasecmp(progname,TO_TIFF_PROGNAME2)==0))
    tiff=true;
  else if ((strcasecmp(progname,TO_JPEG_PROGNAME1)==0)||
      (strcasecmp(progname,TO_JPEG_PROGNAME2)==0))
    jpeg=true;
  else {
    tiff=true;
    jpeg=true;
  }

  // tiff specific options
  if (tiff) {
    fprintf(stderr,"\t-c schm\tTIFF compression scheme\n");
    fprintf(stderr,"\t-r #\tTIFF rows per strip setting\n");
  }

  // jpeg specific options
  if (jpeg) {
    fprintf(stderr,"\t-q #\tJPEG quality setting (0 to 100, default is 75)\n");
    fprintf(stderr,"\t+P\tcreate progressive JPEG file\n");
  }

  fprintf(stderr,"\nVersion: %s\n",VERSION);
}


char** ParseGlobalOptions(OutputOptions& options, int& argc, char*argv[]) {
  // parse global options
  argv = options.ParseOptions(argc,argv);
  if ((argv==0)||(argc<=0))
    return 0;

  // next argv is input filename
  options.inputname = *argv++;
  --argc;

  return argv;
}


char** ParseLocalOptions(OutputOptions& options, int& argc, char*argv[]) {
  // parse local options
  argv = options.ParseOptions(argc,argv);
  if (argv==0)
    return 0;

  // check for output filename
  if (argc>0) {
    options.outputname = *argv++;
    --argc;
  }

  return argv;
}


main (int argc, char*argv[]) {

  // set program name
  progname = *argv++;
  --argc;
  if (strrchr(progname,'/')!=NULL)
    progname=strrchr(progname,'/')+1;

  // global options
  OutputOptions globaloptions;
  argv = ParseGlobalOptions(globaloptions, argc, argv);
  if (argv==0) {
    Usage();
    return 1;
  }

  // read a little input file to figure out what it is
  FILE* input = fopen(globaloptions.inputname,"r");
  if (input==NULL) {
    fprintf(stderr,"failed to open input file %s\n",globaloptions.inputname);
    return 1;
  }

  const char* inputfile = strrchr(globaloptions.inputname,'/');
  inputfile = inputfile ? ++inputfile : globaloptions.inputname;

  // check file type
  if (GFile_JPEG::supportsFile(input)) {
    fprintf(stderr,"Reading JPEG file %s...\n",inputfile);
    globaloptions.srcimage = new GFile_JPEG(globaloptions.inputname,globaloptions.fastdebayer);
  }
  
  else if (GFile_TIFF::supportsFile(input)) {

    // check for DC120 file
    if (GFile_DC120::supportsFile(input)) {
      KDCFile* kdcfile = new KDCFile(globaloptions.inputname);
      if (!kdcfile->isOK()) {
	fprintf(stderr,"error reading input file %s\n",globaloptions.inputname);
	return 1;
      }

      fprintf(stderr,"Reading KDC file %s from DC120...\n",inputfile);
      fprintf(stderr,"  Description: %s\n",kdcfile->ImageDescription);
      fprintf(stderr,"  DateTime:    %s\n",kdcfile->DateTime);
      fprintf(stderr,"  Make:        %s\n",kdcfile->Make);
      fprintf(stderr,"  Model:       %s\n",kdcfile->Model);
      fprintf(stderr,"  Software:    %s\n",kdcfile->Software);
      
      globaloptions.kdcfile = kdcfile;
      globaloptions.srcimage = new GFile_DC120(*kdcfile,globaloptions.fastdebayer);
    }

    // standard TIFF file
    else {
      fprintf(stderr,"Reading TIFF file %s...\n",inputfile);
      globaloptions.srcimage = new GFile_TIFF(globaloptions.inputname,globaloptions.fastdebayer);
    }

  }

  else {
    fprintf(stderr,"unrecognized image format\n");
    fclose(input);
    return 1;
  }

  fclose(input);

  if (!globaloptions.srcimage->isOK()) {
    fprintf(stderr,"error reading input file %s\n",globaloptions.inputname);
    return 1;
  }

  // grain reduction messes with the source image (hence: global only option)
  if (globaloptions.grainreduction) {
    bool extra=globaloptions.grainreduction>1;
    fprintf(stderr,"Reducing image graininess%s... ",extra?" (extra)":"");
    int count = globaloptions.srcimage->ReduceGrain(extra);
    int totalpixels = globaloptions.srcimage->getWidth()*globaloptions.srcimage->getHeight();
    fprintf(stderr,"%.1f%% grain... done.\n",((float)count*100)/totalpixels);
  }
  
  // parse local options and write output files
  do {

    fprintf(stderr,"\n");
    
    OutputOptions options(globaloptions);
    argv = ParseLocalOptions(options, argc, argv);
    if (argv==0) {
      Usage();
      return 1;
    }
    
    const char* outputextension = 
      options.outputname ? strrchr(options.outputname,'.') : 0;
    
    // figure out if we are to output TIFF or JPEG
    if ((strcasecmp(progname,TO_TIFF_PROGNAME1)==0)||
	(strcasecmp(progname,TO_TIFF_PROGNAME2)==0))
      options.outputtiff=true;
    else if ((strcasecmp(progname,TO_JPEG_PROGNAME1)==0)||
	     (strcasecmp(progname,TO_JPEG_PROGNAME2)==0))
      options.outputjpeg=true;
    
    else if ((outputextension)&&
	     ((strcasecmp(outputextension,TO_TIFF_EXT1)==0)||
	      (strcasecmp(outputextension,TO_TIFF_EXT2)==0)))
      options.outputtiff=true;
    else if ((outputextension)&&
	     ((strcasecmp(outputextension,TO_JPEG_EXT1)==0)||
	      (strcasecmp(outputextension,TO_JPEG_EXT2)==0)))
      options.outputjpeg=true;
    
    else if ((options.compresstype)||(options.rowsperstrip))
      options.outputtiff=true;
    
    else if (options.quality!=DEFAULT_JPEG_QUALITY)
      options.outputjpeg=true;
    
    else {
      fprintf(stderr,"\nCannot figure out whether to write a TIFF or JPEG.  Please name the binary\none of: %s, %s, %s or %s;",TO_TIFF_PROGNAME1,TO_TIFF_PROGNAME2,TO_JPEG_PROGNAME1,TO_JPEG_PROGNAME2);
      fprintf(stderr," set an output filename with\nan extension of one of: %s, %s, %s or %s; or set a TIFF specific\nor JPEG specific option.\n\n",TO_TIFF_EXT1,TO_TIFF_EXT2,TO_JPEG_EXT1,TO_JPEG_EXT2);
      return 1;
    }

    options.SetOutputParameters();
    options.SetOutputFilename();

    const char* outputfile = strrchr(options.outputname,'/');
    outputfile = outputfile ? ++outputfile : options.outputname;

    // do not overwrite destination unless "-o" was specified
    if (!options.overwrite && FileExists(options.outputname)) {
      fprintf(stderr,"NO OUTPUT WRITTEN!\n");
      fprintf(stderr,"output file '%s' exists and -o not specified\n",
	      options.outputname);
      continue;
    }

    // write output image
    if (options.outputtiff) {
      fprintf(stderr,"Writing TIFF file %s... ",outputfile);
      WriteTiffImage(options);
      fprintf(stderr,"done.\n");
    }
    else if (options.outputjpeg) {
      fprintf(stderr,"Writing JPEG file %s... ",outputfile);
      WriteJpegImage(options);
      fprintf(stderr,"done.\n");
    }
    else
      fprintf(stderr,"NO OUTPUT WRITTEN!\n");


  } while (argc>0);
    
  // clean up
  delete globaloptions.srcimage;
  delete globaloptions.kdcfile;
  return 0;
}

