// -*- mode: C++ -*-
// Copyright 2017-2019 The Procyon Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef PN_OUTPUT_
#define PN_OUTPUT_

#include <pn/procyon.h>
#include <pn/arg>
#include <pn/array>
#include <pn/data>
#include <pn/map>
#include <pn/string>
#include <pn/value>
#include <type_traits>

namespace pn {

enum {
    dump_default = PN_DUMP_DEFAULT,
    dump_short   = PN_DUMP_SHORT,
};

extern output out;
extern output err;

class output {
  public:
    output() : _c_obj{pn_file_output(nullptr)} {}
    explicit output(string_view path, text_mode mode, bool append = false);
    explicit output(FILE* f) : _c_obj{pn_file_output(f)} {}
    explicit output(pn_output_t out) : _c_obj{out} {}
    output(const output&) = delete;
    output(output&& other) : _c_obj{pn_file_output(nullptr)} { std::swap(_c_obj, other._c_obj); }
    output& operator=(const output&) = delete;
    output& operator=(output&& other) { return std::swap(_c_obj, other._c_obj), *this; }
    ~output() { pn_output_close(&_c_obj); }

            operator bool() const { return c_obj()->type && !eof() && !error(); }
    output& check() &;
    output  check() && { return check(), std::move(*this); }

    template <typename... arguments>
    output& write(const arguments&... args);

    template <typename argument>
    output& dump(const argument& x, int flags = dump_default);

    template <typename... arguments>
    output& format(const char* fmt, const arguments&... arg);

    bool error() const { return pn_output_error(c_obj()); }
    bool eof() const { return pn_output_eof(c_obj()); }

    pn_output_t*       c_obj() { return &_c_obj; }
    const pn_output_t* c_obj() const { return &_c_obj; }

  private:
    void check_ok();

    pn_output_t _c_obj;
};

class output_view {
  public:
    output_view() : _c_obj{pn_file_output(nullptr)} {}
    output_view(const output& out) : _c_obj{*out.c_obj()} {}
    output_view(FILE* f) : _c_obj{pn_file_output(f)} {}
    output_view(pn_output_t out) : _c_obj{out} {}

                operator bool() const { return c_obj()->type && !eof() && !error(); }
    output_view check();

    template <typename... arguments>
    output_view& write(const arguments&... args);

    template <typename argument>
    output_view& dump(const argument& x, int flags = dump_default);

    template <typename... arguments>
    output_view& format(const char* fmt, const arguments&... arg);

    bool error() const { return pn_output_error(c_obj()); }
    bool eof() const { return pn_output_eof(c_obj()); }

    pn_output_t*       c_obj() { return &_c_obj; }
    const pn_output_t* c_obj() const { return &_c_obj; }

  private:
    pn_output_t _c_obj;
};

template <typename... args>
[[clang::warn_unused_result]] string format(const char* fmt, const args&... arg);

template <typename arg>
[[clang::warn_unused_result]] string dump(const arg& x, int flags = dump_default);

namespace internal {

template <typename tuple, int... i>
bool apply_format(
        pn_output_t* out, const char* input_format, const char* output_format, const tuple& args,
        indexes<i...>) {
    return pn_format(out, input_format, output_format, std::get<i>(args)...);
}

template <typename tuple>
bool format(
        pn_output_t* out, const char* output_format, const char* input_format, const tuple& args) {
    return apply_format(
            out, output_format, input_format, args,
            typename index_range<std::tuple_size<tuple>::value>::type());
}

template <typename tuple, int... i>
void apply_dump(pn_output_t* out, int flags, char format, const tuple& args, indexes<i...>) {
    pn_dump(out, flags, format, std::get<i>(args)...);
}

template <typename tuple>
void dump(pn_output_t* out, int flags, char format, const tuple& args) {
    apply_dump(
            out, flags, format, args, typename index_range<std::tuple_size<tuple>::value>::type());
}

template <typename tuple, int... i>
void apply_write(pn_output_t* out, const char* format, const tuple& args, indexes<i...>) {
    pn_write(out, format, std::get<i>(args)...);
}

template <typename tuple>
void write(pn_output_t* out, const char* format, const tuple& args) {
    apply_write(out, format, args, typename index_range<std::tuple_size<tuple>::value>::type());
}

}  // namespace internal

template <typename... args>
string format(const char* fmt, const args&... arg) {
    string out;
    out.output().format(fmt, std::forward<const args&>(arg)...).check();
    return out;
}

template <typename arg>
string dump(const arg& x, int flags) {
    string out;
    out.output().dump(x, flags).check();
    return out;
}

template <typename... args>
output& output::write(const args&... arg) {
    internal::write(
            c_obj(), (char[]){internal::arg<typename std::decay<args>::type>::code..., '\0'},
            std::tuple_cat(internal::arg<typename std::decay<args>::type>::write_args(arg)...));
    return *this;
}

template <typename... args>
output_view& output_view::write(const args&... arg) {
    internal::write(
            c_obj(), (char[]){internal::arg<typename std::decay<args>::type>::code..., '\0'},
            std::tuple_cat(internal::arg<typename std::decay<args>::type>::write_args(arg)...));
    return *this;
}

template <typename arg>
output& output::dump(const arg& x, int flags) {
    internal::dump(
            c_obj(), flags, internal::arg<typename std::decay<arg>::type>::code,
            internal::arg<typename std::decay<arg>::type>::write_args(x));
    return *this;
}

template <typename arg>
output_view& output_view::dump(const arg& x, int flags) {
    internal::dump(
            c_obj(), flags, internal::arg<typename std::decay<arg>::type>::code,
            internal::arg<typename std::decay<arg>::type>::write_args(x));
    return *this;
}

template <typename... args>
output& output::format(const char* fmt, const args&... arg) {
    internal::format(
            c_obj(), fmt, (char[]){internal::arg<typename std::decay<args>::type>::code..., '\0'},
            std::tuple_cat(internal::arg<typename std::decay<args>::type>::write_args(arg)...));
    return *this;
}

template <typename... args>
output_view& output_view::format(const char* fmt, const args&... arg) {
    internal::format(
            c_obj(), fmt, (char[]){internal::arg<typename std::decay<args>::type>::code..., '\0'},
            std::tuple_cat(internal::arg<typename std::decay<args>::type>::write_args(arg)...));
    return *this;
}

}  // namespace pn

#endif  // PN_OUTPUT_
