/* vim:set ts=2 sw=2 sts=2 et: */
/**
 * \author     Marcus Holland-Moritz (github@mhxnet.de)
 * \copyright  Copyright (c) Marcus Holland-Moritz
 *
 * This file is part of dwarfs.
 *
 * dwarfs is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * dwarfs is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with dwarfs.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#pragma once

#include <algorithm>
#include <iosfwd>
#include <optional>
#include <span>
#include <sstream>
#include <stdexcept>
#include <type_traits>
#include <unordered_map>

#include <fmt/format.h>

namespace dwarfs::writer {

enum class contextual_option_policy {
  normal,
  fallback,
};

template <typename OptionType, typename ContextParser, typename OptionParser>
class contextual_option_parser;

template <typename Policy>
class contextual_option {
 public:
  using policy_type = Policy;
  using context_argument_type = policy_type::ContextArgumentType;
  using context_type = policy_type::ContextType;
  using value_type = policy_type::ValueType;

  template <typename OptionType, typename ContextParser, typename OptionParser>
  friend class contextual_option_parser;

  contextual_option() = default;
  explicit contextual_option(value_type const& def)
      : default_{def} {}
  explicit contextual_option(value_type&& def)
      : default_{std::move(def)} {}

  void set_default(value_type const& val) { default_ = val; }
  void set_default(value_type&& val) { default_ = std::move(val); }

  bool add_contextual(
      context_type const& ctx, value_type const& val,
      contextual_option_policy policy = contextual_option_policy::normal) {
    return contextual_.emplace(ctx, val).second ||
           policy == contextual_option_policy::fallback;
  }

  std::optional<value_type>
  get_optional(context_argument_type const& arg) const {
    if constexpr (std::is_same_v<context_type, context_argument_type>) {
      return get_optional_impl(arg);
    } else {
      return get_optional_impl(policy_type::context_from_arg(arg));
    }
  }

  value_type get(context_argument_type const& arg) const {
    if constexpr (std::is_same_v<context_type, context_argument_type>) {
      return get_impl(arg);
    } else {
      return get_impl(policy_type::context_from_arg(arg));
    }
  }

  std::optional<value_type> get_optional() const { return default_; }

  value_type get() const { return default_.value(); }

  template <typename T>
  bool any_is(T&& pred) const {
    auto&& p = std::forward<T>(pred);
    return std::ranges::any_of(contextual_, p,
                               &decltype(contextual_)::value_type::second) ||
           (default_ && p(*default_));
  }

  template <typename T>
  void visit_contextual(T&& visitor) const {
    auto&& v = std::forward<T>(visitor);
    for (auto const& [ctx, val] : contextual_) {
      v(ctx, val);
    }
  }

 private:
  std::optional<value_type> get_optional_impl(context_type const& ctx) const {
    if (auto it = contextual_.find(ctx); it != contextual_.end()) {
      return it->second;
    }
    return default_;
  }

  value_type get_impl(context_type const& ctx) const {
    if (auto it = contextual_.find(ctx); it != contextual_.end()) {
      return it->second;
    }
    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
    return default_.value(); // throw if unset
  }

  std::optional<value_type> default_;
  std::unordered_map<context_type, value_type> contextual_;
};

template <typename OptionType, typename ContextParser, typename OptionParser>
class contextual_option_parser {
 public:
  using option_type = OptionType;
  using policy_type = option_type::policy_type;

  contextual_option_parser(std::string_view name, OptionType& opt,
                           ContextParser const& cp, OptionParser const& op)
      : opt_{opt}
      , cp_{cp}
      , op_{op}
      , name_{name} {}

  void parse(std::string_view arg, contextual_option_policy policy =
                                       contextual_option_policy::normal) const {
    try {
      auto pos = arg.find("::");

      if (pos == std::string_view::npos) {
        opt_.set_default(op_.parse(arg));
      } else {
        auto ctx = arg.substr(0, pos);
        auto val = op_.parse(arg.substr(pos + 2));
        for (auto c : cp_.parse(ctx)) {
          add_contextual(c, val, policy);
        }
      }
    } catch (std::exception const& e) {
      throw std::runtime_error(
          fmt::format("failed to parse value '{}' for option '{}': {}", arg,
                      name_, e.what()));
    }
  }

  void parse_fallback(std::string_view arg) const {
    parse(arg, contextual_option_policy::fallback);
  }

  void parse(std::span<std::string const> list) const {
    for (auto const& arg : list) {
      parse(arg);
    }
  }

  void parse(std::span<std::string_view const> list) const {
    for (auto const& arg : list) {
      parse(arg);
    }
  }

  void dump(std::ostream& os) const {
    os << "[" << name_ << "]\n";
    os << "  default: ";
    if (opt_.default_) {
      os << op_.to_string(*opt_.default_) << "\n";
    } else {
      os << "(no default set)\n";
    }
    for (auto const& [ctx, val] : opt_.contextual_) {
      os << "  [" << cp_.to_string(ctx) << "]: " << op_.to_string(val) << "\n";
    }
  }

  std::string as_string() const {
    std::ostringstream oss;
    dump(oss);
    return oss.str();
  }

  std::string const& name() const { return name_; }

  bool has_category_resolver() const { return cp_.has_resolver(); }

 private:
  void add_contextual(option_type::context_type const& ctx,
                      option_type::value_type const& val,
                      contextual_option_policy policy) const {
    if (!opt_.add_contextual(ctx, val, policy)) {
      throw std::runtime_error(fmt::format(
          "duplicate context '{}' for option '{}'", cp_.to_string(ctx), name_));
    }
  }

  OptionType& opt_;
  ContextParser const& cp_;
  OptionParser const& op_;
  std::string const name_;
};

} // namespace dwarfs::writer
