// Pour commencer on fait ceci:
// Usage: testf0r [options] <width> <height> <library_file> <test_file>
//
// Un jour on aura ca:
// Usage: testf0r <subcommand> [options] [args]
//
// Subcommands:
// help [SUBCOMMAND...]
// create [options] <library_file> <test_file>
// run [options] <library_file> <test_file>
// info <library_file>
//

#include "frei0r.h"

#include <qimage.h>

#include <dlfcn.h>
#include <cstdlib>

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <cassert>

#define N_COMMANDLINE_REQUIRED_HELP   1
#define N_COMMANDLINE_REQUIRED_CREATE 5
#define N_COMMANDLINE_REQUIRED_RUN    2
#define N_COMMANDLINE_REQUIRED_INFO   1

#define MAX_N_COMMANDLINE_ARGUMENTS   64

#define BUFFER_SIZE 1024


using namespace std;

void printHelp(const string subcommand = "");

void error(const string& errormsg);
void assertError(bool assert, const string& errormsg = "");
//void assertHelp(bool assert, const string subcommand = "") { assertError(assert, "", true, subcommand); }

const string nextString(int& idx, int argc, char **argv);
int nextInt(int& idx, int argc, char **argv, bool *is_int = 0);
double nextDouble(int& idx, int argc, char **argv, bool *is_double = 0);

void readFrame(uint32_t *inframe, const string& filename, const string& format, int width, int height);
void writeFrame(const uint32_t *outframe, const string& filename, const string& format, int width, int height);
void printInfo(f0r_plugin_info_t plugin_info, const vector<f0r_param_info_t>& param_infos);

void writeTestFile(int width, int height, const string& outfile, int opt_argc, char **opt_argv, const string& testfile);
void readTestFile(int& width, int& height, string& outfile, int& opt_argc, char**& opt_argv, const string& testfile);

int main(int argc, char **argv)
{
  // Arguments from the commandline.
  vector<f0r_param_info_t> param_infos;
  map<const string, int> param_infos_map;
  string libfile, outfile, testfile;
  string outrunfile = "";
  
  // Arguments for the call.
  f0r_instance_t instance;
  f0r_plugin_info_t plugin_info;
  int width = 0;
  int height = 0;
  double time = 0.0;
  uint32_t *inframe1;
  uint32_t *inframe2;
  uint32_t *inframe3;
  uint32_t *outframe;
  string infile1, infile2, infile3;
  string file_format = "JPEG";

  // Used in the validation process (nextInt(), nextDouble() calls).
  bool type_ok;

  // If true, f0r_update2 does exists (and thus has to be used).
  bool update2_exists;

  // Handle on the .so plugin.
  void *handle = 0;

  // Interface function pointers.
  int (*f0r_init)();
  void (*f0r_get_plugin_info)(f0r_plugin_info_t* pluginInfo);
  void (*f0r_get_param_info)(f0r_param_info_t* info, int param_index);
  f0r_instance_t (*f0r_construct)(unsigned int width, unsigned int height);
  void (*f0r_destruct)(f0r_instance_t instance);
  void (*f0r_set_param_value)(f0r_instance_t instance, f0r_param_t param, int param_index);
  void (*f0r_get_param_value)(f0r_instance_t instance, f0r_param_t param, int param_index);
  void (*f0r_update)(f0r_instance_t instance, double time, const uint32_t* inframe, uint32_t* outframe);
  void (*f0r_update2)(f0r_instance_t instance, double time,
                      const uint32_t* inframe1, const uint32_t* inframe2, const uint32_t* inframe3, uint32_t* outframe);

  // Main switch (subcommand) //////////////////////////////////////////////////////////////////////////

  assertError(argc > 1, "Too few argument.");
  int arg = 1;
  const string subcommand = nextString(arg, argc, argv);

  // Subcommand : help /////////////////////////////////////////////////////////////////////////////////
  if (subcommand == "help")
  {
    if (arg == argc) // finished (just asking for general help)
      printHelp();
    else // needs help on a specific subcommand
      printHelp(nextString(arg, argc, argv));
  }

  // Subcommand : info /////////////////////////////////////////////////////////////////////////////////
  else if (subcommand == "info")
  {
    // Read required.
    libfile = nextString(arg, argc, argv);

    // Create dynamic handle to the library file.
    handle = dlopen(libfile.c_str(), RTLD_LAZY);
    assertError(handle);
  
    // Get interface function pointers
    *(void**) (&f0r_init) = dlsym(handle, "f0r_init"); 
    *(void**) (&f0r_get_plugin_info) = dlsym(handle, "f0r_get_plugin_info");
    *(void**) (&f0r_get_param_info) = dlsym(handle, "f0r_get_param_info");

    // Get infos.
    (*f0r_get_plugin_info)(&plugin_info);

    // Get the list of params.
    param_infos.resize(plugin_info.num_params);
    for (int i=0; i<plugin_info.num_params; ++i)
    {
      (*f0r_get_param_info)(&param_infos[i], i);
      param_infos_map[param_infos[i].name] = i;
    }

    // Print infos.
    printInfo(plugin_info, param_infos);
  }

  // Subcommand : create ///////////////////////////////////////////////////////////////////////////////
  else if (subcommand == "create" || subcommand == "run")
  {
    char **opt_argv;
    int opt_argc;
    //    ok le but c'est que create va juste sauver la commandline d'options dans le fichier de test pis
    //    run va la reloader; i.e. on remplace argv par un autre char ** pis dans le cas de run c'est carrément
    //    lu dans le fichier
    if (subcommand == "create")
    {
      // Number of arguments that are options (i.e. non required).
      int n_option_args = argc - N_COMMANDLINE_REQUIRED_CREATE - 2;
      assertError(n_option_args >= 0);

      // Read required.
      arg = n_option_args + 2;
      width = nextInt(arg, argc, argv, &type_ok); assertError(type_ok);
      height = nextInt(arg, argc, argv, &type_ok); assertError(type_ok);
      libfile = nextString(arg, argc, argv);
      testfile = nextString(arg, argc, argv);
      outfile = nextString(arg, argc, argv);

      // Set option count/pointer.
      opt_argc = n_option_args;
      opt_argv = argv + 2;

      // Save options to test file.
      writeTestFile(width, height, outfile, opt_argc, opt_argv, testfile);
    }
    else // subcommand == "run"
    {
      // Number of arguments that are options (i.e. non required).
      int n_option_args = argc - N_COMMANDLINE_REQUIRED_RUN - 2;
      assertError(n_option_args >= 0);
      
      // Read options
      arg = 2;
      if (n_option_args > 0)
      {
        const string option_name = nextString(arg, argc, argv);
        if (option_name == "-o")
          outrunfile = nextString(arg, argc, argv);
        else
          error("Unknown option: " + option_name);
      }
      
      // Read required.
      libfile = nextString(arg, argc, argv);
      testfile = nextString(arg, argc, argv);
      
      // Read test file options.
      opt_argv = (char**)malloc(MAX_N_COMMANDLINE_ARGUMENTS*sizeof(char*));
      for (int i=0; i<MAX_N_COMMANDLINE_ARGUMENTS; ++i)
        opt_argv[i] = (char*)malloc(BUFFER_SIZE*sizeof(char*));
      readTestFile(width, height, outfile, opt_argc, opt_argv, testfile);
    }
    
    // Allocate frames.
    inframe1 = (uint32_t*)malloc(width*height*sizeof(uint32_t));
    inframe2 = (uint32_t*)malloc(width*height*sizeof(uint32_t));
    inframe3 = (uint32_t*)malloc(width*height*sizeof(uint32_t));
    outframe = (uint32_t*)malloc(width*height*sizeof(uint32_t));

    // Create dynamic handle to the library file.
    handle = dlopen(libfile.c_str(), RTLD_LAZY);
    assertError(handle);
  
    // Get interface function pointers
    *(void**) (&f0r_init) = dlsym(handle, "f0r_init"); 
    *(void**) (&f0r_get_plugin_info) = dlsym(handle, "f0r_get_plugin_info");
    *(void**) (&f0r_get_param_info) = dlsym(handle, "f0r_get_param_info");
    *(void**) (&f0r_construct) = dlsym(handle, "f0r_construct");
    *(void**) (&f0r_destruct) = dlsym(handle, "f0r_destruct");
    *(void**) (&f0r_set_param_value) = dlsym(handle, "f0r_set_param_value");
    *(void**) (&f0r_get_param_value) = dlsym(handle, "f0r_get_param_value");
    *(void**) (&f0r_update) = dlsym(handle, "f0r_update");

    const char* err = dlerror();
    if (err)
      error(err);

    // f0r_update2
    *(void**) (&f0r_update2) = dlsym(handle, "f0r_update2");
    update2_exists = (!dlerror());
    
    // Init.
    (*f0r_init)();

    // Construct plugin instance.
    instance = (*f0r_construct)(width, height);

    // Get infos.
    (*f0r_get_plugin_info)(&plugin_info);

    // Get the list of params.
    param_infos.resize(plugin_info.num_params);
    for (int i=0; i<plugin_info.num_params; ++i)
    {
      (*f0r_get_param_info)(&param_infos[i], i);
      param_infos_map[param_infos[i].name] = i;
    }
  
    // Read options.
    arg = 0;
    //    int opt_argc = n_option_args + 2; // used in next*() calls because argument indices start at 1
    while (arg < opt_argc)
    {
      const string option_name = nextString(arg, opt_argc, opt_argv);

      // Parameter.
      if (option_name == "-p")
      {
        // Get information about next asked param.
        f0r_param_info_t param_info;
        int param_index = nextInt(arg, opt_argc, opt_argv, &type_ok);
        if (type_ok)
        {
          // Verify if the index is valid.
          assertError( 0 <= param_index && param_index < plugin_info.num_params);
        }
        else
        {
          // Try to get it by the param name.
          const string param_name = nextString(arg, opt_argc, opt_argv);
          map<const string, int>::const_iterator it = param_infos_map.find(param_name);
          assert (it != param_infos_map.end()); // not found
          param_index = it->second;
        }

        // We've got the info.
        param_info = param_infos[param_index];

        // Set parameter.
        switch (param_info.type)
        {
        case F0R_PARAM_BOOL:
          {
            double value = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            (*f0r_set_param_value)(instance, new f0r_param_bool(value), param_index);
          }
          break;
        case F0R_PARAM_DOUBLE:
          {
            double value = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            (*f0r_set_param_value)(instance, new f0r_param_double(value), param_index);
          }
          break;
        case F0R_PARAM_COLOR:
          {
            f0r_param_color *color = new f0r_param_color;
            color->r = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            color->g = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            color->b = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            (*f0r_set_param_value)(instance, color, param_index);
          }
          break;
        case F0R_PARAM_POSITION:
          {
            f0r_param_position *position = new f0r_param_position;
            position->x = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            position->y = nextDouble(arg, opt_argc, opt_argv, &type_ok); assertError(type_ok);
            (*f0r_set_param_value)(instance, position, param_index);
          }
          break;
        default:
          error("Unrecognized info type.");
        }
      }

      // Input file.
      else if (option_name == "-i1")
        infile1 = nextString(arg, opt_argc, opt_argv);
      else if (option_name == "-i2")
        infile2 = nextString(arg, opt_argc, opt_argv);
      else if (option_name == "-i3")
        infile3 = nextString(arg, opt_argc, opt_argv);
    
      else if (option_name == "-format")
        file_format = nextString(arg, opt_argc, opt_argv);
    
      else
        error("Unknown option: " + option_name + ".");
    
    }

    // Read input frames.
    if (!infile1.empty())
      readFrame(inframe1, infile1, file_format, width, height);
    if (!infile2.empty())
      readFrame(inframe2, infile2, file_format, width, height);
    if (!infile3.empty())
      readFrame(inframe3, infile3, file_format, width, height);
    
    // Perform update.
    if (update2_exists)
      (*f0r_update2)(instance, time, inframe1, inframe2, inframe3, outframe);
    else
      (*f0r_update)(instance, time, inframe1, outframe);

    if (subcommand == "create")
    {
      // Write output to file.
      writeFrame(outframe, outfile, file_format, width, height);
    }
    else // subcommand == "run"
    {
      // Compare output with that from file.
      uint32_t *testframe = (uint32_t*)malloc(width*height*sizeof(uint32_t));
      readFrame(testframe, outfile, file_format, width, height);

      // Compare frames pixel by pixel.
      double sum = 0.0;
      for (int i=0; i<width*height; ++i)
        sum += (testframe[i] == outframe[i] ? 0.0 : 1.0);
      sum /= (double)(width*height) * 100.0;
      cout << "Difference with test image: " << sum << "%." << endl;

      // Cleanup.
      free(testframe);

      // Write output image if option was set.
      if (!outrunfile.empty())
        writeFrame(outframe, outrunfile, file_format, width, height);
    }
    
    // Cleanup the mess.
    (*f0r_destruct)(instance);
    dlclose(handle);
  
    free(inframe1);
    free(inframe2);
    free(inframe3);
    free(outframe);

    if (subcommand == "run")
    {
      for (int i=0; i<opt_argc; ++i)
        free(opt_argv[i]);
      free(opt_argv);
    }
  }
  else if (subcommand == "run")
  {
    
  }
  
}

void error(const string& errormsg)
{
  if (!errormsg.empty())
    cerr << errormsg << endl;
  cout << "Type 'testfrei0r help' for usage." << endl;
  exit(-1);
}

void assertError(bool assert, const string& errormsg)
{
  if (!assert)
    error(errormsg);
}

void printSubCommands()
{
  cout << "List of subcommands:" << endl;
  cout << "  help    Displays help." << endl;
  cout << "  create  Creates a new test." << endl;
  cout << "  run     Performs a previously created test." << endl;
  cout << "  info    Displays informations about a plugin." << endl;
}

void printHelp(const string subcommand)
{
  string prefix = "Usage: testf0r ";
  if (subcommand == "help")
  {
    cout << prefix << "help [<subcommand>]" << endl;
    cout << "  Displays a help notice." << endl;
    printSubCommands();
  }
  else if (subcommand == "create")
  {
    cout << prefix << "create [options] <width> <height> <library_file> <output_test_file> <output_image_file>" << endl;
    cout << "  Creates a new test." << endl;
    cout << "  Options: " << endl;
    cout << "    -p (<name>|<index>) <value>        Sets a boolean or a double param to some value." << endl;
    cout << "    -p (<name>|<index>) <r> <g> <b>    Sets a color param to some value." << endl;
    cout << "    -p (<name>|<index>) <x> <y>        Sets a position param to some value." << endl;
    cout << "    -i1,-i2,-i3 <input_image_file>     Loads input 1, 2 or 3 from a file." << endl;
    cout << "    -format <format>                   Sets the image format of the input/output files." << endl;
  }
  else if (subcommand == "run")
  {
    cout << prefix << "run [options] <library_file> <test_file>" << endl;
    cout << "  Performs a previously created test." << endl;
    cout << "  Options: " << endl;
    cout << "    -o <output_image_file>             Generates an output image." << endl;
  }
  else if (subcommand == "info")
  {
    cout << prefix << "info <library_file>" << endl;
    cout << "  Displays informations about a plugin." << endl;    
  }
  else
  {
    cout << prefix << "<subcommand> [options] [args]" << endl;
    printSubCommands();
  }
}

const string nextString(int& idx, int argc, char **argv)
{
  assertError(0 <= idx && idx < argc, "Parsing error.");
  return argv[idx++];
}

int nextInt(int& idx, int argc, char **argv, bool *is_int)
{
  assertError(0 <= idx && idx < argc, "Parsing error.");
  char *endptr;
  //  char *endptr = (char*)malloc(BUFFER_SIZE*sizeof(char));
  int k = (int)strtol(argv[idx], &endptr, 10);
  if (is_int)
  {
    if (argv[idx][0] != '\0' && endptr[0] != '\0')
      *is_int = false;
    else
    {
      *is_int = true;
      idx++;
    }
  }
  else // we don't care
    idx++;
  return k;
}

double nextDouble(int& idx, int argc, char **argv, bool *is_double)
{
  assertError(0 <= idx && idx < argc, "Parsing error.");
  //  char *endptr = (char*)malloc(BUFFER_SIZE*sizeof(char));
  char *endptr;
  double k = (double)strtod(argv[idx], &endptr);
  if (is_double)
  {
    if (argv[idx][0] != '\0' && endptr[0] != '\0')
      *is_double = false;
    else
    {
      *is_double = true;
      idx++;
    }
  }
  else // we don't care
    idx++;
  return k;
}

void printInfo(f0r_plugin_info_t plugin_info, const vector<f0r_param_info_t>& param_infos)
{
  //  cout << "# Plugin info ############" << endl;
  cout << "Name             : " << plugin_info.name << endl;
  cout << "Author           : " << plugin_info.author << endl;
  cout << plugin_info.explanation << endl;
  cout << "Parameters [" << plugin_info.num_params << " total]" << endl;
  for (int i=0; i<plugin_info.num_params; ++i)
  {
    cout << "  [" << i << "] " << param_infos[i].name << " ";
    switch (param_infos[i].type)
    {
    case F0R_PARAM_BOOL:
      cout << "(bool)" << " ";
      break;
    case F0R_PARAM_DOUBLE:
      cout << "(double)" << " ";
      break;
    case F0R_PARAM_COLOR:
      cout << "(color)" << " ";
      break;
    case F0R_PARAM_POSITION:
      cout << "(position)" << " ";
      break;
    default:
      error("Unrecognized info type.");
    }
    cout << param_infos[i].explanation << endl;
  }
  //  cout << "##########################" << endl;
}

void readFrame(uint32_t *inframe, const string& filename, const string& format, int width, int height)
{
  QImage img(filename.c_str());

  assertError(!img.isNull(), "Problem while opening file '" + filename + "'.");
  assertError(img.width() == width && img.height() == height, "Image file '" + filename + "' has wrong dimensions.");
  
  unsigned char *it = (unsigned char*) inframe;
  
  // Reading image from file.
  for (int y = 0; y < img.height(); ++y)
    for (int x = 0; x < img.width(); ++x)
    {
      QRgb pix = img.pixel(x,y);
      it[0] = qRed(pix);
      it[1] = qGreen(pix);
      it[2] = qBlue(pix);
      it[3] = qAlpha(pix);
      
      it += 4;
    }

  img.save(filename.c_str(), format.c_str());
}

void writeFrame(const uint32_t *outframe, const string& filename, const string& format, int width, int height)
{
  QImage img(width, height, QImage::Format_ARGB32);

  assertError(!img.isNull(), "Problem while opening file '" + filename + "'.");
    
  unsigned char *it = (unsigned char*) outframe;
  
  // Write image to file.
  for (int y = 0; y < img.height(); ++y)
    for (int x = 0; x < img.width(); ++x)
    {
      img.setPixel(x,y, qRgba((int)it[0], (int)it[1], (int)it[2], (int)it[3]));
      it += 4;
    }

  img.save(filename.c_str(), format.c_str());
}

void writeTestFile(int width, int height, const string& outfile, int opt_argc, char **opt_argv, const string& testfile)
{
  ofstream f(testfile.c_str());
  assertError(f, "Cannot open file " + testfile + " for writing.");

  f << width << endl << height << endl << outfile << endl << opt_argc << endl;
  for (int i=0; i<opt_argc; ++i)
    f << opt_argv[i] << " ";
  f << endl;
}

void readTestFile(int& width, int& height, string& outfile, int& opt_argc, char**& opt_argv, const string& testfile)
{
  ifstream f(testfile.c_str());
  assertError(f, "Cannot open file " + testfile + " for reading.");

  f >> width;
  f >> height;
  f >> outfile;
  f >> opt_argc;
  string tmp;
  for (int i=0; i<opt_argc; ++i)
  {
    f >> tmp;
    strcpy(opt_argv[i], tmp.c_str());
  }
}
