/**
 * @file selector_factory.h
 * @author Sean Massung
 *
 * All files in META are dual-licensed under the MIT and NCSA licenses. For more
 * details, consult the file LICENSE.mit and LICENSE.ncsa in the root of the
 * project.
 */

#ifndef META_FEATURE_SELECTOR_FACTORY_H_
#define META_FEATURE_SELECTOR_FACTORY_H_

#include "meta/classify/binary_dataset_view.h"
#include "meta/classify/multiclass_dataset.h"
#include "meta/classify/multiclass_dataset_view.h"
#include "meta/config.h"
#include "meta/features/feature_selector.h"
#include "meta/regression/regression_dataset_view.h"
#include "meta/util/factory.h"
#include "meta/util/shim.h"

namespace cpptoml
{
class table;
}

namespace meta
{
namespace features
{

/**
 * Exception for selector_factory operations.
 */
class selector_factory_exception : public std::runtime_error
{
  public:
    using std::runtime_error::runtime_error;
};

/**
 * Factory that is responsible for creating selectors from configuration
 * files. Clients should use the register_selector method instead of this
 * class directly to add their own selectors.
 */
class selector_factory
    : public util::factory<selector_factory, feature_selector,
                           const cpptoml::table&, uint64_t, uint64_t>
{
    friend base_factory;

  private:
    /**
     * Constructs the selector_factory singleton.
     */
    selector_factory();

    /**
     * Registers a single-index selector. Used internally.
     */
    template <class Selector>
    void reg();
};

/**
 * Convenience method for creating a selector using the factory.
 *
 * @param config The configuration table that specifies the configuration
 * for the selector to be created
 * @param docs The labeled dataset or dataset view (i.e., binary or multiclass
 * or regression)
 * to be passed to the selector being created
 *
 * @return a unique_ptr to the selector created from the given
 * configuration
 */
template <class LabeledDatasetContainer>
std::unique_ptr<feature_selector>
make_selector(const cpptoml::table& config, const LabeledDatasetContainer& docs)
{
    static_assert(
        std::is_same<classify::binary_dataset, LabeledDatasetContainer>::value
            || std::is_same<classify::binary_dataset_view,
                            LabeledDatasetContainer>::value
            || std::is_same<classify::multiclass_dataset,
                            LabeledDatasetContainer>::value
            || std::is_same<classify::multiclass_dataset_view,
                            LabeledDatasetContainer>::value,
        "docs should be a binary/multiclass dataset or dataset "
        "view");

    auto table = config.get_table("features");
    if (!table)
        throw selector_factory_exception{
            "[features] table missing from config file"};

    auto prefix = table->get_as<std::string>("prefix");
    if (!prefix)
        throw selector_factory_exception{"no prefix in [features] table"};

    auto method = table->get_as<std::string>("method");
    if (!method)
        throw selector_factory_exception{
            "feature selection method required in [features] table"};

    auto features_per_class
        = table->get_as<uint64_t>("features-per-class").value_or(20);

    auto selector = selector_factory::get().create(
        *method, *table, docs.total_labels(), docs.total_features());

    selector->init(docs, features_per_class); // make_selector is a friend

    return selector;
}

/**
 * Factory method for creating a feature selector. This should be specialized if
 * your selector requires special construction behavior (e.g., reading
 * parameters).
 */
template <class Selector>
std::unique_ptr<feature_selector>
factory_make_selector(const cpptoml::table& config, uint64_t total_labels,
                      uint64_t total_features)
{
    auto prefix = config.get_as<std::string>("prefix");
    if (!prefix)
        throw selector_factory_exception{"no prefix in [features] table"};

    auto method = config.get_as<std::string>("method");
    if (!method)
        throw selector_factory_exception{
            "feature selection method required in [features] table"};

    return make_unique<Selector>(*prefix + "/" + *method, total_labels,
                                 total_features);
}

/**
 * Registration method for selectors. Clients should use this method to
 * register any new selectors they write.
 */
template <class Selector>
void register_selector()
{
    selector_factory::get().add(Selector::id, factory_make_selector<Selector>);
}
}
}
#endif
