#pragma once

#include <matjson.hpp>
#include "../loader/Log.hpp"
#include <set>
#include <variant>
#include <Geode/utils/Result.hpp>

namespace geode {
    struct JsonChecker;

    template <typename T, typename = void>
    struct is_iterable : std::false_type {};

    template <typename T>
    struct is_iterable<
        T, std::void_t<decltype(std::begin(std::declval<T>())), decltype(std::end(std::declval<T>()))>> :
        std::true_type {};

    template <typename T>
    constexpr bool is_iterable_v = is_iterable<T>::value;

    namespace {
        using value_t = matjson::Type;

        constexpr char const* jsonValueTypeToString(value_t type) {
            switch (type) {
                default:
                case value_t::Null: return "null";
                case value_t::Object: return "object";
                case value_t::Array: return "array";
                case value_t::String: return "string";
                case value_t::Bool: return "boolean";
                case value_t::Number: return "number";
            }
        }

        template <class T>
        constexpr value_t getJsonType() {
            if constexpr (std::is_same_v<T, bool>) {
                return value_t::Bool;
            }
            else if constexpr (std::is_floating_point_v<T>) {
                return value_t::Number;
            }
            else if constexpr (std::is_unsigned_v<T>) {
                return value_t::Number;
            }
            else if constexpr (std::is_integral_v<T>) {
                return value_t::Number;
            }
            else if constexpr (std::is_constructible_v<T, std::string>) {
                return value_t::String;
            }
            else if constexpr (is_iterable_v<T>) {
                return value_t::Array;
            }
            return value_t::Null;
        }

        bool jsonConvertibleTo(value_t value, value_t to) {
            // if we don't know the type we're passing into,
            // everything's valid
            if (to == value_t::Null) return true;

            if (value == value_t::Number) {
                return to == value_t::Number;
            }

            return value == to;
        }
    }

    template <class T>
    using JsonValueValidator = std::function<bool(T const&)>;

    struct JsonMaybeObject;
    struct JsonMaybeValue;

    class GEODE_DLL JsonExpectedValue final {
    protected:
        class Impl;
        std::unique_ptr<Impl> m_impl;

        JsonExpectedValue();
        JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key);

        static const char* matJsonTypeToString(matjson::Type ty);

        bool hasError() const;
        void setError(std::string_view error);

        matjson::Value const& getJSONRef() const;

        template <class... Args>
        void setError(fmt::format_string<Args...> error, Args&&... args) {
            this->setError(fmt::format(error, std::forward<Args>(args)...));
        }

        template <class T>
        std::optional<T> tryGet() {
            if (this->hasError()) return std::nullopt;
            if constexpr (std::is_same_v<T, matjson::Value>) {
                return this->getJSONRef();
            }
            else {
                try {
                    if (this->getJSONRef().is<T>()) {
                        return this->getJSONRef().as<T>();
                    }
                    else {
                        this->setError(
                            "unexpected type {}",
                            this->matJsonTypeToString(this->getJSONRef().type())
                        );
                    }
                }
                // matjson can throw variant exceptions too so you need to do this
                catch(std::exception const& e) {
                    this->setError("unable to parse json: {}", e);
                }
            }
            return std::nullopt;
        }

    public:
        JsonExpectedValue(matjson::Value const& value, std::string_view rootScopeName);
        ~JsonExpectedValue();

        JsonExpectedValue(JsonExpectedValue&&);
        JsonExpectedValue& operator=(JsonExpectedValue&&);
        JsonExpectedValue(JsonExpectedValue const&) = delete;
        JsonExpectedValue& operator=(JsonExpectedValue const&) = delete;

        /**
         * Get a copy of the underlying raw JSON value
         */
        matjson::Value json() const;
        /**
         * Get the key name of this JSON value. If this is an array index, 
         * returns the index as a string. If this is the root object, 
         * returns the root scope name. 
         */
        std::string key() const;

        /**
         * Check the type of this JSON value. Does not set an error. If an 
         * error is already set, always returns false
         */
        bool is(matjson::Type type) const;
        bool isNull() const;
        bool isBool() const;
        bool isNumber() const;
        bool isString() const;
        bool isArray() const;
        bool isObject() const;
        /**
         * Asserts that this JSON value is of the specified type. If it is 
         * not, an error is set and all subsequent operations are no-ops
         * @returns Itself
         */
        JsonExpectedValue& assertIs(matjson::Type type);
        JsonExpectedValue& assertIsNull();
        JsonExpectedValue& assertIsBool();
        JsonExpectedValue& assertIsNumber();
        JsonExpectedValue& assertIsString();
        JsonExpectedValue& assertIsArray();
        JsonExpectedValue& assertIsObject();
        /**
         * Asserts that this JSON value is one of a list of specified types
         * @returns Itself
         */
        JsonExpectedValue& assertIs(std::initializer_list<matjson::Type> type);

        // -- Dealing with values --

        template <class T>
        T get(T const& defaultValue = T()) {
            if (auto v = this->tryGet<T>()) {
                return *std::move(v);
            }
            return defaultValue;
        }
        template <class T>
        JsonExpectedValue& into(T& value) {
            if (auto v = this->tryGet<T>()) {
                value = *std::move(v);
            }
            return *this;
        }
        template <class T>
        JsonExpectedValue& into(std::optional<T>& value) {
            if (auto v = this->tryGet<T>()) {
                value.emplace(*std::move(v));
            }
            return *this;
        }
        template <class T>
        JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
            { predicate(std::declval<T>()) } -> std::convertible_to<bool>;
        } {
            if (this->hasError()) return *this;
            if (auto v = this->tryGet<T>()) {
                if (!predicate(*v)) {
                    this->setError("json value is not {}", name);
                }
            }
            return *this;
        }
        template <class T>
        JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
            { predicate(std::declval<T>()) } -> std::convertible_to<Result<>>;
        } {
            if (this->hasError()) return *this;
            if (auto v = this->tryGet<T>()) {
                auto p = predicate(*v);
                if (!p) {
                    this->setError("json value is not {}: {}", name, p.unwrapErr());
                }
            }
            return *this;
        }

        // -- Dealing with objects --

        /**
         * Check if this object has an optional key. Asserts that this JSON 
         * value is an object. If the key doesn't exist, returns a 
         * `JsonExpectValue` that does nothing
         * @returns The key, which is a no-op value if it didn't exist
         */
        JsonExpectedValue has(std::string_view key);
        /**
         * Check if this object has an optional key. Asserts that this JSON 
         * value is an object. If the key doesn't exist, sets an error and 
         * returns a `JsonExpectValue` that does nothing
         * @returns The key, which is a no-op value if it didn't exist
         */
        JsonExpectedValue needs(std::string_view key);
        /**
         * Asserts that this JSON value is an object. Get all object 
         * properties
         */
        std::vector<std::pair<std::string, JsonExpectedValue>> properties();
        /**
         * Asserts that this JSON value is an object. Logs unknown keys to 
         * the console as warnings
         */
        void checkUnknownKeys();

        // -- Dealing with arrays --

        /**
         * Asserts that this JSON value is an array. Returns the length of 
         * the array, or 0 on error
         */
        size_t length();
        /**
         * Asserts that this JSON value is an array. Returns the value at 
         * the specified index. If there is no value at that index, sets an 
         * error
         */
        JsonExpectedValue at(size_t index);
        /**
         * Asserts that this JSON value is an array. Returns the array items
         * @warning The old JsonChecker used `items` for iterating object 
         * properties - on this new API that function is called `properties`!
         */
        std::vector<JsonExpectedValue> items();

        operator bool() const;

        Result<> ok();
        template <class T>
        Result<T> ok(T value) {
            auto ok = this->ok();
            if (!ok) {
                return Err(ok.unwrapErr());
            }
            return Ok(std::forward<T>(value));
        }
    };
    GEODE_DLL JsonExpectedValue checkJson(matjson::Value const& json, std::string_view rootScopeName);
}