#pragma once

#include <xrpl/basics/ToString.h>
#include <xrpl/basics/contract.h>
#include <xrpl/json/Output.h>
#include <xrpl/json/json_value.h>

#include <memory>

namespace Json {

/**
 *  Writer implements an O(1)-space, O(1)-granular output JSON writer.
 *
 *  O(1)-space means that it uses a fixed amount of memory, and that there are
 *  no heap allocations at each step of the way.
 *
 *  O(1)-granular output means the writer only outputs in small segments of a
 *  bounded size, using a bounded number of CPU cycles in doing so.  This is
 *  very helpful in scheduling long jobs.
 *
 *  The tradeoff is that you have to fill items in the JSON tree as you go,
 *  and you can never go backward.
 *
 *  Writer can write single JSON tokens, but the typical use is to write out an
 *  entire JSON object.  For example:
 *
 *      {
 *          Writer w (out);
 *
 *          w.startObject ();          // Start the root object.
 *          w.set ("hello", "world");
 *          w.set ("goodbye", 23);
 *          w.finishObject ();         // Finish the root object.
 *      }
 *
 *  which outputs the string
 *
 *      {"hello":"world","goodbye":23}
 *
 *  There can be an object inside an object:
 *
 *      {
 *          Writer w (out);
 *
 *          w.startObject ();                // Start the root object.
 *          w.set ("hello", "world");
 *
 *          w.startObjectSet ("subobject");  // Start a sub-object.
 *          w.set ("goodbye", 23);           // Add a key, value assignment.
 *          w.finishObject ();               // Finish the sub-object.
 *
 *          w.finishObject ();               // Finish the root-object.
 *      }
 *
 *  which outputs the string
 *
 *     {"hello":"world","subobject":{"goodbye":23}}.
 *
 *  Arrays work similarly
 *
 *      {
 *          Writer w (out);
 *          w.startObject ();           // Start the root object.
 *
 *          w.startArraySet ("hello");  // Start an array.
 *          w.append (23)               // Append some items.
 *          w.append ("skidoo")
 *          w.finishArray ();           // Finish the array.
 *
 *          w.finishObject ();          // Finish the root object.
 *      }
 *
 *  which outputs the string
 *
 *      {"hello":[23,"skidoo"]}.
 *
 *
 *  If you've reached the end of a long object, you can just use finishAll()
 *  which finishes all arrays and objects that you have started.
 *
 *      {
 *          Writer w (out);
 *          w.startObject ();           // Start the root object.
 *
 *          w.startArraySet ("hello");  // Start an array.
 *          w.append (23)               // Append an item.
 *
 *          w.startArrayAppend ()       // Start a sub-array.
 *          w.append ("one");
 *          w.append ("two");
 *
 *          w.startObjectAppend ();     // Append a sub-object.
 *          w.finishAll ();             // Finish everything.
 *      }
 *
 *  which outputs the string
 *
 *      {"hello":[23,["one","two",{}]]}.
 *
 *  For convenience, the destructor of Writer calls w.finishAll() which makes
 *  sure that all arrays and objects are closed.  This means that you can throw
 *  an exception, or have a coroutine simply clean up the stack, and be sure
 *  that you do in fact generate a complete JSON object.
 */

class Writer
{
public:
    enum CollectionType { array, object };

    explicit Writer(Output const& output);
    Writer(Writer&&) noexcept;
    Writer&
    operator=(Writer&&) noexcept;

    ~Writer();

    /** Start a new collection at the root level. */
    void startRoot(CollectionType);

    /** Start a new collection inside an array. */
    void startAppend(CollectionType);

    /** Start a new collection inside an object. */
    void
    startSet(CollectionType, std::string const& key);

    /** Finish the collection most recently started. */
    void
    finish();

    /** Finish all objects and arrays.  After finishArray() has been called, no
     *  more operations can be performed. */
    void
    finishAll();

    /** Append a value to an array.
     *
     *  Scalar must be a scalar - that is, a number, boolean, string, string
     *  literal, nullptr or Json::Value
     */
    template <typename Scalar>
    void
    append(Scalar t)
    {
        rawAppend();
        output(t);
    }

    /** Add a comma before this next item if not the first item in an array.
        Useful if you are writing the actual array yourself. */
    void
    rawAppend();

    /** Add a key, value assignment to an object.
     *
     *  Scalar must be a scalar - that is, a number, boolean, string, string
     *  literal, or nullptr.
     *
     *  While the JSON spec doesn't explicitly disallow this, you should avoid
     *  calling this method twice with the same tag for the same object.
     *
     *  If CHECK_JSON_WRITER is defined, this function throws an exception if if
     *  the tag you use has already been used in this object.
     */
    template <typename Type>
    void
    set(std::string const& tag, Type t)
    {
        rawSet(tag);
        output(t);
    }

    /** Emit just "tag": as part of an object.  Useful if you are writing the
        actual value data yourself. */
    void
    rawSet(std::string const& key);

    // You won't need to call anything below here until you are writing single
    // items (numbers, strings, bools, null) to a JSON stream.

    /*** Output a string. */
    void
    output(std::string const&);

    /*** Output a literal constant or C string. */
    void
    output(char const*);

    /*** Output a Json::Value. */
    void
    output(Json::Value const&);

    /** Output a null. */
    void output(std::nullptr_t);

    /** Output a float. */
    void
    output(float);

    /** Output a double. */
    void
    output(double);

    /** Output a bool. */
    void
    output(bool);

    /** Output numbers or booleans. */
    template <typename Type>
    void
    output(Type t)
    {
        implOutput(std::to_string(t));
    }

    void
    output(Json::StaticString const& t)
    {
        output(t.c_str());
    }

private:
    class Impl;
    std::unique_ptr<Impl> impl_;

    void
    implOutput(std::string const&);
};

inline void
check(bool condition, std::string const& message)
{
    if (!condition)
        xrpl::Throw<std::logic_error>(message);
}

}  // namespace Json
