/*----------------------------------------------------------------------------*/
/* Modifications Copyright (c) 2017-2018 FIRST. All Rights Reserved.          */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/
/*
    __ _____ _____ _____
 __|  |   __|     |   | |  JSON for Modern C++
|  |  |__   |  |  | | | |  version 3.1.2
|_____|_____|_____|_|___|  https://github.com/nlohmann/json

Licensed under the MIT License <http://opensource.org/licenses/MIT>.
Copyright (c) 2013-2018 Niels Lohmann <http://nlohmann.me>.

Permission is hereby  granted, free of charge, to any  person obtaining a copy
of this software and associated  documentation files (the "Software"), to deal
in the Software  without restriction, including without  limitation the rights
to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#define WPI_JSON_IMPLEMENTATION
#include "wpi/json.h"

#include "wpi/raw_ostream.h"

namespace wpi {
namespace detail {

exception::exception(int id_, const Twine& what_arg)
    : id(id_), m(what_arg.str()) {}

parse_error parse_error::create(int id_, std::size_t byte_, const Twine& what_arg)
{
    return parse_error(id_, byte_, "[json.exception.parse_error." + Twine(id_) + "] parse error" +
                    (byte_ != 0 ? (" at " + Twine(byte_)) : Twine("")) +
                    ": " + what_arg);
}

invalid_iterator invalid_iterator::create(int id_, const Twine& what_arg)
{
    return invalid_iterator(id_, "[json.exception.invalid_iterator." + Twine(id_) + "] " + what_arg);
}

type_error type_error::create(int id_, const Twine& what_arg)
{
    return type_error(id_, "[json.exception.type_error." + Twine(id_) + "] " + what_arg);
}

out_of_range out_of_range::create(int id_, const Twine& what_arg)
{
    return out_of_range(id_, "[json.exception.out_of_range." + Twine(id_) + "] " + what_arg);
}

other_error other_error::create(int id_, const Twine& what_arg)
{
    return other_error(id_, "[json.exception.other_error." + Twine(id_) + "] " + what_arg);
}

}  // namespace detail

json::json_value::json_value(value_t t)
{
    switch (t)
    {
        case value_t::object:
        {
            object = create<object_t>();
            break;
        }

        case value_t::array:
        {
            array = create<array_t>();
            break;
        }

        case value_t::string:
        {
            string = create<std::string>("");
            break;
        }

        case value_t::boolean:
        {
            boolean = false;
            break;
        }

        case value_t::number_integer:
        {
            number_integer = 0;
            break;
        }

        case value_t::number_unsigned:
        {
            number_unsigned = 0u;
            break;
        }

        case value_t::number_float:
        {
            number_float = 0.0;
            break;
        }

        case value_t::null:
        {
            object = nullptr;  // silence warning, see #821
            break;
        }

        default:
        {
            object = nullptr;  // silence warning, see #821
            if (JSON_UNLIKELY(t == value_t::null))
            {
                JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.1.2")); // LCOV_EXCL_LINE
            }
            break;
        }
    }
}

void json::json_value::destroy(value_t t) noexcept
{
    switch (t)
    {
        case value_t::object:
        {
            std::allocator<object_t> alloc;
            std::allocator_traits<decltype(alloc)>::destroy(alloc, object);
            std::allocator_traits<decltype(alloc)>::deallocate(alloc, object, 1);
            break;
        }

        case value_t::array:
        {
            std::allocator<array_t> alloc;
            std::allocator_traits<decltype(alloc)>::destroy(alloc, array);
            std::allocator_traits<decltype(alloc)>::deallocate(alloc, array, 1);
            break;
        }

        case value_t::string:
        {
            std::allocator<std::string> alloc;
            std::allocator_traits<decltype(alloc)>::destroy(alloc, string);
            std::allocator_traits<decltype(alloc)>::deallocate(alloc, string, 1);
            break;
        }

        default:
        {
            break;
        }
    }
}

json::json(initializer_list_t init,
           bool type_deduction,
           value_t manual_type)
{
    // check if each element is an array with two elements whose first
    // element is a string
    bool is_an_object = std::all_of(init.begin(), init.end(),
                                    [](const detail::json_ref<json>& element_ref)
    {
        return (element_ref->is_array() and element_ref->size() == 2 and (*element_ref)[0].is_string());
    });

    // adjust type if type deduction is not wanted
    if (not type_deduction)
    {
        // if array is wanted, do not create an object though possible
        if (manual_type == value_t::array)
        {
            is_an_object = false;
        }

        // if object is wanted but impossible, throw an exception
        if (JSON_UNLIKELY(manual_type == value_t::object and not is_an_object))
        {
            JSON_THROW(type_error::create(301, "cannot create object from initializer list"));
        }
    }

    if (is_an_object)
    {
        // the initializer list is a list of pairs -> create object
        m_type = value_t::object;
        m_value = value_t::object;

        std::for_each(init.begin(), init.end(), [this](const detail::json_ref<json>& element_ref)
        {
            auto element = element_ref.moved_or_copied();
            m_value.object->try_emplace(
                *((*element.m_value.array)[0].m_value.string),
                std::move((*element.m_value.array)[1]));
        });
    }
    else
    {
        // the initializer list describes an array -> create array
        m_type = value_t::array;
        m_value.array = create<array_t>(init.begin(), init.end());
    }

    assert_invariant();
}

json::json(size_type cnt, const json& val)
    : m_type(value_t::array)
{
    m_value.array = create<array_t>(cnt, val);
    assert_invariant();
}

json::json(const json& other)
    : m_type(other.m_type)
{
    // check of passed value is valid
    other.assert_invariant();

    switch (m_type)
    {
        case value_t::object:
        {
            m_value = *other.m_value.object;
            break;
        }

        case value_t::array:
        {
            m_value = *other.m_value.array;
            break;
        }

        case value_t::string:
        {
            m_value = *other.m_value.string;
            break;
        }

        case value_t::boolean:
        {
            m_value = other.m_value.boolean;
            break;
        }

        case value_t::number_integer:
        {
            m_value = other.m_value.number_integer;
            break;
        }

        case value_t::number_unsigned:
        {
            m_value = other.m_value.number_unsigned;
            break;
        }

        case value_t::number_float:
        {
            m_value = other.m_value.number_float;
            break;
        }

        default:
            break;
    }

    assert_invariant();
}

json json::meta()
{
    json result;

    result["copyright"] = "(C) 2013-2017 Niels Lohmann, (C) 2017-2018 FIRST";
    result["name"] = "WPI version of JSON for Modern C++";
    result["url"] = "https://github.com/wpilibsuite/allwpilib";
    result["version"]["string"] =
        std::to_string(NLOHMANN_JSON_VERSION_MAJOR) + "." +
        std::to_string(NLOHMANN_JSON_VERSION_MINOR) + "." +
        std::to_string(NLOHMANN_JSON_VERSION_PATCH);
    result["version"]["major"] = NLOHMANN_JSON_VERSION_MAJOR;
    result["version"]["minor"] = NLOHMANN_JSON_VERSION_MINOR;
    result["version"]["patch"] = NLOHMANN_JSON_VERSION_PATCH;

#ifdef _WIN32
    result["platform"] = "win32";
#elif defined __linux__
    result["platform"] = "linux";
#elif defined __APPLE__
    result["platform"] = "apple";
#elif defined __unix__
    result["platform"] = "unix";
#else
    result["platform"] = "unknown";
#endif

#if defined(__ICC) || defined(__INTEL_COMPILER)
    result["compiler"] = {{"family", "icc"}, {"version", __INTEL_COMPILER}};
#elif defined(__clang__)
    result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}};
#elif defined(__GNUC__) || defined(__GNUG__)
    result["compiler"] = {{"family", "gcc"}, {"version", std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__)}};
#elif defined(__HP_cc) || defined(__HP_aCC)
    result["compiler"] = "hp"
#elif defined(__IBMCPP__)
    result["compiler"] = {{"family", "ilecpp"}, {"version", __IBMCPP__}};
#elif defined(_MSC_VER)
    result["compiler"] = {{"family", "msvc"}, {"version", _MSC_VER}};
#elif defined(__PGI)
    result["compiler"] = {{"family", "pgcpp"}, {"version", __PGI}};
#elif defined(__SUNPRO_CC)
    result["compiler"] = {{"family", "sunpro"}, {"version", __SUNPRO_CC}};
#else
    result["compiler"] = {{"family", "unknown"}, {"version", "unknown"}};
#endif

#ifdef __cplusplus
    result["compiler"]["c++"] = std::to_string(__cplusplus);
#else
    result["compiler"]["c++"] = "unknown";
#endif
    return result;
}

json::reference json::at(size_type idx)
{
    // at only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        JSON_TRY
        {
            return m_value.array->at(idx);
        }
        JSON_CATCH (std::out_of_range&)
        {
            // create better exception explanation
            JSON_THROW(out_of_range::create(401, "array index " + Twine(idx) + " is out of range"));
        }
    }
    else
    {
        JSON_THROW(type_error::create(304, "cannot use at() with " + Twine(type_name())));
    }
}

json::const_reference json::at(size_type idx) const
{
    // at only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        JSON_TRY
        {
            return m_value.array->at(idx);
        }
        JSON_CATCH (std::out_of_range&)
        {
            // create better exception explanation
            JSON_THROW(out_of_range::create(401, "array index " + Twine(idx) + " is out of range"));
        }
    }
    else
    {
        JSON_THROW(type_error::create(304, "cannot use at() with " + Twine(type_name())));
    }
}

json::reference json::at(StringRef key)
{
    // at only works for objects
    if (JSON_LIKELY(is_object()))
    {
        auto it = m_value.object->find(key);
        if (it == m_value.object->end())
        {
            // create better exception explanation
            JSON_THROW(out_of_range::create(403, "key '" + Twine(key) + "' not found"));
        }
        return it->second;
    }
    else
    {
        JSON_THROW(type_error::create(304, "cannot use at() with " + Twine(type_name())));
    }
}

json::const_reference json::at(StringRef key) const
{
    // at only works for objects
    if (JSON_LIKELY(is_object()))
    {
        auto it = m_value.object->find(key);
        if (it == m_value.object->end())
        {
            // create better exception explanation
            JSON_THROW(out_of_range::create(403, "key '" + Twine(key) + "' not found"));
        }
        return it->second;
    }
    else
    {
        JSON_THROW(type_error::create(304, "cannot use at() with " + Twine(type_name())));
    }
}

json::reference json::operator[](size_type idx)
{
    // implicitly convert null value to an empty array
    if (is_null())
    {
        m_type = value_t::array;
        m_value.array = create<array_t>();
        assert_invariant();
    }

    // operator[] only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        // fill up array with null values if given idx is outside range
        if (idx >= m_value.array->size())
        {
            m_value.array->insert(m_value.array->end(),
                                  idx - m_value.array->size() + 1,
                                  json());
        }

        return m_value.array->operator[](idx);
    }

    JSON_THROW(type_error::create(305, "cannot use operator[] with " + Twine(type_name())));
}

json::const_reference json::operator[](size_type idx) const
{
    // const operator[] only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        return m_value.array->operator[](idx);
    }

    JSON_THROW(type_error::create(305, "cannot use operator[] with " + Twine(type_name())));
}

json::reference json::operator[](StringRef key)
{
    // implicitly convert null value to an empty object
    if (is_null())
    {
        m_type = value_t::object;
        m_value.object = create<object_t>();
        assert_invariant();
    }

    // operator[] only works for objects
    if (JSON_LIKELY(is_object()))
    {
        return m_value.object->operator[](key);
    }

    JSON_THROW(type_error::create(305, "cannot use operator[] with " + Twine(type_name())));
}

json::const_reference json::operator[](StringRef key) const
{
    // const operator[] only works for objects
    if (JSON_LIKELY(is_object()))
    {
        assert(m_value.object->find(key) != m_value.object->end());
        return m_value.object->find(key)->second;
    }

    JSON_THROW(type_error::create(305, "cannot use operator[] with " + Twine(type_name())));
}

json::size_type json::erase(StringRef key)
{
    // this erase only works for objects
    if (JSON_LIKELY(is_object()))
    {
        return m_value.object->erase(key);
    }

    JSON_THROW(type_error::create(307, "cannot use erase() with " + Twine(type_name())));
}

void json::erase(const size_type idx)
{
    // this erase only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        if (JSON_UNLIKELY(idx >= size()))
        {
            JSON_THROW(out_of_range::create(401, "array index " + Twine(idx) + " is out of range"));
        }

        m_value.array->erase(m_value.array->begin() + static_cast<difference_type>(idx));
    }
    else
    {
        JSON_THROW(type_error::create(307, "cannot use erase() with " + Twine(type_name())));
    }
}

json::iterator json::find(StringRef key)
{
    auto result = end();

    if (is_object())
    {
        result.m_it.object_iterator = m_value.object->find(key);
    }

    return result;
}

json::const_iterator json::find(StringRef key) const
{
    auto result = cend();

    if (is_object())
    {
        result.m_it.object_iterator = m_value.object->find(key);
    }

    return result;
}

json::size_type json::count(StringRef key) const
{
    // return 0 for all nonobject types
    return is_object() ? m_value.object->count(key) : 0;
}

bool json::empty() const noexcept
{
    switch (m_type)
    {
        case value_t::null:
        {
            // null values are empty
            return true;
        }

        case value_t::array:
        {
            // delegate call to array_t::empty()
            return m_value.array->empty();
        }

        case value_t::object:
        {
            // delegate call to object_t::empty()
            return m_value.object->empty();
        }

        default:
        {
            // all other types are nonempty
            return false;
        }
    }
}

json::size_type json::size() const noexcept
{
    switch (m_type)
    {
        case value_t::null:
        {
            // null values are empty
            return 0;
        }

        case value_t::array:
        {
            // delegate call to array_t::size()
            return m_value.array->size();
        }

        case value_t::object:
        {
            // delegate call to object_t::size()
            return m_value.object->size();
        }

        default:
        {
            // all other types have size 1
            return 1;
        }
    }
}

json::size_type json::max_size() const noexcept
{
    switch (m_type)
    {
        case value_t::array:
        {
            // delegate call to array_t::max_size()
            return m_value.array->max_size();
        }

        case value_t::object:
        {
            // delegate call to std::allocator<object_t>::max_size()
            return std::allocator_traits<object_t>::max_size(*m_value.object);
        }

        default:
        {
            // all other types have max_size() == size()
            return size();
        }
    }
}

void json::clear() noexcept
{
    switch (m_type)
    {
        case value_t::number_integer:
        {
            m_value.number_integer = 0;
            break;
        }

        case value_t::number_unsigned:
        {
            m_value.number_unsigned = 0;
            break;
        }

        case value_t::number_float:
        {
            m_value.number_float = 0.0;
            break;
        }

        case value_t::boolean:
        {
            m_value.boolean = false;
            break;
        }

        case value_t::string:
        {
            m_value.string->clear();
            break;
        }

        case value_t::array:
        {
            m_value.array->clear();
            break;
        }

        case value_t::object:
        {
            m_value.object->clear();
            break;
        }

        default:
            break;
    }
}

void json::push_back(json&& val)
{
    // push_back only works for null objects or arrays
    if (JSON_UNLIKELY(not(is_null() or is_array())))
    {
        JSON_THROW(type_error::create(308, "cannot use push_back() with " + Twine(type_name())));
    }

    // transform null object into an array
    if (is_null())
    {
        m_type = value_t::array;
        m_value = value_t::array;
        assert_invariant();
    }

    // add element to array (move semantics)
    m_value.array->push_back(std::move(val));
    // invalidate object
    val.m_type = value_t::null;
}

void json::push_back(const json& val)
{
    // push_back only works for null objects or arrays
    if (JSON_UNLIKELY(not(is_null() or is_array())))
    {
        JSON_THROW(type_error::create(308, "cannot use push_back() with " + Twine(type_name())));
    }

    // transform null object into an array
    if (is_null())
    {
        m_type = value_t::array;
        m_value = value_t::array;
        assert_invariant();
    }

    // add element to array
    m_value.array->push_back(val);
}

void json::push_back(initializer_list_t init)
{
    if (is_object() and init.size() == 2 and (*init.begin())->is_string())
    {
        std::string key = init.begin()->moved_or_copied();
        push_back(std::pair<StringRef, json>(key, (init.begin() + 1)->moved_or_copied()));
    }
    else
    {
        push_back(json(init));
    }
}

json::iterator json::insert(const_iterator pos, const json& val)
{
    // insert only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        // check if iterator pos fits to this JSON value
        if (JSON_UNLIKELY(pos.m_object != this))
        {
            JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
        }

        // insert to array and return iterator
        iterator result(this);
        result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, val);
        return result;
    }

    JSON_THROW(type_error::create(309, "cannot use insert() with " + Twine(type_name())));
}

json::iterator json::insert(const_iterator pos, size_type cnt, const json& val)
{
    // insert only works for arrays
    if (JSON_LIKELY(is_array()))
    {
        // check if iterator pos fits to this JSON value
        if (JSON_UNLIKELY(pos.m_object != this))
        {
            JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
        }

        // insert to array and return iterator
        iterator result(this);
        result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, cnt, val);
        return result;
    }

    JSON_THROW(type_error::create(309, "cannot use insert() with " + Twine(type_name())));
}

json::iterator json::insert(const_iterator pos, const_iterator first, const_iterator last)
{
    // insert only works for arrays
    if (JSON_UNLIKELY(not is_array()))
    {
        JSON_THROW(type_error::create(309, "cannot use insert() with " + Twine(type_name())));
    }

    // check if iterator pos fits to this JSON value
    if (JSON_UNLIKELY(pos.m_object != this))
    {
        JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
    }

    // check if range iterators belong to the same JSON object
    if (JSON_UNLIKELY(first.m_object != last.m_object))
    {
        JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
    }

    if (JSON_UNLIKELY(first.m_object == this))
    {
        JSON_THROW(invalid_iterator::create(211, "passed iterators may not belong to container"));
    }

    // insert to array and return iterator
    iterator result(this);
    result.m_it.array_iterator = m_value.array->insert(
                                     pos.m_it.array_iterator,
                                     first.m_it.array_iterator,
                                     last.m_it.array_iterator);
    return result;
}

json::iterator json::insert(const_iterator pos, initializer_list_t ilist)
{
    // insert only works for arrays
    if (JSON_UNLIKELY(not is_array()))
    {
        JSON_THROW(type_error::create(309, "cannot use insert() with " + Twine(type_name())));
    }

    // check if iterator pos fits to this JSON value
    if (JSON_UNLIKELY(pos.m_object != this))
    {
        JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value"));
    }

    // insert to array and return iterator
    iterator result(this);
    result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, ilist.begin(), ilist.end());
    return result;
}

void json::insert(const_iterator first, const_iterator last)
{
    // insert only works for objects
    if (JSON_UNLIKELY(not is_object()))
    {
        JSON_THROW(type_error::create(309, "cannot use insert() with " + Twine(type_name())));
    }

    // check if range iterators belong to the same JSON object
    if (JSON_UNLIKELY(first.m_object != last.m_object))
    {
        JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
    }

    // passed iterators must belong to objects
    if (JSON_UNLIKELY(not first.m_object->is_object()))
    {
        JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects"));
    }

    for (auto it = first.m_it.object_iterator, end = last.m_it.object_iterator; it != end; ++it)
    {
        m_value.object->insert(std::make_pair(it->first(), it->second));
    }
}

void json::update(const_reference j)
{
    // implicitly convert null value to an empty object
    if (is_null())
    {
        m_type = value_t::object;
        m_value.object = create<object_t>();
        assert_invariant();
    }

    if (JSON_UNLIKELY(not is_object()))
    {
        JSON_THROW(type_error::create(312, "cannot use update() with " + Twine(type_name())));
    }
    if (JSON_UNLIKELY(not j.is_object()))
    {
        JSON_THROW(type_error::create(312, "cannot use update() with " + Twine(j.type_name())));
    }

    for (auto it = j.cbegin(); it != j.cend(); ++it)
    {
        m_value.object->operator[](it.key()) = it.value();
    }
}

void json::update(const_iterator first, const_iterator last)
{
    // implicitly convert null value to an empty object
    if (is_null())
    {
        m_type = value_t::object;
        m_value.object = create<object_t>();
        assert_invariant();
    }

    if (JSON_UNLIKELY(not is_object()))
    {
        JSON_THROW(type_error::create(312, "cannot use update() with " + Twine(type_name())));
    }

    // check if range iterators belong to the same JSON object
    if (JSON_UNLIKELY(first.m_object != last.m_object))
    {
        JSON_THROW(invalid_iterator::create(210, "iterators do not fit"));
    }

    // passed iterators must belong to objects
    if (JSON_UNLIKELY(not first.m_object->is_object()
                      or not last.m_object->is_object()))
    {
        JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects"));
    }

    for (auto it = first; it != last; ++it)
    {
        m_value.object->operator[](it.key()) = it.value();
    }
}

bool operator==(json::const_reference lhs, json::const_reference rhs) noexcept
{
    const auto lhs_type = lhs.type();
    const auto rhs_type = rhs.type();

    if (lhs_type == rhs_type)
    {
        switch (lhs_type)
        {
            case json::value_t::array:
                return (*lhs.m_value.array == *rhs.m_value.array);

            case json::value_t::object:
                return (*lhs.m_value.object == *rhs.m_value.object);

            case json::value_t::null:
                return true;

            case json::value_t::string:
                return (*lhs.m_value.string == *rhs.m_value.string);

            case json::value_t::boolean:
                return (lhs.m_value.boolean == rhs.m_value.boolean);

            case json::value_t::number_integer:
                return (lhs.m_value.number_integer == rhs.m_value.number_integer);

            case json::value_t::number_unsigned:
                return (lhs.m_value.number_unsigned == rhs.m_value.number_unsigned);

            case json::value_t::number_float:
                return (lhs.m_value.number_float == rhs.m_value.number_float);

            default:
                return false;
        }
    }
    else if (lhs_type == json::value_t::number_integer and rhs_type == json::value_t::number_float)
    {
        return (static_cast<double>(lhs.m_value.number_integer) == rhs.m_value.number_float);
    }
    else if (lhs_type == json::value_t::number_float and rhs_type == json::value_t::number_integer)
    {
        return (lhs.m_value.number_float == static_cast<double>(rhs.m_value.number_integer));
    }
    else if (lhs_type == json::value_t::number_unsigned and rhs_type == json::value_t::number_float)
    {
        return (static_cast<double>(lhs.m_value.number_unsigned) == rhs.m_value.number_float);
    }
    else if (lhs_type == json::value_t::number_float and rhs_type == json::value_t::number_unsigned)
    {
        return (lhs.m_value.number_float == static_cast<double>(rhs.m_value.number_unsigned));
    }
    else if (lhs_type == json::value_t::number_unsigned and rhs_type == json::value_t::number_integer)
    {
        return (static_cast<int64_t>(lhs.m_value.number_unsigned) == rhs.m_value.number_integer);
    }
    else if (lhs_type == json::value_t::number_integer and rhs_type == json::value_t::number_unsigned)
    {
        return (lhs.m_value.number_integer == static_cast<int64_t>(rhs.m_value.number_unsigned));
    }

    return false;
}

bool operator<(json::const_reference lhs, json::const_reference rhs) noexcept
{
    const auto lhs_type = lhs.type();
    const auto rhs_type = rhs.type();

    if (lhs_type == rhs_type)
    {
        switch (lhs_type)
        {
            case json::value_t::array:
                return (*lhs.m_value.array) < (*rhs.m_value.array);

            case json::value_t::object:
                return *lhs.m_value.object < *rhs.m_value.object;

            case json::value_t::null:
                return false;

            case json::value_t::string:
                return *lhs.m_value.string < *rhs.m_value.string;

            case json::value_t::boolean:
                return lhs.m_value.boolean < rhs.m_value.boolean;

            case json::value_t::number_integer:
                return lhs.m_value.number_integer < rhs.m_value.number_integer;

            case json::value_t::number_unsigned:
                return lhs.m_value.number_unsigned < rhs.m_value.number_unsigned;

            case json::value_t::number_float:
                return lhs.m_value.number_float < rhs.m_value.number_float;

            default:
                return false;
        }
    }
    else if (lhs_type == json::value_t::number_integer and rhs_type == json::value_t::number_float)
    {
        return static_cast<double>(lhs.m_value.number_integer) < rhs.m_value.number_float;
    }
    else if (lhs_type == json::value_t::number_float and rhs_type == json::value_t::number_integer)
    {
        return lhs.m_value.number_float < static_cast<double>(rhs.m_value.number_integer);
    }
    else if (lhs_type == json::value_t::number_unsigned and rhs_type == json::value_t::number_float)
    {
        return static_cast<double>(lhs.m_value.number_unsigned) < rhs.m_value.number_float;
    }
    else if (lhs_type == json::value_t::number_float and rhs_type == json::value_t::number_unsigned)
    {
        return lhs.m_value.number_float < static_cast<double>(rhs.m_value.number_unsigned);
    }
    else if (lhs_type == json::value_t::number_integer and rhs_type == json::value_t::number_unsigned)
    {
        return lhs.m_value.number_integer < static_cast<int64_t>(rhs.m_value.number_unsigned);
    }
    else if (lhs_type == json::value_t::number_unsigned and rhs_type == json::value_t::number_integer)
    {
        return static_cast<int64_t>(lhs.m_value.number_unsigned) < rhs.m_value.number_integer;
    }

    // We only reach this line if we cannot compare values. In that case,
    // we compare types. Note we have to call the operator explicitly,
    // because MSVC has problems otherwise.
    return operator<(lhs_type, rhs_type);
}

const char* json::type_name() const noexcept
{
    {
        switch (m_type)
        {
            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::boolean:
                return "boolean";
            case value_t::discarded:
                return "discarded";
            default:
                return "number";
        }
    }
}

json json::patch(const json& json_patch) const
{
    // make a working copy to apply the patch to
    json result = *this;

    // the valid JSON Patch operations
    enum class patch_operations {add, remove, replace, move, copy, test, invalid};

    const auto get_op = [](const std::string & op)
    {
        if (op == "add")
        {
            return patch_operations::add;
        }
        if (op == "remove")
        {
            return patch_operations::remove;
        }
        if (op == "replace")
        {
            return patch_operations::replace;
        }
        if (op == "move")
        {
            return patch_operations::move;
        }
        if (op == "copy")
        {
            return patch_operations::copy;
        }
        if (op == "test")
        {
            return patch_operations::test;
        }

        return patch_operations::invalid;
    };

    // wrapper for "add" operation; add value at ptr
    const auto operation_add = [&result](json_pointer & ptr, json val)
    {
        // adding to the root of the target document means replacing it
        if (ptr.is_root())
        {
            result = val;
        }
        else
        {
            // make sure the top element of the pointer exists
            json_pointer top_pointer = ptr.top();
            if (top_pointer != ptr)
            {
                result.at(top_pointer);
            }

            // get reference to parent of JSON pointer ptr
            const auto last_path = ptr.pop_back();
            json& parent = result[ptr];

            switch (parent.m_type)
            {
                case value_t::null:
                case value_t::object:
                {
                    // use operator[] to add value
                    parent[last_path] = val;
                    break;
                }

                case value_t::array:
                {
                    if (last_path == "-")
                    {
                        // special case: append to back
                        parent.push_back(val);
                    }
                    else
                    {
                        const auto idx = json_pointer::array_index(last_path);
                        if (JSON_UNLIKELY(static_cast<size_type>(idx) > parent.size()))
                        {
                            // avoid undefined behavior
                            JSON_THROW(out_of_range::create(401, "array index " + Twine(idx) + " is out of range"));
                        }
                        else
                        {
                            // default case: insert add offset
                            parent.insert(parent.begin() + static_cast<difference_type>(idx), val);
                        }
                    }
                    break;
                }

                default:
                {
                    // if there exists a parent it cannot be primitive
                    assert(false);  // LCOV_EXCL_LINE
                }
            }
        }
    };

    // wrapper for "remove" operation; remove value at ptr
    const auto operation_remove = [&result](json_pointer & ptr)
    {
        // get reference to parent of JSON pointer ptr
        const auto last_path = ptr.pop_back();
        json& parent = result.at(ptr);

        // remove child
        if (parent.is_object())
        {
            // perform range check
            auto it = parent.find(last_path);
            if (JSON_LIKELY(it != parent.end()))
            {
                parent.erase(it);
            }
            else
            {
                JSON_THROW(out_of_range::create(403, "key '" + Twine(last_path) + "' not found"));
            }
        }
        else if (parent.is_array())
        {
            // note erase performs range check
            parent.erase(static_cast<size_type>(json_pointer::array_index(last_path)));
        }
    };

    // type check: top level value must be an array
    if (JSON_UNLIKELY(not json_patch.is_array()))
    {
        JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
    }

    // iterate and apply the operations
    for (const auto& val : json_patch)
    {
        // wrapper to get a value for an operation
        const auto get_value = [&val](const std::string & op,
                                      const std::string & member,
                                      bool string_type) -> json &
        {
            // find value
            auto it = val.m_value.object->find(member);

            // context-sensitive error message
            const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";

            // check if desired value is present
            if (JSON_UNLIKELY(it == val.m_value.object->end()))
            {
                JSON_THROW(parse_error::create(105, 0, Twine(error_msg) + " must have member '" + Twine(member) + "'"));
            }

            // check if result is of type string
            if (JSON_UNLIKELY(string_type and not it->second.is_string()))
            {
                JSON_THROW(parse_error::create(105, 0, Twine(error_msg) + " must have string member '" + Twine(member) + "'"));
            }

            // no error: return value
            return it->second;
        };

        // type check: every element of the array must be an object
        if (JSON_UNLIKELY(not val.is_object()))
        {
            JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
        }

        // collect mandatory members
        const std::string op = get_value("op", "op", true);
        const std::string path = get_value(op, "path", true);
        json_pointer ptr(path);

        switch (get_op(op))
        {
            case patch_operations::add:
            {
                operation_add(ptr, get_value("add", "value", false));
                break;
            }

            case patch_operations::remove:
            {
                operation_remove(ptr);
                break;
            }

            case patch_operations::replace:
            {
                // the "path" location must exist - use at()
                result.at(ptr) = get_value("replace", "value", false);
                break;
            }

            case patch_operations::move:
            {
                const std::string from_path = get_value("move", "from", true);
                json_pointer from_ptr(from_path);

                // the "from" location must exist - use at()
                json v = result.at(from_ptr);

                // The move operation is functionally identical to a
                // "remove" operation on the "from" location, followed
                // immediately by an "add" operation at the target
                // location with the value that was just removed.
                operation_remove(from_ptr);
                operation_add(ptr, v);
                break;
            }

            case patch_operations::copy:
            {
                const std::string from_path = get_value("copy", "from", true);
                const json_pointer from_ptr(from_path);

                // the "from" location must exist - use at()
                json v = result.at(from_ptr);

                // The copy is functionally identical to an "add"
                // operation at the target location using the value
                // specified in the "from" member.
                operation_add(ptr, v);
                break;
            }

            case patch_operations::test:
            {
                bool success = false;
                JSON_TRY
                {
                    // check if "value" matches the one at "path"
                    // the "path" location must exist - use at()
                    success = (result.at(ptr) == get_value("test", "value", false));
                }
                JSON_CATCH (out_of_range&)
                {
                    // ignore out of range errors: success remains false
                }

                // throw an exception if test fails
                if (JSON_UNLIKELY(not success))
                {
                    JSON_THROW(other_error::create(501, "unsuccessful: " + Twine(val.dump())));
                }

                break;
            }

            case patch_operations::invalid:
            {
                // op must be "add", "remove", "replace", "move", "copy", or
                // "test"
                JSON_THROW(parse_error::create(105, 0, "operation value '" + Twine(op) + "' is invalid"));
            }
        }
    }

    return result;
}

json json::diff(const json& source, const json& target,
                       const std::string& path)
{
    // the patch
    json result(value_t::array);

    // if the values are the same, return empty patch
    if (source == target)
    {
        return result;
    }

    if (source.type() != target.type())
    {
        // different types: replace value
        result.push_back(
        {
            {"op", "replace"}, {"path", path}, {"value", target}
        });
    }
    else
    {
        switch (source.type())
        {
            case value_t::array:
            {
                // first pass: traverse common elements
                std::size_t i = 0;
                while (i < source.size() and i < target.size())
                {
                    // recursive call to compare array values at index i
                    auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i));
                    result.insert(result.end(), temp_diff.begin(), temp_diff.end());
                    ++i;
                }

                // i now reached the end of at least one array
                // in a second pass, traverse the remaining elements

                // remove my remaining elements
                const auto end_index = static_cast<difference_type>(result.size());
                while (i < source.size())
                {
                    // add operations in reverse order to avoid invalid
                    // indices
                    result.insert(result.begin() + end_index, object(
                    {
                        {"op", "remove"},
                        {"path", path + "/" + std::to_string(i)}
                    }));
                    ++i;
                }

                // add other remaining elements
                while (i < target.size())
                {
                    result.push_back(
                    {
                        {"op", "add"},
                        {"path", path + "/" + std::to_string(i)},
                        {"value", target[i]}
                    });
                    ++i;
                }

                break;
            }

            case value_t::object:
            {
                // first pass: traverse this object's elements
                for (auto it = source.cbegin(); it != source.cend(); ++it)
                {
                    // escape the key name to be used in a JSON patch
                    const auto key = json_pointer::escape(it.key());

                    if (target.find(it.key()) != target.end())
                    {
                        // recursive call to compare object values at key it
                        auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key);
                        result.insert(result.end(), temp_diff.begin(), temp_diff.end());
                    }
                    else
                    {
                        // found a key that is not in o -> remove it
                        result.push_back(object(
                        {
                            {"op", "remove"}, {"path", path + "/" + key}
                        }));
                    }
                }

                // second pass: traverse other object's elements
                for (auto it = target.cbegin(); it != target.cend(); ++it)
                {
                    if (source.find(it.key()) == source.end())
                    {
                        // found a key that is not in this -> add it
                        const auto key = json_pointer::escape(it.key());
                        result.push_back(
                        {
                            {"op", "add"}, {"path", path + "/" + key},
                            {"value", it.value()}
                        });
                    }
                }

                break;
            }

            default:
            {
                // both primitive type: replace value
                result.push_back(
                {
                    {"op", "replace"}, {"path", path}, {"value", target}
                });
                break;
            }
        }
    }

    return result;
}

void json::merge_patch(const json& patch)
{
    if (patch.is_object())
    {
        if (not is_object())
        {
            *this = object();
        }
        for (auto it = patch.begin(); it != patch.end(); ++it)
        {
            if (it.value().is_null())
            {
                erase(it.key());
            }
            else
            {
                operator[](it.key()).merge_patch(it.value());
            }
        }
    }
    else
    {
        *this = patch;
    }
}

}  // namespace wpi
