// Copyright (c), ETH Zurich and UNC Chapel Hill.
// 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 ETH Zurich and UNC Chapel Hill 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 HOLDERS 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 "colmap/util/logging.h"
#include "colmap/util/types.h"

#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>

#include <boost/program_options.hpp>

namespace colmap {

// Base class for option managers providing core infrastructure for
// command-line parsing, configuration file I/O, and option registration.
class BaseOptionManager {
 public:
  NON_COPYABLE(BaseOptionManager)

  explicit BaseOptionManager(bool add_project_options = true);
  BaseOptionManager(BaseOptionManager&&) = default;
  BaseOptionManager& operator=(BaseOptionManager&&) = default;
  virtual ~BaseOptionManager() = default;

  void AddRandomOptions();
  void AddLogOptions();
  void AddDatabaseOptions();
  void AddImageOptions();

  template <typename T>
  void AddRequiredOption(const std::string& name,
                         T* option,
                         const std::string& help_text = "");
  template <typename T>
  void AddDefaultOption(const std::string& name,
                        T* option,
                        const std::string& help_text = "");

  // Register an enum option with automatic string-to-enum conversion.
  // Uses the ToString/FromString functions generated by MAKE_ENUM_CLASS.
  // The conversion is applied automatically after parsing.
  //
  // Example:
  //   MAKE_ENUM_CLASS(MyEnum, 0, VALUE_A, VALUE_B);
  //   AddDefaultEnumOption("my_option",
  //                        &options.my_enum,
  //                        MyEnumToString,
  //                        MyEnumFromString);
  template <typename EnumT>
  void AddDefaultEnumOption(const std::string& name,
                            EnumT* option,
                            std::string_view (*to_string_fn)(EnumT),
                            EnumT (*from_string_fn)(std::string_view),
                            const std::string& help_text = "");

  // Reset all internal state. If reset_logging is true, restore glog defaults.
  // Higher-level applications may override the logging configuration.
  virtual void Reset(bool reset_logging = true);
  virtual void ResetOptions(bool reset_paths);

  virtual bool Check();

  bool Parse(int argc, char** argv);
  virtual bool Read(const std::filesystem::path& path,
                    bool allow_unregistered = true);
  bool ReRead(const std::filesystem::path& path,
              bool reset_logging = true,
              bool allow_unregistered = true);
  void Write(const std::filesystem::path& path) const;

  std::shared_ptr<std::filesystem::path> project_path;
  std::shared_ptr<std::filesystem::path> database_path;
  std::shared_ptr<std::filesystem::path> image_path;

 protected:
  template <typename T>
  void RegisterOption(const std::string& name, const T* option);

  // Hook for subclasses to perform post-parse processing.
  // Called after successful parsing but before Check().
  virtual void PostParse();

  // Hook for subclasses to print custom help message.
  virtual void PrintHelp() const;

  // Hook for subclasses to add all their options. Called by ReRead().
  // Base implementation adds common options (random, log, database, image).
  // Subclasses should call BaseOptionManager::AddAllOptions() first.
  virtual void AddAllOptions();

  std::shared_ptr<boost::program_options::options_description> desc_;

  // Log destination choice: {stderr, stdout, file, stderr_and_file}.
  std::string log_target_ = "stderr_and_file";

  std::vector<std::pair<std::string, const bool*>> options_bool_;
  std::vector<std::pair<std::string, const int*>> options_int_;
  std::vector<std::pair<std::string, const double*>> options_double_;
  std::vector<std::pair<std::string, const std::string*>> options_string_;
  std::vector<std::pair<std::string, const std::filesystem::path*>>
      options_path_;

  // Storage for enum options: string value and conversion callback.
  // Uses unique_ptr for pointer stability when the vector grows.
  struct EnumOptionInfo {
    std::string value;            // String value for parsing
    std::function<void()> apply;  // Callback to apply string->enum conversion
  };
  std::vector<std::unique_ptr<EnumOptionInfo>> enum_options_;

  bool added_random_options_ = false;
  bool added_log_options_ = false;
  bool added_database_options_ = false;
  bool added_image_options_ = false;

 private:
  // Non-virtual implementations called from constructor and virtual methods.
  // These avoid the clang-tidy warning about virtual calls during construction.
  void ResetImpl(bool reset_logging);
  void ResetOptionsImpl(bool reset_paths);

  // Apply string->enum conversions for all registered enum options.
  void ApplyEnumConversions();

  // Map simplified log output options to glog flags.
  void ApplyLogFlags();
};

template <typename T>
void BaseOptionManager::AddRequiredOption(const std::string& name,
                                          T* option,
                                          const std::string& help_text) {
  if constexpr (std::is_same<T, std::filesystem::path>::value) {
    // Boost program options does not support std::filesystem::path by default.
    // We treat it as a string and manualy convert it using a notifier.
    desc_->add_options()(
        name.c_str(),
        boost::program_options::value<std::string>()->required()->notifier(
            [option](const std::string& val) { *option = val; }),
        help_text.c_str());
  } else {
    desc_->add_options()(name.c_str(),
                         boost::program_options::value<T>(option)->required(),
                         help_text.c_str());
  }
  RegisterOption(name, option);
}

template <typename T>
void BaseOptionManager::AddDefaultOption(const std::string& name,
                                         T* option,
                                         const std::string& help_text) {
  if constexpr (std::is_floating_point<T>::value) {
    desc_->add_options()(
        name.c_str(),
        boost::program_options::value<T>(option)->default_value(
            *option, StringPrintf("%.3g", *option)),
        help_text.c_str());
  } else if constexpr (std::is_same<T, std::filesystem::path>::value) {
    // Boost program options does not support std::filesystem::path by default.
    // We treat it as a string and manualy convert it using a notifier.
    desc_->add_options()(
        name.c_str(),
        boost::program_options::value<std::string>()
            ->default_value(option->string())
            ->notifier([option](const std::string& val) { *option = val; }),
        help_text.c_str());
  } else {
    desc_->add_options()(
        name.c_str(),
        boost::program_options::value<T>(option)->default_value(*option),
        help_text.c_str());
  }
  RegisterOption(name, option);
}

template <typename T>
void BaseOptionManager::RegisterOption(const std::string& name,
                                       const T* option) {
  if constexpr (std::is_same<T, bool>::value) {
    options_bool_.emplace_back(name, reinterpret_cast<const bool*>(option));
  } else if constexpr (std::is_same<T, int>::value) {
    options_int_.emplace_back(name, reinterpret_cast<const int*>(option));
  } else if constexpr (std::is_same<T, double>::value) {
    options_double_.emplace_back(name, reinterpret_cast<const double*>(option));
  } else if constexpr (std::is_same<T, std::string>::value) {
    options_string_.emplace_back(name,
                                 reinterpret_cast<const std::string*>(option));
  } else if constexpr (std::is_same<T, std::filesystem::path>::value) {
    options_path_.emplace_back(
        name, reinterpret_cast<const std::filesystem::path*>(option));
  } else {
    static_assert(always_false<T>::value, "Unsupported option type");
  }
}

template <typename EnumT>
void BaseOptionManager::AddDefaultEnumOption(
    const std::string& name,
    EnumT* option,
    std::string_view (*to_string_fn)(EnumT),
    EnumT (*from_string_fn)(std::string_view),
    const std::string& help_text) {
  // Create storage for this enum option (unique_ptr for pointer stability)
  auto info = std::make_unique<EnumOptionInfo>();
  info->value = std::string(to_string_fn(*option));

  EnumOptionInfo* info_ptr = info.get();
  info->apply = [info_ptr, option, from_string_fn]() {
    *option = from_string_fn(info_ptr->value);
  };

  // Register as a string option pointing to our storage
  AddDefaultOption(name, &info->value, help_text);

  enum_options_.push_back(std::move(info));
}

}  // namespace colmap
