// Copyright 2013 Daniel Parker
// Distributed under the Boost license, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// See https://github.com/danielaparker/jsoncons for latest version

#ifndef JSONCONS_DETAIL_WRITE_NUMBER_HPP
#define JSONCONS_DETAIL_WRITE_NUMBER_HPP

#include <stdexcept>
#include <string>
#include <cmath>
#include <locale>
#include <limits> // std::numeric_limits
#include <exception>
#include <stdio.h> // snprintf
#include <jsoncons/config/jsoncons_config.hpp>
#include <jsoncons/json_options.hpp>
#include <jsoncons/detail/grisu3.hpp>
#include <jsoncons/detail/parse_number.hpp>
#include <jsoncons/more_type_traits.hpp>

namespace jsoncons { 
namespace detail {

    inline
    char to_hex_character(uint8_t c)
    {
        return (char)((c < 10) ? ('0' + c) : ('A' - 10 + c));
    }

    // from_integer

    template<class Integer,class Result>
    typename std::enable_if<type_traits::is_integer<Integer>::value,std::size_t>::type
    from_integer(Integer value, Result& result)
    {
        using char_type = typename Result::value_type;

        char_type buf[255];
        char_type *p = buf;
        const char_type* last = buf+255;

        bool is_negative = value < 0;

        if (value < 0)
        {
            do
            {
                *p++ = static_cast<char_type>(48 - (value % 10));
            }
            while ((value /= 10) && (p < last));
        }
        else
        {

            do
            {
                *p++ = static_cast<char_type>(48 + value % 10);
            }
            while ((value /= 10) && (p < last));
        }
        JSONCONS_ASSERT(p != last);

        std::size_t count = (p - buf);
        if (is_negative)
        {
            result.push_back('-');
            ++count;
        }
        while (--p >= buf)
        {
            result.push_back(*p);
        }

        return count;
    }

    // integer_to_string_hex

    template<class Integer,class Result>
    typename std::enable_if<type_traits::is_integer<Integer>::value,std::size_t>::type
    integer_to_string_hex(Integer value, Result& result)
    {
        using char_type = typename Result::value_type;

        char_type buf[255];
        char_type *p = buf;
        const char_type* last = buf+255;

        bool is_negative = value < 0;

        if (value < 0)
        {
            do
            {
                *p++ = to_hex_character(0-(value % 16));
            }
            while ((value /= 16) && (p < last));
        }
        else
        {

            do
            {
                *p++ = to_hex_character(value % 16);
            }
            while ((value /= 16) && (p < last));
        }
        JSONCONS_ASSERT(p != last);

        std::size_t count = (p - buf);
        if (is_negative)
        {
            result.push_back('-');
            ++count;
        }
        while (--p >= buf)
        {
            result.push_back(*p);
        }

        return count;
    }

    // write_double

    // fast exponent
    template <class Result>
    void fill_exponent(int K, Result& result)
    {
        if (K < 0)
        {
            result.push_back('-');
            K = -K;
        }
        else
        {
            result.push_back('+'); // compatibility with sprintf
        }

        if (K < 10)
        {
            result.push_back('0'); // compatibility with sprintf
            result.push_back((char)('0' + K));
        }
        else if (K < 100)
        {
            result.push_back((char)('0' + K / 10)); K %= 10;
            result.push_back((char)('0' + K));
        }
        else if (K < 1000)
        {
            result.push_back((char)('0' + K / 100)); K %= 100;
            result.push_back((char)('0' + K / 10)); K %= 10;
            result.push_back((char)('0' + K));
        }
        else
        {
            jsoncons::detail::from_integer(K, result);
        }
    }

    template <class Result>
    void prettify_string(const char *buffer, std::size_t length, int k, int min_exp, int max_exp, Result& result)
    {
        int nb_digits = (int)length;
        int offset;
        /* v = buffer * 10^k
           kk is such that 10^(kk-1) <= v < 10^kk
           this way kk gives the position of the decimal point.
        */
        int kk = nb_digits + k;

        if (nb_digits <= kk && kk <= max_exp)
        {
            /* the first digits are already in. Add some 0s and call it a day. */
            /* the max_exp is a personal choice. Only 16 digits could possibly be relevant.
             * Basically we want to print 12340000000 rather than 1234.0e7 or 1.234e10 */
            for (int i = 0; i < nb_digits; ++i)
            {
                result.push_back(buffer[i]);
            }
            for (int i = nb_digits; i < kk; ++i)
            {
                result.push_back('0');
            }
            result.push_back('.');
            result.push_back('0');
        } 
        else if (0 < kk && kk <= max_exp)
        {
            /* comma number. Just insert a '.' at the correct location. */
            for (int i = 0; i < kk; ++i)
            {
                result.push_back(buffer[i]);
            }
            result.push_back('.');
            for (int i = kk; i < nb_digits; ++i)
            {
                result.push_back(buffer[i]);
            }
        } 
        else if (min_exp < kk && kk <= 0)
        {
            offset = 2 - kk;

            result.push_back('0');
            result.push_back('.');
            for (int i = 2; i < offset; ++i) 
                result.push_back('0');
            for (int i = 0; i < nb_digits; ++i)
            {
                result.push_back(buffer[i]);
            }
        } 
        else if (nb_digits == 1)
        {
            result.push_back(buffer[0]);
            result.push_back('e');
            fill_exponent(kk - 1, result);
        } 
        else
        {
            result.push_back(buffer[0]);
            result.push_back('.');
            for (int i = 1; i < nb_digits; ++i)
            {
                result.push_back(buffer[i]);
            }
            result.push_back('e');
            fill_exponent(kk - 1, result);
        }
    }

    template<class Result>
    void dump_buffer(const char *buffer, std::size_t length, char decimal_point, Result& result)
    {
        const char *sbeg = buffer;
        const char *send = sbeg + length;

        if (sbeg != send)
        {
            bool needs_dot = true;
            for (const char* q = sbeg; q < send; ++q)
            {
                switch (*q)
                {
                case '-':
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '+':
                    result.push_back(*q);
                    break;
                case 'e':
                case 'E':
                    result.push_back('e');
                    needs_dot = false;
                    break;
                default:
                    if (*q == decimal_point)
                    {
                        needs_dot = false;
                        result.push_back('.');
                    }
                    break;
                }
            }
            if (needs_dot)
            {
                result.push_back('.');
                result.push_back('0');
                needs_dot = true;
            }
        }
    }

    template<class Result>
    bool dtoa_scientific(double val, char decimal_point, Result& result)
    {
        if (val == 0)
        {
            result.push_back('0');
            result.push_back('.');
            result.push_back('0');
            return true;
        }

        jsoncons::detail::chars_to to_double_;

        char buffer[100];
        int precision = std::numeric_limits<double>::digits10;
        int length = snprintf(buffer, sizeof(buffer), "%1.*e", precision, val);
        if (length < 0)
        {
            return false;
        }
        if (to_double_(buffer, sizeof(buffer)) != val)
        {
            const int precision2 = std::numeric_limits<double>::max_digits10;
            length = snprintf(buffer, sizeof(buffer), "%1.*e", precision2, val);
            if (length < 0)
            {
                return false;
            }
        }
        dump_buffer(buffer, static_cast<std::size_t>(length), decimal_point, result);
        return true;
    }

    template<class Result>
    bool dtoa_general(double val, char decimal_point, Result& result, std::false_type)
    {
        if (val == 0)
        {
            result.push_back('0');
            result.push_back('.');
            result.push_back('0');
            return true;
        }

        jsoncons::detail::chars_to to_double_;

        char buffer[100];
        int precision = std::numeric_limits<double>::digits10;
        int length = snprintf(buffer, sizeof(buffer), "%1.*g", precision, val);
        if (length < 0)
        {
            return false;
        }
        if (to_double_(buffer, sizeof(buffer)) != val)
        {
            const int precision2 = std::numeric_limits<double>::max_digits10;
            length = snprintf(buffer, sizeof(buffer), "%1.*g", precision2, val);
            if (length < 0)
            {
                return false;
            }
        }
        dump_buffer(buffer, length, decimal_point, result);
        return true;
    }

    template<class Result>
    bool dtoa_general(double v, char decimal_point, Result& result, std::true_type)
    {
        if (v == 0)
        {
            result.push_back('0');
            result.push_back('.');
            result.push_back('0');
            return true;
        }

        int length = 0;
        int k;

        char buffer[100];

        double u = std::signbit(v) ? -v : v;
        if (jsoncons::detail::grisu3(u, buffer, &length, &k))
        {
            if (std::signbit(v))
            {
                result.push_back('-');
            }
            // min exp: -4 is consistent with sprintf
            // max exp: std::numeric_limits<double>::max_digits10
            jsoncons::detail::prettify_string(buffer, length, k, -4, std::numeric_limits<double>::max_digits10, result);
            return true;
        }
        else
        {
            return dtoa_general(v, decimal_point, result, std::false_type());
        }
    }

    template<class Result>
    bool dtoa_fixed(double val, char decimal_point, Result& result, std::false_type)
    {
        if (val == 0)
        {
            result.push_back('0');
            result.push_back('.');
            result.push_back('0');
            return true;
        }

        jsoncons::detail::chars_to to_double_;

        char buffer[100];
        int precision = std::numeric_limits<double>::digits10;
        int length = snprintf(buffer, sizeof(buffer), "%1.*f", precision, val);
        if (length < 0)
        {
            return false;
        }
        if (to_double_(buffer, sizeof(buffer)) != val)
        {
            const int precision2 = std::numeric_limits<double>::max_digits10;
            length = snprintf(buffer, sizeof(buffer), "%1.*f", precision2, val);
            if (length < 0)
            {
                return false;
            }
        }
        dump_buffer(buffer, length, decimal_point, result);
        return true;
    }

    template<class Result>
    bool dtoa_fixed(double v, char decimal_point, Result& result, std::true_type)
    {
        if (v == 0)
        {
            result.push_back('0');
            result.push_back('.');
            result.push_back('0');
            return true;
        }

        int length = 0;
        int k;

        char buffer[100];

        double u = std::signbit(v) ? -v : v;
        if (jsoncons::detail::grisu3(u, buffer, &length, &k))
        {
            if (std::signbit(v))
            {
                result.push_back('-');
            }
            jsoncons::detail::prettify_string(buffer, length, k, std::numeric_limits<int>::lowest(), (std::numeric_limits<int>::max)(), result);
            return true;
        }
        else
        {
            return dtoa_fixed(v, decimal_point, result, std::false_type());
        }
    }

    template<class Result>
    bool dtoa_fixed(double v, char decimal_point, Result& result)
    {
        return dtoa_fixed(v, decimal_point, result, std::integral_constant<bool, std::numeric_limits<double>::is_iec559>());
    }

    template<class Result>
    bool dtoa_general(double v, char decimal_point, Result& result)
    {
        return dtoa_general(v, decimal_point, result, std::integral_constant<bool, std::numeric_limits<double>::is_iec559>());
    }

    class write_double
    {
    private:
        chars_to to_double_;
        float_chars_format float_format_;
        int precision_;
        char decimal_point_;
    public:
        write_double(float_chars_format float_format, int precision)
           : float_format_(float_format), precision_(precision), decimal_point_('.')
        {
    #if !defined(JSONCONS_NO_LOCALECONV)
            struct lconv *lc = localeconv();
            if (lc != nullptr && lc->decimal_point[0] != 0)
            {
                decimal_point_ = lc->decimal_point[0];
            }
    #endif
        }
        write_double(const write_double&) = default;

        write_double& operator=(const write_double&) = default;

        template<class Result>
        std::size_t operator()(double val, Result& result)
        {
            std::size_t count = 0;

            char number_buffer[200];
            int length = 0;

            switch (float_format_)
            {
            case float_chars_format::fixed:
                {
                    if (precision_ > 0)
                    {
                        length = snprintf(number_buffer, sizeof(number_buffer), "%1.*f", precision_, val);
                        if (length < 0)
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                        dump_buffer(number_buffer, length, decimal_point_, result);
                    }
                    else
                    {
                        if (!dtoa_fixed(val, decimal_point_, result))
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                    }
                }
                break;
            case float_chars_format::scientific:
                {
                    if (precision_ > 0)
                    {
                        length = snprintf(number_buffer, sizeof(number_buffer), "%1.*e", precision_, val);
                        if (length < 0)
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                        dump_buffer(number_buffer, length, decimal_point_, result);
                    }
                    else
                    {
                        if (!dtoa_scientific(val, decimal_point_, result))
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                    }
                }
                break;
            case float_chars_format::general:
                {
                    if (precision_ > 0)
                    {
                        length = snprintf(number_buffer, sizeof(number_buffer), "%1.*g", precision_, val);
                        if (length < 0)
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                        dump_buffer(number_buffer, length, decimal_point_, result);
                    }
                    else
                    {
                        if (!dtoa_general(val, decimal_point_, result))
                        {
                            JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                        }
                    }             
                    break;
                }
                default:
                    JSONCONS_THROW(json_runtime_error<std::invalid_argument>("write_double failed."));
                    break;
            }
            return count;
        }
    };

} // namespace detail
} // namespace jsoncons

#endif