#pragma once

#include <clingo/core.hh>

#include <clingo/config.h>

namespace Clingo {

//! @addtogroup cpp_config
//! Configuration of search and enumeration algorithms.
//!
//! Entries in a configuration are organized hierarchically. Subentries are
//! either accessed by name for map entries or by offset for array entries.
//! Value entries have a string value that can be inspected or modified.
//!
//! @{

//! Enumeration of configuration types.
enum class ConfigType : clingo_config_type_bitset_t {
    //! The configuration entry is a double value.
    value = clingo_config_type_value,
    //! The configuration entry is a array of configurations.
    array = clingo_config_type_array,
    //! The configuration entry is a map of configurations.
    map = clingo_config_type_map,
};
//! Enable bitset operations for the configuration type.
CLINGO_ENABLE_BITSET_ENUM(ConfigType);

class ConfigArrayView;
class ConfigArray;
class ConfigMapView;
class ConfigMap;

//! Class modeling an immutable configuration entry.
class ConfigView {
  public:
    //! Construct from the underlying C API type and a key.
    //!
    //! @param stats the underlying C API type
    //! @param key the key of the configuration entry
    explicit ConfigView(clingo_config_t const *stats, ProgramId key) : cfg_{stats}, key_{key} {}

    //! Cast the configuration to the underlying C API type.
    //!
    //! @param stats the configuration to cast
    //! @return the underlying C API type
    friend auto c_cast(ConfigView const &stats) -> clingo_config_t const * { return stats.cfg_; }

    //! Get the type of the configuration entry.
    //!
    //! @return the type of the configuration entry
    [[nodiscard]] auto type() const -> ConfigType {
        return static_cast<ConfigType>(Detail::call<clingo_config_type>(cfg_, key_));
    }

    //! Access the configuration entry as an array.
    //!
    //! @return the configuration entry as an array
    [[nodiscard]] auto array() const -> ConfigArrayView;

    //! Get the configuration entry at the given index in the array.
    //!
    //! @param index the index of the configuration entry
    //! @return the configuration entry at the given index
    [[nodiscard]] auto at(size_t index) const -> ConfigView;

    //! @copydoc ConfigView::at()
    [[nodiscard]] auto operator[](size_t index) const -> ConfigView { return at(index); }

    //! Access the configuration entry as a map.
    //!
    //! @return The configuration entry as a map.
    [[nodiscard]] auto map() const -> ConfigMapView;

    //! Get the configuration entry with the given name in the map.
    //!
    //! @param name the name of the configuration entry in the map
    //! @return the configuration entry with the given name
    [[nodiscard]] auto get(std::string_view name) const -> ConfigView;

    //! @copydoc ConfigView::get()
    [[nodiscard]] auto operator[](std::string_view name) const -> ConfigView { return get(name); }

    //! Get the value of the configuration entry.
    //!
    //! The return might be empty if no value is assigned to the configuration
    //! entry.
    //!
    //! @return the value of the configuration entry
    [[nodiscard]] auto value() const -> std::optional<std::string_view> {
        if (intersects(type(), ConfigType::value)) {
            clingo_string_t value;
            bool assigned = false;
            Detail::handle_error(clingo_config_value_get(cfg_, key_, &value, &assigned));
            if (assigned) {
                return std::string_view{value.data, value.size};
            }
            return std::nullopt;
        }
        throw std::logic_error{"not a value"};
    }

    //! @copydoc ConfigView::value()
    [[nodiscard]] auto operator*() const -> std::optional<std::string_view> { return value(); }

    //! Get the description of the configuration entry.
    //!
    //! The description corresponds to the text that appears in the help output
    //! of clingo or derived systems.
    //!
    //! @return the description of the configuration entry
    [[nodiscard]] auto description() const -> std::string_view {
        auto [data, size] = Detail::call<clingo_config_description>(cfg_, key_);
        return {data, size};
    }

    //! Get a string representation of the configuration entry.
    //!
    //! This returns a string in a YAML-like format that can be used to inspect
    //! the configuration entry.
    //!
    //! @return a string representation of the configuration entry
    [[nodiscard]] auto to_string() const -> std::string {
        auto bld = StringBuilder{};
        Detail::handle_error(clingo_config_to_string(cfg_, key_, c_cast(bld)));
        return std::string{bld.str()};
    }

  private:
    friend class Config;

    clingo_config_t const *cfg_;
    clingo_id_t key_;
};

//! Class modeling a mutable configuration entry.
class Config : public ConfigView {
  public:
    //! Construct from the underlying C API type and a key.
    //!
    //! @param stats the underlying C API type
    //! @param key the key of the configuration entry
    explicit Config(clingo_config_t *stats, ProgramId key) : ConfigView{stats, key} {}

    //! Cast the configuration to the underlying C API type.
    //!
    //! @param stats the configuration to cast
    //! @return the underlying C API type
    friend auto c_cast(Config const &stats) -> clingo_config_t * { return stats.cfg_(); }

    //! @copydoc ConfigView::array()
    [[nodiscard]] auto array() const -> ConfigArray;
    //! @copydoc ConfigView::at()
    [[nodiscard]] auto at(size_t index) const -> Config;
    //! @copydoc ConfigView::at()
    [[nodiscard]] auto operator[](size_t index) const -> Config { return at(index); }
    //! @copydoc ConfigView::map()
    [[nodiscard]] auto map() const -> ConfigMap;
    //! @copydoc ConfigView::get()
    [[nodiscard]] auto get(std::string_view name) const -> Config;
    //! @copydoc ConfigView::get()
    [[nodiscard]] auto operator[](std::string_view name) const -> Config { return get(name); }
    using ConfigView::value;
    //! Set the value of the configuration entry.
    //!
    //! @param value the new value of the configuration entry
    void value(std::string_view value) const {
        if (intersects(type(), ConfigType::value)) {
            Detail::handle_error(clingo_config_value_set(cfg_(), key_, value.data(), value.size()));
        } else {
            throw std::logic_error{"not a value"};
        }
    }
    //! @copydoc Config::value()
    //! @return the configuration entry itself
    auto operator=(std::string_view value) const -> Config { // NOLINT
        this->value(value);
        return *this;
    }

    //! Add a new configuration entry with a custom handler.
    //!
    //! Adds a configuration entry with the given name and description, and
    //! binds it to a handler. The handler can provide get/set logic for the
    //! entry.
    //!
    //! Handler can implement:
    //! - get() -> std::string
    //! - get(std::optional<size_t>) -> std::string
    //! - set(std::string_view) -> void
    //! - set(std::optional<size_t>, std::string_view) -> void
    //! - size() -> size_t
    //!
    //! Entries that have a value should implement get and/or set; entries with
    //! values under an array must implement get/set with an index. Array
    //! entries must implement size.
    //!
    //! @note It is up to the user to handle array insertion. Possible options
    //! include increasing the size of an array upon assignment of values or by
    //! setting a special size field that controls the size of the array.
    //!
    //! @tparam Handler type of the handler
    //! @param name entry name
    //! @param description entry description
    //! @param handler handler object for custom get/set logic
    template <class Handler> void add(std::string_view name, std::string_view description, Handler &&handler) const {
        using HandlerType = std::remove_cvref_t<Handler>;

        static constexpr auto has_get = requires { handler.get(); };
        static constexpr auto has_array_get = requires { handler.get(std::optional<size_t>{}); };
        static constexpr auto has_set = requires { std::declval<HandlerType>().set(std::string_view{}); };
        static constexpr auto has_array_set =
            requires { std::declval<HandlerType>().set(std::optional<size_t>{}, std::string_view{}); };
        static constexpr auto is_array = requires { handler.size(); };

        static_assert(!has_get || !has_array_get, "entry cannot have both get with and without index");
        static_assert(!has_set || !has_array_set, "entry cannot have both set with and without index");

        static constexpr auto c_entry = clingo_config_entry_t{
            has_get || has_array_get ? +[](size_t const *index, void *data, clingo_string_t *value, bool *has_value) -> bool {
                CLINGO_TRY {
                    auto *self = static_cast<HandlerType *>(data);
                    thread_local std::optional<std::string> str;
                    if constexpr (has_array_get) {
                        str = self->get(index != nullptr ? std::optional{*index} : std::nullopt);
                    }
                    if constexpr (has_get) {
                        if (index != nullptr) {
                            throw std::logic_error{"cannot get array value"};
                        }
                        str = self->get();
                    }
                    if (str.has_value() && value != nullptr) {
                        value->data = str->data();
                        value->size = str->size();
                    }
                    if (has_value != nullptr) {
                        *has_value = str.has_value();
                    }
                }
                CLINGO_CATCH;
            } : nullptr,
            has_set || has_array_set ? +[](size_t const *index, char const *value, size_t size, void *data) -> bool {
                CLINGO_TRY {
                    auto *self = static_cast<HandlerType *>(data);
                    if constexpr (has_array_set) {
                        self->set(index != nullptr ? std::optional{*index} : std::nullopt,
                                  std::string_view{value, size});
                    }
                    if constexpr (has_set) {
                        if (index != nullptr) {
                            throw std::logic_error{"cannot set array value"};
                        }
                        self->set(std::string_view{value, size});
                    }
                }
                CLINGO_CATCH;
            } : nullptr,
            is_array ? +[](void *data, size_t *size, bool *has_size) -> bool {
                CLINGO_TRY {
                    auto *self = static_cast<HandlerType *>(data);
                    if constexpr (is_array) {
                        if (size != nullptr) {
                            *size = self->size();
                        }
                    }
                    if (has_size != nullptr) {
                        *has_size = is_array;
                    }
                }
                CLINGO_CATCH;
            } : nullptr,
            [](void *data) { std::unique_ptr<HandlerType>{static_cast<HandlerType *>(data)}; },
        };

        auto copy = std::make_unique<HandlerType>(std::forward<Handler>(handler));
        Detail::handle_error(clingo_config_add(cfg_(), key_, name.data(), name.size(), description.data(),
                                               description.size(), &c_entry, copy.release()));
    }

    //! Add a new configuration entry.
    //!
    //! Adds a configuration entry with the given name and description.
    //! The entry is just for organizational purposes and has no value.
    //!
    //! @param name entry name
    //! @param description entry description
    void add(std::string_view name, std::string_view description) const {
        Detail::handle_error(clingo_config_add(cfg_(), key_, name.data(), name.size(), description.data(),
                                               description.size(), nullptr, nullptr));
    }

  private:
    [[nodiscard]] auto cfg_() const -> clingo_config_t * {
        // NOLINTNEXTLINE
        return const_cast<clingo_config_t *>(ConfigView::cfg_);
    }
};

//! Class modeling an immutable array of configuration entries.
class ConfigArrayView {
  public:
    //! The value type.
    using value_type = ConfigView;
    //! The size type.
    using size_type = std::size_t;
    //! The difference type.
    using difference_type = std::ptrdiff_t;
    //! The reference type.
    using reference = value_type;
    //! The pointer type.
    using pointer = Detail::ArrowProxy<value_type>;
    //! The iterator type.
    using iterator = Detail::RandomAccessIterator<ConfigArrayView>;

    //! Construct from the underlying C API type and a key.
    //!
    //! @param stats the underlying C API type
    //! @param key the key of the configuration entry
    explicit ConfigArrayView(clingo_config_t const *stats, ProgramId key) : cfg_{stats}, key_{key} {}

    //! @copydoc ConfigView::at()
    [[nodiscard]] auto at(size_t index) const -> ConfigView { return ConfigView{cfg_, at_(index)}; }
    //! @copydoc ConfigView::at()
    [[nodiscard]] auto operator[](size_t index) const -> ConfigView { return at(index); }
    //! Get the size of the array.
    //!
    //! @return the size of the array
    [[nodiscard]] auto size() const -> size_t { return Detail::call<clingo_config_array_size>(cfg_, key_); }
    //! Get an iterator to the beginning of the array.
    //!
    //! @return an iterator to the beginning of the array
    [[nodiscard]] auto begin() const -> iterator { return iterator{*this, 0}; }
    //! Get an iterator to the end of the array.
    //!
    //! @return an iterator to the end of the array
    [[nodiscard]] auto end() const -> iterator { return iterator{*this, size()}; }

  private:
    friend class ConfigArray;
    [[nodiscard]] auto at_(size_t index) const -> clingo_id_t {
        return Detail::call<clingo_config_array_at>(cfg_, key_, index);
    }

    clingo_config_t const *cfg_;
    clingo_id_t key_;
};

inline auto ConfigView::array() const -> ConfigArrayView {
    if (intersects(type(), ConfigType::array)) {
        return ConfigArrayView{cfg_, key_};
    }
    throw std::logic_error{"not an array"};
}

inline auto ConfigView::at(size_t index) const -> ConfigView {
    return array().at(index);
}

//! Class modeling a mutable array of configuration entries.
class ConfigArray : public ConfigArrayView {
  public:
    //! The value type.
    using value_type = Config;
    //! The size type.
    using size_type = std::size_t;
    //! The difference type.
    using difference_type = std::ptrdiff_t;
    //! The reference type.
    using reference = value_type;
    //! The pointer type.
    using pointer = Detail::ArrowProxy<value_type>;
    //! The iterator type.
    using iterator = Detail::RandomAccessIterator<ConfigArray>;

    //! @copydoc ConfigArrayView::ConfigArrayView()
    explicit ConfigArray(clingo_config_t *stats, ProgramId key) : ConfigArrayView{stats, key} {}

    //! @copydoc ConfigArrayView::at()
    [[nodiscard]] auto at(size_t index) const -> Config { return Config{cfg_(), at_(index)}; }
    //! @copydoc ConfigArrayView::at()
    [[nodiscard]] auto operator[](size_t index) const -> Config { return at(index); }
    //! @copydoc ConfigArrayView::begin()
    [[nodiscard]] auto begin() const -> iterator { return iterator{*this, 0}; }
    //! @copydoc ConfigArrayView::begin()
    [[nodiscard]] auto end() const -> iterator { return iterator{*this, size()}; }

  private:
    [[nodiscard]] auto cfg_() const -> clingo_config_t * {
        // NOLINTNEXTLINE
        return const_cast<clingo_config_t *>(ConfigArrayView::cfg_);
    }
};

[[nodiscard]] inline auto Config::array() const -> ConfigArray {
    if (intersects(type(), ConfigType::array)) {
        return ConfigArray{cfg_(), key_};
    }
    throw std::logic_error{"not an array"};
}

inline auto Config::at(size_t index) const -> Config {
    return array().at(index);
}

//! Class modeling an immutable map of configuration entries.
class ConfigMapView {
  public:
    //! The key type.
    using key_type = std::string_view;
    //! The mapped type.
    using mapped_type = ConfigView;
    //! The value type.
    using value_type = std::pair<key_type, mapped_type>;
    //! The size type.
    using size_type = std::size_t;
    //! The difference type.
    using difference_type = std::ptrdiff_t;
    //! The reference type.
    using reference = value_type;
    //! The pointer type.
    using pointer = Detail::ArrowProxy<value_type>;
    //! The iterator type.
    using iterator = Detail::RandomAccessIterator<ConfigMapView>;

    //! Construct from the underlying C API type and a key.
    //!
    //! @param stats the underlying C API type
    //! @param key the key of the configuration entry
    explicit ConfigMapView(clingo_config_t const *stats, ProgramId key) : cfg_{stats}, key_{key} {}

    //! Get the size of the map.
    //!
    //! @return the size of the map
    [[nodiscard]] auto size() const -> size_t { return Detail::call<clingo_config_map_size>(cfg_, key_); }

    //! Get the name configuration entry pair at the given index in the map.
    //!
    //! @param index the index of the configuration entry
    //! @return the name configuration entry pair at the given index
    [[nodiscard]] auto at(size_t index) const -> value_type {
        auto [name, subkey] = at_(index);
        return {name, ConfigView{cfg_, subkey}};
    }

    //! Get the configuration entry with the given name in the map.
    //!
    //! @param name the name of the configuration entry
    //! @return the configuration entry with the given name
    [[nodiscard]] auto get(std::string_view name) const -> ConfigView { return ConfigView{cfg_, at_(name)}; }

    //! @copydoc ConfigMapView::get()
    [[nodiscard]] auto operator[](std::string_view name) const -> ConfigView { return get(name); }

    //! Check if the map contains a configuration entry with the given name.
    //!
    //! @param name the name of the configuration entry
    //! @return true if the map contains an entry with the given name, false otherwise
    [[nodiscard]] auto contains(std::string_view name) const -> bool {
        return Detail::call<clingo_config_map_at>(cfg_, key_, name.data(), name.size(), nullptr);
    }

    //! Get an iterator to the beginning of the map.
    //!
    //! @return an iterator to the beginning of the map
    [[nodiscard]] auto begin() const -> iterator { return iterator{*this, 0}; }

    //! Get an iterator to the end of the map.
    //!
    //! @return an iterator to the end of the map
    [[nodiscard]] auto end() const -> iterator { return iterator{*this, size()}; }

  private:
    friend class ConfigMap;

    [[nodiscard]] auto at_(std::string_view name) const -> clingo_id_t {
        bool has_subkey = false;
        clingo_id_t subkey = 0;
        Detail::handle_error(clingo_config_map_at(cfg_, key_, name.data(), name.size(), &subkey, &has_subkey));
        if (!has_subkey) {
            throw std::out_of_range{"subkey not found"};
        }
        return subkey;
    }

    [[nodiscard]] auto at_(size_t index) const -> std::pair<std::string_view, clingo_id_t> {
        auto [data, size] = Detail::call<clingo_config_map_subkey_name>(cfg_, key_, index);
        auto str = std::string_view{data, size};
        return {str, at_(str)};
    }

    clingo_config_t const *cfg_;
    clingo_id_t key_;
};

inline auto ConfigView::map() const -> ConfigMapView {
    if (intersects(type(), ConfigType::map)) {
        return ConfigMapView{cfg_, key_};
    }
    throw std::logic_error{"not a map"};
}

inline auto ConfigView::get(std::string_view name) const -> ConfigView {
    return map().get(name);
}

//! Class modeling a mutable map of configuration entries.
class ConfigMap : public ConfigMapView {
  public:
    //! The key type.
    using key_type = std::string_view;
    //! The mapped type.
    using mapped_type = Config;
    //! The value type.
    using value_type = std::pair<key_type, mapped_type>;
    //! The size type.
    using size_type = std::size_t;
    //! The difference type.
    using difference_type = std::ptrdiff_t;
    //! The reference type.
    using reference = value_type;
    //! The pointer type.
    using pointer = Detail::ArrowProxy<value_type>;
    //! The iterator type.
    using iterator = Detail::RandomAccessIterator<ConfigMap>;

    //! @copydoc ConfigMapView::ConfigMapView()
    explicit ConfigMap(clingo_config_t *stats, ProgramId key) : ConfigMapView{stats, key} {}

    //! @copydoc ConfigMapView::at()
    [[nodiscard]] auto at(size_t index) const -> value_type {
        auto [name, subkey] = at_(index);
        return {name, Config{cfg_(), subkey}};
    }
    //! @copydoc ConfigMapView::get()
    [[nodiscard]] auto get(std::string_view name) const -> Config { return Config{cfg_(), at_(name)}; }
    //! @copydoc ConfigMapView::get()
    [[nodiscard]] auto operator[](std::string_view name) const -> Config { return get(name); }
    //! @copydoc ConfigMapView::begin()
    [[nodiscard]] auto begin() const -> iterator { return iterator{*this, 0}; }
    //! @copydoc ConfigMapView::end()
    [[nodiscard]] auto end() const -> iterator { return iterator{*this, size()}; }

  private:
    [[nodiscard]] auto cfg_() const -> clingo_config_t * {
        // NOLINTNEXTLINE
        return const_cast<clingo_config_t *>(ConfigMapView::cfg_);
    }
};

inline auto Config::map() const -> ConfigMap {
    if (intersects(type(), ConfigType::map)) {
        return ConfigMap{cfg_(), key_};
    }
    throw std::logic_error{"not a map"};
}

inline auto Config::get(std::string_view name) const -> Config {
    return map().get(name);
}

//! @}

} // namespace Clingo
