#ifndef NETWORKIT_AUXILIARY_STRING_BUILDER_HPP_
#define NETWORKIT_AUXILIARY_STRING_BUILDER_HPP_

#include <algorithm>
#include <cassert>
#include <iterator>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <utility>

#include <networkit/auxiliary/TemplateUtils.hpp>

namespace Aux {

template <typename... T>
std::string toString(const T &...args);

template <typename... T>
std::ostream &printToStream(std::ostream &stream, const T &...args);

template <typename... T>
std::string toStringF(std::string_view format, const T &...args);

template <typename... T>
std::ostream &printToStreamF(std::ostream &stream, std::string_view format, const T &...args);

// Implementation
/////////////////

namespace Impl {

// Categories of how a type might be printable
enum class PrintableCategory {
    UNPRINTABLE,
    ITERATABLE,
    PAIR,
    TUPLE,
    STREAMABLE,
    Unprintable = UNPRINTABLE, // this + following added for backwards compatibility
    Iteratable = ITERATABLE,
    Pair = PAIR,
    Tuple = TUPLE,
    Streamable = STREAMABLE
};

template <typename T>
constexpr bool isStreamable();
template <typename T>
constexpr bool isPair();
template <typename T>
constexpr bool isTuple();
template <typename T>
constexpr bool isIteratable();

template <typename T>
constexpr PrintableCategory getPrintableCategory() {
    return isStreamable<T>()   ? PrintableCategory::STREAMABLE
           : isPair<T>()       ? PrintableCategory::PAIR
           : isTuple<T>()      ? PrintableCategory::TUPLE
           : isIteratable<T>() ? PrintableCategory::ITERATABLE
                               :
                               /* else: */ PrintableCategory::UNPRINTABLE;
}
template <PrintableCategory Tag>
struct PrintableCategoryTag {};
using IteratableTag = PrintableCategoryTag<PrintableCategory::ITERATABLE>;
using PairTag = PrintableCategoryTag<PrintableCategory::PAIR>;
using TupleTag = PrintableCategoryTag<PrintableCategory::TUPLE>;
using StreamableTag = PrintableCategoryTag<PrintableCategory::STREAMABLE>;
using UnprintableTag = PrintableCategoryTag<PrintableCategory::UNPRINTABLE>;

template <typename T, typename... Args>
void printToStream(std::ostream &stream, const T &, const Args &...);
inline void printToStream(std::ostream &) {}

template <typename T>
void printToStreamTagged(std::ostream &stream, const T &, IteratableTag);
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &, PairTag);
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &, TupleTag);
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &, StreamableTag);

// Claim that this function exists somewhere else to keep the errors clean
// (calling this function is not a problem since the error is already caught earlier
// in printToStream, and calling it directly will result in a linker-error)
template <typename T>
extern void printToStreamTagged(std::ostream &, const T &, UnprintableTag);

template <typename T, typename... Args>
void printToStream(std::ostream &stream, const T &arg, const Args &...args) {
    static_assert(getPrintableCategory<T>() != PrintableCategory::UNPRINTABLE,
                  "printToStream must not be called with an unprintable argument");
    printToStreamTagged(stream, arg, PrintableCategoryTag<getPrintableCategory<T>()>{});
    printToStream(stream, args...);
}

inline std::tuple<std::string_view::const_iterator, bool>
printFormatPartToStream(std::ostream &stream, std::string_view::const_iterator begin,
                        std::string_view::const_iterator end);

inline void printToStreamF(std::ostream &stream, std::string_view::const_iterator format_begin,
                           std::string_view::const_iterator format_end) {
    bool printArgument;
    using iterator = std::string_view::const_iterator;
    iterator it;
    std::tie(it, printArgument) = printFormatPartToStream(stream, format_begin, format_end);
    if (printArgument) {
        throw std::invalid_argument{"formatstring requests more arguments then provided"};
    }
}
template <typename T, typename... Args>
void printToStreamF(std::ostream &stream, std::string_view::const_iterator format_begin,
                    std::string_view::const_iterator format_end, const T &arg,
                    const Args &...args) {
    bool printArgument;
    using iterator = std::string_view::const_iterator;
    iterator it;
    std::tie(it, printArgument) = printFormatPartToStream(stream, format_begin, format_end);
    if (printArgument) {
        printToStream(stream, arg);
        printToStreamF(stream, it, format_end, args...);
    } else {
        assert(it == format_end);
        return;
    }
}

/**
 * Write the formatstring until the first occurance of "%s"
 * to stream, '%%' will be replaced by '%'.
 *
 * @returns The iterator that points one after the last consumed
 * character and whether a further argument should be printed.
 */
inline auto printFormatPartToStream(std::ostream &stream, std::string_view::const_iterator begin,
                                    std::string_view::const_iterator end)
    -> std::tuple<std::string_view::const_iterator, bool> {
    auto it = begin;
    while (it != end) {
        auto nextPercent = std::find(it, end, '%');
        stream.write(&*it, std::distance(it, nextPercent));

        if (nextPercent == end) {
            it = nextPercent;
            break;
        }

        it = ++nextPercent;

        if (it == end) {
            throw std::invalid_argument{"formatstrings must not end on unmatched '%'"};
        } else if (*it == '%') {
            stream.put('%');
            ++it;
        } else if (*it == 's') {
            ++it;
            return std::make_tuple(it, true);
        } else {
            throw std::invalid_argument{"formatstring contains illegal format-specifier"};
        }
    }
    assert(it == end);
    return std::make_tuple(end, false);
}

// Brace Yourself: Templatemetaprogramming is coming
////////////////////////////////////////////////////

// Iteratable
struct IsIteratableHelper {
    static std::false_type isIteratable(...);

    template <
        typename T, class Iterator = decltype(std::begin(std::declval<T>())),
        AUX_REQUIRE(EndIteratorValid, isSame<Iterator, decltype(std::end(std::declval<T>()))>()),
        AUX_REQUIRE(HasInputIterator,
                    isBaseOrSame<std::input_iterator_tag,
                                 typename std::iterator_traits<Iterator>::iterator_category>())>
    static std::true_type isIteratable(const T &);
};
template <typename T>
constexpr bool isIteratable() {
    return decltype(IsIteratableHelper::isIteratable(std::declval<T>()))::value;
}

// Pair
template <typename T>
struct IsPairHelper : std::false_type {};
template <typename T1, typename T2>
struct IsPairHelper<std::pair<T1, T2>> : std::true_type {};
template <typename T>
constexpr bool isPair() {
    return IsPairHelper<T>::value;
}

// Tuple
template <typename T>
struct IsTupleHelper : std::false_type {};
template <typename... T>
struct IsTupleHelper<std::tuple<T...>> : std::true_type {};
template <typename T>
constexpr bool isTuple() {
    return IsTupleHelper<T>::value;
}

// Streamable
struct IsStreamableHelper {
    static std::false_type isStreamable(...);

    template <
        typename T,
        typename _streamT = typename std::decay<decltype(std::declval<std::ostream &>()
                                                         << std::declval<const T &>())>::type,
        typename std::enable_if<std::is_same<std::ostream, _streamT>::value
                                || std::is_base_of<std::ostream, _streamT>::value> * = nullptr>
    static std::true_type isStreamable(const T &);
};
template <typename T>
constexpr bool isStreamable() {
    return decltype(IsStreamableHelper::isStreamable(std::declval<T>()))::value;
}

// And now: implement the actual printing:
//////////////////////////////////////////

// Streamable
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &arg, StreamableTag) {
    stream << arg;
}

// Pair
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &arg, PairTag) {
    stream << '(';
    printToStream(stream, arg.first);
    stream << ", ";
    printToStream(stream, arg.second);
    stream << ')';
}

// Tuple
template <typename Tuple, int I, int TupleSize>
struct printTupleHelper {
    static void print(std::ostream &stream, const Tuple &arg) {
        printToStream(stream, std::get<I - 1>(arg));
        stream << ", ";
        printTupleHelper<Tuple, I + 1, TupleSize>::print(stream, arg);
    }
};
template <typename Tuple, int I>
struct printTupleHelper<Tuple, I, I> {
    static void print(std::ostream &stream, const Tuple &arg) {
        printToStream(stream, std::get<I - 1>(arg));
    }
};
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &arg, TupleTag) {
    stream << '(';
    printTupleHelper<T, 1, std::tuple_size<T>::value>::print(stream, arg);
    stream << ')';
}

// Iteratable
template <typename T>
void printToStreamTagged(std::ostream &stream, const T &arg, IteratableTag) {
    auto it = std::begin(arg);
    auto end = std::end(arg);
    bool firstpass = true;
    stream << '[';
    while (it != end) {
        if (firstpass) {
            firstpass = false;
        } else {
            stream << ", ";
        }
        printToStream(stream, *it);
        ++it;
    }
    stream << ']';
}

} // namespace Impl

// Finally: put together the public interface:
//////////////////////////////////////////////

template <typename... T>
std::string toString(const T &...args) {
    std::stringstream stream;
    printToStream(stream, args...);
    return stream.str();
}

template <typename... T>
std::ostream &printToStream(std::ostream &stream, const T &...args) {
    Impl::printToStream(stream, args...);
    return stream;
}

template <typename... T>
std::string toStringF(std::string_view format, const T &...args) {
    std::stringstream stream;
    printToStreamF(stream, format, args...);
    return stream.str();
}

template <typename... T>
std::ostream &printToStreamF(std::ostream &stream, std::string_view format, const T &...args) {
    Impl::printToStreamF(stream, format.begin(), format.end(), args...);
    return stream;
}

} // namespace Aux

#endif // NETWORKIT_AUXILIARY_STRING_BUILDER_HPP_
