/**
 * Copyright (c) 2018, Timothy Stack
 *
 * 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 Timothy Stack 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 REGENTS 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 REGENTS 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.
 *
 * @file yajlpp_def.hh
 */

#ifndef yajlpp_def_hh
#define yajlpp_def_hh

#include <chrono>

#include "base/date_time_scanner.hh"
#include "base/time_util.hh"
#include "config.h"
#include "mapbox/variant.hpp"
#include "relative_time.hh"
#include "yajlpp.hh"

struct json_null_t {
    bool operator==(const json_null_t& other) const { return true; }
};

using json_any_t
    = mapbox::util::variant<json_null_t, bool, int64_t, double, std::string>;

struct json_path_container;

struct json_path_handler : public json_path_handler_base {
    template<typename P>
    json_path_handler(P path, int (*null_func)(yajlpp_parse_context*))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_null = (int (*)(void*)) null_func;
    }

    template<typename P>
    json_path_handler(P path, int (*bool_func)(yajlpp_parse_context*, int))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_boolean = (int (*)(void*, int)) bool_func;
    }

    template<typename P>
    json_path_handler(P path, int (*int_func)(yajlpp_parse_context*, long long))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_integer = (int (*)(void*, long long)) int_func;
    }

    template<typename P>
    json_path_handler(P path, int (*double_func)(yajlpp_parse_context*, double))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_double = (int (*)(void*, double)) double_func;
    }

    template<typename P>
    json_path_handler(P path,
                      int (*number_func)(yajlpp_parse_context*,
                                         const char* numberVal,
                                         size_t numberLen))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_number
            = (int (*)(void*, const char*, size_t)) number_func;
    }

    template<typename P>
    json_path_handler(P path) : json_path_handler_base(path)
    {
    }

    template<typename P>
    json_path_handler(P path,
                      int (*str_func)(yajlpp_parse_context*,
                                      const unsigned char*,
                                      size_t))
        : json_path_handler_base(path)
    {
        this->jph_callbacks.yajl_string
            = (int (*)(void*, const unsigned char*, size_t)) str_func;
    }

    json_path_handler(const std::string& path,
                      const std::shared_ptr<const lnav::pcre2pp::code>& re)
        : json_path_handler_base(path, re)
    {
    }

    json_path_handler& add_cb(int (*null_func)(yajlpp_parse_context*))
    {
        this->jph_callbacks.yajl_null = (int (*)(void*)) null_func;
        return *this;
    }

    json_path_handler& add_cb(int (*bool_func)(yajlpp_parse_context*, int))
    {
        this->jph_callbacks.yajl_boolean = (int (*)(void*, int)) bool_func;
        return *this;
    }

    json_path_handler& add_cb(int (*int_func)(yajlpp_parse_context*, long long))
    {
        this->jph_callbacks.yajl_integer = (int (*)(void*, long long)) int_func;
        return *this;
    }

    json_path_handler& add_cb(int (*double_func)(yajlpp_parse_context*, double))
    {
        this->jph_callbacks.yajl_double = (int (*)(void*, double)) double_func;
        return *this;
    }

    json_path_handler& add_cb(int (*number_func)(yajlpp_parse_context*,
                                                 const char*,
                                                 size_t))
    {
        this->jph_callbacks.yajl_number
            = (int (*)(void*, const char*, size_t)) number_func;
        return *this;
    }

    json_path_handler& add_cb(int (*str_func)(yajlpp_parse_context*,
                                              const unsigned char*,
                                              size_t))
    {
        this->jph_callbacks.yajl_string
            = (int (*)(void*, const unsigned char*, size_t)) str_func;
        return *this;
    }

    json_path_handler& with_synopsis(const char* synopsis)
    {
        this->jph_synopsis = synopsis;
        return *this;
    }

    json_path_handler& with_description(const char* description)
    {
        this->jph_description = description;
        return *this;
    }

    json_path_handler& with_min_length(size_t len)
    {
        this->jph_min_length = len;
        return *this;
    }

    json_path_handler& with_max_length(size_t len)
    {
        this->jph_max_length = len;
        return *this;
    }

    json_path_handler& with_enum_values(const enum_value_t values[])
    {
        this->jph_enum_values = values;
        return *this;
    }

    template<typename T, std::size_t N>
    json_path_handler& with_pattern(const T (&re)[N])
    {
        this->jph_pattern_re = re;
        this->jph_pattern = lnav::pcre2pp::code::from_const(re).to_shared();
        return *this;
    }

    json_path_handler& with_min_value(long long val)
    {
        this->jph_min_value = val;
        return *this;
    }

    template<typename R, typename T>
    json_path_handler& with_obj_provider(
        R* (*provider)(const yajlpp_provider_context& pc, T* root))
    {
        this->jph_obj_provider
            = [provider](const yajlpp_provider_context& ypc, void* root) {
                  return (R*) provider(ypc, (T*) root);
              };
        return *this;
    }

    template<typename R>
    json_path_handler& with_size_provider(size_t (*provider)(const R* root))
    {
        this->jph_size_provider
            = [provider](const void* root) { return provider((R*) root); };
        return *this;
    }

    template<typename T>
    json_path_handler& with_path_provider(
        void (*provider)(T* root, std::vector<std::string>& paths_out))
    {
        this->jph_path_provider
            = [provider](void* root, std::vector<std::string>& paths_out) {
                  provider((T*) root, paths_out);
              };
        return *this;
    }

    template<typename T>
    json_path_handler& with_obj_deleter(
        void (*provider)(const yajlpp_provider_context& pc, T* root))
    {
        this->jph_obj_deleter
            = [provider](const yajlpp_provider_context& ypc, void* root) {
                  provider(ypc, (T*) root);
              };
        return *this;
    }

    static int null_field_cb(yajlpp_parse_context* ypc)
    {
        return ypc->ypc_current_handler->jph_null_cb(ypc);
    }

    static int bool_field_cb(yajlpp_parse_context* ypc, int val)
    {
        return ypc->ypc_current_handler->jph_bool_cb(ypc, val);
    }

    static int str_field_cb2(yajlpp_parse_context* ypc,
                             const unsigned char* str,
                             size_t len)
    {
        return ypc->ypc_current_handler->jph_str_cb(ypc, str, len);
    }

    static int int_field_cb(yajlpp_parse_context* ypc, long long val)
    {
        return ypc->ypc_current_handler->jph_integer_cb(ypc, val);
    }

    static int dbl_field_cb(yajlpp_parse_context* ypc, double val)
    {
        return ypc->ypc_current_handler->jph_double_cb(ypc, val);
    }

    template<typename T, typename U>
    static inline U& get_field(T& input, U(T::*field))
    {
        return input.*field;
    }

    template<typename T, typename U, typename... V>
    static inline auto get_field(T& input, U(T::*field), V... args)
        -> decltype(get_field(input.*field, args...))
    {
        return get_field(input.*field, args...);
    }

    template<typename T, typename U, typename... V>
    static inline auto get_field(void* input, U(T::*field), V... args)
        -> decltype(get_field(*((T*) input), field, args...))
    {
        return get_field(*((T*) input), field, args...);
    }

    template<typename R, typename T, typename... Args>
    struct LastIs {
        static constexpr bool value = LastIs<R, Args...>::value;
    };

    template<typename R, typename T>
    struct LastIs<R, T> {
        static constexpr bool value = false;
    };

    template<typename R, typename T>
    struct LastIs<R, R T::*> {
        static constexpr bool value = true;
    };

    template<typename T, typename... Args>
    struct LastIsEnum {
        using value_type = typename LastIsEnum<Args...>::value_type;
        static constexpr bool value = LastIsEnum<Args...>::value;
    };

    template<typename T, typename U>
    struct LastIsEnum<U T::*> {
        using value_type = U;

        static constexpr bool value = std::is_enum<U>::value;
    };

    template<typename T, typename U>
    struct LastIsEnum<nonstd::optional<U> T::*> {
        using value_type = U;

        static constexpr bool value = std::is_enum<U>::value;
    };

    template<typename T, typename... Args>
    struct LastIsInteger {
        static constexpr bool value = LastIsInteger<Args...>::value;
    };

    template<typename T, typename U>
    struct LastIsInteger<U T::*> {
        static constexpr bool value
            = std::is_integral<U>::value && !std::is_same<U, bool>::value;
    };

    template<typename T, typename U>
    struct LastIsInteger<nonstd::optional<U> T::*> {
        static constexpr bool value
            = std::is_integral<U>::value && !std::is_same<U, bool>::value;
    };

    template<typename T, typename... Args>
    struct LastIsFloat {
        static constexpr bool value = LastIsFloat<Args...>::value;
    };

    template<typename T, typename U>
    struct LastIsFloat<U T::*> {
        static constexpr bool value = std::is_same<U, double>::value;
    };

    template<typename T, typename U>
    struct LastIsFloat<nonstd::optional<U> T::*> {
        static constexpr bool value = std::is_same<U, double>::value;
    };

    template<typename T, typename... Args>
    struct LastIsVector {
        using value_type = typename LastIsVector<Args...>::value_type;
        static constexpr bool value = LastIsVector<Args...>::value;
    };

    template<typename T, typename U>
    struct LastIsVector<std::vector<U> T::*> {
        using value_type = U;
        static constexpr bool value = true;
    };

    template<typename T, typename U>
    struct LastIsVector<U T::*> {
        using value_type = void;
        static constexpr bool value = false;
    };

    template<typename T>
    static bool is_field_set(const nonstd::optional<T>& field)
    {
        return field.has_value();
    }

    template<typename T>
    static bool is_field_set(const T&)
    {
        return true;
    }

    template<typename... Args,
             std::enable_if_t<LastIs<bool, Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(bool_field_cb);
        this->jph_bool_cb = [args...](yajlpp_parse_context* ypc, int val) {
            auto* obj = ypc->ypc_obj_stack.top();

            json_path_handler::get_field(obj, args...) = static_cast<bool>(val);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field);
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };

        return *this;
    }

    template<
        typename... Args,
        std::enable_if_t<LastIs<std::vector<std::string>, Args...>::value, bool>
        = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            json_path_handler::get_field(obj, args...)
                .emplace_back(std::move(value_str));

            return 1;
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIsVector<Args...>::value, bool> = true,
             std::enable_if_t<
                 !std::is_same<typename LastIsVector<Args...>::value_type,
                               std::string>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->jph_obj_provider
            = [args...](const yajlpp_provider_context& ypc, void* root) {
                  auto& vec = json_path_handler::get_field(root, args...);

                  if (ypc.ypc_index >= vec.size()) {
                      vec.resize(ypc.ypc_index + 1);
                  }

                  return &vec[ypc.ypc_index];
              };
        this->jph_size_provider = [args...](void* root) {
            auto& vec = json_path_handler::get_field(root, args...);

            return vec.size();
        };

        return *this;
    }

    template<typename T, typename U>
    json_path_handler& for_child(positioned_property<U>(T::*field))
    {
        this->jph_obj_provider
            = [field](const yajlpp_provider_context& ypc, void* root) -> void* {
            auto& child = json_path_handler::get_field(root, field);

            if (ypc.ypc_parse_context != nullptr && child.pp_path.empty()) {
                child.pp_path = ypc.ypc_parse_context->get_full_path();
            }
            return &child.pp_value;
        };

        return *this;
    }

    template<typename... Args>
    json_path_handler& for_child(Args... args)
    {
        this->jph_obj_provider = [args...](const yajlpp_provider_context& ypc,
                                           void* root) -> void* {
            auto& child = json_path_handler::get_field(root, args...);

            return &child;
        };

        return *this;
    }

    template<typename... Args,
             std::enable_if_t<
                 LastIs<std::map<std::string, std::string>, Args...>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto key = ypc->get_path_fragment(-1);

            json_path_handler::get_field(obj, args...)[key]
                = std::string((const char*) str, len);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            {
                yajlpp_generator gen(handle);

                for (const auto& pair : field) {
                    gen(pair.first);
                    gen(pair.second);
                }
            }

            return yajl_gen_status_ok;
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<
                 LastIs<std::map<std::string, nonstd::optional<std::string>>,
                        Args...>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto key = ypc->get_path_fragment(-1);

            json_path_handler::get_field(obj, args...)[key]
                = std::string((const char*) str, len);

            return 1;
        };
        this->add_cb(null_field_cb);
        this->jph_null_cb = [args...](yajlpp_parse_context* ypc) {
            auto* obj = ypc->ypc_obj_stack.top();
            auto key = ypc->get_path_fragment(-1);

            json_path_handler::get_field(obj, args...)[key] = nonstd::nullopt;

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            {
                yajlpp_generator gen(handle);

                for (const auto& pair : field) {
                    gen(pair.first);
                    gen(pair.second);
                }
            }

            return yajl_gen_status_ok;
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<
                 LastIs<std::map<std::string, json_any_t>, Args...>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(bool_field_cb);
        this->jph_bool_cb = [args...](yajlpp_parse_context* ypc, int val) {
            auto* obj = ypc->ypc_obj_stack.top();
            auto key = ypc->get_path_fragment(-1);

            json_path_handler::get_field(obj, args...)[key] = val ? true
                                                                  : false;

            return 1;
        };
        this->add_cb(int_field_cb);
        this->jph_integer_cb
            = [args...](yajlpp_parse_context* ypc, long long val) {
                  auto* obj = ypc->ypc_obj_stack.top();
                  auto key = ypc->get_path_fragment(-1);

                  json_path_handler::get_field(obj, args...)[key]
                      = static_cast<int64_t>(val);

                  return 1;
              };
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto* obj = ypc->ypc_obj_stack.top();
            auto key = ypc->get_path_fragment(-1);

            json_path_handler::get_field(obj, args...)[key]
                = std::string((const char*) str, len);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            {
                yajlpp_generator gen(handle);

                for (const auto& pair : field) {
                    gen(pair.first);
                    pair.second.match([&gen](json_null_t v) { gen(); },
                                      [&gen](bool v) { gen(v); },
                                      [&gen](int64_t v) { gen(v); },
                                      [&gen](double v) { gen(v); },
                                      [&gen](const std::string& v) { gen(v); });
                }
            }

            return yajl_gen_status_ok;
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIs<std::string, Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            json_path_handler::get_field(obj, args...) = std::move(value_str);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field);
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIs<timeval, Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto jph = ypc->ypc_current_handler;

            date_time_scanner dts;
            timeval tv{};
            exttm tm;

            if (dts.scan((char*) str, len, nullptr, &tm, tv) == nullptr) {
                ypc->report_error(
                    lnav::console::user_message::error(
                        attr_line_t("unrecognized timestamp ")
                            .append_quoted(
                                string_fragment::from_bytes(str, len)))
                        .with_snippet(ypc->get_snippet())
                        .with_help(jph->get_help_text(ypc)));
            } else {
                json_path_handler::get_field(obj, args...) = tv;
            }

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);
            char buf[64];

            auto buf_len = lnav::strftime_rfc3339(
                buf, sizeof(buf), field.tv_sec, field.tv_usec, 'T');

            return gen(string_fragment::from_bytes(buf, buf_len));
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };
        return *this;
    }

    template<
        typename... Args,
        std::enable_if_t<LastIs<nonstd::optional<std::string>, Args...>::value,
                         bool>
        = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            json_path_handler::get_field(obj, args...) = std::move(value_str);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (!field) {
                return yajl_gen_status_ok;
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field.value());
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<
                 LastIs<positioned_property<std::string>, Args...>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            auto& field = json_path_handler::get_field(obj, args...);

            field.pp_path = ypc->get_full_path();
            field.pp_location.sl_source = ypc->ypc_source;
            field.pp_location.sl_line_number = ypc->get_line_number();
            field.pp_value = std::move(value_str);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field.pp_value == field_def.pp_value) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field.pp_value);
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIs<intern_string_t, Args...>::value, bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            json_path_handler::get_field(obj, args...)
                = intern_string::lookup(value_str);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field);
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<
                 LastIs<positioned_property<intern_string_t>, Args...>::value,
                 bool>
             = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto value_str = std::string((const char*) str, len);
            auto jph = ypc->ypc_current_handler;

            jph->validate_string(*ypc, value_str);
            auto& field = json_path_handler::get_field(obj, args...);
            field.pp_path = ypc->get_full_path();
            field.pp_location.sl_source = ypc->ypc_source;
            field.pp_location.sl_line_number = ypc->get_line_number();
            field.pp_value = intern_string::lookup(value_str);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field.pp_value == field_def.pp_value) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field.pp_value);
        };
        return *this;
    }

    template<typename>
    struct int_ {
        typedef int type;
    };
    template<
        typename C,
        typename T,
        typename int_<decltype(T::from(
            intern_string_t{}, source_location{}, string_fragment{}))>::type
        = 0,
        typename... Args>
    json_path_handler& for_field(Args... args, T C::*ptr_arg)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args..., ptr_arg](yajlpp_parse_context* ypc,
                                              const unsigned char* str,
                                              size_t len) {
            auto* obj = ypc->ypc_obj_stack.top();
            auto value_frag = string_fragment::from_bytes(str, len);
            const auto* jph = ypc->ypc_current_handler;
            auto loc = source_location{ypc->ypc_source, ypc->get_line_number()};

            auto from_res = T::from(ypc->get_full_path(), loc, value_frag);
            if (from_res.isErr()) {
                jph->report_error(
                    ypc, value_frag.to_string(), from_res.unwrapErr());
            } else {
                json_path_handler::get_field(obj, args..., ptr_arg)
                    = from_res.unwrap();
            }

            return 1;
        };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIsInteger<Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(int_field_cb);
        this->jph_integer_cb
            = [args...](yajlpp_parse_context* ypc, long long val) {
                  auto jph = ypc->ypc_current_handler;
                  auto* obj = ypc->ypc_obj_stack.top();

                  if (val < jph->jph_min_value) {
                      jph->report_min_value_error(ypc, val);
                      return 1;
                  }

                  json_path_handler::get_field(obj, args...) = val;

                  return 1;
              };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (!is_field_set(field)) {
                return yajl_gen_status_ok;
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field);
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };

        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIsFloat<Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(dbl_field_cb);
        this->jph_double_cb = [args...](yajlpp_parse_context* ypc, double val) {
            auto jph = ypc->ypc_current_handler;
            auto* obj = ypc->ypc_obj_stack.top();

            if (val < jph->jph_min_value) {
                jph->report_min_value_error(ypc, val);
                return 1;
            }

            json_path_handler::get_field(obj, args...) = val;

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (!is_field_set(field)) {
                return yajl_gen_status_ok;
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(field);
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };

        return *this;
    }

    template<
        typename... Args,
        std::enable_if_t<LastIs<std::chrono::seconds, Args...>::value, bool>
        = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto handler = ypc->ypc_current_handler;
            auto parse_res = relative_time::from_str(
                string_fragment::from_bytes(str, len));

            if (parse_res.isErr()) {
                auto parse_error = parse_res.unwrapErr();
                auto value_str = std::string((const char*) str, len);

                handler->report_duration_error(ypc, value_str, parse_error);
                return 1;
            }

            json_path_handler::get_field(obj, args...) = std::chrono::seconds(
                parse_res.template unwrap().to_timeval().tv_sec);

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(relative_time::from_timeval(
                           {static_cast<time_t>(field.count()), 0})
                           .to_string());
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };
        return *this;
    }

    template<typename... Args,
             std::enable_if_t<LastIsEnum<Args...>::value, bool> = true>
    json_path_handler& for_field(Args... args)
    {
        this->add_cb(str_field_cb2);
        this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
                                     const unsigned char* str,
                                     size_t len) {
            auto obj = ypc->ypc_obj_stack.top();
            auto handler = ypc->ypc_current_handler;
            auto res = handler->to_enum_value(string_fragment(str, 0, len));

            if (res) {
                json_path_handler::get_field(obj, args...)
                    = (typename LastIsEnum<Args...>::value_type) res.value();
            } else {
                handler->report_enum_error(ypc,
                                           std::string((const char*) str, len));
            }

            return 1;
        };
        this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
                                           const json_path_handler_base& jph,
                                           yajl_gen handle) {
            const auto& field = json_path_handler::get_field(
                ygc.ygc_obj_stack.top(), args...);

            if (!ygc.ygc_default_stack.empty()) {
                const auto& field_def = json_path_handler::get_field(
                    ygc.ygc_default_stack.top(), args...);

                if (field == field_def) {
                    return yajl_gen_status_ok;
                }
            }

            if (!is_field_set(field)) {
                return yajl_gen_status_ok;
            }

            if (ygc.ygc_depth) {
                yajl_gen_string(handle, jph.jph_property);
            }

            yajlpp_generator gen(handle);

            return gen(jph.to_enum_string(field));
        };
        this->jph_field_getter
            = [args...](void* root, nonstd::optional<std::string> name) {
                  return (void*) &json_path_handler::get_field(root, args...);
              };

        return *this;
    }

    json_path_handler& with_children(const json_path_container& container);

    json_path_handler& with_example(const std::string& example)
    {
        this->jph_examples.emplace_back(example);
        return *this;
    }
};

struct json_path_container {
    json_path_container(std::initializer_list<json_path_handler> children)
        : jpc_children(children)
    {
    }

    json_path_container& with_definition_id(const std::string& id)
    {
        this->jpc_definition_id = id;
        return *this;
    }

    json_path_container& with_schema_id(const std::string& id)
    {
        this->jpc_schema_id = id;
        return *this;
    }

    json_path_container& with_description(std::string desc)
    {
        this->jpc_description = std::move(desc);
        return *this;
    }

    void gen_schema(yajlpp_gen_context& ygc) const;

    void gen_properties(yajlpp_gen_context& ygc) const;

    std::string jpc_schema_id;
    std::string jpc_definition_id;
    std::string jpc_description;
    std::vector<json_path_handler> jpc_children;
};

template<typename T>
class yajlpp_parser {
public:
    yajlpp_parser(intern_string_t src, const json_path_container* container)
        : yp_parse_context(src, container)
    {
        this->yp_handle = yajl_alloc(&this->yp_parse_context.ypc_callbacks,
                                     nullptr,
                                     &this->yp_parse_context);
        this->yp_parse_context.with_handle(this->yp_handle);
        this->yp_parse_context.template with_obj(this->yp_obj);
        this->yp_parse_context.ypc_userdata = this;
        this->yp_parse_context.with_error_reporter(
            [](const auto& ypc, const auto& um) {
                auto* yp = static_cast<yajlpp_parser<T>*>(ypc.ypc_userdata);

                yp->yp_errors.template emplace_back(um);
            });
    }

    yajlpp_parser& with_ignore_unused(bool value)
    {
        this->yp_parse_context.with_ignore_unused(value);

        return *this;
    }

    Result<T, std::vector<lnav::console::user_message>> of(
        const string_fragment& json)
    {
        if (this->yp_parse_context.parse_doc(json)) {
            return Ok(std::move(this->yp_obj));
        }

        return Err(std::move(this->yp_errors));
    }

private:
    yajlpp_parse_context yp_parse_context;
    auto_mem<yajl_handle_t> yp_handle{yajl_free};
    std::vector<lnav::console::user_message> yp_errors;
    T yp_obj;
};

template<typename T>
struct typed_json_path_container : public json_path_container {
    typed_json_path_container(std::initializer_list<json_path_handler> children)
        : json_path_container(children)
    {
    }

    typed_json_path_container<T>& with_schema_id2(const std::string& id)
    {
        this->jpc_schema_id = id;
        return *this;
    }

    typed_json_path_container<T>& with_description2(std::string desc)
    {
        this->jpc_description = std::move(desc);
        return *this;
    }

    yajlpp_parser<T> parser_for(intern_string_t src) const
    {
        return yajlpp_parser<T>{src, this};
    }

    std::string to_string(const T& obj) const
    {
        yajlpp_gen gen;
        yajlpp_gen_context ygc(gen, *this);
        ygc.template with_obj(obj);
        ygc.ygc_depth = 1;
        ygc.gen();

        return gen.to_string_fragment().to_string();
    }

    json_string to_json_string(const T& obj) const
    {
        yajlpp_gen gen;
        yajlpp_gen_context ygc(gen, *this);
        ygc.template with_obj(obj);
        ygc.ygc_depth = 1;
        ygc.gen();

        return json_string{gen.get_handle()};
    }
};

namespace yajlpp {
inline json_path_handler
property_handler(const std::string& path)
{
    return {path};
}

template<typename T, std::size_t N>
inline json_path_handler
pattern_property_handler(const T (&path)[N])
{
    return {lnav::pcre2pp::code::from_const(path, PCRE2_ANCHORED).to_shared()};
}

}  // namespace yajlpp

#endif
