// --------------------------------------------------------------------------
//                   OpenMS -- Open-Source Mass Spectrometry
// --------------------------------------------------------------------------
// Copyright The OpenMS Team -- Eberhard Karls University Tuebingen,
// ETH Zurich, and Freie Universitaet Berlin 2002-2023.
//
// This software is released under a three-clause BSD license:
//  * 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 any author or any participating institution
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission.
// For a full list of authors, refer to the file AUTHORS.
// --------------------------------------------------------------------------
// 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 ANY OF THE AUTHORS OR THE CONTRIBUTING
// INSTITUTIONS 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.
//
// --------------------------------------------------------------------------
// $Maintainer: Hendrik Weisser $
// $Authors: Hendrik Weisser $
// --------------------------------------------------------------------------

#pragma once

#include <OpenMS/DATASTRUCTURES/DefaultParamHandler.h>

#include <svm.h>

#include <map>
#include <vector>
#include <utility> // for "pair"
#include <tuple>

namespace OpenMS
{
  /**
     @brief Simple interface to support vector machines for classification and regression (via LIBSVM).

     This class supports:
     - (multi-class) classification and regression with an epsilon-SVR.
     - linear or RBF kernel.
     
     It uses cross-validation to optimize the respective SVM/SVR parameters @e C, @e p (SVR-only) and (RBF kernel only) @e gamma.

     Usage:
     SVM models are generated by the the setup() method.
     SimpleSVM provides two common use cases for convinience:
     - all data: Passing all data (training + test) as @p predictors to setup and training on a subset.
     - training only: Passing exclusivly training data as @p predictors to setup.
     The parameter @p outcomes of setup() defines in both cases the training set; it contains the indexes of observations 
     (corresponding to positions in the vectors in @p predictors) together with the class labels (or regression values) for training.
     
     Given @e N observations of @e M predictors, the data are coded as a map of predictors (size @e M), each a numeric vector of values for different observations (size @e N).

     To predict class labels (or regression values) based on a model, use one of the predict() methods:
     - if setup with all data: the parameter @p indexes of predict() takes a vector of indexes corresponding to the observations for which predictions should be made. 
       (With an empty vector, the default, predictions are made for all observations, including those used for training.)
     - if setup with training data only: by passing a new PredictorMap.     

     
     @htmlinclude OpenMS_SimpleSVM.parameters
  */
  class OPENMS_DLLAPI SimpleSVM :
    public DefaultParamHandler
  {

  public:
    /// Mapping from predictor name to vector of predictor values
    typedef std::map<String, std::vector<double> > PredictorMap;

    /// Mapping from predictor name to predictor min and max
    typedef std::map<String, std::pair<double, double> > ScaleMap;

    /// SVM/SVR prediction result
    struct Prediction
    {
      /// Predicted class label (or regression value)
      double outcome;

      /// Class label (or regression value) and their predicted probabilities
      std::map<double, double> probabilities;
    };

    /// Default constructor
    SimpleSVM();

    /// Destructor
    ~SimpleSVM() override;

    /**
       @brief Load data and train a model.

       @param predictors Mapping from predictor name to vector of predictor values (for different observations). All vectors should have the same length; values will be changed by scaling.
       @param outcomes Mapping from observation index to class label or regression value in the training set.
       @param classification true (default) if SVM classification should be used, SVR otherwise

       @throw Exception::IllegalArgument if @p predictors is empty
       @throw Exception::InvalidValue if an invalid index is used in @p outcomes
       @throw Exception::MissingInformation if there are fewer than two class labels in @p outcomes, or if there are not enough observations for cross-validation
    */
    void setup(PredictorMap& predictors, const std::map<Size, double>& outcomes, bool classification = true);

    /**
       @brief Predict class labels or regression values (and probabilities).

       @param predictions Output vector of prediction results (same order as @p indexes).
       @param indexes Vector of observation indexes for which predictions are desired. If empty (default), predictions are made for all observations.

       @throw Exception::Precondition if no model has been trained
       @throw Exception::InvalidValue if an invalid index is used in @p indexes
    */
    void predict(std::vector<Prediction>& predictions,
                 std::vector<Size> indexes = std::vector<Size>()) const;

    /**
       @brief Predict class labels or regression values (and probabilities).

       @param predictors Mapping from predictor name to vector of predictor values (for different observations). All vectors should have the same length; values will be changed by scaling applied to training data in setup.
       @param predictions Output vector of prediction results (same order as @p indexes).
       
       @throw Exception::Precondition if no model has been trained
       @throw Exception::InvalidValue if an invalid index is used in @p indexes
    **/
    void predict(PredictorMap& predictors, std::vector<Prediction>& predictions) const;

    /**
       @brief Get the weights used for features (predictors) in the SVM model

       Currently only supported for two-class classification.
       If a linear kernel is used, the weights are informative for ranking features.

       @throw Exception::Precondition if no model has been trained, or if the classification involves more than two classes
    */
    void getFeatureWeights(std::map<String, double>& feature_weights) const;

    /// Write cross-validation (parameter optimization) results to a CSV file
    void writeXvalResults(const String& path) const;

    /// Get data range of predictors before scaling to [0, 1]
    const ScaleMap& getScaling() const;

  protected:
    void clear_();

    /// Classification (or regression) performance for different param. combinations (C/gamma/p):
    typedef std::vector<std::vector<std::vector<double>>> SVMPerformance;

    /// Values of predictors (LIBSVM format)
    std::vector<std::vector<struct svm_node> > nodes_;

    /// SVM training data (LIBSVM format)
    struct svm_problem data_;

    /// SVM parameters (LIBSVM format)
    struct svm_parameter svm_params_;

    /// Pointer to SVM model (LIBSVM format)
    struct svm_model* model_;

    /// Names of predictors in the model (excluding uninformative ones)
    std::vector<String> predictor_names_;

    /// Number of partitions for cross-validation
    Size n_parts_;

    /// Parameter values to try during optimization
    std::vector<double> log2_C_, log2_gamma_, log2_p_;

    /// Mapping from predictor name to predictor min and max
    ScaleMap scaling_;

    /// Cross-validation results
    SVMPerformance performance_;

    /// Dummy function to suppress LIBSVM output
    static void printNull_(const char*) {}

    /// Scale predictor values to range 0-1
    void scaleData_(PredictorMap& predictors);

    /// Convert predictors to LIBSVM format
    void convertData_(const PredictorMap& predictors);

    /// Choose best SVM parameters based on cross-validation results
    std::tuple<double, double, double> chooseBestParameters_(bool higher_better) const;

    /// Run cross-validation to optimize SVM parameters
    void optimizeParameters_(bool classification);
  };
}
