// Copyright (c) 1997-2001  ETH Zurich (Switzerland).
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org).
//
// $URL$
// $Id$
// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial
//
//
// Author(s)     : Kaspar Fischer <fischerk@inf.ethz.ch>

#include <iostream>
#include <iomanip>
#include <sstream>
#include <fstream>
#include <string>
#include <locale>
#include <map>
#include <cstdlib>
#include <CGAL/config.h>

#ifndef CGAL_USE_GMP
#include <CGAL/MP_Float.h>   // with normalization switched on
#else
#include <CGAL/Gmpz.h>
#include <CGAL/Gmpzf.h>
#endif
#include <CGAL/Exact_rational.h>

#include <CGAL/QP_solver/QP_solver.h>
#include <CGAL/QP_solver/QP_full_exact_pricing.h>
#include <CGAL/QP_solver/QP_exact_bland_pricing.h>
#include <CGAL/QP_solver/QP_partial_exact_pricing.h>
#include <CGAL/QP_solver/QP_full_filtered_pricing.h>
#include <CGAL/QP_solver/QP_partial_filtered_pricing.h>
#include <CGAL/QP_functions.h>
#include <CGAL/QP_models.h>

// Note: The following #define's allow faster compilation for individual
// test cases.  For instance, to only compile code for a solver
// specialized for linear, nonsymmetric, has-equalities-only-and-full-rank,
// and standard form with internal integer arithmetic, pass
//
//   -DQP_L -DQP_NOT_S -DQP_R -DQP_F -DQP_INT
//
// to GNU g++.  (Or even simpler, work with test_MPS.cpp.)
#if (!defined(QP_L) && !defined(QP_S) && !defined(QP_R) && !defined(QP_F) && \
      !defined(QP_NOT_L) && !defined(QP_NOT_S) && !defined(QP_NOT_R) && \
      !defined(QP_NOTF))
#define QP_L     true
#define QP_S     true
#define QP_R     true
#define QP_F     true
#define QP_NOT_L true
#define QP_NOT_S true
#define QP_NOT_R true
#define QP_NOT_F true
#endif
#if (!defined(QP_INT) && !defined(QP_DOUBLE) && !defined(QP_RATIONAL))
#define QP_INT      true
#define QP_DOUBLE   true
#define QP_RATIONAL true
#endif

enum Input_type { Int_type, Double_type, Rational_type };
using CGAL::Tag_true;
using CGAL::Tag_false;
typedef std::map<std::string,int>::const_iterator Key_const_iterator;
typedef std::map<std::string,int>::iterator Key_iterator;
typedef std::pair<std::string,int> Arg;

#ifdef CGAL_USE_GMP
typedef CGAL::Gmpzf Float;
typedef CGAL::Gmpz Integer;
#else
typedef CGAL::MP_Float Float;
typedef CGAL::MP_Float Integer;
//typedef CGAL::Quotient<CGAL::MP_Float> Rational;  // too slow
#endif
typedef CGAL::Exact_rational Rational;

template<typename T>
bool is_double(const T&)            { return false; }
bool is_double(const double&)       { return true; }
template<typename T>
bool is_int(const T&)               { return false; }
bool is_int(const int&)             { return true; }
template<typename T>
bool is_Integer(const T&)           { return false; }
bool is_Integer(const Integer&)     { return true; }
template<typename T>
bool is_rational(const T&)          { return false; }
bool is_rational(const Rational&)   { return true; }

int string_to_int(const std::string& s) {
  std::stringstream strm(s);
  int t;
  strm >> t;
  return t;
}

// Replaces the first occurrence of '%' in msg by replacement.
std::string replace1(const std::string& msg,const std::string& replacement)
{
  std::string result(msg);
  const std::string::size_type pos = result.find('%');
  assert(pos < result.size());
  result.replace(pos,1,replacement);
  return result;
}

void bailout(const char *msg)
{
  std::cout << "Error: " << msg << '.' << std::endl;
  std::exit(1);
}

void bailout1(const char *msg,const std::string& param)
{
  std::cout << "Error: " << replace1(msg,param) << '.' << std::endl;
  std::exit(1);
}

void usage()
{
  using std::cout;
  using std::endl;
  cout << "Normal usage:      ./test_solver < test_solver.cin\n"
       << "Alternative usage: ./test_solver verb strategy file [options]\n"
       << "\n"
       << "In the second form, verb is an integer (0-5), strategy is from\n"
       << "{fe,ff,pe,pf}, and file is a path to a MPS file. In addition,\n"
       << "you can specify any of the following additional options:\n"
       << "  +l       use the dedicated LP-solver on this instance\n"
       << "  +f       use the dedicated solver for instances in standard\n"
       << "           form (i.e., all variables have bounds [0,+inf]).\n"
       << "  int      assume the numbers in the MPS file are ints and use\n"
       << "           arbitrary-precision integers internally\n"
       << "  double   assume the numbers in the MPS file are doubles and\n"
       << "           use arbitrary-precision doubles internally\n"
       << "  rational assume the numbers in the MPS file are rationals\n"
       << "           use arbitrary-precision rationals internally\n"
       << "If any option is not specified, the program will test all\n"
       << "possible combinations (e.g., if '+l' is not specified, it will\n"
       << "check whether the problem is linear and run both, the dedicated\n"
       << "solver for the linear case and the non-dedicated solver on the\n"
       << " instance).\n"
       << "You can also negate the options 'l'or 'f' by\n"
       << "replacing the '-' by a '+'.\n";
  std::exit(1);
}

namespace Token { // A simple token reader with put-back facility.

  bool have_put_back_ = false;
  std::string put_back_token;

  std::string token(std::istream& in)
  {
    if (have_put_back_) {
      have_put_back_ = false;
      return put_back_token;
    }
    std::string t;
    in >> t;
    return t;
  }

  void put_token_back(const std::string& t)
  {
    have_put_back_ = true;
    put_back_token = t;
  }

} // namespace Token

// Clears options and reads from in the arguments as specified in usage().
// Returns true iff a new set of options could be parsed; if so, filename
// receives the name of the MPS file to solve.
bool parse_options(std::istream& in,std::map<std::string,int>& options,
                   std::string &filename)
{
  options.clear();
  std::ws(in); // read white-space
  if (in.eof())
    return false;

  // read verbosity:
  std::string t = Token::token(in);
  const int v = string_to_int(t);
  if (v<0 || v>5)
    bailout("illegal verbosity");
  options.insert(Arg("Verbosity",v));

  // read strategy:
  std::string st = Token::token(in);
  CGAL::Quadratic_program_pricing_strategy type = CGAL::QP_CHOOSE_DEFAULT;
  if (st=="fe")
    type = CGAL::QP_DANTZIG;
  else if (st=="eb")
    type = CGAL::QP_BLAND;
  else if (st=="ff")
    type = CGAL::QP_FILTERED_DANTZIG;
  else if (st=="pe")
    type = CGAL::QP_PARTIAL_DANTZIG;
  else if (st=="pf")
    type = CGAL::QP_PARTIAL_FILTERED_DANTZIG;
  else
    bailout1("illegal pricing strategy '%'",st);
  options.insert(Arg("Strategy",static_cast<int>(type)));

  // read file name:
  in >> filename;
  if (filename.size() == 0)
    bailout("no filename specified");

  // read additional options:
  std::ws(in);
  bool eof = in.eof();
  t = Token::token(in);
  while (!eof && !isdigit(t[0])) {
    // process:
    bool good;
    if (t == "+l")
      good = options.insert(Arg("Is linear",1)).second;
    else if (t == "-l")
      good = options.insert(Arg("Is linear",0)).second;
    else if (t == "+f")
      good = options.insert(Arg("Is in standard form",1)).second;
    else if (t == "-f")
      good = options.insert(Arg("Is in standard form",0)).second;
    else {
      Input_type input = Int_type; // to kill warnings
      if (t == "int")
        input = Int_type;
      else if (t == "double")
        input = Double_type;
      else if (t == "rational")
        input = Rational_type;
      else
        bailout1("unknown input number type '%'",t);
      good = options.insert(Arg("Input type",static_cast<int>(input))).second;
    }

    // process duplicate errors:
    if (!good)
      bailout1("duplicate/contradictory option for '%'",t);

    // advance:
    ws(in);
    eof = in.eof();
    t = Token::token(in);
  }
  Token::put_token_back(t);

  // output:
  const int Width = 15;
  using std::cout;
  using std::left;
  using std::setw;
  using std::endl;
  cout << endl << left << setw(Width) << "Processing:" << filename << endl;
  for (Key_const_iterator it = options.begin();
       it != options.end(); ++it) {
    cout << " " << it->first << left << setw(Width-it->first.size()-1) <<  ":";
    if (it-> first == "Strategy")
      cout << st << endl;
    else
      cout << it->second << endl;
  }

  return true;
}

template<typename Is_linear,
         typename Is_nonnegative,
         typename IT,
         typename ET>
bool process(const std::string& filename,
             const std::map<std::string,int>& options)
{
  using std::cout;
  using std::endl;
  using CGAL::check_tag;

  // extract verbosity:
  const int verbosity = options.find("Verbosity")->second;

  // read QP instance
  std::ifstream in(filename.c_str());
  if (!in)
    bailout1("could not open file '%'",filename);
  typedef CGAL::Quadratic_program_from_mps <IT> QP_instance;
  QP_instance qp(in);
  in.close();

  std::string comment = qp.get_comment();

  // check for the number-type:
  Input_type type;
  std::string number_type;
  if (comment.find("Number-type: integer") < comment.size()) {
    type = Int_type;
    number_type = "Gmpz";
  } else if (comment.find("Number-type: rational") < comment.size()) {
    type = Rational_type;
    number_type = "Gmpq";
  } else { // "Number-type: floating-point" or none specifier:
    type = Double_type;
    number_type = "double";
  }
  // now, some combinations of IT and the file's number-type are
  // incompatible:
  //    file's input type  | input type IT to be used in parsing the file
  //    -----------------------------------------------------------------
  //    double             | int  (can't convert double to it)
  //    int                |
  //    rational           | int + double (can't convert rational to them)
  if ((type==Double_type && is_int(IT()) ) ||
      (type==Rational_type && (is_double(IT()) || is_int(IT()))))
    return true;

  // also, Is_linear/Is_nonnegative tags are incompatible with
  // nonlinear/nonnegative programs
  if ((check_tag(Is_linear()) && !qp.is_linear()) ||
      (check_tag(Is_nonnegative()) && !qp.is_nonnegative()))
    return true;

  // finally, filtered pricing strategies are only to be used if the input
  // type is double
  Key_const_iterator it = options.find("Strategy");
  if (!is_double(IT()) &&
      (it->second == CGAL::QP_FILTERED_DANTZIG ||
       it->second == CGAL::QP_PARTIAL_FILTERED_DANTZIG))
    return true;

  if (verbosity > 0)
    cout << "- Running a solver specialized for: "
         << (check_tag(Is_linear())? "linear " : "")
         << (check_tag(Is_nonnegative())? "standard-form " : "")
         << "file-IT=" << number_type << ' '
         << "IT=" << (is_double(IT())? "double" :
                      (is_rational(IT())? "Gmpq" : "int")) << ' '
         << "ET=" << (is_Integer(ET())? "Gmpz" :
                      (is_rational(ET())? "Gmpq" : "Double"))
         << endl;

  // check for format errors in MPS file:
  if (!qp.is_valid()) {
    cout << "Input is not a valid MPS file." << endl
         << "Error: " << qp.get_error() << endl;
    return false;
  }

  // check name <-> index mapping
  for (int j=0; j<qp.get_n(); ++j)
    if (j != qp.variable_index_by_name (qp.variable_name_by_index (j))) {
      cout << "incorrect name <-> index mapping (variables)" << endl;
      return false;
    }
  for (int i=0; i<qp.get_m(); ++i)
    if (i != qp.constraint_index_by_name (qp.constraint_name_by_index (i))) {
      cout << "incorrect name <-> index mapping (constraints)" << endl;
      return false;
    }

  // print program (using QMATRIX format), read it back in and check
  // whether it still agrees with the original program
  std::stringstream inout;
  // if we have doubles, adjust precision to accommodate high-precision doubles
  if (is_double(IT())) inout << std::setprecision (12);
  CGAL::QP_functions_detail::print_program
    (inout, qp, std::string("test_io_mps"),
     Is_linear(),Is_nonnegative());

  // test readers
  CGAL::Quadratic_program_from_mps<IT> qp2(inout);
  assert (qp2.is_valid());
  if (!CGAL::QP_functions_detail::are_equal_qp (qp, qp2)) {
    cout << "Warning: MPS reader (QP) and MPS writer disagree.\n" << endl;
  }

  // now copy from the iterators, check for equality
  CGAL::Quadratic_program<IT>
    qp4 (qp.get_n(), qp.get_m(), qp.get_a(), qp.get_b(), qp.get_r(),
         qp.get_fl(), qp.get_l(), qp.get_fu(), qp.get_u(),
         qp.get_d(), qp.get_c(), qp.get_c0());
  if (!CGAL::QP_functions_detail::are_equal_qp (qp, qp4)) {
    cout << "Program not correctly copied.\n" << endl;
    return false;
  }
  // test consistency of types
  if (qp.is_linear() != qp4.is_linear()) {
    cout << "Program types inconsistent (linearity): "
         << qp.is_linear() << " vs. " << qp4.is_linear()
         << "\n"<< endl;
    return false;
  }
  if (qp.is_nonnegative() != qp4.is_nonnegative()) {
    cout << "Program types inconsistent (nonnegativity): "
         << qp.is_nonnegative() << " vs. " << qp4.is_nonnegative()
         << "\n"<< endl;
    return false;
  }


  // now comes the actual, output-generating run
  // make options
  CGAL::Quadratic_program_options solver_options;
  solver_options.set_verbosity(verbosity);
  solver_options.set_pricing_strategy
    (static_cast<CGAL::Quadratic_program_pricing_strategy>
     (options.find("Strategy")->second));
  solver_options.set_auto_validation(true);

  CGAL::Quadratic_program_solution<ET> solution =
    CGAL::QP_functions_detail::solve_program
    (qp, ET(0), Is_linear(), Is_nonnegative(), solver_options);
  // output solution + number of iterations
  cout << CGAL::to_double(solution.objective_value()) << "("
       << solution.number_of_iterations() << ") ";
  bool is_valid = solution.is_valid();

  // the last step: solve poblem from copied QP, if full exact pricing
  // and general form
  if (options.find("Strategy")->second == CGAL::QP_DANTZIG &&
      !check_tag(Is_linear()) &&
      !check_tag(Is_nonnegative()))
    {
    // general form
    typedef CGAL::Quadratic_program<IT>
      LocalQP;
    CGAL::Quadratic_program_options local_options;
    local_options.set_verbosity(0);
    local_options.set_pricing_strategy(CGAL::QP_DANTZIG);
    local_options.set_auto_validation(true);
    LocalQP qplocal (qp.get_n(), qp.get_m(), qp.get_a(), qp.get_b(),
                     qp.get_r(),
                     qp.get_fl(), qp.get_l(), qp.get_fu(), qp.get_u(),
                     qp.get_d(), qp.get_c(), qp.get_c0());
    CGAL::solve_quadratic_program (qplocal, ET(), local_options);
    std::cout << "(c) ";
  }

  if (verbosity > 0 || !is_valid)
    cout << "  Solution is valid: " << is_valid << endl;
  return is_valid;
}

template<typename Is_linear,
         typename Is_nonnegative>
bool processType(const std::string& filename,
                 const std::map<std::string,int>& options)
{
  // look up value:
  Key_const_iterator it = options.find("Input type");
  const bool processOnlyOneValue = it != options.end();
  Input_type value = Int_type;
  if (processOnlyOneValue)
    value = static_cast<Input_type>(it->second);

  // do only this particular value or all possibilities:
  bool success = true;

#ifdef QP_INT
  if (!processOnlyOneValue || value==Int_type)
    if (!process<Is_linear,Is_nonnegative,
        int,Integer>(filename,options))
      success = false;
#endif
#ifdef QP_DOUBLE
  if (!processOnlyOneValue || value==Double_type)
    if (!process<Is_linear,Is_nonnegative,
        double,Float>(filename,options))
      success = false;
#endif
#ifdef QP_RATIONAL
  if (!processOnlyOneValue || value==Rational_type)
    if (!process<Is_linear,Is_nonnegative,
        Rational,Rational>(filename,options))
      success = false;
#endif

  return success;
}

template<typename Is_linear>
bool processFType(const std::string& filename,
                  const std::map<std::string,int>& options)
{
  Key_const_iterator it = options.find("Is in standard form");
  const bool processOnlyOneValue = it != options.end();
  bool value = false;
  if (processOnlyOneValue)
    value = it->second > 0;
  bool success = true;
#ifdef QP_F
  if (!processOnlyOneValue || value==true)
    if (!processType<Is_linear,Tag_true>(filename,options))
      success = false;
#endif
#ifdef QP_NOT_F
  if (!processOnlyOneValue || value==false)
    if (!processType<Is_linear,Tag_false>(filename,options))
      success = false;
#endif
  return success;
}

bool processLFType(const std::string& filename,
                   const std::map<std::string,int>& options)
{
  std::cout << " Solution:   ";
  Key_const_iterator it = options.find("Is linear");
  const bool processOnlyOneValue = it != options.end();
  bool value = false;
  if (processOnlyOneValue)
    value = it->second > 0;
  bool success = true;
#ifdef QP_L
  if (!processOnlyOneValue || value==true)
    if (!processFType<Tag_true>(filename,options))
      success = false;
#endif
#ifdef QP_NOT_L
  if (!processOnlyOneValue || value==false)
    if (!processFType<Tag_false>(filename,options))
      success = false;
#endif
  return success;
}

int main(const int ac,const char **av) {
  using std::cout;
  using std::endl;
  using CGAL::Tag_true;
  using CGAL::Tag_false;

  std::cerr.precision(17);
  std::cout.precision(17);

  // determine in which mode we run:
  const bool readFromStdIn = ac == 1;
  std::stringstream args;
  if (!readFromStdIn) {
    if (ac < 4)
      usage();
    for (int i=1; i<ac; ++i) // convert av to a stream
      args << ' ' << av[i];
    cout << "Reading arguments from command line..." << endl;
  } else
    cout << "Reading from standard in..." << endl;
  std::istream in(readFromStdIn? std::cin.rdbuf() : args.rdbuf());

  // process input file(s):
  std::map<std::string,int> options;
  std::string filename;
  bool success = true;
  while (parse_options(in,options,filename))
    if (!processLFType(filename,options))
      success = false;

  // final output:
  cout << endl;
  if (!success) {
    cout << "Warning: some test cases were not handled correctly." << endl;
    return 1;
  } else {
    cout << "All test cases were successfully passed." << endl;
    return 0;
  }
}
