//
// Copyright (c) 2019 Vinnie Falco (vinnie.falco@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/json
//

#ifndef BOOST_JSON_IMPL_VALUE_IPP
#define BOOST_JSON_IMPL_VALUE_IPP

#include <boost/container_hash/hash.hpp>
#include <boost/json/value.hpp>
#include <boost/json/parser.hpp>
#include <cstring>
#include <istream>
#include <limits>
#include <new>
#include <utility>

namespace boost {
namespace json {

namespace
{

int parse_depth_xalloc = std::ios::xalloc();
int parse_flags_xalloc = std::ios::xalloc();

struct value_hasher
{
    std::size_t& seed;

    template< class T >
    void operator()( T&& t ) const noexcept
    {
        boost::hash_combine( seed, t );
    }
};

enum class stream_parse_flags
{
    allow_comments = 1 << 0,
    allow_trailing_commas = 1 << 1,
    allow_invalid_utf8 = 1 << 2,
};

long
to_bitmask( parse_options const& opts )
{
    using E = stream_parse_flags;
    return
        (opts.allow_comments ?
            static_cast<long>(E::allow_comments) : 0) |
        (opts.allow_trailing_commas ?
            static_cast<long>(E::allow_trailing_commas) : 0) |
        (opts.allow_invalid_utf8 ?
            static_cast<long>(E::allow_invalid_utf8) : 0);
}

parse_options
get_parse_options( std::istream& is )
{
    long const flags = is.iword(parse_flags_xalloc);

    using E = stream_parse_flags;
    parse_options opts;
    opts.allow_comments =
        flags & static_cast<long>(E::allow_comments) ? true : false;
    opts.allow_trailing_commas =
        flags & static_cast<long>(E::allow_trailing_commas) ? true : false;
    opts.allow_invalid_utf8 =
        flags & static_cast<long>(E::allow_invalid_utf8) ? true : false;
    return opts;
}

} // namespace

value::
~value() noexcept
{
    switch(kind())
    {
    case json::kind::null:
    case json::kind::bool_:
    case json::kind::int64:
    case json::kind::uint64:
    case json::kind::double_:
        sca_.~scalar();
        break;

    case json::kind::string:
        str_.~string();
        break;

    case json::kind::array:
        arr_.~array();
        break;

    case json::kind::object:
        obj_.~object();
        break;
    }
}

value::
value(
    value const& other,
    storage_ptr sp)
{
    switch(other.kind())
    {
    case json::kind::null:
        ::new(&sca_) scalar(
            std::move(sp));
        break;

    case json::kind::bool_:
        ::new(&sca_) scalar(
            other.sca_.b,
            std::move(sp));
        break;

    case json::kind::int64:
        ::new(&sca_) scalar(
            other.sca_.i,
            std::move(sp));
        break;

    case json::kind::uint64:
        ::new(&sca_) scalar(
            other.sca_.u,
            std::move(sp));
        break;

    case json::kind::double_:
        ::new(&sca_) scalar(
            other.sca_.d,
            std::move(sp));
        break;

    case json::kind::string:
        ::new(&str_) string(
            other.str_,
            std::move(sp));
        break;

    case json::kind::array:
        ::new(&arr_) array(
            other.arr_,
            std::move(sp));
        break;

    case json::kind::object:
        ::new(&obj_) object(
            other.obj_,
            std::move(sp));
        break;
    }
}

value::
value(value&& other) noexcept
{
    relocate(this, other);
    ::new(&other.sca_) scalar(sp_);
}

value::
value(
    value&& other,
    storage_ptr sp)
{
    switch(other.kind())
    {
    case json::kind::null:
        ::new(&sca_) scalar(
            std::move(sp));
        break;

    case json::kind::bool_:
        ::new(&sca_) scalar(
            other.sca_.b, std::move(sp));
        break;

    case json::kind::int64:
        ::new(&sca_) scalar(
            other.sca_.i, std::move(sp));
        break;

    case json::kind::uint64:
        ::new(&sca_) scalar(
            other.sca_.u, std::move(sp));
        break;

    case json::kind::double_:
        ::new(&sca_) scalar(
            other.sca_.d, std::move(sp));
        break;

    case json::kind::string:
        ::new(&str_) string(
            std::move(other.str_),
            std::move(sp));
        break;

    case json::kind::array:
        ::new(&arr_) array(
            std::move(other.arr_),
            std::move(sp));
        break;

    case json::kind::object:
        ::new(&obj_) object(
            std::move(other.obj_),
            std::move(sp));
        break;
    }
}

//----------------------------------------------------------
//
// Conversion
//
//----------------------------------------------------------

value::
value(
    std::initializer_list<value_ref> init,
    storage_ptr sp)
{
    if(value_ref::maybe_object(init))
    {
        ::new(&obj_) object(
            value_ref::make_object(
                init, std::move(sp)));
    }
    else
    {
        if( init.size() == 1 )
        {
            ::new(&sca_) scalar();
            value temp = init.begin()->make_value( std::move(sp) );
            swap(temp);
        }
        else
        {
            ::new(&arr_) array(
                value_ref::make_array(
                    init, std::move(sp)));
        }
    }
}

//----------------------------------------------------------
//
// Assignment
//
//----------------------------------------------------------

value&
value::
operator=(value const& other)
{
    value(other,
        storage()).swap(*this);
    return *this;
}

value&
value::
operator=(value&& other)
{
    value(std::move(other),
        storage()).swap(*this);
    return *this;
}

value&
value::
operator=(
    std::initializer_list<value_ref> init)
{
    value(init,
        storage()).swap(*this);
    return *this;
}

value&
value::
operator=(string_view s)
{
    value(s, storage()).swap(*this);
    return *this;
}

value&
value::
operator=(char const* s)
{
    value(s, storage()).swap(*this);
    return *this;
}

value&
value::
operator=(string const& str)
{
    value(str, storage()).swap(*this);
    return *this;
}

value&
value::
operator=(string&& str)
{
    value(std::move(str),
        storage()).swap(*this);
    return *this;
}

value&
value::
operator=(array const& arr)
{
    value(arr, storage()).swap(*this);
    return *this;
}

value&
value::
operator=(array&& arr)
{
    value(std::move(arr),
        storage()).swap(*this);
    return *this;
}

value&
value::
operator=(object const& obj)
{
    value(obj, storage()).swap(*this);
    return *this;
}

value&
value::
operator=(object&& obj)
{
    value(std::move(obj),
        storage()).swap(*this);
    return *this;
}

//----------------------------------------------------------
//
// Accessors
//
//----------------------------------------------------------

system::result<array&>
value::try_as_array() noexcept
{
    if( is_array() )
        return arr_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_array);
    return ec;
}

system::result<array const&>
value::try_as_array() const noexcept
{
    if( is_array() )
        return arr_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_array);
    return ec;
}

system::result<object&>
value::try_as_object() noexcept
{
    if( is_object() )
        return obj_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_object);
    return ec;
}

system::result<object const&>
value::try_as_object() const noexcept
{
    if( is_object() )
        return obj_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_object);
    return ec;
}

system::result<string&>
value::try_as_string() noexcept
{
    if( is_string() )
        return str_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_string);
    return ec;
}

system::result<string const&>
value::try_as_string() const noexcept
{
    if( is_string() )
        return str_;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_string);
    return ec;
}

system::result<std::int64_t&>
value::try_as_int64() noexcept
{
    if( is_int64() )
        return sca_.i;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_int64);
    return ec;
}

system::result<std::int64_t>
value::try_as_int64() const noexcept
{
    if( is_int64() )
        return sca_.i;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_int64);
    return ec;
}

system::result<std::uint64_t&>
value::try_as_uint64() noexcept
{
    if( is_uint64() )
        return sca_.u;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_uint64);
    return ec;
}

system::result<std::uint64_t>
value::try_as_uint64() const noexcept
{
    if( is_uint64() )
        return sca_.u;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_uint64);
    return ec;
}

system::result<double&>
value::try_as_double() noexcept
{
    if( is_double() )
        return sca_.d;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_double);
    return ec;
}

system::result<double>
value::try_as_double() const noexcept
{
    if( is_double() )
        return sca_.d;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_double);
    return ec;
}

system::result<bool&>
value::try_as_bool() noexcept
{
    if( is_bool() )
        return sca_.b;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_bool);
    return ec;
}

system::result<bool>
value::try_as_bool() const noexcept
{
    if( is_bool() )
        return sca_.b;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_bool);
    return ec;
}

system::result<std::nullptr_t>
value::try_as_null() const noexcept
{
    if( is_null() )
        return nullptr;

    system::error_code ec;
    BOOST_JSON_FAIL(ec, error::not_null);
    return ec;
}

boost::system::result<value&>
value::try_at(string_view key) noexcept
{
    auto r = try_as_object();
    if( !r )
        return r.error();
    return r->try_at(key);
}

boost::system::result<value const&>
value::try_at(string_view key) const noexcept
{
    auto r = try_as_object();
    if( !r )
        return r.error();
    return r->try_at(key);
}

boost::system::result<value&>
value::try_at(std::size_t pos) noexcept
{
    auto r = try_as_array();
    if( !r )
        return r.error();
    return r->try_at(pos);
}

boost::system::result<value const&>
value::try_at(std::size_t pos) const noexcept
{
    auto r = try_as_array();
    if( !r )
        return r.error();
    return r->try_at(pos);
}

object const&
value::as_object(source_location const& loc) const&
{
    return try_as_object().value(loc);
}

array const&
value::as_array(source_location const& loc) const&
{
    return try_as_array().value(loc);
}

string const&
value::as_string(source_location const& loc) const&
{
    return try_as_string().value(loc);
}

std::int64_t&
value::as_int64(source_location const& loc)
{
    return try_as_int64().value(loc);
}

std::int64_t
value::as_int64(source_location const& loc) const
{
    return try_as_int64().value(loc);
}

std::uint64_t&
value::as_uint64(source_location const& loc)
{
    return try_as_uint64().value(loc);
}

std::uint64_t
value::as_uint64(source_location const& loc) const
{
    return try_as_uint64().value(loc);
}

double&
value::as_double(source_location const& loc)
{
    return try_as_double().value(loc);
}

double
value::as_double(source_location const& loc) const
{
    return try_as_double().value(loc);
}

bool&
value::as_bool(source_location const& loc)
{
    return try_as_bool().value(loc);
}

bool
value::as_bool(source_location const& loc) const
{
    return try_as_bool().value(loc);
}

//----------------------------------------------------------
//
// Modifiers
//
//----------------------------------------------------------

string&
value::
emplace_string() noexcept
{
    return *::new(&str_) string(destroy());
}

array&
value::
emplace_array() noexcept
{
    return *::new(&arr_) array(destroy());
}

object&
value::
emplace_object() noexcept
{
    return *::new(&obj_) object(destroy());
}

void
value::
swap(value& other)
{
    if(*storage() == *other.storage())
    {
        // fast path
        union U
        {
            value tmp;
            U(){}
            ~U(){}
        };
        U u;
        relocate(&u.tmp, *this);
        relocate(this, other);
        relocate(&other, u.tmp);
        return;
    }

    // copy
    value temp1(
        std::move(*this),
        other.storage());
    value temp2(
        std::move(other),
        this->storage());
    other.~value();
    ::new(&other) value(pilfer(temp1));
    this->~value();
    ::new(this) value(pilfer(temp2));
}

std::istream&
operator>>(
    std::istream& is,
    value& jv)
{
    using Traits = std::istream::traits_type;

    // sentry prepares the stream for reading and finalizes it in destructor
    std::istream::sentry sentry(is);
    if( !sentry )
        return is;

    parse_options opts = get_parse_options( is );
    if( auto depth = static_cast<std::size_t>( is.iword(parse_depth_xalloc) ) )
        opts.max_depth = depth;

    unsigned char parser_buf[BOOST_JSON_STACK_BUFFER_SIZE / 2];
    stream_parser p( {}, opts, parser_buf );
    p.reset( jv.storage() );

    char read_buf[BOOST_JSON_STACK_BUFFER_SIZE / 2];
    std::streambuf& buf = *is.rdbuf();
    std::ios::iostate err = std::ios::goodbit;
#ifndef BOOST_NO_EXCEPTIONS
    try
#endif
    {
        while( true )
        {
            system::error_code ec;

            // we peek the buffer; this either makes sure that there's no
            // more input, or makes sure there's something in the internal
            // buffer (so in_avail will return a positive number)
            std::istream::int_type c = is.rdbuf()->sgetc();
            // if we indeed reached EOF, we check if we parsed a full JSON
            // document; if not, we error out
            if( Traits::eq_int_type(c, Traits::eof()) )
            {
                err |= std::ios::eofbit;
                p.finish(ec);
                if( ec.failed() )
                    break;
            }

            // regardless of reaching EOF, we might have parsed a full JSON
            // document; if so, we successfully finish
            if( p.done() )
            {
                jv = p.release();
                return is;
            }

            // at this point we definitely have more input, specifically in
            // buf's internal buffer; we also definitely haven't parsed a whole
            // document
            std::streamsize available = buf.in_avail();
            // if this assert fails, the streambuf is buggy
            BOOST_ASSERT( available > 0 );

            available = ( std::min )(
                static_cast<std::size_t>(available), sizeof(read_buf) );
            // we read from the internal buffer of buf into our buffer
            available = buf.sgetn( read_buf, available );

            std::size_t consumed = p.write_some(
                read_buf, static_cast<std::size_t>(available), ec );
            // if the parser hasn't consumed the entire input we've took from
            // buf, we put the remaining data back; this should succeed,
            // because we only read data from buf's internal buffer
            while( consumed++ < static_cast<std::size_t>(available) )
            {
                std::istream::int_type const status = buf.sungetc();
                BOOST_ASSERT( status != Traits::eof() );
                (void)status;
            }

            if( ec.failed() )
                break;
        }
    }
#ifndef BOOST_NO_EXCEPTIONS
    catch(...)
    {
        try
        {
            is.setstate(std::ios::badbit);
        }
        // we ignore the exception, because we need to throw the original
        // exception instead
        catch( std::ios::failure const& ) { }

        if( is.exceptions() & std::ios::badbit )
            throw;
    }
#endif

    is.setstate(err | std::ios::failbit);
    return is;
}

std::istream&
operator>>(
    std::istream& is,
    parse_options const& opts)
{
    is.iword(parse_flags_xalloc) = to_bitmask(opts);
    is.iword(parse_depth_xalloc) = static_cast<long>(opts.max_depth);
    return is;
}

//----------------------------------------------------------
//
// private
//
//----------------------------------------------------------

storage_ptr
value::
destroy() noexcept
{
    switch(kind())
    {
    case json::kind::null:
    case json::kind::bool_:
    case json::kind::int64:
    case json::kind::uint64:
    case json::kind::double_:
        break;

    case json::kind::string:
    {
        auto sp = str_.storage();
        str_.~string();
        return sp;
    }

    case json::kind::array:
    {
        auto sp = arr_.storage();
        arr_.~array();
        return sp;
    }

    case json::kind::object:
    {
        auto sp = obj_.storage();
        obj_.~object();
        return sp;
    }

    }
    return std::move(sp_);
}

bool
value::
equal(value const& other) const noexcept
{
    switch(kind())
    {
    default: // unreachable()?
    case json::kind::null:
        return other.kind() == json::kind::null;

    case json::kind::bool_:
        return
            other.kind() == json::kind::bool_ &&
            get_bool() == other.get_bool();

    case json::kind::int64:
        switch(other.kind())
        {
        case json::kind::int64:
            return get_int64() == other.get_int64();
        case json::kind::uint64:
            if(get_int64() < 0)
                return false;
            return static_cast<std::uint64_t>(
                get_int64()) == other.get_uint64();
        default:
            return false;
        }

    case json::kind::uint64:
        switch(other.kind())
        {
        case json::kind::uint64:
            return get_uint64() == other.get_uint64();
        case json::kind::int64:
            if(other.get_int64() < 0)
                return false;
            return static_cast<std::uint64_t>(
                other.get_int64()) == get_uint64();
        default:
            return false;
        }

    case json::kind::double_:
        return
            other.kind() == json::kind::double_ &&
            get_double() == other.get_double();

    case json::kind::string:
        return
            other.kind() == json::kind::string &&
            get_string() == other.get_string();

    case json::kind::array:
        return
            other.kind() == json::kind::array &&
            get_array() == other.get_array();

    case json::kind::object:
        return
            other.kind() == json::kind::object &&
            get_object() == other.get_object();
    }
}

//----------------------------------------------------------
//
// key_value_pair
//
//----------------------------------------------------------

// empty keys point here
BOOST_JSON_REQUIRE_CONST_INIT
char const
key_value_pair::empty_[1] = { 0 };

key_value_pair::
key_value_pair(
    pilfered<json::value> key,
    pilfered<json::value> value) noexcept
    : value_(value)
{
    std::size_t len;
    key_ = access::release_key(key.get(), len);
    len_ = static_cast<std::uint32_t>(len);
}

key_value_pair::
key_value_pair(
    key_value_pair const& other,
    storage_ptr sp)
    : value_(other.value_, std::move(sp))
{
    auto p = reinterpret_cast<
        char*>(value_.storage()->
            allocate(other.len_ + 1,
                alignof(char)));
    std::memcpy(
        p, other.key_, other.len_);
    len_ = other.len_;
    p[len_] = 0;
    key_ = p;
}

//----------------------------------------------------------

namespace detail
{

std::size_t
hash_value_impl( value const& jv ) noexcept
{
    std::size_t seed = 0;

    kind const k = jv.kind();
    boost::hash_combine( seed, k != kind::int64 ? k : kind::uint64 );

    visit( value_hasher{seed}, jv );
    return seed;
}

} // namespace detail
} // namespace json
} // namespace boost

//----------------------------------------------------------
//
// std::hash specialization
//
//----------------------------------------------------------

std::size_t
std::hash<::boost::json::value>::operator()(
    ::boost::json::value const& jv) const noexcept
{
    return ::boost::hash< ::boost::json::value >()( jv );
}

//----------------------------------------------------------

#endif
