/*
 * Software License Agreement (BSD License)
 *
 *  Point Cloud Library (PCL) - www.pointclouds.org
 *  Copyright (c) 2010-2012, Willow Garage, Inc.
 *  Copyright (c) 2000-2012 Chih-Chung Chang and Chih-Jen Lin
 *
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above
 *     copyright notice, this list of conditions and the following
 *     disclaimer in the documentation and/or other materials provided
 *     with the distribution.
 *   * Neither the name of copyright holders nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 *  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 *  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 */

#pragma once

#include <pcl/console/print.h> // for PCL_ERROR
#include <pcl/ml/svm.h>

#include <cassert> // for assert
#include <cstdio>
#include <cstdlib>
#include <limits> // for numeric_limits
#include <string> // for string
#include <vector>
// NOLINTNEXTLINE(bugprone-macro-parentheses)
#define Malloc(type, n) static_cast<type*>(malloc((n) * sizeof(type)))

namespace pcl {

/** The structure stores the parameters for the classificationa nd must be initialized
 *  and passed to the training method pcl::SVMTrain.
 *
 * \param svm_type {C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR}
 * \param kernel_type {LINEAR, POLY, RBF, SIGMOID, PRECOMPUTED}
 * \param probability sets the probability estimates
 */
struct SVMParam : svm_parameter {
  SVMParam()
  {
    svm_type = C_SVC;  // C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR
    kernel_type = RBF; // LINEAR, POLY, RBF, SIGMOID, PRECOMPUTED
    degree = 3;        // for poly
    gamma = 0;         // 1/num_features {for poly/rbf/sigmoid}
    coef0 = 0;         //  for poly/sigmoid

    nu = 0.5;         // for NU_SVC, ONE_CLASS, and NU_SVR
    cache_size = 100; // in MB
    C = 1;            // for C_SVC, EPSILON_SVR and NU_SVR
    eps = 1e-3;       // stopping criteria
    p = 0.1;          // for EPSILON_SVR
    shrinking = 0;    // use the shrinking heuristics
    probability = 0;  // do probability estimates

    nr_weight = 0;          // for C_SVC
    weight_label = nullptr; // for C_SVC
    weight = nullptr;       // for C_SVC
  }
};

/** The structure initialize a model created by the SVM (Support Vector Machines)
 *  classifier (pcl::SVMTrain).
 */
struct SVMModel : svm_model {
  SVMModel()
  {
    l = 0;
    probA = nullptr;
    probB = nullptr;
  }
};

/** The structure initialize a single feature value for the classification using
 *  SVM (Support Vector Machines).
 */
struct SVMDataPoint {
  /// It's the feature index. It has to be an integer number greater or equal to zero
  int idx{-1};
  /// The value assigned to the correspondent feature.
  float value{0.0f};

  SVMDataPoint() = default;
};

/** The structure stores the features and the label of a single sample which has to be
 *  used for the training or the classification of the SVM (Support Vector Machines).
 */
struct SVMData {
  /// Pointer to the label value. It is a mandatory to train the classifier
  double label;
  /// Vector of features for the specific sample.
  std::vector<pcl::SVMDataPoint> SV;

  SVMData() : label(std::numeric_limits<double>::signaling_NaN()) {}
};

/** Base class for SVM SVM (Support Vector Machines). */
class SVM {
protected:
  std::vector<SVMData> training_set_; // Basic training set
  svm_problem prob_;    // contains the problem (vector of samples with their features)
  SVMModel model_;      // model of the classifier
  svm_scaling scaling_; // for the best model training, the input dataset is scaled and
                        // the scaling factors are stored here
  SVMParam param_;      // it stores the training parameters
  std::string class_name_; // The SVM class name.

  char* line_{nullptr};     // buffer for line reading
  int max_line_len_{10000}; // max line length in the input file
  bool labelled_training_set_{
      true}; // it stores whether the input set of samples is labelled

  /** Set for output printings during classification. */
  static void
  printNull(const char*){};

  /** To read a line from the input file. Stored in "line_". */
  char*
  readline(FILE* input);

  /** Outputs an error in file reading. */
  void
  exitInputError(int line_num)
  {
    fprintf(stderr, "Wrong input format at line %d\n", line_num);
    exit(1);
  }

  /** Get a string representation of the name of this class. */
  inline const std::string&
  getClassName() const
  {
    return (class_name_);
  }

  /** Convert the input format (vector of SVMData) into a readable format for libSVM. */
  void
  adaptInputToLibSVM(std::vector<SVMData> training_set, svm_problem& prob);

  /** Convert the libSVM format (svm_problem) into a easier output format. */
  void
  adaptLibSVMToInput(std::vector<SVMData>& training_set, svm_problem prob) const;

  /** Load a problem from an extern file. */
  bool
  loadProblem(const char* filename, svm_problem& prob);

  /** Save the raw problem in an extern file.*/
  bool
  saveProblem(const char* filename, bool labelled);

  /** Save the problem (with normalized values) in an extern file.*/
  bool
  saveProblemNorm(const char* filename, svm_problem prob_, bool labelled);

public:
  /**  Constructor. */
  SVM() : prob_() {}

  /** Destructor. */
  ~SVM()
  {
    svm_destroy_param(&param_); // delete parameters

    if (scaling_.max > 0)
      free(scaling_.obj); // delete scaling factors

    // delete the problem
    if (prob_.l > 0) {
      free(prob_.x);
      free(prob_.y);
    }
  }

  /** Return the labels order from the classifier model. */
  void
  getLabel(std::vector<int>& labels)
  {
    int nr_class = svm_get_nr_class(&model_);
    int* labels_ = static_cast<int*>(malloc(nr_class * sizeof(int)));
    svm_get_labels(&model_, labels_);

    for (int j = 0; j < nr_class; j++)
      labels.push_back(labels_[j]);

    free(labels_);
  };

  /** Save the classifier model in an extern file (in svmlight format). */
  void
  saveClassifierModel(const char* filename)
  {
    // exit if model has no data
    if (model_.l == 0)
      return;

    if (svm_save_model(filename, &model_)) {
      fprintf(stderr, "can't save model to file %s\n", filename);
      exit(1);
    }
  };
};

/** SVM (Support Vector Machines) training class for the SVM machine learning.
 *
 *  It creates a model for the classifier from a labelled input dataset.
 *
 *  OPTIONAL: pcl::SVMParam has to be given as input to vary the default training method
 *  and parameters.
 */
class SVMTrain : public SVM {
protected:
  using SVM::class_name_;
  using SVM::labelled_training_set_;
  using SVM::line_;
  using SVM::max_line_len_;
  using SVM::model_;
  using SVM::param_;
  using SVM::prob_;
  using SVM::scaling_;
  using SVM::training_set_;

  /// Set to 1 to see the training output
  bool debug_{false};
  /// Set too 1 for cross validating the classifier
  int cross_validation_{0};
  /// Number of folds to be used during cross validation. It indicates in how many parts
  /// is split the input training set.
  int nr_fold_{0};

  /** To cross validate the classifier. It is automatic for probability estimate. */
  void
  doCrossValidation();

  /** It extracts scaling factors from the input training_set.
   *
   *  The scaling of the training_set is a mandatory for a good training of the
   *  classifier. */
  void
  scaleFactors(std::vector<SVMData> training_set, svm_scaling& scaling);

public:
  /** Constructor. */
  SVMTrain()
  {
    class_name_ = "SVMTrain";
    svm_set_print_string_function(
        &printNull); // Default to NULL to not print debugging info
  }

  /** Destructor. */
  ~SVMTrain()
  {
    if (model_.l > 0)
      svm_free_model_content(&model_);
  }

  /** Change default training parameters (pcl::SVMParam). */
  void
  setParameters(SVMParam param)
  {
    param_ = param;
  }

  /** Return the current training parameters. */
  SVMParam
  getParameters()
  {
    return param_;
  }

  /** Return the result of the training. */
  SVMModel
  getClassifierModel()
  {
    return model_;
  }

  /** It adds/store the training set with labelled data. */
  void
  setInputTrainingSet(std::vector<SVMData> training_set)
  {
    training_set_.insert(training_set_.end(), training_set.begin(), training_set.end());
  }

  /** Return the current training set. */
  std::vector<SVMData>
  getInputTrainingSet()
  {
    return training_set_;
  }

  /** Reset the training set. */
  void
  resetTrainingSet()
  {
    training_set_.clear();
  }

  /** Start the training of the SVM classifier.
   *
   * \return false if fails
   */
  bool
  trainClassifier();

  /** Read in a problem (in svmlight format).
   *
   * \return false if fails
   */
  bool
  loadProblem(const char* filename)
  {
    return SVM::loadProblem(filename, prob_);
  };

  /** Set to 1 for debugging info. */
  void
  setDebugMode(bool in)
  {
    debug_ = in;

    if (in)
      svm_set_print_string_function(nullptr);
    else
      svm_set_print_string_function(&printNull);
  };

  /** Save the raw training set in a file (in svmlight format).
   *
   * \return false if fails
   */
  bool
  saveTrainingSet(const char* filename)
  {
    return SVM::saveProblem(filename, true);
  };

  /** Save the normalized training set in a file (in svmlight format).
   *
   * \return false if fails
   */
  bool
  saveNormTrainingSet(const char* filename)
  {
    return SVM::saveProblemNorm(filename, prob_, true);
  };
};

/** SVM (Support Vector Machines) classification of a dataset.
 *
 *  It can be used both for testing a classifier model and for classify of new data.
 */
class SVMClassify : public SVM {
protected:
  using SVM::class_name_;
  using SVM::labelled_training_set_;
  using SVM::line_;
  using SVM::max_line_len_;
  using SVM::model_;
  using SVM::param_;
  using SVM::prob_;
  using SVM::scaling_;
  using SVM::training_set_;

  bool model_extern_copied_{
      false}; // Set to 0 if the model is loaded from an extern file.
  bool predict_probability_{false};             // Set to 1 to predict probabilities.
  std::vector<std::vector<double>> prediction_; // It stores the resulting prediction.

  /** It scales the input dataset using the model information. */
  void
  scaleProblem(svm_problem& input, svm_scaling scaling);

public:
  /** Constructor. */
  SVMClassify() { class_name_ = "SvmClassify"; }

  /** Destructor. */
  ~SVMClassify()
  {
    if (!model_extern_copied_ && model_.l > 0)
      svm_free_model_content(&model_);
  }

  /** It adds/store the training set with labelled data. */
  void
  setInputTrainingSet(std::vector<SVMData> training_set)
  {
    assert(training_set.size() > 0);

    if (scaling_.max == 0) {
      // to be sure to have loaded the scaling
      PCL_ERROR("[pcl::%s::setInputTrainingSet] Classifier model not loaded!\n",
                getClassName().c_str());
      return;
    }

    training_set_.insert(training_set_.end(), training_set.begin(), training_set.end());
    SVM::adaptInputToLibSVM(training_set_, prob_);
  }

  /** Return the current training set. */
  std::vector<SVMData>
  getInputTrainingSet()
  {
    return training_set_;
  }

  /** Reset the training set. */
  void
  resetTrainingSet()
  {
    training_set_.clear();
  }

  /** Read in a classifier model (in svmlight format).
   *
   * \return false if fails
   */
  bool
  loadClassifierModel(const char* filename);

  /** Get the result of the classification. */
  void
  getClassificationResult(std::vector<std::vector<double>>& out)
  {
    out.clear();
    out.insert(out.begin(), prediction_.begin(), prediction_.end());
  }

  /** Save the classification result in an extern file. */
  void
  saveClassificationResult(const char* filename);

  /** Set the classifier model. */
  void
  setClassifierModel(SVMModel model)
  {
    // model (inner pointers are references)
    model_ = model;
    int i = 0;

    while (model_.scaling[i].index != -1)
      i++;

    scaling_.max = i;
    scaling_.obj = Malloc(struct svm_node, i + 1);
    scaling_.obj[i].index = -1;

    // Performing full scaling copy
    for (int j = 0; j < i; j++) {
      scaling_.obj[j] = model_.scaling[j];
    }

    model_extern_copied_ = true;
  };

  /** Read in a raw classification problem (in svmlight format).
   *
   *  The values are normalized using the classifier model information.
   *
   * \return false if fails
   */
  bool
  loadClassProblem(const char* filename)
  {
    assert(model_.l != 0);

    bool out = SVM::loadProblem(filename, prob_);
    SVM::adaptLibSVMToInput(training_set_, prob_);
    scaleProblem(prob_, scaling_);
    return out;
  };

  /** Read in a normalized classification problem (in svmlight format).
   *
   *  The data is kept without normalizing.
   *
   * \return false if fails
   */
  bool
  loadNormClassProblem(const char* filename)
  {
    bool out = SVM::loadProblem(filename, prob_);
    SVM::adaptLibSVMToInput(training_set_, prob_);
    return out;
  };

  /** Set whether the classification has to be done with the probability estimate. (The
   *  classifier model has to support it). */
  void
  setProbabilityEstimates(bool set)
  {
    predict_probability_ = set;
  };

  /** Start the classification on labelled input dataset.
   *
   *  It returns the accuracy percentage. To get the classification result, use
   *  getClassificationResult().
   *
   * \return false if fails
   */
  bool
  classificationTest();

  /** Start the classification on un-labelled input dataset.
   *
   *  To get the classification result, use getClassificationResult().
   *
   * \return false if fails
   */
  bool
  classification();

  /** Start the classification on a single set. */
  std::vector<double>
  classification(SVMData in);

  /** Save the raw classification problem in a file (in svmlight format).
   *
   * \return false if fails
   */
  bool
  saveClassProblem(const char* filename)
  {
    return SVM::saveProblem(filename, false);
  };

  /** Save the normalized classification problem in a file (in svmlight format).
   *
   * \return false if fails
   */
  bool
  saveNormClassProblem(const char* filename)
  {
    return SVM::saveProblemNorm(filename, prob_, false);
  };
};

} // namespace pcl
