/**
 * @file cpptoml.h
 * @author Chase Geigle
 * @date May 2013
 */

#ifndef CPPTOML_H
#define CPPTOML_H

#include <algorithm>
#include <cassert>
#include <clocale>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

#if __cplusplus > 201103L
#define CPPTOML_DEPRECATED(reason) [[deprecated(reason)]]
#elif defined(__clang__)
#define CPPTOML_DEPRECATED(reason) __attribute__((deprecated(reason)))
#elif defined(__GNUG__)
#define CPPTOML_DEPRECATED(reason) __attribute__((deprecated))
#elif defined(_MSC_VER)
#if _MSC_VER < 1910
#define CPPTOML_DEPRECATED(reason) __declspec(deprecated)
#else
#define CPPTOML_DEPRECATED(reason) [[deprecated(reason)]]
#endif
#endif

namespace cpptoml
{
    class writer; // forward declaration
    class base;   // forward declaration
#if defined(CPPTOML_USE_MAP)
    // a std::map will ensure that entries a sorted, albeit at a slight
    // performance penalty relative to the (default) unordered_map
    using string_to_base_map = std::map<std::string, std::shared_ptr<base>>;
#else
    // by default an unordered_map is used for best performance as the
    // toml specification does not require entries to be sorted
    using string_to_base_map = std::unordered_map<std::string, std::shared_ptr<base>>;
#endif

    // if defined, `base` will retain type information in form of an enum class
    // such that static_cast can be used instead of dynamic_cast
    // #define CPPTOML_NO_RTTI

    template <class T>
    class option
    {
    public:
        option() : empty_{true}
        {
            // nothing
        }

        option(T value) : empty_{false}, value_(std::move(value))
        {
            // nothing
        }

        explicit operator bool() const
        {
            return !empty_;
        }

        const T &operator*() const
        {
            return value_;
        }

        const T *operator->() const
        {
            return &value_;
        }

        template <class U>
        T value_or(U &&alternative) const
        {
            if (!empty_)
                return value_;
            return static_cast<T>(std::forward<U>(alternative));
        }

    private:
        bool empty_;
        T value_;
    };

    struct local_date
    {
        int year = 0;
        int month = 0;
        int day = 0;
    };

    struct local_time
    {
        int hour = 0;
        int minute = 0;
        int second = 0;
        int microsecond = 0;
    };

    struct zone_offset
    {
        int hour_offset = 0;
        int minute_offset = 0;
    };

    struct local_datetime : local_date, local_time
    {
    };

    struct offset_datetime : local_datetime, zone_offset
    {
        static inline struct offset_datetime from_zoned(const struct tm &t)
        {
            offset_datetime dt;
            dt.year = t.tm_year + 1900;
            dt.month = t.tm_mon + 1;
            dt.day = t.tm_mday;
            dt.hour = t.tm_hour;
            dt.minute = t.tm_min;
            dt.second = t.tm_sec;

            char buf[16];
            strftime(buf, 16, "%z", &t);

            int offset = std::stoi(buf);
            dt.hour_offset = offset / 100;
            dt.minute_offset = offset % 100;
            return dt;
        }

        CPPTOML_DEPRECATED("from_local has been renamed to from_zoned")
        static inline struct offset_datetime from_local(const struct tm &t)
        {
            return from_zoned(t);
        }

        static inline struct offset_datetime from_utc(const struct tm &t)
        {
            offset_datetime dt;
            dt.year = t.tm_year + 1900;
            dt.month = t.tm_mon + 1;
            dt.day = t.tm_mday;
            dt.hour = t.tm_hour;
            dt.minute = t.tm_min;
            dt.second = t.tm_sec;
            return dt;
        }
    };

    CPPTOML_DEPRECATED("datetime has been renamed to offset_datetime")
    typedef offset_datetime datetime;

    class fill_guard
    {
    public:
        fill_guard(std::ostream &os) : os_(os), fill_{os.fill()}
        {
            // nothing
        }

        ~fill_guard()
        {
            os_.fill(fill_);
        }

    private:
        std::ostream &os_;
        std::ostream::char_type fill_;
    };

    inline std::ostream &operator<<(std::ostream &os, const local_date &dt)
    {
        fill_guard g{os};
        os.fill('0');

        using std::setw;
        os << setw(4) << dt.year << "-" << setw(2) << dt.month << "-" << setw(2)
           << dt.day;

        return os;
    }

    inline std::ostream &operator<<(std::ostream &os, const local_time &ltime)
    {
        fill_guard g{os};
        os.fill('0');

        using std::setw;
        os << setw(2) << ltime.hour << ":" << setw(2) << ltime.minute << ":"
           << setw(2) << ltime.second;

        if (ltime.microsecond > 0)
        {
            os << ".";
            int power = 100000;
            for (int curr_us = ltime.microsecond; curr_us; power /= 10)
            {
                auto num = curr_us / power;
                os << num;
                curr_us -= num * power;
            }
        }

        return os;
    }

    inline std::ostream &operator<<(std::ostream &os, const zone_offset &zo)
    {
        fill_guard g{os};
        os.fill('0');

        using std::setw;

        if (zo.hour_offset != 0 || zo.minute_offset != 0)
        {
            if (zo.hour_offset > 0)
            {
                os << "+";
            }
            else
            {
                os << "-";
            }
            os << setw(2) << std::abs(zo.hour_offset) << ":" << setw(2)
               << std::abs(zo.minute_offset);
        }
        else
        {
            os << "Z";
        }

        return os;
    }

    inline std::ostream &operator<<(std::ostream &os, const local_datetime &dt)
    {
        return os << static_cast<const local_date &>(dt) << "T"
                  << static_cast<const local_time &>(dt);
    }

    inline std::ostream &operator<<(std::ostream &os, const offset_datetime &dt)
    {
        return os << static_cast<const local_datetime &>(dt)
                  << static_cast<const zone_offset &>(dt);
    }

    template <class T, class... Ts>
    struct is_one_of;

    template <class T, class V>
    struct is_one_of<T, V> : std::is_same<T, V>
    {
    };

    template <class T, class V, class... Ts>
    struct is_one_of<T, V, Ts...>
    {
        const static bool value = std::is_same<T, V>::value || is_one_of<T, Ts...>::value;
    };

    template <class T>
    class value;

    template <class T>
    struct valid_value
        : is_one_of<T, std::string, int64_t, double, bool, local_date, local_time,
                    local_datetime, offset_datetime>
    {
    };

    template <class T, class Enable = void>
    struct value_traits;

    template <class T>
    struct valid_value_or_string_convertible
    {

        const static bool value = valid_value<typename std::decay<T>::type>::value || std::is_convertible<T, std::string>::value;
    };

    template <class T>
    struct value_traits<T, typename std::enable_if<
                               valid_value_or_string_convertible<T>::value>::type>
    {
        using value_type = typename std::conditional<
            valid_value<typename std::decay<T>::type>::value,
            typename std::decay<T>::type, std::string>::type;

        using type = value<value_type>;

        static value_type construct(T &&val)
        {
            return value_type(val);
        }
    };

    template <class T>
    struct value_traits<
        T,
        typename std::enable_if<
            !valid_value_or_string_convertible<T>::value && std::is_floating_point<typename std::decay<T>::type>::value>::type>
    {
        using value_type = typename std::decay<T>::type;

        using type = value<double>;

        static value_type construct(T &&val)
        {
            return value_type(val);
        }
    };

    template <class T>
    struct value_traits<
        T, typename std::enable_if<
               !valid_value_or_string_convertible<T>::value && !std::is_floating_point<typename std::decay<T>::type>::value && std::is_signed<typename std::decay<T>::type>::value>::type>
    {
        using value_type = int64_t;

        using type = value<int64_t>;

        static value_type construct(T &&val)
        {
            if (val < (std::numeric_limits<int64_t>::min)())
                throw std::underflow_error{"constructed value cannot be "
                                           "represented by a 64-bit signed "
                                           "integer"};

            if (val > (std::numeric_limits<int64_t>::max)())
                throw std::overflow_error{"constructed value cannot be represented "
                                          "by a 64-bit signed integer"};

            return static_cast<int64_t>(val);
        }
    };

    template <class T>
    struct value_traits<
        T, typename std::enable_if<
               !valid_value_or_string_convertible<T>::value && std::is_unsigned<typename std::decay<T>::type>::value>::type>
    {
        using value_type = int64_t;

        using type = value<int64_t>;

        static value_type construct(T &&val)
        {
            if (val > static_cast<uint64_t>((std::numeric_limits<int64_t>::max)()))
                throw std::overflow_error{"constructed value cannot be represented "
                                          "by a 64-bit signed integer"};

            return static_cast<int64_t>(val);
        }
    };

    class array;
    class table;
    class table_array;

    template <class T>
    struct array_of_trait
    {
        using return_type = option<std::vector<T>>;
    };

    template <>
    struct array_of_trait<array>
    {
        using return_type = option<std::vector<std::shared_ptr<array>>>;
    };

    template <class T>
    inline std::shared_ptr<typename value_traits<T>::type> make_value(T &&val);
    inline std::shared_ptr<array> make_array();

    namespace detail
    {
        template <class T>
        inline std::shared_ptr<T> make_element();
    }

    inline std::shared_ptr<table> make_table();
    inline std::shared_ptr<table_array> make_table_array(bool is_inline = false);

#if defined(CPPTOML_NO_RTTI)
    /// Base type used to store underlying data type explicitly if RTTI is disabled
    enum class base_type
    {
        NONE,
        STRING,
        LOCAL_TIME,
        LOCAL_DATE,
        LOCAL_DATETIME,
        OFFSET_DATETIME,
        INT,
        FLOAT,
        BOOL,
        TABLE,
        ARRAY,
        TABLE_ARRAY
    };

    /// Type traits class to convert C++ types to enum member
    template <class T>
    struct base_type_traits;

    template <>
    struct base_type_traits<std::string>
    {
        static const base_type type = base_type::STRING;
    };

    template <>
    struct base_type_traits<local_time>
    {
        static const base_type type = base_type::LOCAL_TIME;
    };

    template <>
    struct base_type_traits<local_date>
    {
        static const base_type type = base_type::LOCAL_DATE;
    };

    template <>
    struct base_type_traits<local_datetime>
    {
        static const base_type type = base_type::LOCAL_DATETIME;
    };

    template <>
    struct base_type_traits<offset_datetime>
    {
        static const base_type type = base_type::OFFSET_DATETIME;
    };

    template <>
    struct base_type_traits<int64_t>
    {
        static const base_type type = base_type::INT;
    };

    template <>
    struct base_type_traits<double>
    {
        static const base_type type = base_type::FLOAT;
    };

    template <>
    struct base_type_traits<bool>
    {
        static const base_type type = base_type::BOOL;
    };

    template <>
    struct base_type_traits<table>
    {
        static const base_type type = base_type::TABLE;
    };

    template <>
    struct base_type_traits<array>
    {
        static const base_type type = base_type::ARRAY;
    };

    template <>
    struct base_type_traits<table_array>
    {
        static const base_type type = base_type::TABLE_ARRAY;
    };
#endif

    /**
     * A generic base TOML value used for type erasure.
     */
    class base : public std::enable_shared_from_this<base>
    {
    public:
        virtual ~base() = default;

        virtual std::shared_ptr<base> clone() const = 0;

        /**
         * Determines if the given TOML element is a value.
         */
        virtual bool is_value() const
        {
            return false;
        }

        /**
         * Determines if the given TOML element is a table.
         */
        virtual bool is_table() const
        {
            return false;
        }

        /**
         * Converts the TOML element into a table.
         */
        std::shared_ptr<table> as_table()
        {
            if (is_table())
                return std::static_pointer_cast<table>(shared_from_this());
            return nullptr;
        }
        /**
         * Determines if the TOML element is an array of "leaf" elements.
         */
        virtual bool is_array() const
        {
            return false;
        }

        /**
         * Converts the TOML element to an array.
         */
        std::shared_ptr<array> as_array()
        {
            if (is_array())
                return std::static_pointer_cast<array>(shared_from_this());
            return nullptr;
        }

        /**
         * Determines if the given TOML element is an array of tables.
         */
        virtual bool is_table_array() const
        {
            return false;
        }

        /**
         * Converts the TOML element into a table array.
         */
        std::shared_ptr<table_array> as_table_array()
        {
            if (is_table_array())
                return std::static_pointer_cast<table_array>(shared_from_this());
            return nullptr;
        }

        /**
         * Attempts to coerce the TOML element into a concrete TOML value
         * of type T.
         */
        template <class T>
        std::shared_ptr<value<T>> as();

        template <class T>
        std::shared_ptr<const value<T>> as() const;

        template <class Visitor, class... Args>
        void accept(Visitor &&visitor, Args &&...args) const;

#if defined(CPPTOML_NO_RTTI)
        base_type type() const
        {
            return type_;
        }

    protected:
        base(const base_type t) : type_(t)
        {
            // nothing
        }

    private:
        const base_type type_ = base_type::NONE;

#else
    protected:
        base()
        {
            // nothing
        }
#endif
    };

    /**
     * A concrete TOML value representing the "leaves" of the "tree".
     */
    template <class T>
    class value : public base
    {
        struct make_shared_enabler
        {
            // nothing; this is a private key accessible only to friends
        };

        template <class U>
        friend std::shared_ptr<typename value_traits<U>::type>
        cpptoml::make_value(U &&val);

    public:
        static_assert(valid_value<T>::value, "invalid value type");

        std::shared_ptr<base> clone() const override;

        value(const make_shared_enabler &, const T &val) : value(val)
        {
            // nothing; note that users cannot actually invoke this function
            // because they lack access to the make_shared_enabler.
        }

        bool is_value() const override
        {
            return true;
        }

        /**
         * Gets the data associated with this value.
         */
        T &get()
        {
            return data_;
        }

        /**
         * Gets the data associated with this value. Const version.
         */
        const T &get() const
        {
            return data_;
        }

    private:
        T data_;

        /**
         * Constructs a value from the given data.
         */
#if defined(CPPTOML_NO_RTTI)
        value(const T &val) : base(base_type_traits<T>::type), data_(val)
        {
        }
#else
        value(const T &val) : data_(val)
        {
        }
#endif

        value(const value &val) = delete;
        value &operator=(const value &val) = delete;
    };

    template <class T>
    std::shared_ptr<typename value_traits<T>::type> make_value(T &&val)
    {
        using value_type = typename value_traits<T>::type;
        using enabler = typename value_type::make_shared_enabler;
        return std::make_shared<value_type>(
            enabler{}, value_traits<T>::construct(std::forward<T>(val)));
    }

    template <class T>
    inline std::shared_ptr<value<T>> base::as()
    {
#if defined(CPPTOML_NO_RTTI)
        if (type() == base_type_traits<T>::type)
            return std::static_pointer_cast<value<T>>(shared_from_this());
        else
            return nullptr;
#else
        return std::dynamic_pointer_cast<value<T>>(shared_from_this());
#endif
    }

    // special case value<double> to allow getting an integer parameter as a
    // double value
    template <>
    inline std::shared_ptr<value<double>> base::as()
    {
#if defined(CPPTOML_NO_RTTI)
        if (type() == base_type::FLOAT)
            return std::static_pointer_cast<value<double>>(shared_from_this());

        if (type() == base_type::INT)
        {
            auto v = std::static_pointer_cast<value<int64_t>>(shared_from_this());
            return make_value<double>(static_cast<double>(v->get()));
        }
#else
        if (auto v = std::dynamic_pointer_cast<value<double>>(shared_from_this()))
            return v;

        if (auto v = std::dynamic_pointer_cast<value<int64_t>>(shared_from_this()))
            return make_value<double>(static_cast<double>(v->get()));
#endif

        return nullptr;
    }

    template <class T>
    inline std::shared_ptr<const value<T>> base::as() const
    {
#if defined(CPPTOML_NO_RTTI)
        if (type() == base_type_traits<T>::type)
            return std::static_pointer_cast<const value<T>>(shared_from_this());
        else
            return nullptr;
#else
        return std::dynamic_pointer_cast<const value<T>>(shared_from_this());
#endif
    }

    // special case value<double> to allow getting an integer parameter as a
    // double value
    template <>
    inline std::shared_ptr<const value<double>> base::as() const
    {
#if defined(CPPTOML_NO_RTTI)
        if (type() == base_type::FLOAT)
            return std::static_pointer_cast<const value<double>>(
                shared_from_this());

        if (type() == base_type::INT)
        {
            auto v = as<int64_t>();
            // the below has to be a non-const value<double> due to a bug in
            // libc++: https://llvm.org/bugs/show_bug.cgi?id=18843
            return make_value<double>(static_cast<double>(v->get()));
        }
#else
        if (auto v = std::dynamic_pointer_cast<const value<double>>(shared_from_this()))
            return v;

        if (auto v = as<int64_t>())
        {
            // the below has to be a non-const value<double> due to a bug in
            // libc++: https://llvm.org/bugs/show_bug.cgi?id=18843
            return make_value<double>(static_cast<double>(v->get()));
        }
#endif

        return nullptr;
    }

    /**
     * Exception class for array insertion errors.
     */
    class array_exception : public std::runtime_error
    {
    public:
        array_exception(const std::string &err) : std::runtime_error{err}
        {
        }
    };

    class array : public base
    {
    public:
        friend std::shared_ptr<array> make_array();

        std::shared_ptr<base> clone() const override;

        virtual bool is_array() const override
        {
            return true;
        }

        using size_type = std::size_t;

        /**
         * arrays can be iterated over
         */
        using iterator = std::vector<std::shared_ptr<base>>::iterator;

        /**
         * arrays can be iterated over.  Const version.
         */
        using const_iterator = std::vector<std::shared_ptr<base>>::const_iterator;

        iterator begin()
        {
            return values_.begin();
        }

        const_iterator begin() const
        {
            return values_.begin();
        }

        iterator end()
        {
            return values_.end();
        }

        const_iterator end() const
        {
            return values_.end();
        }

        /**
         * Obtains the array (vector) of base values.
         */
        std::vector<std::shared_ptr<base>> &get()
        {
            return values_;
        }

        /**
         * Obtains the array (vector) of base values. Const version.
         */
        const std::vector<std::shared_ptr<base>> &get() const
        {
            return values_;
        }

        std::shared_ptr<base> at(size_t idx) const
        {
            return values_.at(idx);
        }

        /**
         * Obtains an array of value<T>s. Note that elements may be
         * nullptr if they cannot be converted to a value<T>.
         */
        template <class T>
        std::vector<std::shared_ptr<value<T>>> array_of() const
        {
            std::vector<std::shared_ptr<value<T>>> result(values_.size());

            std::transform(values_.begin(), values_.end(), result.begin(),
                           [&](std::shared_ptr<base> v)
                           {
                               return v->as<T>();
                           });

            return result;
        }

        /**
         * Obtains a option<vector<T>>. The option will be empty if the array
         * contains values that are not of type T.
         */
        template <class T>
        inline typename array_of_trait<T>::return_type get_array_of() const
        {
            std::vector<T> result;
            result.reserve(values_.size());

            for (const auto &val : values_)
            {
                if (auto v = val->as<T>())
                    result.push_back(v->get());
                else
                    return {};
            }

            return {std::move(result)};
        }

        /**
         * Obtains an array of arrays. Note that elements may be nullptr
         * if they cannot be converted to a array.
         */
        std::vector<std::shared_ptr<array>> nested_array() const
        {
            std::vector<std::shared_ptr<array>> result(values_.size());

            std::transform(values_.begin(), values_.end(), result.begin(),
                           [&](std::shared_ptr<base> v) -> std::shared_ptr<array>
                           {
                               if (v->is_array())
                                   return std::static_pointer_cast<array>(v);
                               return std::shared_ptr<array>{};
                           });

            return result;
        }

        /**
         * Add a value to the end of the array
         */
        template <class T>
        void push_back(const std::shared_ptr<value<T>> &val)
        {
            if (values_.empty() || values_[0]->as<T>())
            {
                values_.push_back(val);
            }
            else
            {
                throw array_exception{"Arrays must be homogenous."};
            }
        }

        /**
         * Add an array to the end of the array
         */
        void push_back(const std::shared_ptr<array> &val)
        {
            if (values_.empty() || values_[0]->is_array())
            {
                values_.push_back(val);
            }
            else
            {
                throw array_exception{"Arrays must be homogenous."};
            }
        }

        /**
         * Convenience function for adding a simple element to the end
         * of the array.
         */
        template <class T>
        void push_back(T &&val, typename value_traits<T>::type * = 0)
        {
            push_back(make_value(std::forward<T>(val)));
        }

        /**
         * Insert a value into the array
         */
        template <class T>
        iterator insert(iterator position, const std::shared_ptr<value<T>> &value)
        {
            if (values_.empty() || values_[0]->as<T>())
            {
                return values_.insert(position, value);
            }
            else
            {
                throw array_exception{"Arrays must be homogenous."};
            }
        }

        /**
         * Insert an array into the array
         */
        iterator insert(iterator position, const std::shared_ptr<array> &value)
        {
            if (values_.empty() || values_[0]->is_array())
            {
                return values_.insert(position, value);
            }
            else
            {
                throw array_exception{"Arrays must be homogenous."};
            }
        }

        /**
         * Convenience function for inserting a simple element in the array
         */
        template <class T>
        iterator insert(iterator position, T &&val,
                        typename value_traits<T>::type * = 0)
        {
            return insert(position, make_value(std::forward<T>(val)));
        }

        /**
         * Erase an element from the array
         */
        iterator erase(iterator position)
        {
            return values_.erase(position);
        }

        /**
         * Clear the array
         */
        void clear()
        {
            values_.clear();
        }

        /**
         * Reserve space for n values.
         */
        void reserve(size_type n)
        {
            values_.reserve(n);
        }

    private:
#if defined(CPPTOML_NO_RTTI)
        array() : base(base_type::ARRAY)
        {
            // empty
        }
#else
        array() = default;
#endif

        template <class InputIterator>
        array(InputIterator begin, InputIterator end) : values_{begin, end}
        {
            // nothing
        }

        array(const array &obj) = delete;
        array &operator=(const array &obj) = delete;

        std::vector<std::shared_ptr<base>> values_;
    };

    inline std::shared_ptr<array> make_array()
    {
        struct make_shared_enabler : public array
        {
            make_shared_enabler()
            {
                // nothing
            }
        };

        return std::make_shared<make_shared_enabler>();
    }

    namespace detail
    {
        template <>
        inline std::shared_ptr<array> make_element<array>()
        {
            return make_array();
        }
    } // namespace detail

    /**
     * Obtains a option<vector<T>>. The option will be empty if the array
     * contains values that are not of type T.
     */
    template <>
    inline typename array_of_trait<array>::return_type
    array::get_array_of<array>() const
    {
        std::vector<std::shared_ptr<array>> result;
        result.reserve(values_.size());

        for (const auto &val : values_)
        {
            if (auto v = val->as_array())
                result.push_back(v);
            else
                return {};
        }

        return {std::move(result)};
    }

    class table;

    class table_array : public base
    {
        friend class table;
        friend std::shared_ptr<table_array> make_table_array(bool);

    public:
        std::shared_ptr<base> clone() const override;

        using size_type = std::size_t;

        /**
         * arrays can be iterated over
         */
        using iterator = std::vector<std::shared_ptr<table>>::iterator;

        /**
         * arrays can be iterated over.  Const version.
         */
        using const_iterator = std::vector<std::shared_ptr<table>>::const_iterator;

        iterator begin()
        {
            return array_.begin();
        }

        const_iterator begin() const
        {
            return array_.begin();
        }

        iterator end()
        {
            return array_.end();
        }

        const_iterator end() const
        {
            return array_.end();
        }

        virtual bool is_table_array() const override
        {
            return true;
        }

        std::vector<std::shared_ptr<table>> &get()
        {
            return array_;
        }

        const std::vector<std::shared_ptr<table>> &get() const
        {
            return array_;
        }

        /**
         * Add a table to the end of the array
         */
        void push_back(const std::shared_ptr<table> &val)
        {
            array_.push_back(val);
        }

        /**
         * Insert a table into the array
         */
        iterator insert(iterator position, const std::shared_ptr<table> &value)
        {
            return array_.insert(position, value);
        }

        /**
         * Erase an element from the array
         */
        iterator erase(iterator position)
        {
            return array_.erase(position);
        }

        /**
         * Clear the array
         */
        void clear()
        {
            array_.clear();
        }

        /**
         * Reserve space for n tables.
         */
        void reserve(size_type n)
        {
            array_.reserve(n);
        }

        /**
         * Whether or not the table array is declared inline. This mostly
         * matters for parsing, where statically defined arrays cannot be
         * appended to using the array-of-table syntax.
         */
        bool is_inline() const
        {
            return is_inline_;
        }

    private:
#if defined(CPPTOML_NO_RTTI)
        table_array(bool is_inline = false)
            : base(base_type::TABLE_ARRAY), is_inline_(is_inline)
        {
            // nothing
        }
#else
        table_array(bool is_inline = false) : is_inline_(is_inline)
        {
            // nothing
        }
#endif

        table_array(const table_array &obj) = delete;
        table_array &operator=(const table_array &rhs) = delete;

        std::vector<std::shared_ptr<table>> array_;
        const bool is_inline_ = false;
    };

    inline std::shared_ptr<table_array> make_table_array(bool is_inline)
    {
        struct make_shared_enabler : public table_array
        {
            make_shared_enabler(bool mse_is_inline) : table_array(mse_is_inline)
            {
                // nothing
            }
        };

        return std::make_shared<make_shared_enabler>(is_inline);
    }

    namespace detail
    {
        template <>
        inline std::shared_ptr<table_array> make_element<table_array>()
        {
            return make_table_array(true);
        }
    } // namespace detail

    // The below are overloads for fetching specific value types out of a value
    // where special casting behavior (like bounds checking) is desired

    template <class T>
    typename std::enable_if<!std::is_floating_point<T>::value && std::is_signed<T>::value,
                            option<T>>::type
    get_impl(const std::shared_ptr<base> &elem)
    {
        if (auto v = elem->as<int64_t>())
        {
            if (v->get() < (std::numeric_limits<T>::min)())
                throw std::underflow_error{
                    "T cannot represent the value requested in get"};

            if (v->get() > (std::numeric_limits<T>::max)())
                throw std::overflow_error{
                    "T cannot represent the value requested in get"};

            return {static_cast<T>(v->get())};
        }
        else
        {
            return {};
        }
    }

    template <class T>
    typename std::enable_if<!std::is_same<T, bool>::value && std::is_unsigned<T>::value,
                            option<T>>::type
    get_impl(const std::shared_ptr<base> &elem)
    {
        if (auto v = elem->as<int64_t>())
        {
            if (v->get() < 0)
                throw std::underflow_error{"T cannot store negative value in get"};

            if (static_cast<uint64_t>(v->get()) > (std::numeric_limits<T>::max)())
                throw std::overflow_error{
                    "T cannot represent the value requested in get"};

            return {static_cast<T>(v->get())};
        }
        else
        {
            return {};
        }
    }

    template <class T>
    typename std::enable_if<!std::is_integral<T>::value || std::is_same<T, bool>::value,
                            option<T>>::type
    get_impl(const std::shared_ptr<base> &elem)
    {
        if (auto v = elem->as<T>())
        {
            return {v->get()};
        }
        else
        {
            return {};
        }
    }

    /**
     * Represents a TOML keytable.
     */
    class table : public base
    {
    public:
        friend class table_array;
        friend std::shared_ptr<table> make_table();

        std::shared_ptr<base> clone() const override;

        /**
         * tables can be iterated over.
         */
        using iterator = string_to_base_map::iterator;

        /**
         * tables can be iterated over. Const version.
         */
        using const_iterator = string_to_base_map::const_iterator;

        iterator begin()
        {
            return map_.begin();
        }

        const_iterator begin() const
        {
            return map_.begin();
        }

        iterator end()
        {
            return map_.end();
        }

        const_iterator end() const
        {
            return map_.end();
        }

        bool is_table() const override
        {
            return true;
        }

        bool empty() const
        {
            return map_.empty();
        }

        /**
         * Determines if this key table contains the given key.
         */
        bool contains(const std::string &key) const
        {
            return map_.find(key) != map_.end();
        }

        /**
         * Determines if this key table contains the given key. Will
         * resolve "qualified keys". Qualified keys are the full access
         * path separated with dots like "grandparent.parent.child".
         */
        bool contains_qualified(const std::string &key) const
        {
            return resolve_qualified(key);
        }

        /**
         * Obtains the base for a given key.
         * @throw std::out_of_range if the key does not exist
         */
        std::shared_ptr<base> get(const std::string &key) const
        {
            return map_.at(key);
        }

        /**
         * Obtains the base for a given key. Will resolve "qualified
         * keys". Qualified keys are the full access path separated with
         * dots like "grandparent.parent.child".
         *
         * @throw std::out_of_range if the key does not exist
         */
        std::shared_ptr<base> get_qualified(const std::string &key) const
        {
            std::shared_ptr<base> p;
            resolve_qualified(key, &p);
            return p;
        }

        /**
         * Obtains a table for a given key, if possible.
         */
        std::shared_ptr<table> get_table(const std::string &key) const
        {
            if (contains(key) && get(key)->is_table())
                return std::static_pointer_cast<table>(get(key));
            return nullptr;
        }

        /**
         * Obtains a table for a given key, if possible. Will resolve
         * "qualified keys".
         */
        std::shared_ptr<table> get_table_qualified(const std::string &key) const
        {
            if (contains_qualified(key) && get_qualified(key)->is_table())
                return std::static_pointer_cast<table>(get_qualified(key));
            return nullptr;
        }

        /**
         * Obtains an array for a given key.
         */
        std::shared_ptr<array> get_array(const std::string &key) const
        {
            if (!contains(key))
                return nullptr;
            return get(key)->as_array();
        }

        /**
         * Obtains an array for a given key. Will resolve "qualified keys".
         */
        std::shared_ptr<array> get_array_qualified(const std::string &key) const
        {
            if (!contains_qualified(key))
                return nullptr;
            return get_qualified(key)->as_array();
        }

        /**
         * Obtains a table_array for a given key, if possible.
         */
        std::shared_ptr<table_array> get_table_array(const std::string &key) const
        {
            if (!contains(key))
                return nullptr;
            return get(key)->as_table_array();
        }

        /**
         * Obtains a table_array for a given key, if possible. Will resolve
         * "qualified keys".
         */
        std::shared_ptr<table_array>
        get_table_array_qualified(const std::string &key) const
        {
            if (!contains_qualified(key))
                return nullptr;
            return get_qualified(key)->as_table_array();
        }

        /**
         * Helper function that attempts to get a value corresponding
         * to the template parameter from a given key.
         */
        template <class T>
        option<T> get_as(const std::string &key) const
        {
            try
            {
                return get_impl<T>(get(key));
            }
            catch (const std::out_of_range &)
            {
                return {};
            }
        }

        /**
         * Helper function that attempts to get a value corresponding
         * to the template parameter from a given key. Will resolve "qualified
         * keys".
         */
        template <class T>
        option<T> get_qualified_as(const std::string &key) const
        {
            try
            {
                return get_impl<T>(get_qualified(key));
            }
            catch (const std::out_of_range &)
            {
                return {};
            }
        }

        /**
         * Helper function that attempts to get an array of values of a given
         * type corresponding to the template parameter for a given key.
         *
         * If the key doesn't exist, doesn't exist as an array type, or one or
         * more keys inside the array type are not of type T, an empty option
         * is returned. Otherwise, an option containing a vector of the values
         * is returned.
         */
        template <class T>
        inline typename array_of_trait<T>::return_type
        get_array_of(const std::string &key) const
        {
            if (auto v = get_array(key))
            {
                std::vector<T> result;
                result.reserve(v->get().size());

                for (const auto &b : v->get())
                {
                    if (auto val = b->as<T>())
                        result.push_back(val->get());
                    else
                        return {};
                }
                return {std::move(result)};
            }

            return {};
        }

        /**
         * Helper function that attempts to get an array of values of a given
         * type corresponding to the template parameter for a given key. Will
         * resolve "qualified keys".
         *
         * If the key doesn't exist, doesn't exist as an array type, or one or
         * more keys inside the array type are not of type T, an empty option
         * is returned. Otherwise, an option containing a vector of the values
         * is returned.
         */
        template <class T>
        inline typename array_of_trait<T>::return_type
        get_qualified_array_of(const std::string &key) const
        {
            if (auto v = get_array_qualified(key))
            {
                std::vector<T> result;
                result.reserve(v->get().size());

                for (const auto &b : v->get())
                {
                    if (auto val = b->as<T>())
                        result.push_back(val->get());
                    else
                        return {};
                }
                return {std::move(result)};
            }

            return {};
        }

        /**
         * Adds an element to the keytable.
         */
        void insert(const std::string &key, const std::shared_ptr<base> &value)
        {
            map_[key] = value;
        }

        /**
         * Convenience shorthand for adding a simple element to the
         * keytable.
         */
        template <class T>
        void insert(const std::string &key, T &&val,
                    typename value_traits<T>::type * = 0)
        {
            insert(key, make_value(std::forward<T>(val)));
        }

        /**
         * Removes an element from the table.
         */
        void erase(const std::string &key)
        {
            map_.erase(key);
        }

    private:
#if defined(CPPTOML_NO_RTTI)
        table() : base(base_type::TABLE)
        {
            // nothing
        }
#else
        table()
        {
            // nothing
        }
#endif

        table(const table &obj) = delete;
        table &operator=(const table &rhs) = delete;

        std::vector<std::string> split(const std::string &value,
                                       char separator) const
        {
            std::vector<std::string> result;
            std::string::size_type p = 0;
            std::string::size_type q;
            while ((q = value.find(separator, p)) != std::string::npos)
            {
                result.emplace_back(value, p, q - p);
                p = q + 1;
            }
            result.emplace_back(value, p);
            return result;
        }

        // If output parameter p is specified, fill it with the pointer to the
        // specified entry and throw std::out_of_range if it couldn't be found.
        //
        // Otherwise, just return true if the entry could be found or false
        // otherwise and do not throw.
        bool resolve_qualified(const std::string &key,
                               std::shared_ptr<base> *p = nullptr) const
        {
            auto parts = split(key, '.');
            auto last_key = parts.back();
            parts.pop_back();

            auto cur_table = this;
            for (const auto &part : parts)
            {
                cur_table = cur_table->get_table(part).get();
                if (!cur_table)
                {
                    if (!p)
                        return false;

                    throw std::out_of_range{key + " is not a valid key"};
                }
            }

            if (!p)
                return cur_table->map_.count(last_key) != 0;

            *p = cur_table->map_.at(last_key);
            return true;
        }

        string_to_base_map map_;
    };

    /**
     * Helper function that attempts to get an array of arrays for a given
     * key.
     *
     * If the key doesn't exist, doesn't exist as an array type, or one or
     * more keys inside the array type are not of type T, an empty option
     * is returned. Otherwise, an option containing a vector of the values
     * is returned.
     */
    template <>
    inline typename array_of_trait<array>::return_type
    table::get_array_of<array>(const std::string &key) const
    {
        if (auto v = get_array(key))
        {
            std::vector<std::shared_ptr<array>> result;
            result.reserve(v->get().size());

            for (const auto &b : v->get())
            {
                if (auto val = b->as_array())
                    result.push_back(val);
                else
                    return {};
            }

            return {std::move(result)};
        }

        return {};
    }

    /**
     * Helper function that attempts to get an array of arrays for a given
     * key. Will resolve "qualified keys".
     *
     * If the key doesn't exist, doesn't exist as an array type, or one or
     * more keys inside the array type are not of type T, an empty option
     * is returned. Otherwise, an option containing a vector of the values
     * is returned.
     */
    template <>
    inline typename array_of_trait<array>::return_type
    table::get_qualified_array_of<array>(const std::string &key) const
    {
        if (auto v = get_array_qualified(key))
        {
            std::vector<std::shared_ptr<array>> result;
            result.reserve(v->get().size());

            for (const auto &b : v->get())
            {
                if (auto val = b->as_array())
                    result.push_back(val);
                else
                    return {};
            }

            return {std::move(result)};
        }

        return {};
    }

    std::shared_ptr<table> make_table()
    {
        struct make_shared_enabler : public table
        {
            make_shared_enabler()
            {
                // nothing
            }
        };

        return std::make_shared<make_shared_enabler>();
    }

    namespace detail
    {
        template <>
        inline std::shared_ptr<table> make_element<table>()
        {
            return make_table();
        }
    } // namespace detail

    template <class T>
    std::shared_ptr<base> value<T>::clone() const
    {
        return make_value(data_);
    }

    inline std::shared_ptr<base> array::clone() const
    {
        auto result = make_array();
        result->reserve(values_.size());
        for (const auto &ptr : values_)
            result->values_.push_back(ptr->clone());
        return result;
    }

    inline std::shared_ptr<base> table_array::clone() const
    {
        auto result = make_table_array(is_inline());
        result->reserve(array_.size());
        for (const auto &ptr : array_)
            result->array_.push_back(ptr->clone()->as_table());
        return result;
    }

    inline std::shared_ptr<base> table::clone() const
    {
        auto result = make_table();
        for (const auto &pr : map_)
            result->insert(pr.first, pr.second->clone());
        return result;
    }

    /**
     * Exception class for all TOML parsing errors.
     */
    class parse_exception : public std::runtime_error
    {
    public:
        parse_exception(const std::string &err) : std::runtime_error{err}
        {
        }

        parse_exception(const std::string &err, std::size_t line_number)
            : std::runtime_error{err + " at line " + std::to_string(line_number)}
        {
        }
    };

    inline bool is_number(char c)
    {
        return c >= '0' && c <= '9';
    }

    inline bool is_hex(char c)
    {
        return is_number(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    }

    /**
     * Helper object for consuming expected characters.
     */
    template <class OnError>
    class consumer
    {
    public:
        consumer(std::string::iterator &it, const std::string::iterator &end,
                 OnError &&on_error)
            : it_(it), end_(end), on_error_(std::forward<OnError>(on_error))
        {
            // nothing
        }

        void operator()(char c)
        {
            if (it_ == end_ || *it_ != c)
                on_error_();
            ++it_;
        }

        template <std::size_t N>
        void operator()(const char (&str)[N])
        {
            std::for_each(std::begin(str), std::end(str) - 1,
                          [&](char c)
                          {
                              (*this)(c);
                          });
        }

        void eat_or(char a, char b)
        {
            if (it_ == end_ || (*it_ != a && *it_ != b))
                on_error_();
            ++it_;
        }

        int eat_digits(int len)
        {
            int val = 0;
            for (int i = 0; i < len; ++i)
            {
                if (!is_number(*it_) || it_ == end_)
                    on_error_();
                val = 10 * val + (*it_++ - '0');
            }
            return val;
        }

        void error()
        {
            on_error_();
        }

    private:
        std::string::iterator &it_;
        const std::string::iterator &end_;
        OnError on_error_;
    };

    template <class OnError>
    consumer<OnError> make_consumer(std::string::iterator &it,
                                    const std::string::iterator &end,
                                    OnError &&on_error)
    {
        return consumer<OnError>(it, end, std::forward<OnError>(on_error));
    }

    // replacement for std::getline to handle incorrectly line-ended files
    // https://stackoverflow.com/questions/6089231/getting-std-ifstream-to-handle-lf-cr-and-crlf
    namespace detail
    {
        inline std::istream &getline(std::istream &input, std::string &line)
        {
            line.clear();

            std::istream::sentry sentry{input, true};
            auto sb = input.rdbuf();

            while (true)
            {
                auto c = sb->sbumpc();
                if (c == '\r')
                {
                    if (sb->sgetc() == '\n')
                        c = sb->sbumpc();
                }

                if (c == '\n')
                    return input;

                if (c == std::istream::traits_type::eof())
                {
                    if (line.empty())
                        input.setstate(std::ios::eofbit);
                    return input;
                }

                line.push_back(static_cast<char>(c));
            }
        }
    } // namespace detail

    /**
     * The parser class.
     */
    class parser
    {
    public:
        /**
         * Parsers are constructed from streams.
         */
        parser(std::istream &stream) : input_(stream)
        {
            // nothing
        }

        parser &operator=(const parser &parser) = delete;

        /**
         * Parses the stream this parser was created on until EOF.
         * @throw parse_exception if there are errors in parsing
         */
        std::shared_ptr<table> parse()
        {
            std::shared_ptr<table> root = make_table();

            table *curr_table = root.get();

            while (detail::getline(input_, line_))
            {
                line_number_++;
                auto it = line_.begin();
                auto end = line_.end();
                consume_whitespace(it, end);
                if (it == end || *it == '#')
                    continue;
                if (*it == '[')
                {
                    curr_table = root.get();
                    parse_table(it, end, curr_table);
                }
                else
                {
                    parse_key_value(it, end, curr_table);
                    consume_whitespace(it, end);
                    eol_or_comment(it, end);
                }
            }
            return root;
        }

    private:
#if defined _MSC_VER
        __declspec(noreturn)
#elif defined __GNUC__
        __attribute__((noreturn))
#endif
            void throw_parse_exception(const std::string &err)
        {
            throw parse_exception{err, line_number_};
        }

        void parse_table(std::string::iterator &it,
                         const std::string::iterator &end, table *&curr_table)
        {
            // remove the beginning keytable marker
            ++it;
            if (it == end)
                throw_parse_exception("Unexpected end of table");
            if (*it == '[')
                parse_table_array(it, end, curr_table);
            else
                parse_single_table(it, end, curr_table);
        }

        void parse_single_table(std::string::iterator &it,
                                const std::string::iterator &end,
                                table *&curr_table)
        {
            if (it == end || *it == ']')
                throw_parse_exception("Table name cannot be empty");

            std::string full_table_name;
            bool inserted = false;

            auto key_end = [](char c)
            {
                return c == ']';
            };

            auto key_part_handler = [&](const std::string &part)
            {
                if (part.empty())
                    throw_parse_exception("Empty component of table name");

                if (!full_table_name.empty())
                    full_table_name += '.';
                full_table_name += part;

                if (curr_table->contains(part))
                {
#if !defined(__PGI)
                    auto b = curr_table->get(part);
#else
                    // Workaround for PGI compiler
                    std::shared_ptr<base> b = curr_table->get(part);
#endif
                    if (b->is_table())
                        curr_table = static_cast<table *>(b.get());
                    else if (b->is_table_array())
                        curr_table = std::static_pointer_cast<table_array>(b)
                                         ->get()
                                         .back()
                                         .get();
                    else
                        throw_parse_exception("Key " + full_table_name + "already exists as a value");
                }
                else
                {
                    inserted = true;
                    curr_table->insert(part, make_table());
                    curr_table = static_cast<table *>(curr_table->get(part).get());
                }
            };

            key_part_handler(parse_key(it, end, key_end, key_part_handler));

            if (it == end)
                throw_parse_exception(
                    "Unterminated table declaration; did you forget a ']'?");

            if (*it != ']')
            {
                std::string errmsg{"Unexpected character in table definition: "};
                errmsg += '"';
                errmsg += *it;
                errmsg += '"';
                throw_parse_exception(errmsg);
            }

            // table already existed
            if (!inserted)
            {
                auto is_value = [](const std::pair<const std::string &,
                                                   const std::shared_ptr<base> &> &p)
                {
                    return p.second->is_value();
                };

                // if there are any values, we can't add values to this table
                // since it has already been defined. If there aren't any
                // values, then it was implicitly created by something like
                // [a.b]
                if (curr_table->empty() || std::any_of(curr_table->begin(), curr_table->end(),
                                                       is_value))
                {
                    throw_parse_exception("Redefinition of table " + full_table_name);
                }
            }

            ++it;
            consume_whitespace(it, end);
            eol_or_comment(it, end);
        }

        void parse_table_array(std::string::iterator &it,
                               const std::string::iterator &end, table *&curr_table)
        {
            ++it;
            if (it == end || *it == ']')
                throw_parse_exception("Table array name cannot be empty");

            auto key_end = [](char c)
            {
                return c == ']';
            };

            std::string full_ta_name;
            auto key_part_handler = [&](const std::string &part)
            {
                if (part.empty())
                    throw_parse_exception("Empty component of table array name");

                if (!full_ta_name.empty())
                    full_ta_name += '.';
                full_ta_name += part;

                if (curr_table->contains(part))
                {
#if !defined(__PGI)
                    auto b = curr_table->get(part);
#else
                    // Workaround for PGI compiler
                    std::shared_ptr<base> b = curr_table->get(part);
#endif

                    // if this is the end of the table array name, add an
                    // element to the table array that we just looked up,
                    // provided it was not declared inline
                    if (it != end && *it == ']')
                    {
                        if (!b->is_table_array())
                        {
                            throw_parse_exception("Key " + full_ta_name + " is not a table array");
                        }

                        auto v = b->as_table_array();

                        if (v->is_inline())
                        {
                            throw_parse_exception("Static array " + full_ta_name + " cannot be appended to");
                        }

                        v->get().push_back(make_table());
                        curr_table = v->get().back().get();
                    }
                    // otherwise, just keep traversing down the key name
                    else
                    {
                        if (b->is_table())
                            curr_table = static_cast<table *>(b.get());
                        else if (b->is_table_array())
                            curr_table = std::static_pointer_cast<table_array>(b)
                                             ->get()
                                             .back()
                                             .get();
                        else
                            throw_parse_exception("Key " + full_ta_name + " already exists as a value");
                    }
                }
                else
                {
                    // if this is the end of the table array name, add a new
                    // table array and a new table inside that array for us to
                    // add keys to next
                    if (it != end && *it == ']')
                    {
                        curr_table->insert(part, make_table_array());
                        auto arr = std::static_pointer_cast<table_array>(
                            curr_table->get(part));
                        arr->get().push_back(make_table());
                        curr_table = arr->get().back().get();
                    }
                    // otherwise, create the implicitly defined table and move
                    // down to it
                    else
                    {
                        curr_table->insert(part, make_table());
                        curr_table = static_cast<table *>(curr_table->get(part).get());
                    }
                }
            };

            key_part_handler(parse_key(it, end, key_end, key_part_handler));

            // consume the last "]]"
            auto eat = make_consumer(it, end, [this]()
                                     { throw_parse_exception("Unterminated table array name"); });
            eat(']');
            eat(']');

            consume_whitespace(it, end);
            eol_or_comment(it, end);
        }

        void parse_key_value(std::string::iterator &it, std::string::iterator &end,
                             table *curr_table)
        {
            auto key_end = [](char c)
            {
                return c == '=';
            };

            auto key_part_handler = [&](const std::string &part)
            {
                // two cases: this key part exists already, in which case it must
                // be a table, or it doesn't exist in which case we must create
                // an implicitly defined table
                if (curr_table->contains(part))
                {
                    auto val = curr_table->get(part);
                    if (val->is_table())
                    {
                        curr_table = static_cast<table *>(val.get());
                    }
                    else
                    {
                        throw_parse_exception("Key " + part + " already exists as a value");
                    }
                }
                else
                {
                    auto newtable = make_table();
                    curr_table->insert(part, newtable);
                    curr_table = newtable.get();
                }
            };

            auto key = parse_key(it, end, key_end, key_part_handler);

            if (curr_table->contains(key))
                throw_parse_exception("Key " + key + " already present");
            if (it == end || *it != '=')
                throw_parse_exception("Value must follow after a '='");
            ++it;
            consume_whitespace(it, end);
            curr_table->insert(key, parse_value(it, end));
            consume_whitespace(it, end);
        }

        template <class KeyEndFinder, class KeyPartHandler>
        std::string
        parse_key(std::string::iterator &it, const std::string::iterator &end,
                  KeyEndFinder &&key_end, KeyPartHandler &&key_part_handler)
        {
            // parse the key as a series of one or more simple-keys joined with '.'
            while (it != end && !key_end(*it))
            {
                auto part = parse_simple_key(it, end);
                consume_whitespace(it, end);

                if (it == end || key_end(*it))
                {
                    return part;
                }

                if (*it != '.')
                {
                    std::string errmsg{"Unexpected character in key: "};
                    errmsg += '"';
                    errmsg += *it;
                    errmsg += '"';
                    throw_parse_exception(errmsg);
                }

                key_part_handler(part);

                // consume the dot
                ++it;
            }

            throw_parse_exception("Unexpected end of key");
        }

        std::string parse_simple_key(std::string::iterator &it,
                                     const std::string::iterator &end)
        {
            consume_whitespace(it, end);

            if (it == end)
                throw_parse_exception("Unexpected end of key (blank key?)");

            if (*it == '"' || *it == '\'')
            {
                return string_literal(it, end, *it);
            }
            else
            {
                auto bke = std::find_if(it, end, [](char c)
                                        { return c == '.' || c == '=' || c == ']'; });
                return parse_bare_key(it, bke);
            }
        }

        std::string parse_bare_key(std::string::iterator &it,
                                   const std::string::iterator &end)
        {
            if (it == end)
            {
                throw_parse_exception("Bare key missing name");
            }

            auto key_end = end;
            --key_end;
            consume_backwards_whitespace(key_end, it);
            ++key_end;
            std::string key{it, key_end};

            if (std::find(it, key_end, '#') != key_end)
            {
                throw_parse_exception("Bare key " + key + " cannot contain #");
            }

            if (std::find_if(it, key_end,
                             [](char c)
                             {
                                 return c == ' ' || c == '\t';
                             }) != key_end)
            {
                throw_parse_exception("Bare key " + key + " cannot contain whitespace");
            }

            if (std::find_if(it, key_end,
                             [](char c)
                             {
                                 return c == '[' || c == ']';
                             }) != key_end)
            {
                throw_parse_exception("Bare key " + key + " cannot contain '[' or ']'");
            }

            it = end;
            return key;
        }

        enum class parse_type
        {
            STRING = 1,
            LOCAL_TIME,
            LOCAL_DATE,
            LOCAL_DATETIME,
            OFFSET_DATETIME,
            INT,
            FLOAT,
            BOOL,
            ARRAY,
            INLINE_TABLE
        };

        std::shared_ptr<base> parse_value(std::string::iterator &it,
                                          std::string::iterator &end)
        {
            parse_type type = determine_value_type(it, end);
            switch (type)
            {
            case parse_type::STRING:
                return parse_string(it, end);
            case parse_type::LOCAL_TIME:
                return parse_time(it, end);
            case parse_type::LOCAL_DATE:
            case parse_type::LOCAL_DATETIME:
            case parse_type::OFFSET_DATETIME:
                return parse_date(it, end);
            case parse_type::INT:
            case parse_type::FLOAT:
                return parse_number(it, end);
            case parse_type::BOOL:
                return parse_bool(it, end);
            case parse_type::ARRAY:
                return parse_array(it, end);
            case parse_type::INLINE_TABLE:
                return parse_inline_table(it, end);
            default:
                throw_parse_exception("Failed to parse value");
            }
        }

        parse_type determine_value_type(const std::string::iterator &it,
                                        const std::string::iterator &end)
        {
            if (it == end)
            {
                throw_parse_exception("Failed to parse value type");
            }
            if (*it == '"' || *it == '\'')
            {
                return parse_type::STRING;
            }
            else if (is_time(it, end))
            {
                return parse_type::LOCAL_TIME;
            }
            else if (auto dtype = date_type(it, end))
            {
                return *dtype;
            }
            else if (is_number(*it) || *it == '-' || *it == '+' || (*it == 'i' && it + 1 != end && it[1] == 'n' && it + 2 != end && it[2] == 'f') || (*it == 'n' && it + 1 != end && it[1] == 'a' && it + 2 != end && it[2] == 'n'))
            {
                return determine_number_type(it, end);
            }
            else if (*it == 't' || *it == 'f')
            {
                return parse_type::BOOL;
            }
            else if (*it == '[')
            {
                return parse_type::ARRAY;
            }
            else if (*it == '{')
            {
                return parse_type::INLINE_TABLE;
            }
            throw_parse_exception("Failed to parse value type");
        }

        parse_type determine_number_type(const std::string::iterator &it,
                                         const std::string::iterator &end)
        {
            // determine if we are an integer or a float
            auto check_it = it;
            if (*check_it == '-' || *check_it == '+')
                ++check_it;

            if (check_it == end)
                throw_parse_exception("Malformed number");

            if (*check_it == 'i' || *check_it == 'n')
                return parse_type::FLOAT;

            while (check_it != end && is_number(*check_it))
                ++check_it;
            if (check_it != end && *check_it == '.')
            {
                ++check_it;
                while (check_it != end && is_number(*check_it))
                    ++check_it;
                return parse_type::FLOAT;
            }
            else
            {
                return parse_type::INT;
            }
        }

        std::shared_ptr<value<std::string>> parse_string(std::string::iterator &it,
                                                         std::string::iterator &end)
        {
            auto delim = *it;
            assert(delim == '"' || delim == '\'');

            // end is non-const here because we have to be able to potentially
            // parse multiple lines in a string, not just one
            auto check_it = it;
            ++check_it;
            if (check_it != end && *check_it == delim)
            {
                ++check_it;
                if (check_it != end && *check_it == delim)
                {
                    it = ++check_it;
                    return parse_multiline_string(it, end, delim);
                }
            }
            return make_value<std::string>(string_literal(it, end, delim));
        }

        std::shared_ptr<value<std::string>>
        parse_multiline_string(std::string::iterator &it,
                               std::string::iterator &end, char delim)
        {
            std::stringstream ss;

            auto is_ws = [](char c)
            {
                return c == ' ' || c == '\t';
            };

            bool consuming = false;
            std::shared_ptr<value<std::string>> ret;

            auto handle_line = [&](std::string::iterator &local_it,
                                   std::string::iterator &local_end)
            {
                if (consuming)
                {
                    local_it = std::find_if_not(local_it, local_end, is_ws);

                    // whole line is whitespace
                    if (local_it == local_end)
                        return;
                }

                consuming = false;

                while (local_it != local_end)
                {
                    // handle escaped characters
                    if (delim == '"' && *local_it == '\\')
                    {
                        auto check = local_it;
                        // check if this is an actual escape sequence or a
                        // whitespace escaping backslash
                        ++check;
                        consume_whitespace(check, local_end);
                        if (check == local_end)
                        {
                            consuming = true;
                            break;
                        }

                        ss << parse_escape_code(local_it, local_end);
                        continue;
                    }

                    // if we can end the string
                    if (std::distance(local_it, local_end) >= 3)
                    {
                        auto check = local_it;
                        // check for """
                        if (*check++ == delim && *check++ == delim && *check++ == delim)
                        {
                            local_it = check;
                            ret = make_value<std::string>(ss.str());
                            break;
                        }
                    }

                    ss << *local_it++;
                }
            };

            // handle the remainder of the current line
            handle_line(it, end);
            if (ret)
                return ret;

            // start eating lines
            while (detail::getline(input_, line_))
            {
                ++line_number_;

                it = line_.begin();
                end = line_.end();

                handle_line(it, end);

                if (ret)
                    return ret;

                if (!consuming)
                    ss << std::endl;
            }

            throw_parse_exception("Unterminated multi-line basic string");
        }

        std::string string_literal(std::string::iterator &it,
                                   const std::string::iterator &end, char delim)
        {
            ++it;
            std::string val;
            while (it != end)
            {
                // handle escaped characters
                if (delim == '"' && *it == '\\')
                {
                    val += parse_escape_code(it, end);
                }
                else if (*it == delim)
                {
                    ++it;
                    consume_whitespace(it, end);
                    return val;
                }
                else
                {
                    val += *it++;
                }
            }
            throw_parse_exception("Unterminated string literal");
        }

        std::string parse_escape_code(std::string::iterator &it,
                                      const std::string::iterator &end)
        {
            ++it;
            if (it == end)
                throw_parse_exception("Invalid escape sequence");
            char value;
            if (*it == 'b')
            {
                value = '\b';
            }
            else if (*it == 't')
            {
                value = '\t';
            }
            else if (*it == 'n')
            {
                value = '\n';
            }
            else if (*it == 'f')
            {
                value = '\f';
            }
            else if (*it == 'r')
            {
                value = '\r';
            }
            else if (*it == '"')
            {
                value = '"';
            }
            else if (*it == '\\')
            {
                value = '\\';
            }
            else if (*it == 'u' || *it == 'U')
            {
                return parse_unicode(it, end);
            }
            else
            {
                throw_parse_exception("Invalid escape sequence");
            }
            ++it;
            return std::string(1, value);
        }

        std::string parse_unicode(std::string::iterator &it,
                                  const std::string::iterator &end)
        {
            bool large = *it++ == 'U';
            auto codepoint = parse_hex(it, end, large ? 0x10000000 : 0x1000);

            if ((codepoint > 0xd7ff && codepoint < 0xe000) || codepoint > 0x10ffff)
            {
                throw_parse_exception(
                    "Unicode escape sequence is not a Unicode scalar value");
            }

            std::string result;
            // See Table 3-6 of the Unicode standard
            if (codepoint <= 0x7f)
            {
                // 1-byte codepoints: 00000000 0xxxxxxx
                // repr: 0xxxxxxx
                result += static_cast<char>(codepoint & 0x7f);
            }
            else if (codepoint <= 0x7ff)
            {
                // 2-byte codepoints: 00000yyy yyxxxxxx
                // repr: 110yyyyy 10xxxxxx
                //
                // 0x1f = 00011111
                // 0xc0 = 11000000
                //
                result += static_cast<char>(0xc0 | ((codepoint >> 6) & 0x1f));
                //
                // 0x80 = 10000000
                // 0x3f = 00111111
                //
                result += static_cast<char>(0x80 | (codepoint & 0x3f));
            }
            else if (codepoint <= 0xffff)
            {
                // 3-byte codepoints: zzzzyyyy yyxxxxxx
                // repr: 1110zzzz 10yyyyyy 10xxxxxx
                //
                // 0xe0 = 11100000
                // 0x0f = 00001111
                //
                result += static_cast<char>(0xe0 | ((codepoint >> 12) & 0x0f));
                result += static_cast<char>(0x80 | ((codepoint >> 6) & 0x1f));
                result += static_cast<char>(0x80 | (codepoint & 0x3f));
            }
            else
            {
                // 4-byte codepoints: 000uuuuu zzzzyyyy yyxxxxxx
                // repr: 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx
                //
                // 0xf0 = 11110000
                // 0x07 = 00000111
                //
                result += static_cast<char>(0xf0 | ((codepoint >> 18) & 0x07));
                result += static_cast<char>(0x80 | ((codepoint >> 12) & 0x3f));
                result += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3f));
                result += static_cast<char>(0x80 | (codepoint & 0x3f));
            }
            return result;
        }

        uint32_t parse_hex(std::string::iterator &it,
                           const std::string::iterator &end, uint32_t place)
        {
            uint32_t value = 0;
            while (place > 0)
            {
                if (it == end)
                    throw_parse_exception("Unexpected end of unicode sequence");

                if (!is_hex(*it))
                    throw_parse_exception("Invalid unicode escape sequence");

                value += place * hex_to_digit(*it++);
                place /= 16;
            }
            return value;
        }

        uint32_t hex_to_digit(char c)
        {
            if (is_number(c))
                return static_cast<uint32_t>(c - '0');
            return 10 + static_cast<uint32_t>(c - ((c >= 'a' && c <= 'f') ? 'a' : 'A'));
        }

        std::shared_ptr<base> parse_number(std::string::iterator &it,
                                           const std::string::iterator &end)
        {
            auto check_it = it;
            auto check_end = find_end_of_number(it, end);

            auto eat_sign = [&]()
            {
                if (check_it != end && (*check_it == '-' || *check_it == '+'))
                    ++check_it;
            };

            auto check_no_leading_zero = [&]()
            {
                if (check_it != end && *check_it == '0' && check_it + 1 != check_end && check_it[1] != '.')
                {
                    throw_parse_exception("Numbers may not have leading zeros");
                }
            };

            auto eat_digits = [&](bool (*check_char)(char))
            {
                auto beg = check_it;
                while (check_it != end && check_char(*check_it))
                {
                    ++check_it;
                    if (check_it != end && *check_it == '_')
                    {
                        ++check_it;
                        if (check_it == end || !check_char(*check_it))
                            throw_parse_exception("Malformed number");
                    }
                }

                if (check_it == beg)
                    throw_parse_exception("Malformed number");
            };

            auto eat_hex = [&]()
            {
                eat_digits(&is_hex);
            };

            auto eat_numbers = [&]()
            {
                eat_digits(&is_number);
            };

            if (check_it != end && *check_it == '0' && check_it + 1 != check_end && (check_it[1] == 'x' || check_it[1] == 'o' || check_it[1] == 'b'))
            {
                ++check_it;
                char base = *check_it;
                ++check_it;
                if (base == 'x')
                {
                    eat_hex();
                    return parse_int(it, check_it, 16);
                }
                else if (base == 'o')
                {
                    auto start = check_it;
                    eat_numbers();
                    auto val = parse_int(start, check_it, 8, "0");
                    it = start;
                    return val;
                }
                else
                { // if (base == 'b')
                    auto start = check_it;
                    eat_numbers();
                    auto val = parse_int(start, check_it, 2);
                    it = start;
                    return val;
                }
            }

            eat_sign();
            check_no_leading_zero();

            if (check_it != end && check_it + 1 != end && check_it + 2 != end)
            {
                if (check_it[0] == 'i' && check_it[1] == 'n' && check_it[2] == 'f')
                {
                    auto val = std::numeric_limits<double>::infinity();
                    if (*it == '-')
                        val = -val;
                    it = check_it + 3;
                    return make_value(val);
                }
                else if (check_it[0] == 'n' && check_it[1] == 'a' && check_it[2] == 'n')
                {
                    auto val = std::numeric_limits<double>::quiet_NaN();
                    if (*it == '-')
                        val = -val;
                    it = check_it + 3;
                    return make_value(val);
                }
            }

            eat_numbers();

            if (check_it != end && (*check_it == '.' || *check_it == 'e' || *check_it == 'E'))
            {
                bool is_exp = *check_it == 'e' || *check_it == 'E';

                ++check_it;
                if (check_it == end)
                    throw_parse_exception("Floats must have trailing digits");

                auto eat_exp = [&]()
                {
                    eat_sign();
                    check_no_leading_zero();
                    eat_numbers();
                };

                if (is_exp)
                    eat_exp();
                else
                    eat_numbers();

                if (!is_exp && check_it != end && (*check_it == 'e' || *check_it == 'E'))
                {
                    ++check_it;
                    eat_exp();
                }

                return parse_float(it, check_it);
            }
            else
            {
                return parse_int(it, check_it);
            }
        }

        std::shared_ptr<value<int64_t>> parse_int(std::string::iterator &it,
                                                  const std::string::iterator &end,
                                                  int base = 10,
                                                  const char *prefix = "")
        {
            std::string v{it, end};
            v = prefix + v;
            v.erase(std::remove(v.begin(), v.end(), '_'), v.end());
            it = end;
            try
            {
                return make_value<int64_t>(std::stoll(v, nullptr, base));
            }
            catch (const std::invalid_argument &ex)
            {
                throw_parse_exception("Malformed number (invalid argument: " + std::string{ex.what()} + ")");
            }
            catch (const std::out_of_range &ex)
            {
                throw_parse_exception("Malformed number (out of range: " + std::string{ex.what()} + ")");
            }
        }

        std::shared_ptr<value<double>> parse_float(std::string::iterator &it,
                                                   const std::string::iterator &end)
        {
            std::string v{it, end};
            v.erase(std::remove(v.begin(), v.end(), '_'), v.end());
            it = end;
            char decimal_point = std::localeconv()->decimal_point[0];
            std::replace(v.begin(), v.end(), '.', decimal_point);
            try
            {
                return make_value<double>(std::stod(v));
            }
            catch (const std::invalid_argument &ex)
            {
                throw_parse_exception("Malformed number (invalid argument: " + std::string{ex.what()} + ")");
            }
            catch (const std::out_of_range &ex)
            {
                throw_parse_exception("Malformed number (out of range: " + std::string{ex.what()} + ")");
            }
        }

        std::shared_ptr<value<bool>> parse_bool(std::string::iterator &it,
                                                const std::string::iterator &end)
        {
            auto eat = make_consumer(it, end, [this]()
                                     { throw_parse_exception("Attempted to parse invalid boolean value"); });

            if (*it == 't')
            {
                eat("true");
                return make_value<bool>(true);
            }
            else if (*it == 'f')
            {
                eat("false");
                return make_value<bool>(false);
            }

            eat.error();
            return nullptr;
        }

        std::string::iterator find_end_of_number(std::string::iterator it,
                                                 std::string::iterator end)
        {
            auto ret = std::find_if(it, end, [](char c)
                                    { return !is_number(c) && c != '_' && c != '.' && c != 'e' && c != 'E' && c != '-' && c != '+' && c != 'x' && c != 'o' && c != 'b'; });
            if (ret != end && ret + 1 != end && ret + 2 != end)
            {
                if ((ret[0] == 'i' && ret[1] == 'n' && ret[2] == 'f') || (ret[0] == 'n' && ret[1] == 'a' && ret[2] == 'n'))
                {
                    ret = ret + 3;
                }
            }
            return ret;
        }

        std::string::iterator find_end_of_date(std::string::iterator it,
                                               std::string::iterator end)
        {
            auto end_of_date = std::find_if(it, end, [](char c)
                                            { return !is_number(c) && c != '-'; });
            if (end_of_date != end && *end_of_date == ' ' && end_of_date + 1 != end && is_number(end_of_date[1]))
                end_of_date++;
            return std::find_if(end_of_date, end, [](char c)
                                { return !is_number(c) && c != 'T' && c != 'Z' && c != ':' && c != '-' && c != '+' && c != '.'; });
        }

        std::string::iterator find_end_of_time(std::string::iterator it,
                                               std::string::iterator end)
        {
            return std::find_if(it, end, [](char c)
                                { return !is_number(c) && c != ':' && c != '.'; });
        }

        local_time read_time(std::string::iterator &it,
                             const std::string::iterator &end)
        {
            auto time_end = find_end_of_time(it, end);

            auto eat = make_consumer(
                it, time_end, [&]()
                { throw_parse_exception("Malformed time"); });

            local_time ltime;

            ltime.hour = eat.eat_digits(2);
            eat(':');
            ltime.minute = eat.eat_digits(2);
            eat(':');
            ltime.second = eat.eat_digits(2);

            int power = 100000;
            if (it != time_end && *it == '.')
            {
                ++it;
                while (it != time_end && is_number(*it))
                {
                    ltime.microsecond += power * (*it++ - '0');
                    power /= 10;
                }
            }

            if (it != time_end)
                throw_parse_exception("Malformed time");

            return ltime;
        }

        std::shared_ptr<value<local_time>>
        parse_time(std::string::iterator &it, const std::string::iterator &end)
        {
            return make_value(read_time(it, end));
        }

        std::shared_ptr<base> parse_date(std::string::iterator &it,
                                         const std::string::iterator &end)
        {
            auto date_end = find_end_of_date(it, end);

            auto eat = make_consumer(
                it, date_end, [&]()
                { throw_parse_exception("Malformed date"); });

            local_date ldate;
            ldate.year = eat.eat_digits(4);
            eat('-');
            ldate.month = eat.eat_digits(2);
            eat('-');
            ldate.day = eat.eat_digits(2);

            if (it == date_end)
                return make_value(ldate);

            eat.eat_or('T', ' ');

            local_datetime ldt;
            static_cast<local_date &>(ldt) = ldate;
            static_cast<local_time &>(ldt) = read_time(it, date_end);

            if (it == date_end)
                return make_value(ldt);

            offset_datetime dt;
            static_cast<local_datetime &>(dt) = ldt;

            int hoff = 0;
            int moff = 0;
            if (*it == '+' || *it == '-')
            {
                auto plus = *it == '+';
                ++it;

                hoff = eat.eat_digits(2);
                dt.hour_offset = (plus) ? hoff : -hoff;
                eat(':');
                moff = eat.eat_digits(2);
                dt.minute_offset = (plus) ? moff : -moff;
            }
            else if (*it == 'Z')
            {
                ++it;
            }

            if (it != date_end)
                throw_parse_exception("Malformed date");

            return make_value(dt);
        }

        std::shared_ptr<base> parse_array(std::string::iterator &it,
                                          std::string::iterator &end)
        {
            // this gets ugly because of the "homogeneity" restriction:
            // arrays can either be of only one type, or contain arrays
            // (each of those arrays could be of different types, though)
            //
            // because of the latter portion, we don't really have a choice
            // but to represent them as arrays of base values...
            ++it;

            // ugh---have to read the first value to determine array type...
            skip_whitespace_and_comments(it, end);

            // edge case---empty array
            if (*it == ']')
            {
                ++it;
                return make_array();
            }

            auto val_end = std::find_if(
                it, end, [](char c)
                { return c == ',' || c == ']' || c == '#'; });
            parse_type type = determine_value_type(it, val_end);
            switch (type)
            {
            case parse_type::STRING:
                return parse_value_array<std::string>(it, end);
            case parse_type::LOCAL_TIME:
                return parse_value_array<local_time>(it, end);
            case parse_type::LOCAL_DATE:
                return parse_value_array<local_date>(it, end);
            case parse_type::LOCAL_DATETIME:
                return parse_value_array<local_datetime>(it, end);
            case parse_type::OFFSET_DATETIME:
                return parse_value_array<offset_datetime>(it, end);
            case parse_type::INT:
                return parse_value_array<int64_t>(it, end);
            case parse_type::FLOAT:
                return parse_value_array<double>(it, end);
            case parse_type::BOOL:
                return parse_value_array<bool>(it, end);
            case parse_type::ARRAY:
                return parse_object_array<array>(&parser::parse_array, '[', it,
                                                 end);
            case parse_type::INLINE_TABLE:
                return parse_object_array<table_array>(
                    &parser::parse_inline_table, '{', it, end);
            default:
                throw_parse_exception("Unable to parse array");
            }
        }

        template <class Value>
        std::shared_ptr<array> parse_value_array(std::string::iterator &it,
                                                 std::string::iterator &end)
        {
            auto arr = make_array();
            while (it != end && *it != ']')
            {
                auto val = parse_value(it, end);
                if (auto v = val->as<Value>())
                    arr->get().push_back(val);
                else
                    throw_parse_exception("Arrays must be homogeneous");
                skip_whitespace_and_comments(it, end);
                if (*it != ',')
                    break;
                ++it;
                skip_whitespace_and_comments(it, end);
            }
            if (it != end)
                ++it;
            return arr;
        }

        template <class Object, class Function>
        std::shared_ptr<Object> parse_object_array(Function &&fun, char delim,
                                                   std::string::iterator &it,
                                                   std::string::iterator &end)
        {
            auto arr = detail::make_element<Object>();

            while (it != end && *it != ']')
            {
                if (*it != delim)
                    throw_parse_exception("Unexpected character in array");

                arr->get().push_back(((*this).*fun)(it, end));
                skip_whitespace_and_comments(it, end);

                if (it == end || *it != ',')
                    break;

                ++it;
                skip_whitespace_and_comments(it, end);
            }

            if (it == end || *it != ']')
                throw_parse_exception("Unterminated array");

            ++it;
            return arr;
        }

        std::shared_ptr<table> parse_inline_table(std::string::iterator &it,
                                                  std::string::iterator &end)
        {
            auto tbl = make_table();
            do
            {
                ++it;
                if (it == end)
                    throw_parse_exception("Unterminated inline table");

                consume_whitespace(it, end);
                if (it != end && *it != '}')
                {
                    parse_key_value(it, end, tbl.get());
                    consume_whitespace(it, end);
                }
            } while (*it == ',');

            if (it == end || *it != '}')
                throw_parse_exception("Unterminated inline table");

            ++it;
            consume_whitespace(it, end);

            return tbl;
        }

        void skip_whitespace_and_comments(std::string::iterator &start,
                                          std::string::iterator &end)
        {
            consume_whitespace(start, end);
            while (start == end || *start == '#')
            {
                if (!detail::getline(input_, line_))
                    throw_parse_exception("Unclosed array");
                line_number_++;
                start = line_.begin();
                end = line_.end();
                consume_whitespace(start, end);
            }
        }

        void consume_whitespace(std::string::iterator &it,
                                const std::string::iterator &end)
        {
            while (it != end && (*it == ' ' || *it == '\t'))
                ++it;
        }

        void consume_backwards_whitespace(std::string::iterator &back,
                                          const std::string::iterator &front)
        {
            while (back != front && (*back == ' ' || *back == '\t'))
                --back;
        }

        void eol_or_comment(const std::string::iterator &it,
                            const std::string::iterator &end)
        {
            if (it != end && *it != '#')
                throw_parse_exception("Unidentified trailing character '" + std::string{*it} + "'---did you forget a '#'?");
        }

        bool is_time(const std::string::iterator &it,
                     const std::string::iterator &end)
        {
            auto time_end = find_end_of_time(it, end);
            auto len = std::distance(it, time_end);

            if (len < 8)
                return false;

            if (it[2] != ':' || it[5] != ':')
                return false;

            if (len > 8)
                return it[8] == '.' && len > 9;

            return true;
        }

        option<parse_type> date_type(const std::string::iterator &it,
                                     const std::string::iterator &end)
        {
            auto date_end = find_end_of_date(it, end);
            auto len = std::distance(it, date_end);

            if (len < 10)
                return {};

            if (it[4] != '-' || it[7] != '-')
                return {};

            if (len >= 19 && (it[10] == 'T' || it[10] == ' ') && is_time(it + 11, date_end))
            {
                // datetime type
                auto time_end = find_end_of_time(it + 11, date_end);
                if (time_end == date_end)
                    return {parse_type::LOCAL_DATETIME};
                else
                    return {parse_type::OFFSET_DATETIME};
            }
            else if (len == 10)
            {
                // just a regular date
                return {parse_type::LOCAL_DATE};
            }

            return {};
        }

        std::istream &input_;
        std::string line_;
        std::size_t line_number_ = 0;
    };

    /**
     * Utility function to parse a file as a TOML file. Returns the root table.
     * Throws a parse_exception if the file cannot be opened.
     */
    inline std::shared_ptr<table> parse_file(const std::string &filename)
    {
#if defined(BOOST_NOWIDE_FSTREAM_INCLUDED_HPP)
        boost::nowide::ifstream file{filename.c_str()};
#elif defined(NOWIDE_FSTREAM_INCLUDED_HPP)
        nowide::ifstream file{filename.c_str()};
#else
        std::ifstream file{filename};
#endif
        if (!file.is_open())
            throw parse_exception{filename + " could not be opened for parsing"};
        parser p{file};
        return p.parse();
    }

    template <class... Ts>
    struct value_accept;

    template <>
    struct value_accept<>
    {
        template <class Visitor, class... Args>
        static void accept(const base &, Visitor &&, Args &&...)
        {
            // nothing
        }
    };

    template <class T, class... Ts>
    struct value_accept<T, Ts...>
    {
        template <class Visitor, class... Args>
        static void accept(const base &b, Visitor &&visitor, Args &&...args)
        {
            if (auto v = b.as<T>())
            {
                visitor.visit(*v, std::forward<Args>(args)...);
            }
            else
            {
                value_accept<Ts...>::accept(b, std::forward<Visitor>(visitor),
                                            std::forward<Args>(args)...);
            }
        }
    };

    /**
     * base implementation of accept() that calls visitor.visit() on the concrete
     * class.
     */
    template <class Visitor, class... Args>
    void base::accept(Visitor &&visitor, Args &&...args) const
    {
        if (is_value())
        {
            using value_acceptor = value_accept<std::string, int64_t, double, bool, local_date,
                                                local_time, local_datetime, offset_datetime>;
            value_acceptor::accept(*this, std::forward<Visitor>(visitor),
                                   std::forward<Args>(args)...);
        }
        else if (is_table())
        {
            visitor.visit(static_cast<const table &>(*this),
                          std::forward<Args>(args)...);
        }
        else if (is_array())
        {
            visitor.visit(static_cast<const array &>(*this),
                          std::forward<Args>(args)...);
        }
        else if (is_table_array())
        {
            visitor.visit(static_cast<const table_array &>(*this),
                          std::forward<Args>(args)...);
        }
    }

    /**
     * Writer that can be passed to accept() functions of cpptoml objects and
     * will output valid TOML to a stream.
     */
    class toml_writer
    {
    public:
        /**
         * Construct a toml_writer that will write to the given stream
         */
        toml_writer(std::ostream &s, const std::string &indent_space = "\t")
            : stream_(s), indent_(indent_space), has_naked_endline_(false)
        {
            // nothing
        }

    public:
        /**
         * Output a base value of the TOML tree.
         */
        template <class T>
        void visit(const value<T> &v, bool = false)
        {
            write(v);
        }

        /**
         * Output a table element of the TOML tree
         */
        void visit(const table &t, bool in_array = false)
        {
            write_table_header(in_array);
            std::vector<std::string> values;
            std::vector<std::string> tables;

            for (const auto &i : t)
            {
                if (i.second->is_table() || i.second->is_table_array())
                {
                    tables.push_back(i.first);
                }
                else
                {
                    values.push_back(i.first);
                }
            }

            for (unsigned int i = 0; i < values.size(); ++i)
            {
                path_.push_back(values[i]);

                if (i > 0)
                    endline();

                write_table_item_header(*t.get(values[i]));
                t.get(values[i])->accept(*this, false);
                path_.pop_back();
            }

            for (unsigned int i = 0; i < tables.size(); ++i)
            {
                path_.push_back(tables[i]);

                if (values.size() > 0 || i > 0)
                    endline();

                write_table_item_header(*t.get(tables[i]));
                t.get(tables[i])->accept(*this, false);
                path_.pop_back();
            }

            endline();
        }

        /**
         * Output an array element of the TOML tree
         */
        void visit(const array &a, bool = false)
        {
            write("[");

            for (unsigned int i = 0; i < a.get().size(); ++i)
            {
                if (i > 0)
                    write(", ");

                if (a.get()[i]->is_array())
                {
                    a.get()[i]->as_array()->accept(*this, true);
                }
                else
                {
                    a.get()[i]->accept(*this, true);
                }
            }

            write("]");
        }

        /**
         * Output a table_array element of the TOML tree
         */
        void visit(const table_array &t, bool = false)
        {
            for (unsigned int j = 0; j < t.get().size(); ++j)
            {
                if (j > 0)
                    endline();

                t.get()[j]->accept(*this, true);
            }

            endline();
        }

        /**
         * Escape a string for output.
         */
        static std::string escape_string(const std::string &str)
        {
            std::string res;
            for (auto it = str.begin(); it != str.end(); ++it)
            {
                if (*it == '\b')
                {
                    res += "\\b";
                }
                else if (*it == '\t')
                {
                    res += "\\t";
                }
                else if (*it == '\n')
                {
                    res += "\\n";
                }
                else if (*it == '\f')
                {
                    res += "\\f";
                }
                else if (*it == '\r')
                {
                    res += "\\r";
                }
                else if (*it == '"')
                {
                    res += "\\\"";
                }
                else if (*it == '\\')
                {
                    res += "\\\\";
                }
                else if (static_cast<uint32_t>(*it) <= UINT32_C(0x001f))
                {
                    res += "\\u";
                    std::stringstream ss;
                    ss << std::hex << static_cast<uint32_t>(*it);
                    res += ss.str();
                }
                else
                {
                    res += *it;
                }
            }
            return res;
        }

    protected:
        /**
         * Write out a string.
         */
        void write(const value<std::string> &v)
        {
            write("\"");
            write(escape_string(v.get()));
            write("\"");
        }

        /**
         * Write out a double.
         */
        void write(const value<double> &v)
        {
            std::stringstream ss;
            ss << std::showpoint
               << std::setprecision(std::numeric_limits<double>::max_digits10)
               << v.get();

            auto double_str = ss.str();
            auto pos = double_str.find("e0");
            if (pos != std::string::npos)
                double_str.replace(pos, 2, "e");
            pos = double_str.find("e-0");
            if (pos != std::string::npos)
                double_str.replace(pos, 3, "e-");

            stream_ << double_str;
            has_naked_endline_ = false;
        }

        /**
         * Write out an integer, local_date, local_time, local_datetime, or
         * offset_datetime.
         */
        template <class T>
        typename std::enable_if<
            is_one_of<T, int64_t, local_date, local_time, local_datetime,
                      offset_datetime>::value>::type
        write(const value<T> &v)
        {
            write(v.get());
        }

        /**
         * Write out a boolean.
         */
        void write(const value<bool> &v)
        {
            write((v.get() ? "true" : "false"));
        }

        /**
         * Write out the header of a table.
         */
        void write_table_header(bool in_array = false)
        {
            if (!path_.empty())
            {
                indent();

                write("[");

                if (in_array)
                {
                    write("[");
                }

                for (unsigned int i = 0; i < path_.size(); ++i)
                {
                    if (i > 0)
                    {
                        write(".");
                    }

                    if (path_[i].find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcde"
                                                   "fghijklmnopqrstuvwxyz0123456789"
                                                   "_-") == std::string::npos)
                    {
                        write(path_[i]);
                    }
                    else
                    {
                        write("\"");
                        write(escape_string(path_[i]));
                        write("\"");
                    }
                }

                if (in_array)
                {
                    write("]");
                }

                write("]");
                endline();
            }
        }

        /**
         * Write out the identifier for an item in a table.
         */
        void write_table_item_header(const base &b)
        {
            if (!b.is_table() && !b.is_table_array())
            {
                indent();

                if (path_.back().find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcde"
                                                   "fghijklmnopqrstuvwxyz0123456789"
                                                   "_-") == std::string::npos)
                {
                    write(path_.back());
                }
                else
                {
                    write("\"");
                    write(escape_string(path_.back()));
                    write("\"");
                }

                write(" = ");
            }
        }

    private:
        /**
         * Indent the proper number of tabs given the size of
         * the path.
         */
        void indent()
        {
            for (std::size_t i = 1; i < path_.size(); ++i)
                write(indent_);
        }

        /**
         * Write a value out to the stream.
         */
        template <class T>
        void write(const T &v)
        {
            stream_ << v;
            has_naked_endline_ = false;
        }

        /**
         * Write an endline out to the stream
         */
        void endline()
        {
            if (!has_naked_endline_)
            {
                stream_ << "\n";
                has_naked_endline_ = true;
            }
        }

    private:
        std::ostream &stream_;
        const std::string indent_;
        std::vector<std::string> path_;
        bool has_naked_endline_;
    };

    inline std::ostream &operator<<(std::ostream &stream, const base &b)
    {
        toml_writer writer{stream};
        b.accept(writer);
        return stream;
    }

    template <class T>
    std::ostream &operator<<(std::ostream &stream, const value<T> &v)
    {
        toml_writer writer{stream};
        v.accept(writer);
        return stream;
    }

    inline std::ostream &operator<<(std::ostream &stream, const table &t)
    {
        toml_writer writer{stream};
        t.accept(writer);
        return stream;
    }

    inline std::ostream &operator<<(std::ostream &stream, const table_array &t)
    {
        toml_writer writer{stream};
        t.accept(writer);
        return stream;
    }

    inline std::ostream &operator<<(std::ostream &stream, const array &a)
    {
        toml_writer writer{stream};
        a.accept(writer);
        return stream;
    }
} // namespace cpptoml
#endif // CPPTOML_H
