// -*- C++ -*-
// -----------------------------------------------------------------------------------------------------
// Copyright (c) 2006-2020, Knut Reinert & Freie Universität Berlin
// Copyright (c) 2016-2020, Knut Reinert & MPI für molekulare Genetik
// This file may be used, modified and/or redistributed under the terms of the 3-clause BSD-License
// shipped with this file and also available at: https://github.com/seqan/seqan3/blob/master/LICENSE.md
// -----------------------------------------------------------------------------------------------------

/*!\file
 * \brief Provides std::from_chars and std::to_chars if not defined in the stl \<charconv\> header.
 * \author Svenja Mehringer <svenja.mehringer AT fu-berlin.de>
 */

#pragma once

#if __has_include(<version>)
#include <version> // from C++20 all feature macros should be defined here
#endif

#include <utility> // __cpp_lib_to_chars may be defined here as currently documented

#if __cpp_lib_to_chars >= 201611
#include <charconv>
#else // else include our own implementation below

#include <seqan3/std/concepts>
#include <sstream>

#include <seqan3/contrib/charconv/charconv>
#include <seqan3/contrib/charconv/charconv.cpp>

/*!\defgroup charconv charconv
 * \ingroup std
 * \brief The \<charconv\> header from C++17's standard library.
 */
namespace seqan3::contrib::charconv
{

//!\brief std::to_chars implementation for floating point via a std::stringstream for default base = 10.
template <std::floating_point value_type>
inline to_chars_result to_chars_floating_point(char * first, char * last, value_type value) noexcept
{
    assert(first != nullptr);
    assert(last != nullptr);

    std::ostringstream ss;
    ss << value;
    auto str = ss.str();

    if (last - first < static_cast<std::ptrdiff_t>(str.size()))
        return {last, std::errc::value_too_large};

    std::copy(str.begin(), str.end(), first);

    return {first + str.size(), std::errc{}};
}

//!\brief Delegates to functions strto[d/f/ld] for floating point value extraction.
template <std::floating_point value_type>
inline from_chars_result from_chars_floating_point(char const * first,
                                                   char const * last,
                                                   value_type & value,
                                                   chars_format fmt = chars_format::general) noexcept
{
    // The locale issue:
    // std::from_chars is documented to be locale independent. The accepted patterns
    // are identical to the one used by strtod in the defailt ("C") locale.
    //
    // The functions strto[d/f/ld] used here are locale dependent but
    // setting the locale manually by std::setlocale is not thread safe.
    // So for the time being this workaround is locale dependent.
    if (*first == '+') // + is permitted in function strto[d/f/ld] but not in from_chars
        return {last, std::errc::invalid_argument};

    float tmp{};
    ptrdiff_t constexpr buffer_size = 100;
    char buffer[buffer_size];

    if (fmt != chars_format::general)
    {
        bool exponent_is_present{false};
        for (auto it = first; it != last; ++it)
        {
            if (*it == 'e' || *it == 'E')
            {
                exponent_is_present = true;
                break;
            }
        }

        if (fmt == chars_format::scientific &&
            !exponent_is_present)
            return {last, std::errc::invalid_argument};

        if (fmt == chars_format::fixed      &&
            exponent_is_present)
            return {last, std::errc::invalid_argument};
    }


    // In contrast to std::from_chars, std::strto[f/d/ld] does not treat the second
    // parameter (str_end) as "end of the sequence to parse" but merely as an out
    // parameter to indicate where the parsing ended. Therefore, if [last] does
    // not point to the end of a null-terminated string, a buffer is needed to
    // represent the truncated sequence and ensure correct from_chars functionality.
    char * start;

    if ((*last != '\0' ) || fmt == chars_format::hex)
    {
        // If hex format is explicitly expected, the 0x prefix is not allowed in the
        // the original sequence according to the std::from_chars cppreference
        // documentation.
        // In order to use strto[f/d/ld], the prefix must be prepended to achieve
        // correct parsing. This will also automatically lead to an error if the
        // original sequence did contain a 0x prefix and thus reflect the correct
        // requirements of std::from_chars.
        ptrdiff_t offset{0};
        if (fmt == chars_format::hex)
        {
            buffer[0] = '0';
            buffer[1] = 'x';
            offset = 2;
        }

        std::copy(first, last, &buffer[offset]);
        buffer[std::min<ptrdiff_t>(buffer_size - offset, last - first)] = '\0';

        start = &buffer[0];
    }
    else
    {
        start = const_cast<char *>(first);
    }

    char * end;

    if constexpr (std::same_as<std::remove_reference_t<value_type>, float>)
    {
        tmp = strtof(start, &end);
    }
    if constexpr (std::same_as<std::remove_reference_t<value_type>, double>)
    {
        tmp = strtod(start, &end);
    }
    if constexpr (std::same_as<std::remove_reference_t<value_type>, long double>)
    {
        tmp = strtold(start, &end);
    }

    last = first + (end - start);

    if (errno == ERANGE)
    {
        return {last, std::errc::result_out_of_range};
    }
    else if (tmp == 0 && end == start)
    {
        return {last, std::errc::invalid_argument};
    }

    // Success.
    value = tmp;
    return {last, {}};
}

} // namespace seqan3::contrib::charconv

namespace seqan3::contrib::charconv
{
// -----------------------------------------------------------------------------
// to_chars for floating point types
// -----------------------------------------------------------------------------

//!\brief std::to_chars overload for floating point via a std::stringstream for default base = 10.
//!\ingroup charconv
template <std::floating_point floating_point_type>
inline to_chars_result to_chars(char * first, char * last, floating_point_type value) noexcept
{
    return to_chars_floating_point(first, last, value);
}

// -----------------------------------------------------------------------------
// from_chars for floating point types
// -----------------------------------------------------------------------------

/*!\brief Parse a char sequence into an floating point value.
 * \ingroup charconv
 * \tparam value_type The type to parse the string into; Must model std::integral.
 * \param[in]      first The start of the string to parse.
 * \param[in]      last  The end of the string to parse.
 * \param[in, out] value The value to store the parsed result in.
 * \param[in]      fmt   The std::chars_format that alters the behaviour of parsing.
 * \returns A std::from_char_result. See detail section return value for more information.
 *
 * \details
 *
 * Analyzes the character sequence [first,last) for a pattern described below.
 * If no characters match the pattern or if the value obtained by parsing the
 * matched characters is not representable in the type of value, value is
 * unmodified, otherwise the characters matching the pattern are interpreted as
 * a text representation of an arithmetic value, which is stored in value.
 *
 * Floating-point parsers: Expects the pattern identical to the one used by
 * std::strtod in the default ("C") locale, except that:
 *
 * - the plus sign is not recognized outside of the exponent (only the minus
 *   sign is permitted at the beginning)
 * - if fmt has std::chars_format::scientific set but not std::chars_format::fixed,
 *   the exponent part is required (otherwise it is optional)
 * - if fmt has std::chars_format::fixed set but not std::chars_format::scientific,
 *   the optional exponent is not permitted
 * - if fmt is std::chars_format::hex, the prefix "0x" or "0X" is not permitted
 *   (the string "0x123" parses as the value "0" with unparsed remainder "x123").
 *
 * \attention This implementation is a workaround until the function is supported
 *            by the compiler. It falls back to use the functions strto[d/f/ld]
 *            before checking the above limitations
 *
 * ### Return value
 * This function is workaround until the function is supported
 * by the compiler. It falls back to use the functions strto[d/f/ld] so the
 * return value is NOT as documented here https://en.cppreference.com/w/cpp/utility/from_chars
 * but:
 *
 * On success, std::from_chars_result::ec is value-initialized. On error,
 * std::from_chars_result::ec is either an
 * std::errc::invalid_argument if an illegal character or format has been
 * encountered, or std::errc::out_of_range if parsing the value would cause an
 * overflow. The std::from_chars_result::ptr value is always set to last.
 *
 * ### The locale issue
 * std::from_chars is documented to be locale independent. The accepted patterns
 * are identical to the one used by strtod in the defailt ("C") locale.
 *
 * The functions strto[d/f/ld] used here are locale dependent but
 * setting the locale manually by std::setlocale is not thread safe.
 * So for the time being this workaround is locale dependent.
 *
 * \sa https://en.cppreference.com/w/cpp/utility/from_chars
 */
template <std::floating_point floating_point_type>
inline from_chars_result from_chars(char const * first,
                                    char const * last,
                                    floating_point_type & value,
                                    chars_format fmt = chars_format::general) noexcept
{
    return from_chars_floating_point(first, last, value, fmt);
}
} // namespace seqan3::contrib::charconv

namespace std
{
namespace
{
using seqan3::contrib::charconv::to_chars;
using seqan3::contrib::charconv::to_chars_result;
using seqan3::contrib::charconv::from_chars;
using seqan3::contrib::charconv::from_chars_result;
using seqan3::contrib::charconv::chars_format;
} // anonymous namespace
} // namespace std

#endif // __cpp_lib_to_chars >= 201611
